blob: 9231a8b1aced75959dd041ee5be5039f7b2dba17 [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
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900243 @property
244 def testdir(self):
Alexandre Detiste24df0a52025-01-16 00:39:20 +0100245 return os.path.join(self._basedir, self._testdir_rel)
Nobuaki Sukegawabd165302016-01-19 11:10:07 +0900246
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900247 def _result_string(self, test):
248 if test.success:
249 if test.retry_count == 0:
250 return 'success'
251 elif test.retry_count == 1:
252 return 'flaky(1 retry)'
253 else:
254 return 'flaky(%d retries)' % test.retry_count
255 elif test.expired:
256 return 'failure(timeout)'
Roger Meier41ad4342015-03-24 22:30:40 +0100257 else:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900258 return 'failure(%d)' % test.returncode
259
260 def _get_revision(self):
261 p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'],
262 cwd=self.testdir, stdout=subprocess.PIPE)
263 out, _ = p.communicate()
264 return out.strip()
265
266 def _format_test(self, test, with_result=True):
267 name = '%s-%s' % (test.server.name, test.client.name)
268 trans = '%s-%s' % (test.transport, test.socket)
269 if not with_result:
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100270 return '{:24s}{:18s}{:25s}'.format(name[:23], test.protocol[:17], trans[:24])
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900271 else:
James E. King III9bea32f2018-03-16 16:07:42 -0400272 return '{:24s}{:18s}{:25s}{:s}\n'.format(name[:23], test.protocol[:17],
273 trans[:24], self._result_string(test))
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900274
275 def _print_test_header(self):
276 self._print_bar()
277 print(
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100278 '{:24s}{:18s}{:25s}{:s}'.format('server-client:', 'protocol:', 'transport:', 'result:'),
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900279 file=self.out)
280
281 def _print_header(self):
282 self._start()
283 print('Apache Thrift - Integration Test Suite', file=self.out)
284 self._print_date()
285 self._print_test_header()
286
287 def _print_unexpected_failure(self):
288 if len(self._unexpected_failure) > 0:
289 self.out.writelines([
290 '*** Following %d failures were unexpected ***:\n' % len(self._unexpected_failure),
291 'If it is introduced by you, please fix it before submitting the code.\n',
292 # 'If not, please report at https://issues.apache.org/jira/browse/THRIFT\n',
293 ])
294 self._print_test_header()
295 for i in self._unexpected_failure:
296 self.out.write(self._format_test(self._tests[i]))
297 self._print_bar()
298 else:
299 print('No unexpected failures.', file=self.out)
300
301 def _print_flaky_success(self):
302 if len(self._flaky_success) > 0:
303 print(
304 'Following %d tests were expected to cleanly succeed but needed retry:' % len(self._flaky_success),
305 file=self.out)
306 self._print_test_header()
307 for i in self._flaky_success:
308 self.out.write(self._format_test(self._tests[i]))
309 self._print_bar()
310
311 def _print_unexpected_success(self):
312 if len(self._unexpected_success) > 0:
313 print(
314 'Following %d tests were known to fail but succeeded (maybe flaky):' % len(self._unexpected_success),
315 file=self.out)
316 self._print_test_header()
317 for i in self._unexpected_success:
318 self.out.write(self._format_test(self._tests[i]))
319 self._print_bar()
320
321 def _http_server_command(self, port):
Alexandre Detisted0787212024-12-26 02:44:13 +0100322 return 'python -m http.server %d' % port
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900323
324 def _print_footer(self):
325 fail_count = len(self._expected_failure) + len(self._unexpected_failure)
326 self._print_bar()
327 self._print_unexpected_success()
328 self._print_flaky_success()
329 self._print_unexpected_failure()
330 self._write_html_data()
331 self._assemble_log('unexpected failures', self._unexpected_failure)
332 self._assemble_log('known failures', self._expected_failure)
333 self.out.writelines([
334 'You can browse results at:\n',
335 '\tfile://%s/%s\n' % (self.testdir, RESULT_HTML),
336 '# If you use Chrome, run:\n',
337 '# \tcd %s\n#\t%s\n' % (self._basedir, self._http_server_command(8001)),
338 '# then browse:\n',
339 '# \thttp://localhost:%d/%s/\n' % (8001, self._testdir_rel),
340 'Full log for each test is here:\n',
James E. King, III375bfee2017-10-26 00:09:34 -0400341 '\ttest/log/server_client_protocol_transport_client.log\n',
342 '\ttest/log/server_client_protocol_transport_server.log\n',
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900343 '%d failed of %d tests in total.\n' % (fail_count, len(self._tests)),
344 ])
345 self._print_exec_time()
346 self._print_date()
347
348 def _render_result(self, test):
349 return [
350 test.server.name,
351 test.client.name,
352 test.protocol,
353 test.transport,
354 test.socket,
355 test.success,
356 test.as_expected,
357 test.returncode,
358 {
359 'server': self.test_logfile(test.name, test.server.kind),
360 'client': self.test_logfile(test.name, test.client.kind),
361 },
362 ]
363
364 def _write_html_data(self):
365 """Writes JSON data to be read by result html"""
366 results = [self._render_result(r) for r in self._tests]
367 with logfile_open(self.out_path, 'w+') as fp:
368 fp.write(json.dumps({
369 'date': self._format_date(),
370 'revision': str(self._revision),
371 'platform': self._platform,
372 'duration': '{:.1f}'.format(self._elapsed),
373 'results': results,
374 }, indent=2))
375
376 def _assemble_log(self, title, indexes):
377 if len(indexes) > 0:
378 def add_prog_log(fp, test, prog_kind):
379 print('*************************** %s message ***************************' % prog_kind,
380 file=fp)
381 path = self.test_logfile(test.name, prog_kind, self.testdir)
382 if os.path.exists(path):
383 with logfile_open(path, 'r') as prog_fp:
384 print(prog_fp.read(), file=fp)
385 filename = title.replace(' ', '_') + '.log'
386 with logfile_open(os.path.join(self.logdir, filename), 'w+') as fp:
387 for test in map(self._tests.__getitem__, indexes):
388 fp.write('TEST: [%s]\n' % test.name)
389 add_prog_log(fp, test, test.server.kind)
390 add_prog_log(fp, test, test.client.kind)
391 fp.write('**********************************************************************\n\n')
392 print('%s are logged to %s/%s/%s' % (title.capitalize(), self._testdir_rel, LOG_DIR, filename))
393
394 def end(self):
395 self._print_footer()
396 return len(self._unexpected_failure) == 0
397
398 def add_test(self, test_dict):
399 test = TestEntry(self.testdir, **test_dict)
400 self._lock.acquire()
401 try:
402 if not self.concurrent:
403 self.out.write(self._format_test(test, False))
404 self.out.flush()
405 self._tests.append(test)
406 return len(self._tests) - 1
407 finally:
408 self._lock.release()
409
410 def add_result(self, index, returncode, expired, retry_count):
411 self._lock.acquire()
412 try:
413 failed = returncode is None or returncode != 0
414 flaky = not failed and retry_count != 0
415 test = self._tests[index]
416 known = test.name in self._known_failures
417 if failed:
418 if known:
419 self._log.debug('%s failed as expected' % test.name)
420 self._expected_failure.append(index)
421 else:
422 self._log.info('unexpected failure: %s' % test.name)
423 self._unexpected_failure.append(index)
424 elif flaky and not known:
425 self._log.info('unexpected flaky success: %s' % test.name)
426 self._flaky_success.append(index)
427 elif not flaky and known:
428 self._log.info('unexpected success: %s' % test.name)
429 self._unexpected_success.append(index)
430 test.success = not failed
431 test.returncode = returncode
432 test.retry_count = retry_count
433 test.expired = expired
434 test.as_expected = known == failed
435 if not self.concurrent:
436 self.out.write(self._result_string(test) + '\n')
437 else:
438 self.out.write(self._format_test(test))
439 finally:
440 self._lock.release()