blob: 587822201eb9ed20390f740094db4fabfa754831 [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
Nobuaki Sukegawaa6ab1f52015-11-28 15:04:39 +090020from __future__ import print_function
Roger Meier41ad4342015-03-24 22:30:40 +010021import datetime
22import json
23import multiprocessing
24import os
25import platform
26import re
27import subprocess
28import sys
29import time
30import traceback
31
Nobuaki Sukegawaa6ab1f52015-11-28 15:04:39 +090032from .compat import logfile_open, path_join, str_join
Nobuaki Sukegawa2de27002015-11-22 01:13:48 +090033from .test import TestEntry
Roger Meier41ad4342015-03-24 22:30:40 +010034
35LOG_DIR = 'log'
Nobuaki Sukegawabd165302016-01-19 11:10:07 +090036RESULT_HTML = 'index.html'
Roger Meier41ad4342015-03-24 22:30:40 +010037RESULT_JSON = 'results.json'
38FAIL_JSON = 'known_failures_%s.json'
39
40
41def generate_known_failures(testdir, overwrite, save, out):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090042 def collect_failures(results):
43 success_index = 5
44 for r in results:
45 if not r[success_index]:
46 yield TestEntry.get_name(*r)
47 try:
48 with logfile_open(path_join(testdir, RESULT_JSON), 'r') as fp:
49 results = json.load(fp)
50 except IOError:
51 sys.stderr.write('Unable to load last result. Did you run tests ?\n')
52 return False
53 fails = collect_failures(results['results'])
54 if not overwrite:
55 known = load_known_failures(testdir)
56 known.extend(fails)
57 fails = known
58 fails_json = json.dumps(sorted(set(fails)), indent=2, separators=(',', ': '))
59 if save:
60 with logfile_open(os.path.join(testdir, FAIL_JSON % platform.system()), 'w+') as fp:
61 fp.write(fails_json)
62 sys.stdout.write('Successfully updated known failures.\n')
63 if out:
64 sys.stdout.write(fails_json)
65 sys.stdout.write('\n')
66 return True
Roger Meier41ad4342015-03-24 22:30:40 +010067
68
69def load_known_failures(testdir):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090070 try:
71 with logfile_open(path_join(testdir, FAIL_JSON % platform.system()), 'r') as fp:
72 return json.load(fp)
73 except IOError:
74 return []
Roger Meier41ad4342015-03-24 22:30:40 +010075
76
77class TestReporter(object):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090078 # Unfortunately, standard library doesn't handle timezone well
79 # DATETIME_FORMAT = '%a %b %d %H:%M:%S %Z %Y'
80 DATETIME_FORMAT = '%a %b %d %H:%M:%S %Y'
Roger Meier41ad4342015-03-24 22:30:40 +010081
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090082 def __init__(self):
83 self._log = multiprocessing.get_logger()
84 self._lock = multiprocessing.Lock()
Roger Meier41ad4342015-03-24 22:30:40 +010085
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090086 @classmethod
87 def test_logfile(cls, test_name, prog_kind, dir=None):
88 relpath = path_join('log', '%s_%s.log' % (test_name, prog_kind))
89 return relpath if not dir else os.path.realpath(path_join(dir, relpath))
Roger Meier41ad4342015-03-24 22:30:40 +010090
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090091 def _start(self):
92 self._start_time = time.time()
Roger Meier41ad4342015-03-24 22:30:40 +010093
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090094 @property
95 def _elapsed(self):
96 return time.time() - self._start_time
Roger Meier41ad4342015-03-24 22:30:40 +010097
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090098 @classmethod
99 def _format_date(cls):
100 return '%s' % datetime.datetime.now().strftime(cls.DATETIME_FORMAT)
Roger Meier41ad4342015-03-24 22:30:40 +0100101
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900102 def _print_date(self):
103 print(self._format_date(), file=self.out)
Roger Meier41ad4342015-03-24 22:30:40 +0100104
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900105 def _print_bar(self, out=None):
106 print(
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100107 '===============================================================================',
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900108 file=(out or self.out))
Roger Meier41ad4342015-03-24 22:30:40 +0100109
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900110 def _print_exec_time(self):
111 print('Test execution took {:.1f} seconds.'.format(self._elapsed), file=self.out)
Roger Meier41ad4342015-03-24 22:30:40 +0100112
113
114class ExecReporter(TestReporter):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900115 def __init__(self, testdir, test, prog):
116 super(ExecReporter, self).__init__()
117 self._test = test
118 self._prog = prog
119 self.logpath = self.test_logfile(test.name, prog.kind, testdir)
Roger Meier41ad4342015-03-24 22:30:40 +0100120 self.out = None
Roger Meier41ad4342015-03-24 22:30:40 +0100121
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900122 def begin(self):
123 self._start()
124 self._open()
125 if self.out and not self.out.closed:
126 self._print_header()
127 else:
128 self._log.debug('Output stream is not available.')
Nobuaki Sukegawa2ba79442016-01-12 19:37:55 +0900129
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900130 def end(self, returncode):
131 self._lock.acquire()
132 try:
133 if self.out and not self.out.closed:
134 self._print_footer(returncode)
135 self._close()
136 self.out = None
137 else:
138 self._log.debug('Output stream is not available.')
139 finally:
140 self._lock.release()
Roger Meier41ad4342015-03-24 22:30:40 +0100141
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900142 def killed(self):
143 print(file=self.out)
144 print('Server process is successfully killed.', file=self.out)
145 self.end(None)
Roger Meier41ad4342015-03-24 22:30:40 +0100146
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900147 def died(self):
148 print(file=self.out)
149 print('*** Server process has died unexpectedly ***', file=self.out)
150 self.end(None)
151
152 _init_failure_exprs = {
153 'server': list(map(re.compile, [
154 '[Aa]ddress already in use',
155 'Could not bind',
156 'EADDRINUSE',
157 ])),
158 'client': list(map(re.compile, [
159 '[Cc]onnection refused',
James E. King III9bea32f2018-03-16 16:07:42 -0400160 'Could not connect to',
James E. King III9aaf2952018-03-20 15:06:08 -0400161 'Could not open UNIX ', # domain socket (rb)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900162 'ECONNREFUSED',
James E. King III9bea32f2018-03-16 16:07:42 -0400163 'econnrefused', # erl
164 'CONNECTION-REFUSED-ERROR', # cl
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900165 'No such file or directory', # domain socket
166 ])),
167 }
168
169 def maybe_false_positive(self):
170 """Searches through log file for socket bind error.
171 Returns True if suspicious expression is found, otherwise False"""
172 try:
173 if self.out and not self.out.closed:
174 self.out.flush()
175 exprs = self._init_failure_exprs[self._prog.kind]
176
177 def match(line):
178 for expr in exprs:
179 if expr.search(line):
James E. King III9bea32f2018-03-16 16:07:42 -0400180 self._log.info("maybe false positive: %s" % line)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900181 return True
182
183 with logfile_open(self.logpath, 'r') as fp:
184 if any(map(match, fp)):
185 return True
186 except (KeyboardInterrupt, SystemExit):
187 raise
188 except Exception as ex:
189 self._log.warn('[%s]: Error while detecting false positive: %s' % (self._test.name, str(ex)))
190 self._log.info(traceback.print_exc())
191 return False
192
193 def _open(self):
194 self.out = logfile_open(self.logpath, 'w+')
195
196 def _close(self):
197 self.out.close()
198
199 def _print_header(self):
200 self._print_date()
201 print('Executing: %s' % str_join(' ', self._prog.command), file=self.out)
202 print('Directory: %s' % self._prog.workdir, file=self.out)
203 print('config:delay: %s' % self._test.delay, file=self.out)
204 print('config:timeout: %s' % self._test.timeout, file=self.out)
205 self._print_bar()
Roger Meier41ad4342015-03-24 22:30:40 +0100206 self.out.flush()
Roger Meier41ad4342015-03-24 22:30:40 +0100207
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900208 def _print_footer(self, returncode=None):
209 self._print_bar()
210 if returncode is not None:
James E. King III9bea32f2018-03-16 16:07:42 -0400211 print('Return code: %d (negative values indicate kill by signal)' % returncode, file=self.out)
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900212 else:
213 print('Process is killed.', file=self.out)
214 self._print_exec_time()
215 self._print_date()
Roger Meier41ad4342015-03-24 22:30:40 +0100216
217
218class SummaryReporter(TestReporter):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900219 def __init__(self, basedir, testdir_relative, concurrent=True):
220 super(SummaryReporter, self).__init__()
221 self._basedir = basedir
222 self._testdir_rel = testdir_relative
223 self.logdir = path_join(self.testdir, LOG_DIR)
224 self.out_path = path_join(self.testdir, RESULT_JSON)
225 self.concurrent = concurrent
226 self.out = sys.stdout
227 self._platform = platform.system()
228 self._revision = self._get_revision()
229 self._tests = []
230 if not os.path.exists(self.logdir):
231 os.mkdir(self.logdir)
232 self._known_failures = load_known_failures(self.testdir)
233 self._unexpected_success = []
234 self._flaky_success = []
235 self._unexpected_failure = []
236 self._expected_failure = []
237 self._print_header()
Roger Meier41ad4342015-03-24 22:30:40 +0100238
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900239 @property
240 def testdir(self):
241 return path_join(self._basedir, self._testdir_rel)
Nobuaki Sukegawabd165302016-01-19 11:10:07 +0900242
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900243 def _result_string(self, test):
244 if test.success:
245 if test.retry_count == 0:
246 return 'success'
247 elif test.retry_count == 1:
248 return 'flaky(1 retry)'
249 else:
250 return 'flaky(%d retries)' % test.retry_count
251 elif test.expired:
252 return 'failure(timeout)'
Roger Meier41ad4342015-03-24 22:30:40 +0100253 else:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900254 return 'failure(%d)' % test.returncode
255
256 def _get_revision(self):
257 p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'],
258 cwd=self.testdir, stdout=subprocess.PIPE)
259 out, _ = p.communicate()
260 return out.strip()
261
262 def _format_test(self, test, with_result=True):
263 name = '%s-%s' % (test.server.name, test.client.name)
264 trans = '%s-%s' % (test.transport, test.socket)
265 if not with_result:
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100266 return '{:24s}{:18s}{:25s}'.format(name[:23], test.protocol[:17], trans[:24])
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900267 else:
James E. King III9bea32f2018-03-16 16:07:42 -0400268 return '{:24s}{:18s}{:25s}{:s}\n'.format(name[:23], test.protocol[:17],
269 trans[:24], self._result_string(test))
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900270
271 def _print_test_header(self):
272 self._print_bar()
273 print(
Gonzalo Aguilar Delgadobc0082e2016-03-04 13:16:22 +0100274 '{:24s}{:18s}{:25s}{:s}'.format('server-client:', 'protocol:', 'transport:', 'result:'),
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900275 file=self.out)
276
277 def _print_header(self):
278 self._start()
279 print('Apache Thrift - Integration Test Suite', file=self.out)
280 self._print_date()
281 self._print_test_header()
282
283 def _print_unexpected_failure(self):
284 if len(self._unexpected_failure) > 0:
285 self.out.writelines([
286 '*** Following %d failures were unexpected ***:\n' % len(self._unexpected_failure),
287 'If it is introduced by you, please fix it before submitting the code.\n',
288 # 'If not, please report at https://issues.apache.org/jira/browse/THRIFT\n',
289 ])
290 self._print_test_header()
291 for i in self._unexpected_failure:
292 self.out.write(self._format_test(self._tests[i]))
293 self._print_bar()
294 else:
295 print('No unexpected failures.', file=self.out)
296
297 def _print_flaky_success(self):
298 if len(self._flaky_success) > 0:
299 print(
300 'Following %d tests were expected to cleanly succeed but needed retry:' % len(self._flaky_success),
301 file=self.out)
302 self._print_test_header()
303 for i in self._flaky_success:
304 self.out.write(self._format_test(self._tests[i]))
305 self._print_bar()
306
307 def _print_unexpected_success(self):
308 if len(self._unexpected_success) > 0:
309 print(
310 'Following %d tests were known to fail but succeeded (maybe flaky):' % len(self._unexpected_success),
311 file=self.out)
312 self._print_test_header()
313 for i in self._unexpected_success:
314 self.out.write(self._format_test(self._tests[i]))
315 self._print_bar()
316
317 def _http_server_command(self, port):
318 if sys.version_info[0] < 3:
319 return 'python -m SimpleHTTPServer %d' % port
320 else:
321 return 'python -m http.server %d' % port
322
323 def _print_footer(self):
324 fail_count = len(self._expected_failure) + len(self._unexpected_failure)
325 self._print_bar()
326 self._print_unexpected_success()
327 self._print_flaky_success()
328 self._print_unexpected_failure()
329 self._write_html_data()
330 self._assemble_log('unexpected failures', self._unexpected_failure)
331 self._assemble_log('known failures', self._expected_failure)
332 self.out.writelines([
333 'You can browse results at:\n',
334 '\tfile://%s/%s\n' % (self.testdir, RESULT_HTML),
335 '# If you use Chrome, run:\n',
336 '# \tcd %s\n#\t%s\n' % (self._basedir, self._http_server_command(8001)),
337 '# then browse:\n',
338 '# \thttp://localhost:%d/%s/\n' % (8001, self._testdir_rel),
339 'Full log for each test is here:\n',
James E. King, III375bfee2017-10-26 00:09:34 -0400340 '\ttest/log/server_client_protocol_transport_client.log\n',
341 '\ttest/log/server_client_protocol_transport_server.log\n',
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900342 '%d failed of %d tests in total.\n' % (fail_count, len(self._tests)),
343 ])
344 self._print_exec_time()
345 self._print_date()
346
347 def _render_result(self, test):
348 return [
349 test.server.name,
350 test.client.name,
351 test.protocol,
352 test.transport,
353 test.socket,
354 test.success,
355 test.as_expected,
356 test.returncode,
357 {
358 'server': self.test_logfile(test.name, test.server.kind),
359 'client': self.test_logfile(test.name, test.client.kind),
360 },
361 ]
362
363 def _write_html_data(self):
364 """Writes JSON data to be read by result html"""
365 results = [self._render_result(r) for r in self._tests]
366 with logfile_open(self.out_path, 'w+') as fp:
367 fp.write(json.dumps({
368 'date': self._format_date(),
369 'revision': str(self._revision),
370 'platform': self._platform,
371 'duration': '{:.1f}'.format(self._elapsed),
372 'results': results,
373 }, indent=2))
374
375 def _assemble_log(self, title, indexes):
376 if len(indexes) > 0:
377 def add_prog_log(fp, test, prog_kind):
378 print('*************************** %s message ***************************' % prog_kind,
379 file=fp)
380 path = self.test_logfile(test.name, prog_kind, self.testdir)
381 if os.path.exists(path):
382 with logfile_open(path, 'r') as prog_fp:
383 print(prog_fp.read(), file=fp)
384 filename = title.replace(' ', '_') + '.log'
385 with logfile_open(os.path.join(self.logdir, filename), 'w+') as fp:
386 for test in map(self._tests.__getitem__, indexes):
387 fp.write('TEST: [%s]\n' % test.name)
388 add_prog_log(fp, test, test.server.kind)
389 add_prog_log(fp, test, test.client.kind)
390 fp.write('**********************************************************************\n\n')
391 print('%s are logged to %s/%s/%s' % (title.capitalize(), self._testdir_rel, LOG_DIR, filename))
392
393 def end(self):
394 self._print_footer()
395 return len(self._unexpected_failure) == 0
396
397 def add_test(self, test_dict):
398 test = TestEntry(self.testdir, **test_dict)
399 self._lock.acquire()
400 try:
401 if not self.concurrent:
402 self.out.write(self._format_test(test, False))
403 self.out.flush()
404 self._tests.append(test)
405 return len(self._tests) - 1
406 finally:
407 self._lock.release()
408
409 def add_result(self, index, returncode, expired, retry_count):
410 self._lock.acquire()
411 try:
412 failed = returncode is None or returncode != 0
413 flaky = not failed and retry_count != 0
414 test = self._tests[index]
415 known = test.name in self._known_failures
416 if failed:
417 if known:
418 self._log.debug('%s failed as expected' % test.name)
419 self._expected_failure.append(index)
420 else:
421 self._log.info('unexpected failure: %s' % test.name)
422 self._unexpected_failure.append(index)
423 elif flaky and not known:
424 self._log.info('unexpected flaky success: %s' % test.name)
425 self._flaky_success.append(index)
426 elif not flaky and known:
427 self._log.info('unexpected success: %s' % test.name)
428 self._unexpected_success.append(index)
429 test.success = not failed
430 test.returncode = returncode
431 test.retry_count = retry_count
432 test.expired = expired
433 test.as_expected = known == failed
434 if not self.concurrent:
435 self.out.write(self._result_string(test) + '\n')
436 else:
437 self.out.write(self._format_test(test))
438 finally:
439 self._lock.release()