blob: 68cfccafab98728c6aa7b937fc2ded04426c1f5e [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 Sukegawa2de27002015-11-22 01:13:48 +090031from .test import TestEntry
Roger Meier41ad4342015-03-24 22:30:40 +010032
33LOG_DIR = 'log'
Nobuaki Sukegawabd165302016-01-19 11:10:07 +090034RESULT_HTML = 'index.html'
Roger Meier41ad4342015-03-24 22:30:40 +010035RESULT_JSON = 'results.json'
36FAIL_JSON = 'known_failures_%s.json'
37
38
Alexandre Detiste24df0a52025-01-16 00:39:20 +010039def logfile_open(*args):
40 return open(*args, errors='replace')
41
42
Roger Meier41ad4342015-03-24 22:30:40 +010043def generate_known_failures(testdir, overwrite, save, out):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090044 def collect_failures(results):
45 success_index = 5
46 for r in results:
47 if not r[success_index]:
48 yield TestEntry.get_name(*r)
49 try:
Alexandre Detiste24df0a52025-01-16 00:39:20 +010050 with logfile_open(os.path.join(testdir, RESULT_JSON), 'r') as fp:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090051 results = json.load(fp)
52 except IOError:
53 sys.stderr.write('Unable to load last result. Did you run tests ?\n')
54 return False
55 fails = collect_failures(results['results'])
56 if not overwrite:
57 known = load_known_failures(testdir)
58 known.extend(fails)
59 fails = known
60 fails_json = json.dumps(sorted(set(fails)), indent=2, separators=(',', ': '))
61 if save:
62 with logfile_open(os.path.join(testdir, FAIL_JSON % platform.system()), 'w+') as fp:
63 fp.write(fails_json)
64 sys.stdout.write('Successfully updated known failures.\n')
65 if out:
66 sys.stdout.write(fails_json)
67 sys.stdout.write('\n')
68 return True
Roger Meier41ad4342015-03-24 22:30:40 +010069
70
71def load_known_failures(testdir):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090072 try:
Alexandre Detiste24df0a52025-01-16 00:39:20 +010073 with logfile_open(os.path.join(testdir, FAIL_JSON % platform.system()), 'r') as fp:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090074 return json.load(fp)
75 except IOError:
76 return []
Roger Meier41ad4342015-03-24 22:30:40 +010077
78
79class TestReporter(object):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090080 # Unfortunately, standard library doesn't handle timezone well
81 # DATETIME_FORMAT = '%a %b %d %H:%M:%S %Z %Y'
82 DATETIME_FORMAT = '%a %b %d %H:%M:%S %Y'
Roger Meier41ad4342015-03-24 22:30:40 +010083
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090084 def __init__(self):
85 self._log = multiprocessing.get_logger()
86 self._lock = multiprocessing.Lock()
Roger Meier41ad4342015-03-24 22:30:40 +010087
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090088 @classmethod
89 def test_logfile(cls, test_name, prog_kind, dir=None):
Alexandre Detiste24df0a52025-01-16 00:39:20 +010090 relpath = os.path.join('log', '%s_%s.log' % (test_name, prog_kind))
91 return relpath if not dir else os.path.realpath(os.path.join(dir, relpath))
Roger Meier41ad4342015-03-24 22:30:40 +010092
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090093 def _start(self):
94 self._start_time = time.time()
Roger Meier41ad4342015-03-24 22:30:40 +010095
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090096 @property
97 def _elapsed(self):
98 return time.time() - self._start_time
Roger Meier41ad4342015-03-24 22:30:40 +010099
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900100 @classmethod
101 def _format_date(cls):
102 return '%s' % datetime.datetime.now().strftime(cls.DATETIME_FORMAT)
Roger Meier41ad4342015-03-24 22:30:40 +0100103
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900104 def _print_date(self):
105 print(self._format_date(), file=self.out)
Roger Meier41ad4342015-03-24 22:30:40 +0100106
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900107 def _print_bar(self, out=None):
108 print(
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100109 '===============================================================================',
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900110 file=(out or self.out))
Roger Meier41ad4342015-03-24 22:30:40 +0100111
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900112 def _print_exec_time(self):
113 print('Test execution took {:.1f} seconds.'.format(self._elapsed), file=self.out)
Roger Meier41ad4342015-03-24 22:30:40 +0100114
115
116class ExecReporter(TestReporter):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900117 def __init__(self, testdir, test, prog):
118 super(ExecReporter, self).__init__()
119 self._test = test
120 self._prog = prog
121 self.logpath = self.test_logfile(test.name, prog.kind, testdir)
Roger Meier41ad4342015-03-24 22:30:40 +0100122 self.out = None
Roger Meier41ad4342015-03-24 22:30:40 +0100123
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900124 def begin(self):
125 self._start()
126 self._open()
127 if self.out and not self.out.closed:
128 self._print_header()
129 else:
130 self._log.debug('Output stream is not available.')
Nobuaki Sukegawa2ba79442016-01-12 19:37:55 +0900131
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900132 def end(self, returncode):
133 self._lock.acquire()
134 try:
135 if self.out and not self.out.closed:
136 self._print_footer(returncode)
137 self._close()
138 self.out = None
139 else:
140 self._log.debug('Output stream is not available.')
141 finally:
142 self._lock.release()
Roger Meier41ad4342015-03-24 22:30:40 +0100143
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900144 def killed(self):
145 print(file=self.out)
146 print('Server process is successfully killed.', file=self.out)
147 self.end(None)
Roger Meier41ad4342015-03-24 22:30:40 +0100148
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900149 def died(self):
150 print(file=self.out)
151 print('*** Server process has died unexpectedly ***', file=self.out)
152 self.end(None)
153
154 _init_failure_exprs = {
155 'server': list(map(re.compile, [
156 '[Aa]ddress already in use',
157 'Could not bind',
158 'EADDRINUSE',
159 ])),
160 'client': list(map(re.compile, [
161 '[Cc]onnection refused',
James E. King III9bea32f2018-03-16 16:07:42 -0400162 'Could not connect to',
James E. King III9aaf2952018-03-20 15:06:08 -0400163 'Could not open UNIX ', # domain socket (rb)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900164 'ECONNREFUSED',
James E. King III9bea32f2018-03-16 16:07:42 -0400165 'econnrefused', # erl
166 'CONNECTION-REFUSED-ERROR', # cl
James E. King III714c77c2018-03-20 19:58:38 -0400167 'connect ENOENT', # nodejs domain socket
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900168 'No such file or directory', # domain socket
James E. King III714c77c2018-03-20 19:58:38 -0400169 'Sockets.TcpClient.Connect', # csharp
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900170 ])),
171 }
172
173 def maybe_false_positive(self):
174 """Searches through log file for socket bind error.
175 Returns True if suspicious expression is found, otherwise False"""
176 try:
177 if self.out and not self.out.closed:
178 self.out.flush()
179 exprs = self._init_failure_exprs[self._prog.kind]
180
181 def match(line):
182 for expr in exprs:
183 if expr.search(line):
James E. King III9bea32f2018-03-16 16:07:42 -0400184 self._log.info("maybe false positive: %s" % line)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900185 return True
186
187 with logfile_open(self.logpath, 'r') as fp:
188 if any(map(match, fp)):
189 return True
190 except (KeyboardInterrupt, SystemExit):
191 raise
192 except Exception as ex:
193 self._log.warn('[%s]: Error while detecting false positive: %s' % (self._test.name, str(ex)))
194 self._log.info(traceback.print_exc())
195 return False
196
197 def _open(self):
198 self.out = logfile_open(self.logpath, 'w+')
199
200 def _close(self):
201 self.out.close()
202
203 def _print_header(self):
204 self._print_date()
Alexandre Detiste24df0a52025-01-16 00:39:20 +0100205 print('Executing: %s' % ' '.join(self._prog.command), file=self.out)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900206 print('Directory: %s' % self._prog.workdir, file=self.out)
207 print('config:delay: %s' % self._test.delay, file=self.out)
208 print('config:timeout: %s' % self._test.timeout, file=self.out)
209 self._print_bar()
Roger Meier41ad4342015-03-24 22:30:40 +0100210 self.out.flush()
Roger Meier41ad4342015-03-24 22:30:40 +0100211
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900212 def _print_footer(self, returncode=None):
213 self._print_bar()
214 if returncode is not None:
James E. King III9bea32f2018-03-16 16:07:42 -0400215 print('Return code: %d (negative values indicate kill by signal)' % returncode, file=self.out)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900216 else:
217 print('Process is killed.', file=self.out)
218 self._print_exec_time()
219 self._print_date()
Roger Meier41ad4342015-03-24 22:30:40 +0100220
221
222class SummaryReporter(TestReporter):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900223 def __init__(self, basedir, testdir_relative, concurrent=True):
224 super(SummaryReporter, self).__init__()
225 self._basedir = basedir
226 self._testdir_rel = testdir_relative
Alexandre Detiste24df0a52025-01-16 00:39:20 +0100227 self.logdir = os.path.join(self.testdir, LOG_DIR)
228 self.out_path = os.path.join(self.testdir, RESULT_JSON)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900229 self.concurrent = concurrent
230 self.out = sys.stdout
231 self._platform = platform.system()
232 self._revision = self._get_revision()
233 self._tests = []
234 if not os.path.exists(self.logdir):
235 os.mkdir(self.logdir)
236 self._known_failures = load_known_failures(self.testdir)
237 self._unexpected_success = []
238 self._flaky_success = []
239 self._unexpected_failure = []
240 self._expected_failure = []
241 self._print_header()
Roger Meier41ad4342015-03-24 22:30:40 +0100242
Carel Combrink5abe53f2025-11-13 06:16:41 +0100243 def __getstate__(self):
244 """Prepare object for pickling - remove unpicklable file handle (Since Python 3.14)"""
245 state = self.__dict__.copy()
246 # Remove the unpicklable file handle
247 state['out'] = None
248 return state
249
250 def __setstate__(self, state):
251 """Restore object after unpickling - restore stdout"""
252 self.__dict__.update(state)
253 # Restore stdout (since that's what it was initialized to)
254 self.out = sys.stdout
255
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900256 @property
257 def testdir(self):
Alexandre Detiste24df0a52025-01-16 00:39:20 +0100258 return os.path.join(self._basedir, self._testdir_rel)
Nobuaki Sukegawabd165302016-01-19 11:10:07 +0900259
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900260 def _result_string(self, test):
261 if test.success:
262 if test.retry_count == 0:
263 return 'success'
264 elif test.retry_count == 1:
265 return 'flaky(1 retry)'
266 else:
267 return 'flaky(%d retries)' % test.retry_count
268 elif test.expired:
269 return 'failure(timeout)'
Roger Meier41ad4342015-03-24 22:30:40 +0100270 else:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900271 return 'failure(%d)' % test.returncode
272
273 def _get_revision(self):
274 p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'],
275 cwd=self.testdir, stdout=subprocess.PIPE)
276 out, _ = p.communicate()
277 return out.strip()
278
279 def _format_test(self, test, with_result=True):
280 name = '%s-%s' % (test.server.name, test.client.name)
281 trans = '%s-%s' % (test.transport, test.socket)
282 if not with_result:
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100283 return '{:24s}{:18s}{:25s}'.format(name[:23], test.protocol[:17], trans[:24])
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900284 else:
James E. King III9bea32f2018-03-16 16:07:42 -0400285 return '{:24s}{:18s}{:25s}{:s}\n'.format(name[:23], test.protocol[:17],
286 trans[:24], self._result_string(test))
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900287
288 def _print_test_header(self):
289 self._print_bar()
290 print(
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100291 '{:24s}{:18s}{:25s}{:s}'.format('server-client:', 'protocol:', 'transport:', 'result:'),
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900292 file=self.out)
293
294 def _print_header(self):
295 self._start()
296 print('Apache Thrift - Integration Test Suite', file=self.out)
297 self._print_date()
298 self._print_test_header()
299
300 def _print_unexpected_failure(self):
301 if len(self._unexpected_failure) > 0:
302 self.out.writelines([
303 '*** Following %d failures were unexpected ***:\n' % len(self._unexpected_failure),
304 'If it is introduced by you, please fix it before submitting the code.\n',
305 # 'If not, please report at https://issues.apache.org/jira/browse/THRIFT\n',
306 ])
307 self._print_test_header()
308 for i in self._unexpected_failure:
309 self.out.write(self._format_test(self._tests[i]))
310 self._print_bar()
311 else:
312 print('No unexpected failures.', file=self.out)
313
314 def _print_flaky_success(self):
315 if len(self._flaky_success) > 0:
316 print(
317 'Following %d tests were expected to cleanly succeed but needed retry:' % len(self._flaky_success),
318 file=self.out)
319 self._print_test_header()
320 for i in self._flaky_success:
321 self.out.write(self._format_test(self._tests[i]))
322 self._print_bar()
323
324 def _print_unexpected_success(self):
325 if len(self._unexpected_success) > 0:
326 print(
327 'Following %d tests were known to fail but succeeded (maybe flaky):' % len(self._unexpected_success),
328 file=self.out)
329 self._print_test_header()
330 for i in self._unexpected_success:
331 self.out.write(self._format_test(self._tests[i]))
332 self._print_bar()
333
334 def _http_server_command(self, port):
Alexandre Detisted0787212024-12-26 02:44:13 +0100335 return 'python -m http.server %d' % port
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900336
337 def _print_footer(self):
338 fail_count = len(self._expected_failure) + len(self._unexpected_failure)
339 self._print_bar()
340 self._print_unexpected_success()
341 self._print_flaky_success()
342 self._print_unexpected_failure()
343 self._write_html_data()
344 self._assemble_log('unexpected failures', self._unexpected_failure)
345 self._assemble_log('known failures', self._expected_failure)
346 self.out.writelines([
347 'You can browse results at:\n',
348 '\tfile://%s/%s\n' % (self.testdir, RESULT_HTML),
349 '# If you use Chrome, run:\n',
350 '# \tcd %s\n#\t%s\n' % (self._basedir, self._http_server_command(8001)),
351 '# then browse:\n',
352 '# \thttp://localhost:%d/%s/\n' % (8001, self._testdir_rel),
353 'Full log for each test is here:\n',
James E. King, III375bfee2017-10-26 00:09:34 -0400354 '\ttest/log/server_client_protocol_transport_client.log\n',
355 '\ttest/log/server_client_protocol_transport_server.log\n',
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900356 '%d failed of %d tests in total.\n' % (fail_count, len(self._tests)),
357 ])
358 self._print_exec_time()
359 self._print_date()
360
361 def _render_result(self, test):
362 return [
363 test.server.name,
364 test.client.name,
365 test.protocol,
366 test.transport,
367 test.socket,
368 test.success,
369 test.as_expected,
370 test.returncode,
371 {
372 'server': self.test_logfile(test.name, test.server.kind),
373 'client': self.test_logfile(test.name, test.client.kind),
374 },
375 ]
376
377 def _write_html_data(self):
378 """Writes JSON data to be read by result html"""
379 results = [self._render_result(r) for r in self._tests]
380 with logfile_open(self.out_path, 'w+') as fp:
381 fp.write(json.dumps({
382 'date': self._format_date(),
383 'revision': str(self._revision),
384 'platform': self._platform,
385 'duration': '{:.1f}'.format(self._elapsed),
386 'results': results,
387 }, indent=2))
388
389 def _assemble_log(self, title, indexes):
390 if len(indexes) > 0:
391 def add_prog_log(fp, test, prog_kind):
392 print('*************************** %s message ***************************' % prog_kind,
393 file=fp)
394 path = self.test_logfile(test.name, prog_kind, self.testdir)
395 if os.path.exists(path):
396 with logfile_open(path, 'r') as prog_fp:
397 print(prog_fp.read(), file=fp)
398 filename = title.replace(' ', '_') + '.log'
399 with logfile_open(os.path.join(self.logdir, filename), 'w+') as fp:
400 for test in map(self._tests.__getitem__, indexes):
401 fp.write('TEST: [%s]\n' % test.name)
402 add_prog_log(fp, test, test.server.kind)
403 add_prog_log(fp, test, test.client.kind)
404 fp.write('**********************************************************************\n\n')
405 print('%s are logged to %s/%s/%s' % (title.capitalize(), self._testdir_rel, LOG_DIR, filename))
406
407 def end(self):
408 self._print_footer()
409 return len(self._unexpected_failure) == 0
410
411 def add_test(self, test_dict):
412 test = TestEntry(self.testdir, **test_dict)
413 self._lock.acquire()
414 try:
415 if not self.concurrent:
416 self.out.write(self._format_test(test, False))
417 self.out.flush()
418 self._tests.append(test)
419 return len(self._tests) - 1
420 finally:
421 self._lock.release()
422
423 def add_result(self, index, returncode, expired, retry_count):
424 self._lock.acquire()
425 try:
426 failed = returncode is None or returncode != 0
427 flaky = not failed and retry_count != 0
428 test = self._tests[index]
429 known = test.name in self._known_failures
430 if failed:
431 if known:
432 self._log.debug('%s failed as expected' % test.name)
433 self._expected_failure.append(index)
434 else:
435 self._log.info('unexpected failure: %s' % test.name)
436 self._unexpected_failure.append(index)
437 elif flaky and not known:
438 self._log.info('unexpected flaky success: %s' % test.name)
439 self._flaky_success.append(index)
440 elif not flaky and known:
441 self._log.info('unexpected success: %s' % test.name)
442 self._unexpected_success.append(index)
443 test.success = not failed
444 test.returncode = returncode
445 test.retry_count = retry_count
446 test.expired = expired
447 test.as_expected = known == failed
448 if not self.concurrent:
449 self.out.write(self._result_string(test) + '\n')
450 else:
451 self.out.write(self._format_test(test))
452 finally:
453 self._lock.release()