blob: cc5f26fe270ebd4c1ca869052c58e72e343e687e [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(
107 '==========================================================================',
108 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',
160 'Could not connect to localhost',
161 'ECONNREFUSED',
162 'No such file or directory', # domain socket
163 ])),
164 }
165
166 def maybe_false_positive(self):
167 """Searches through log file for socket bind error.
168 Returns True if suspicious expression is found, otherwise False"""
169 try:
170 if self.out and not self.out.closed:
171 self.out.flush()
172 exprs = self._init_failure_exprs[self._prog.kind]
173
174 def match(line):
175 for expr in exprs:
176 if expr.search(line):
177 return True
178
179 with logfile_open(self.logpath, 'r') as fp:
180 if any(map(match, fp)):
181 return True
182 except (KeyboardInterrupt, SystemExit):
183 raise
184 except Exception as ex:
185 self._log.warn('[%s]: Error while detecting false positive: %s' % (self._test.name, str(ex)))
186 self._log.info(traceback.print_exc())
187 return False
188
189 def _open(self):
190 self.out = logfile_open(self.logpath, 'w+')
191
192 def _close(self):
193 self.out.close()
194
195 def _print_header(self):
196 self._print_date()
197 print('Executing: %s' % str_join(' ', self._prog.command), file=self.out)
198 print('Directory: %s' % self._prog.workdir, file=self.out)
199 print('config:delay: %s' % self._test.delay, file=self.out)
200 print('config:timeout: %s' % self._test.timeout, file=self.out)
201 self._print_bar()
Roger Meier41ad4342015-03-24 22:30:40 +0100202 self.out.flush()
Roger Meier41ad4342015-03-24 22:30:40 +0100203
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900204 def _print_footer(self, returncode=None):
205 self._print_bar()
206 if returncode is not None:
207 print('Return code: %d' % returncode, file=self.out)
208 else:
209 print('Process is killed.', file=self.out)
210 self._print_exec_time()
211 self._print_date()
Roger Meier41ad4342015-03-24 22:30:40 +0100212
213
214class SummaryReporter(TestReporter):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900215 def __init__(self, basedir, testdir_relative, concurrent=True):
216 super(SummaryReporter, self).__init__()
217 self._basedir = basedir
218 self._testdir_rel = testdir_relative
219 self.logdir = path_join(self.testdir, LOG_DIR)
220 self.out_path = path_join(self.testdir, RESULT_JSON)
221 self.concurrent = concurrent
222 self.out = sys.stdout
223 self._platform = platform.system()
224 self._revision = self._get_revision()
225 self._tests = []
226 if not os.path.exists(self.logdir):
227 os.mkdir(self.logdir)
228 self._known_failures = load_known_failures(self.testdir)
229 self._unexpected_success = []
230 self._flaky_success = []
231 self._unexpected_failure = []
232 self._expected_failure = []
233 self._print_header()
Roger Meier41ad4342015-03-24 22:30:40 +0100234
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900235 @property
236 def testdir(self):
237 return path_join(self._basedir, self._testdir_rel)
Nobuaki Sukegawabd165302016-01-19 11:10:07 +0900238
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900239 def _result_string(self, test):
240 if test.success:
241 if test.retry_count == 0:
242 return 'success'
243 elif test.retry_count == 1:
244 return 'flaky(1 retry)'
245 else:
246 return 'flaky(%d retries)' % test.retry_count
247 elif test.expired:
248 return 'failure(timeout)'
Roger Meier41ad4342015-03-24 22:30:40 +0100249 else:
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900250 return 'failure(%d)' % test.returncode
251
252 def _get_revision(self):
253 p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'],
254 cwd=self.testdir, stdout=subprocess.PIPE)
255 out, _ = p.communicate()
256 return out.strip()
257
258 def _format_test(self, test, with_result=True):
259 name = '%s-%s' % (test.server.name, test.client.name)
260 trans = '%s-%s' % (test.transport, test.socket)
261 if not with_result:
262 return '{:24s}{:13s}{:25s}'.format(name[:23], test.protocol[:12], trans[:24])
263 else:
264 return '{:24s}{:13s}{:25s}{:s}\n'.format(name[:23], test.protocol[:12], trans[:24], self._result_string(test))
265
266 def _print_test_header(self):
267 self._print_bar()
268 print(
269 '{:24s}{:13s}{:25s}{:s}'.format('server-client:', 'protocol:', 'transport:', 'result:'),
270 file=self.out)
271
272 def _print_header(self):
273 self._start()
274 print('Apache Thrift - Integration Test Suite', file=self.out)
275 self._print_date()
276 self._print_test_header()
277
278 def _print_unexpected_failure(self):
279 if len(self._unexpected_failure) > 0:
280 self.out.writelines([
281 '*** Following %d failures were unexpected ***:\n' % len(self._unexpected_failure),
282 'If it is introduced by you, please fix it before submitting the code.\n',
283 # 'If not, please report at https://issues.apache.org/jira/browse/THRIFT\n',
284 ])
285 self._print_test_header()
286 for i in self._unexpected_failure:
287 self.out.write(self._format_test(self._tests[i]))
288 self._print_bar()
289 else:
290 print('No unexpected failures.', file=self.out)
291
292 def _print_flaky_success(self):
293 if len(self._flaky_success) > 0:
294 print(
295 'Following %d tests were expected to cleanly succeed but needed retry:' % len(self._flaky_success),
296 file=self.out)
297 self._print_test_header()
298 for i in self._flaky_success:
299 self.out.write(self._format_test(self._tests[i]))
300 self._print_bar()
301
302 def _print_unexpected_success(self):
303 if len(self._unexpected_success) > 0:
304 print(
305 'Following %d tests were known to fail but succeeded (maybe flaky):' % len(self._unexpected_success),
306 file=self.out)
307 self._print_test_header()
308 for i in self._unexpected_success:
309 self.out.write(self._format_test(self._tests[i]))
310 self._print_bar()
311
312 def _http_server_command(self, port):
313 if sys.version_info[0] < 3:
314 return 'python -m SimpleHTTPServer %d' % port
315 else:
316 return 'python -m http.server %d' % port
317
318 def _print_footer(self):
319 fail_count = len(self._expected_failure) + len(self._unexpected_failure)
320 self._print_bar()
321 self._print_unexpected_success()
322 self._print_flaky_success()
323 self._print_unexpected_failure()
324 self._write_html_data()
325 self._assemble_log('unexpected failures', self._unexpected_failure)
326 self._assemble_log('known failures', self._expected_failure)
327 self.out.writelines([
328 'You can browse results at:\n',
329 '\tfile://%s/%s\n' % (self.testdir, RESULT_HTML),
330 '# If you use Chrome, run:\n',
331 '# \tcd %s\n#\t%s\n' % (self._basedir, self._http_server_command(8001)),
332 '# then browse:\n',
333 '# \thttp://localhost:%d/%s/\n' % (8001, self._testdir_rel),
334 'Full log for each test is here:\n',
335 '\ttest/log/client_server_protocol_transport_client.log\n',
336 '\ttest/log/client_server_protocol_transport_server.log\n',
337 '%d failed of %d tests in total.\n' % (fail_count, len(self._tests)),
338 ])
339 self._print_exec_time()
340 self._print_date()
341
342 def _render_result(self, test):
343 return [
344 test.server.name,
345 test.client.name,
346 test.protocol,
347 test.transport,
348 test.socket,
349 test.success,
350 test.as_expected,
351 test.returncode,
352 {
353 'server': self.test_logfile(test.name, test.server.kind),
354 'client': self.test_logfile(test.name, test.client.kind),
355 },
356 ]
357
358 def _write_html_data(self):
359 """Writes JSON data to be read by result html"""
360 results = [self._render_result(r) for r in self._tests]
361 with logfile_open(self.out_path, 'w+') as fp:
362 fp.write(json.dumps({
363 'date': self._format_date(),
364 'revision': str(self._revision),
365 'platform': self._platform,
366 'duration': '{:.1f}'.format(self._elapsed),
367 'results': results,
368 }, indent=2))
369
370 def _assemble_log(self, title, indexes):
371 if len(indexes) > 0:
372 def add_prog_log(fp, test, prog_kind):
373 print('*************************** %s message ***************************' % prog_kind,
374 file=fp)
375 path = self.test_logfile(test.name, prog_kind, self.testdir)
376 if os.path.exists(path):
377 with logfile_open(path, 'r') as prog_fp:
378 print(prog_fp.read(), file=fp)
379 filename = title.replace(' ', '_') + '.log'
380 with logfile_open(os.path.join(self.logdir, filename), 'w+') as fp:
381 for test in map(self._tests.__getitem__, indexes):
382 fp.write('TEST: [%s]\n' % test.name)
383 add_prog_log(fp, test, test.server.kind)
384 add_prog_log(fp, test, test.client.kind)
385 fp.write('**********************************************************************\n\n')
386 print('%s are logged to %s/%s/%s' % (title.capitalize(), self._testdir_rel, LOG_DIR, filename))
387
388 def end(self):
389 self._print_footer()
390 return len(self._unexpected_failure) == 0
391
392 def add_test(self, test_dict):
393 test = TestEntry(self.testdir, **test_dict)
394 self._lock.acquire()
395 try:
396 if not self.concurrent:
397 self.out.write(self._format_test(test, False))
398 self.out.flush()
399 self._tests.append(test)
400 return len(self._tests) - 1
401 finally:
402 self._lock.release()
403
404 def add_result(self, index, returncode, expired, retry_count):
405 self._lock.acquire()
406 try:
407 failed = returncode is None or returncode != 0
408 flaky = not failed and retry_count != 0
409 test = self._tests[index]
410 known = test.name in self._known_failures
411 if failed:
412 if known:
413 self._log.debug('%s failed as expected' % test.name)
414 self._expected_failure.append(index)
415 else:
416 self._log.info('unexpected failure: %s' % test.name)
417 self._unexpected_failure.append(index)
418 elif flaky and not known:
419 self._log.info('unexpected flaky success: %s' % test.name)
420 self._flaky_success.append(index)
421 elif not flaky and known:
422 self._log.info('unexpected success: %s' % test.name)
423 self._unexpected_success.append(index)
424 test.success = not failed
425 test.returncode = returncode
426 test.retry_count = retry_count
427 test.expired = expired
428 test.as_expected = known == failed
429 if not self.concurrent:
430 self.out.write(self._result_string(test) + '\n')
431 else:
432 self.out.write(self._format_test(test))
433 finally:
434 self._lock.release()