blob: 7e1b0c747e9900b2bdc5b50383617c3c21bfb775 [file] [log] [blame]
Roger Meier41ad4342015-03-24 22:30:40 +01001#
2# Licensed to the Apache Software Foundation (ASF) under one
3# or more contributor license agreements. See the NOTICE file
4# distributed with this work for additional information
5# regarding copyright ownership. The ASF licenses this file
6# to you under the Apache License, Version 2.0 (the
7# "License"); you may not use this file except in compliance
8# with the License. You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing,
13# software distributed under the License is distributed on an
14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15# KIND, either express or implied. See the License for the
16# specific language governing permissions and limitations
17# under the License.
18#
19
20import datetime
21import json
22import multiprocessing
23import os
24import platform
25import re
26import subprocess
27import sys
28import time
29import traceback
30
Nobuaki Sukegawaa6ab1f52015-11-28 15:04:39 +090031from .compat import logfile_open, path_join, str_join
Nobuaki Sukegawa2de27002015-11-22 01:13:48 +090032from .test import TestEntry
Roger Meier41ad4342015-03-24 22:30:40 +010033
34LOG_DIR = 'log'
Nobuaki Sukegawabd165302016-01-19 11:10:07 +090035RESULT_HTML = 'index.html'
Roger Meier41ad4342015-03-24 22:30:40 +010036RESULT_JSON = 'results.json'
37FAIL_JSON = 'known_failures_%s.json'
38
39
40def generate_known_failures(testdir, overwrite, save, out):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090041 def collect_failures(results):
42 success_index = 5
43 for r in results:
44 if not r[success_index]:
45 yield TestEntry.get_name(*r)
46 try:
47 with logfile_open(path_join(testdir, RESULT_JSON), 'r') as fp:
48 results = json.load(fp)
49 except IOError:
50 sys.stderr.write('Unable to load last result. Did you run tests ?\n')
51 return False
52 fails = collect_failures(results['results'])
53 if not overwrite:
54 known = load_known_failures(testdir)
55 known.extend(fails)
56 fails = known
57 fails_json = json.dumps(sorted(set(fails)), indent=2, separators=(',', ': '))
58 if save:
59 with logfile_open(os.path.join(testdir, FAIL_JSON % platform.system()), 'w+') as fp:
60 fp.write(fails_json)
61 sys.stdout.write('Successfully updated known failures.\n')
62 if out:
63 sys.stdout.write(fails_json)
64 sys.stdout.write('\n')
65 return True
Roger Meier41ad4342015-03-24 22:30:40 +010066
67
68def load_known_failures(testdir):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090069 try:
70 with logfile_open(path_join(testdir, FAIL_JSON % platform.system()), 'r') as fp:
71 return json.load(fp)
72 except IOError:
73 return []
Roger Meier41ad4342015-03-24 22:30:40 +010074
75
76class TestReporter(object):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090077 # Unfortunately, standard library doesn't handle timezone well
78 # DATETIME_FORMAT = '%a %b %d %H:%M:%S %Z %Y'
79 DATETIME_FORMAT = '%a %b %d %H:%M:%S %Y'
Roger Meier41ad4342015-03-24 22:30:40 +010080
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090081 def __init__(self):
82 self._log = multiprocessing.get_logger()
83 self._lock = multiprocessing.Lock()
Roger Meier41ad4342015-03-24 22:30:40 +010084
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090085 @classmethod
86 def test_logfile(cls, test_name, prog_kind, dir=None):
87 relpath = path_join('log', '%s_%s.log' % (test_name, prog_kind))
88 return relpath if not dir else os.path.realpath(path_join(dir, relpath))
Roger Meier41ad4342015-03-24 22:30:40 +010089
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090090 def _start(self):
91 self._start_time = time.time()
Roger Meier41ad4342015-03-24 22:30:40 +010092
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090093 @property
94 def _elapsed(self):
95 return time.time() - self._start_time
Roger Meier41ad4342015-03-24 22:30:40 +010096
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090097 @classmethod
98 def _format_date(cls):
99 return '%s' % datetime.datetime.now().strftime(cls.DATETIME_FORMAT)
Roger Meier41ad4342015-03-24 22:30:40 +0100100
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900101 def _print_date(self):
102 print(self._format_date(), file=self.out)
Roger Meier41ad4342015-03-24 22:30:40 +0100103
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900104 def _print_bar(self, out=None):
105 print(
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100106 '===============================================================================',
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900107 file=(out or self.out))
Roger Meier41ad4342015-03-24 22:30:40 +0100108
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900109 def _print_exec_time(self):
110 print('Test execution took {:.1f} seconds.'.format(self._elapsed), file=self.out)
Roger Meier41ad4342015-03-24 22:30:40 +0100111
112
113class ExecReporter(TestReporter):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900114 def __init__(self, testdir, test, prog):
115 super(ExecReporter, self).__init__()
116 self._test = test
117 self._prog = prog
118 self.logpath = self.test_logfile(test.name, prog.kind, testdir)
Roger Meier41ad4342015-03-24 22:30:40 +0100119 self.out = None
Roger Meier41ad4342015-03-24 22:30:40 +0100120
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900121 def begin(self):
122 self._start()
123 self._open()
124 if self.out and not self.out.closed:
125 self._print_header()
126 else:
127 self._log.debug('Output stream is not available.')
Nobuaki Sukegawa2ba79442016-01-12 19:37:55 +0900128
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900129 def end(self, returncode):
130 self._lock.acquire()
131 try:
132 if self.out and not self.out.closed:
133 self._print_footer(returncode)
134 self._close()
135 self.out = None
136 else:
137 self._log.debug('Output stream is not available.')
138 finally:
139 self._lock.release()
Roger Meier41ad4342015-03-24 22:30:40 +0100140
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900141 def killed(self):
142 print(file=self.out)
143 print('Server process is successfully killed.', file=self.out)
144 self.end(None)
Roger Meier41ad4342015-03-24 22:30:40 +0100145
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900146 def died(self):
147 print(file=self.out)
148 print('*** Server process has died unexpectedly ***', file=self.out)
149 self.end(None)
150
151 _init_failure_exprs = {
152 'server': list(map(re.compile, [
153 '[Aa]ddress already in use',
154 'Could not bind',
155 'EADDRINUSE',
156 ])),
157 'client': list(map(re.compile, [
158 '[Cc]onnection refused',
James E. King III9bea32f2018-03-16 16:07:42 -0400159 'Could not connect to',
James E. King III9aaf2952018-03-20 15:06:08 -0400160 'Could not open UNIX ', # domain socket (rb)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900161 'ECONNREFUSED',
James E. King III9bea32f2018-03-16 16:07:42 -0400162 'econnrefused', # erl
163 'CONNECTION-REFUSED-ERROR', # cl
James E. King III714c77c2018-03-20 19:58:38 -0400164 'connect ENOENT', # nodejs domain socket
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900165 'No such file or directory', # domain socket
James E. King III714c77c2018-03-20 19:58:38 -0400166 'Sockets.TcpClient.Connect', # csharp
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900167 ])),
168 }
169
170 def maybe_false_positive(self):
171 """Searches through log file for socket bind error.
172 Returns True if suspicious expression is found, otherwise False"""
173 try:
174 if self.out and not self.out.closed:
175 self.out.flush()
176 exprs = self._init_failure_exprs[self._prog.kind]
177
178 def match(line):
179 for expr in exprs:
180 if expr.search(line):
James E. King III9bea32f2018-03-16 16:07:42 -0400181 self._log.info("maybe false positive: %s" % line)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900182 return True
183
184 with logfile_open(self.logpath, 'r') as fp:
185 if any(map(match, fp)):
186 return True
187 except (KeyboardInterrupt, SystemExit):
188 raise
189 except Exception as ex:
190 self._log.warn('[%s]: Error while detecting false positive: %s' % (self._test.name, str(ex)))
191 self._log.info(traceback.print_exc())
192 return False
193
194 def _open(self):
195 self.out = logfile_open(self.logpath, 'w+')
196
197 def _close(self):
198 self.out.close()
199
200 def _print_header(self):
201 self._print_date()
202 print('Executing: %s' % str_join(' ', self._prog.command), file=self.out)
203 print('Directory: %s' % self._prog.workdir, file=self.out)
204 print('config:delay: %s' % self._test.delay, file=self.out)
205 print('config:timeout: %s' % self._test.timeout, file=self.out)
206 self._print_bar()
Roger Meier41ad4342015-03-24 22:30:40 +0100207 self.out.flush()
Roger Meier41ad4342015-03-24 22:30:40 +0100208
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900209 def _print_footer(self, returncode=None):
210 self._print_bar()
211 if returncode is not None:
James E. King III9bea32f2018-03-16 16:07:42 -0400212 print('Return code: %d (negative values indicate kill by signal)' % returncode, file=self.out)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900213 else:
214 print('Process is killed.', file=self.out)
215 self._print_exec_time()
216 self._print_date()
Roger Meier41ad4342015-03-24 22:30:40 +0100217
218
219class SummaryReporter(TestReporter):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900220 def __init__(self, basedir, testdir_relative, concurrent=True):
221 super(SummaryReporter, self).__init__()
222 self._basedir = basedir
223 self._testdir_rel = testdir_relative
224 self.logdir = path_join(self.testdir, LOG_DIR)
225 self.out_path = path_join(self.testdir, RESULT_JSON)
226 self.concurrent = concurrent
227 self.out = sys.stdout
228 self._platform = platform.system()
229 self._revision = self._get_revision()
230 self._tests = []
231 if not os.path.exists(self.logdir):
232 os.mkdir(self.logdir)
233 self._known_failures = load_known_failures(self.testdir)
234 self._unexpected_success = []
235 self._flaky_success = []
236 self._unexpected_failure = []
237 self._expected_failure = []
238 self._print_header()
Roger Meier41ad4342015-03-24 22:30:40 +0100239
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900240 @property
241 def testdir(self):
242 return path_join(self._basedir, self._testdir_rel)
Nobuaki Sukegawabd165302016-01-19 11:10:07 +0900243
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900244 def _result_string(self, test):
245 if test.success:
246 if test.retry_count == 0:
247 return 'success'
248 elif test.retry_count == 1:
249 return 'flaky(1 retry)'
250 else:
251 return 'flaky(%d retries)' % test.retry_count
252 elif test.expired:
253 return 'failure(timeout)'
Roger Meier41ad4342015-03-24 22:30:40 +0100254 else:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900255 return 'failure(%d)' % test.returncode
256
257 def _get_revision(self):
258 p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'],
259 cwd=self.testdir, stdout=subprocess.PIPE)
260 out, _ = p.communicate()
261 return out.strip()
262
263 def _format_test(self, test, with_result=True):
264 name = '%s-%s' % (test.server.name, test.client.name)
265 trans = '%s-%s' % (test.transport, test.socket)
266 if not with_result:
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100267 return '{:24s}{:18s}{:25s}'.format(name[:23], test.protocol[:17], trans[:24])
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900268 else:
James E. King III9bea32f2018-03-16 16:07:42 -0400269 return '{:24s}{:18s}{:25s}{:s}\n'.format(name[:23], test.protocol[:17],
270 trans[:24], self._result_string(test))
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900271
272 def _print_test_header(self):
273 self._print_bar()
274 print(
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100275 '{:24s}{:18s}{:25s}{:s}'.format('server-client:', 'protocol:', 'transport:', 'result:'),
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900276 file=self.out)
277
278 def _print_header(self):
279 self._start()
280 print('Apache Thrift - Integration Test Suite', file=self.out)
281 self._print_date()
282 self._print_test_header()
283
284 def _print_unexpected_failure(self):
285 if len(self._unexpected_failure) > 0:
286 self.out.writelines([
287 '*** Following %d failures were unexpected ***:\n' % len(self._unexpected_failure),
288 'If it is introduced by you, please fix it before submitting the code.\n',
289 # 'If not, please report at https://issues.apache.org/jira/browse/THRIFT\n',
290 ])
291 self._print_test_header()
292 for i in self._unexpected_failure:
293 self.out.write(self._format_test(self._tests[i]))
294 self._print_bar()
295 else:
296 print('No unexpected failures.', file=self.out)
297
298 def _print_flaky_success(self):
299 if len(self._flaky_success) > 0:
300 print(
301 'Following %d tests were expected to cleanly succeed but needed retry:' % len(self._flaky_success),
302 file=self.out)
303 self._print_test_header()
304 for i in self._flaky_success:
305 self.out.write(self._format_test(self._tests[i]))
306 self._print_bar()
307
308 def _print_unexpected_success(self):
309 if len(self._unexpected_success) > 0:
310 print(
311 'Following %d tests were known to fail but succeeded (maybe flaky):' % len(self._unexpected_success),
312 file=self.out)
313 self._print_test_header()
314 for i in self._unexpected_success:
315 self.out.write(self._format_test(self._tests[i]))
316 self._print_bar()
317
318 def _http_server_command(self, port):
Alexandre Detisted0787212024-12-26 02:44:13 +0100319 return 'python -m http.server %d' % port
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900320
321 def _print_footer(self):
322 fail_count = len(self._expected_failure) + len(self._unexpected_failure)
323 self._print_bar()
324 self._print_unexpected_success()
325 self._print_flaky_success()
326 self._print_unexpected_failure()
327 self._write_html_data()
328 self._assemble_log('unexpected failures', self._unexpected_failure)
329 self._assemble_log('known failures', self._expected_failure)
330 self.out.writelines([
331 'You can browse results at:\n',
332 '\tfile://%s/%s\n' % (self.testdir, RESULT_HTML),
333 '# If you use Chrome, run:\n',
334 '# \tcd %s\n#\t%s\n' % (self._basedir, self._http_server_command(8001)),
335 '# then browse:\n',
336 '# \thttp://localhost:%d/%s/\n' % (8001, self._testdir_rel),
337 'Full log for each test is here:\n',
James E. King, III375bfee2017-10-26 00:09:34 -0400338 '\ttest/log/server_client_protocol_transport_client.log\n',
339 '\ttest/log/server_client_protocol_transport_server.log\n',
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900340 '%d failed of %d tests in total.\n' % (fail_count, len(self._tests)),
341 ])
342 self._print_exec_time()
343 self._print_date()
344
345 def _render_result(self, test):
346 return [
347 test.server.name,
348 test.client.name,
349 test.protocol,
350 test.transport,
351 test.socket,
352 test.success,
353 test.as_expected,
354 test.returncode,
355 {
356 'server': self.test_logfile(test.name, test.server.kind),
357 'client': self.test_logfile(test.name, test.client.kind),
358 },
359 ]
360
361 def _write_html_data(self):
362 """Writes JSON data to be read by result html"""
363 results = [self._render_result(r) for r in self._tests]
364 with logfile_open(self.out_path, 'w+') as fp:
365 fp.write(json.dumps({
366 'date': self._format_date(),
367 'revision': str(self._revision),
368 'platform': self._platform,
369 'duration': '{:.1f}'.format(self._elapsed),
370 'results': results,
371 }, indent=2))
372
373 def _assemble_log(self, title, indexes):
374 if len(indexes) > 0:
375 def add_prog_log(fp, test, prog_kind):
376 print('*************************** %s message ***************************' % prog_kind,
377 file=fp)
378 path = self.test_logfile(test.name, prog_kind, self.testdir)
379 if os.path.exists(path):
380 with logfile_open(path, 'r') as prog_fp:
381 print(prog_fp.read(), file=fp)
382 filename = title.replace(' ', '_') + '.log'
383 with logfile_open(os.path.join(self.logdir, filename), 'w+') as fp:
384 for test in map(self._tests.__getitem__, indexes):
385 fp.write('TEST: [%s]\n' % test.name)
386 add_prog_log(fp, test, test.server.kind)
387 add_prog_log(fp, test, test.client.kind)
388 fp.write('**********************************************************************\n\n')
389 print('%s are logged to %s/%s/%s' % (title.capitalize(), self._testdir_rel, LOG_DIR, filename))
390
391 def end(self):
392 self._print_footer()
393 return len(self._unexpected_failure) == 0
394
395 def add_test(self, test_dict):
396 test = TestEntry(self.testdir, **test_dict)
397 self._lock.acquire()
398 try:
399 if not self.concurrent:
400 self.out.write(self._format_test(test, False))
401 self.out.flush()
402 self._tests.append(test)
403 return len(self._tests) - 1
404 finally:
405 self._lock.release()
406
407 def add_result(self, index, returncode, expired, retry_count):
408 self._lock.acquire()
409 try:
410 failed = returncode is None or returncode != 0
411 flaky = not failed and retry_count != 0
412 test = self._tests[index]
413 known = test.name in self._known_failures
414 if failed:
415 if known:
416 self._log.debug('%s failed as expected' % test.name)
417 self._expected_failure.append(index)
418 else:
419 self._log.info('unexpected failure: %s' % test.name)
420 self._unexpected_failure.append(index)
421 elif flaky and not known:
422 self._log.info('unexpected flaky success: %s' % test.name)
423 self._flaky_success.append(index)
424 elif not flaky and known:
425 self._log.info('unexpected success: %s' % test.name)
426 self._unexpected_success.append(index)
427 test.success = not failed
428 test.returncode = returncode
429 test.retry_count = retry_count
430 test.expired = expired
431 test.as_expected = known == failed
432 if not self.concurrent:
433 self.out.write(self._result_string(test) + '\n')
434 else:
435 self.out.write(self._format_test(test))
436 finally:
437 self._lock.release()