Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 1 | # |
| 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 Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 20 | from __future__ import print_function |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 21 | import datetime |
| 22 | import json |
| 23 | import multiprocessing |
| 24 | import os |
| 25 | import platform |
| 26 | import re |
| 27 | import subprocess |
| 28 | import sys |
| 29 | import time |
| 30 | import traceback |
| 31 | |
Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 32 | from .compat import logfile_open, path_join, str_join |
Nobuaki Sukegawa | 2de2700 | 2015-11-22 01:13:48 +0900 | [diff] [blame] | 33 | from .test import TestEntry |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 34 | |
| 35 | LOG_DIR = 'log' |
Nobuaki Sukegawa | bd16530 | 2016-01-19 11:10:07 +0900 | [diff] [blame^] | 36 | RESULT_HTML = 'index.html' |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 37 | RESULT_JSON = 'results.json' |
| 38 | FAIL_JSON = 'known_failures_%s.json' |
| 39 | |
| 40 | |
| 41 | def generate_known_failures(testdir, overwrite, save, out): |
| 42 | 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: |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 48 | with logfile_open(path_join(testdir, RESULT_JSON), 'r') as fp: |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 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 |
Nobuaki Sukegawa | f5b795d | 2015-03-29 14:48:48 +0900 | [diff] [blame] | 58 | fails_json = json.dumps(sorted(set(fails)), indent=2, separators=(',', ': ')) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 59 | if save: |
Nobuaki Sukegawa | e68ccc2 | 2015-12-13 21:45:39 +0900 | [diff] [blame] | 60 | with logfile_open(os.path.join(testdir, FAIL_JSON % platform.system()), 'w+') as fp: |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 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 |
| 67 | |
| 68 | |
| 69 | def load_known_failures(testdir): |
| 70 | try: |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 71 | with logfile_open(path_join(testdir, FAIL_JSON % platform.system()), 'r') as fp: |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 72 | return json.load(fp) |
| 73 | except IOError: |
| 74 | return [] |
| 75 | |
| 76 | |
| 77 | class TestReporter(object): |
| 78 | # 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' |
| 81 | |
| 82 | def __init__(self): |
| 83 | self._log = multiprocessing.get_logger() |
| 84 | self._lock = multiprocessing.Lock() |
| 85 | |
| 86 | @classmethod |
Nobuaki Sukegawa | 783660a | 2015-04-12 00:32:40 +0900 | [diff] [blame] | 87 | def test_logfile(cls, test_name, prog_kind, dir=None): |
Nobuaki Sukegawa | 378b727 | 2016-01-03 17:04:50 +0900 | [diff] [blame] | 88 | relpath = path_join('log', '%s_%s.log' % (test_name, prog_kind)) |
Nobuaki Sukegawa | 2de2700 | 2015-11-22 01:13:48 +0900 | [diff] [blame] | 89 | return relpath if not dir else os.path.realpath(path_join(dir, relpath)) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 90 | |
| 91 | def _start(self): |
| 92 | self._start_time = time.time() |
| 93 | |
| 94 | @property |
| 95 | def _elapsed(self): |
| 96 | return time.time() - self._start_time |
| 97 | |
| 98 | @classmethod |
| 99 | def _format_date(cls): |
| 100 | return '%s' % datetime.datetime.now().strftime(cls.DATETIME_FORMAT) |
| 101 | |
| 102 | def _print_date(self): |
| 103 | self.out.write('%s\n' % self._format_date()) |
| 104 | |
| 105 | def _print_bar(self, out=None): |
| 106 | (out or self.out).write( |
| 107 | '======================================================================\n') |
| 108 | |
| 109 | def _print_exec_time(self): |
| 110 | self.out.write('Test execution took {:.1f} seconds.\n'.format(self._elapsed)) |
| 111 | |
| 112 | |
| 113 | class ExecReporter(TestReporter): |
| 114 | def __init__(self, testdir, test, prog): |
| 115 | super(ExecReporter, self).__init__() |
| 116 | self._test = test |
| 117 | self._prog = prog |
Nobuaki Sukegawa | 783660a | 2015-04-12 00:32:40 +0900 | [diff] [blame] | 118 | self.logpath = self.test_logfile(test.name, prog.kind, testdir) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 119 | self.out = None |
| 120 | |
| 121 | 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.') |
| 128 | |
| 129 | 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() |
| 140 | |
| 141 | def killed(self): |
Nobuaki Sukegawa | 2ba7944 | 2016-01-12 19:37:55 +0900 | [diff] [blame] | 142 | self.out.write('Process is killed.\n') |
| 143 | self.end(None) |
| 144 | |
| 145 | def died(self): |
| 146 | self.out.write('Process is died unexpectedly.\n') |
Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 147 | self.end(None) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 148 | |
| 149 | _init_failure_exprs = { |
| 150 | 'server': list(map(re.compile, [ |
| 151 | '[Aa]ddress already in use', |
| 152 | 'Could not bind', |
| 153 | 'EADDRINUSE', |
| 154 | ])), |
| 155 | 'client': list(map(re.compile, [ |
| 156 | '[Cc]onnection refused', |
| 157 | 'Could not connect to localhost', |
| 158 | 'ECONNREFUSED', |
| 159 | 'No such file or directory', # domain socket |
| 160 | ])), |
| 161 | } |
| 162 | |
| 163 | def maybe_false_positive(self): |
| 164 | """Searches through log file for socket bind error. |
| 165 | Returns True if suspicious expression is found, otherwise False""" |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 166 | try: |
| 167 | if self.out and not self.out.closed: |
| 168 | self.out.flush() |
Nobuaki Sukegawa | 9b35a7c | 2015-11-17 11:01:41 +0900 | [diff] [blame] | 169 | exprs = self._init_failure_exprs[self._prog.kind] |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 170 | |
Nobuaki Sukegawa | 9b35a7c | 2015-11-17 11:01:41 +0900 | [diff] [blame] | 171 | def match(line): |
| 172 | for expr in exprs: |
| 173 | if expr.search(line): |
| 174 | return True |
| 175 | |
| 176 | with logfile_open(self.logpath, 'r') as fp: |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 177 | if any(map(match, fp)): |
| 178 | return True |
| 179 | except (KeyboardInterrupt, SystemExit): |
| 180 | raise |
| 181 | except Exception as ex: |
| 182 | self._log.warn('[%s]: Error while detecting false positive: %s' % (self._test.name, str(ex))) |
| 183 | self._log.info(traceback.print_exc()) |
| 184 | return False |
| 185 | |
| 186 | def _open(self): |
Nobuaki Sukegawa | e68ccc2 | 2015-12-13 21:45:39 +0900 | [diff] [blame] | 187 | self.out = logfile_open(self.logpath, 'w+') |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 188 | |
| 189 | def _close(self): |
| 190 | self.out.close() |
| 191 | |
| 192 | def _print_header(self): |
| 193 | self._print_date() |
Nobuaki Sukegawa | 2de2700 | 2015-11-22 01:13:48 +0900 | [diff] [blame] | 194 | self.out.write('Executing: %s\n' % str_join(' ', self._prog.command)) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 195 | self.out.write('Directory: %s\n' % self._prog.workdir) |
| 196 | self.out.write('config:delay: %s\n' % self._test.delay) |
| 197 | self.out.write('config:timeout: %s\n' % self._test.timeout) |
| 198 | self._print_bar() |
| 199 | self.out.flush() |
| 200 | |
| 201 | def _print_footer(self, returncode=None): |
| 202 | self._print_bar() |
| 203 | if returncode is not None: |
| 204 | self.out.write('Return code: %d\n' % returncode) |
| 205 | else: |
| 206 | self.out.write('Process is killed.\n') |
| 207 | self._print_exec_time() |
| 208 | self._print_date() |
| 209 | |
| 210 | |
| 211 | class SummaryReporter(TestReporter): |
Nobuaki Sukegawa | bd16530 | 2016-01-19 11:10:07 +0900 | [diff] [blame^] | 212 | def __init__(self, basedir, testdir_relative, concurrent=True): |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 213 | super(SummaryReporter, self).__init__() |
Nobuaki Sukegawa | bd16530 | 2016-01-19 11:10:07 +0900 | [diff] [blame^] | 214 | self._basedir = basedir |
| 215 | self._testdir_rel = testdir_relative |
| 216 | self.logdir = path_join(self.testdir, LOG_DIR) |
| 217 | self.out_path = path_join(self.testdir, RESULT_JSON) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 218 | self.concurrent = concurrent |
| 219 | self.out = sys.stdout |
| 220 | self._platform = platform.system() |
| 221 | self._revision = self._get_revision() |
| 222 | self._tests = [] |
| 223 | if not os.path.exists(self.logdir): |
| 224 | os.mkdir(self.logdir) |
Nobuaki Sukegawa | bd16530 | 2016-01-19 11:10:07 +0900 | [diff] [blame^] | 225 | self._known_failures = load_known_failures(self.testdir) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 226 | self._unexpected_success = [] |
| 227 | self._unexpected_failure = [] |
| 228 | self._expected_failure = [] |
| 229 | self._print_header() |
| 230 | |
Nobuaki Sukegawa | bd16530 | 2016-01-19 11:10:07 +0900 | [diff] [blame^] | 231 | @property |
| 232 | def testdir(self): |
| 233 | return path_join(self._basedir, self._testdir_rel) |
| 234 | |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 235 | def _get_revision(self): |
| 236 | p = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], |
| 237 | cwd=self.testdir, stdout=subprocess.PIPE) |
| 238 | out, _ = p.communicate() |
| 239 | return out.strip() |
| 240 | |
| 241 | def _format_test(self, test, with_result=True): |
| 242 | name = '%s-%s' % (test.server.name, test.client.name) |
| 243 | trans = '%s-%s' % (test.transport, test.socket) |
| 244 | if not with_result: |
| 245 | return '{:19s}{:13s}{:25s}'.format(name[:18], test.protocol[:12], trans[:24]) |
| 246 | else: |
| 247 | result = 'success' if test.success else ( |
| 248 | 'timeout' if test.expired else 'failure') |
| 249 | result_string = '%s(%d)' % (result, test.returncode) |
| 250 | return '{:19s}{:13s}{:25s}{:s}\n'.format(name[:18], test.protocol[:12], trans[:24], result_string) |
| 251 | |
| 252 | def _print_test_header(self): |
| 253 | self._print_bar() |
| 254 | self.out.write( |
| 255 | '{:19s}{:13s}{:25s}{:s}\n'.format('server-client:', 'protocol:', 'transport:', 'result:')) |
| 256 | |
| 257 | def _print_header(self): |
| 258 | self._start() |
| 259 | self.out.writelines([ |
| 260 | 'Apache Thrift - Integration Test Suite\n', |
| 261 | ]) |
| 262 | self._print_date() |
| 263 | self._print_test_header() |
| 264 | |
| 265 | def _print_unexpected_failure(self): |
| 266 | if len(self._unexpected_failure) > 0: |
| 267 | self.out.writelines([ |
| 268 | '*** Following %d failures were unexpected ***:\n' % len(self._unexpected_failure), |
| 269 | 'If it is introduced by you, please fix it before submitting the code.\n', |
| 270 | # 'If not, please report at https://issues.apache.org/jira/browse/THRIFT\n', |
| 271 | ]) |
| 272 | self._print_test_header() |
| 273 | for i in self._unexpected_failure: |
| 274 | self.out.write(self._format_test(self._tests[i])) |
| 275 | self._print_bar() |
| 276 | else: |
| 277 | self.out.write('No unexpected failures.\n') |
| 278 | |
| 279 | def _print_unexpected_success(self): |
| 280 | if len(self._unexpected_success) > 0: |
| 281 | self.out.write( |
| 282 | 'Following %d tests were known to fail but succeeded (it\'s normal):\n' % len(self._unexpected_success)) |
| 283 | self._print_test_header() |
| 284 | for i in self._unexpected_success: |
| 285 | self.out.write(self._format_test(self._tests[i])) |
| 286 | self._print_bar() |
| 287 | |
Nobuaki Sukegawa | f5b795d | 2015-03-29 14:48:48 +0900 | [diff] [blame] | 288 | def _http_server_command(self, port): |
| 289 | if sys.version_info[0] < 3: |
| 290 | return 'python -m SimpleHTTPServer %d' % port |
| 291 | else: |
| 292 | return 'python -m http.server %d' % port |
| 293 | |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 294 | def _print_footer(self): |
| 295 | fail_count = len(self._expected_failure) + len(self._unexpected_failure) |
| 296 | self._print_bar() |
| 297 | self._print_unexpected_success() |
| 298 | self._print_unexpected_failure() |
| 299 | self._write_html_data() |
| 300 | self._assemble_log('unexpected failures', self._unexpected_failure) |
| 301 | self._assemble_log('known failures', self._expected_failure) |
| 302 | self.out.writelines([ |
| 303 | 'You can browse results at:\n', |
Nobuaki Sukegawa | bd16530 | 2016-01-19 11:10:07 +0900 | [diff] [blame^] | 304 | '\tfile://%s/%s\n' % (self._basedir, RESULT_HTML), |
Nobuaki Sukegawa | f5b795d | 2015-03-29 14:48:48 +0900 | [diff] [blame] | 305 | '# If you use Chrome, run:\n', |
Nobuaki Sukegawa | bd16530 | 2016-01-19 11:10:07 +0900 | [diff] [blame^] | 306 | '# \tcd %s\n#\t%s\n' % (self._basedir, self._http_server_command(8001)), |
Nobuaki Sukegawa | f5b795d | 2015-03-29 14:48:48 +0900 | [diff] [blame] | 307 | '# then browse:\n', |
Nobuaki Sukegawa | bd16530 | 2016-01-19 11:10:07 +0900 | [diff] [blame^] | 308 | '# \thttp://localhost:%d/%s/\n' % (8001, self._testdir_rel), |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 309 | 'Full log for each test is here:\n', |
| 310 | '\ttest/log/client_server_protocol_transport_client.log\n', |
| 311 | '\ttest/log/client_server_protocol_transport_server.log\n', |
| 312 | '%d failed of %d tests in total.\n' % (fail_count, len(self._tests)), |
| 313 | ]) |
| 314 | self._print_exec_time() |
| 315 | self._print_date() |
| 316 | |
| 317 | def _render_result(self, test): |
| 318 | return [ |
| 319 | test.server.name, |
| 320 | test.client.name, |
| 321 | test.protocol, |
| 322 | test.transport, |
| 323 | test.socket, |
| 324 | test.success, |
| 325 | test.as_expected, |
| 326 | test.returncode, |
| 327 | { |
Nobuaki Sukegawa | 783660a | 2015-04-12 00:32:40 +0900 | [diff] [blame] | 328 | 'server': self.test_logfile(test.name, test.server.kind), |
| 329 | 'client': self.test_logfile(test.name, test.client.kind), |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 330 | }, |
| 331 | ] |
| 332 | |
| 333 | def _write_html_data(self): |
| 334 | """Writes JSON data to be read by result html""" |
| 335 | results = [self._render_result(r) for r in self._tests] |
Nobuaki Sukegawa | e68ccc2 | 2015-12-13 21:45:39 +0900 | [diff] [blame] | 336 | with logfile_open(self.out_path, 'w+') as fp: |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 337 | fp.write(json.dumps({ |
| 338 | 'date': self._format_date(), |
| 339 | 'revision': str(self._revision), |
| 340 | 'platform': self._platform, |
| 341 | 'duration': '{:.1f}'.format(self._elapsed), |
| 342 | 'results': results, |
| 343 | }, indent=2)) |
| 344 | |
| 345 | def _assemble_log(self, title, indexes): |
| 346 | if len(indexes) > 0: |
| 347 | def add_prog_log(fp, test, prog_kind): |
Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 348 | print('*************************** %s message ***************************' % prog_kind, |
| 349 | file=fp) |
Nobuaki Sukegawa | 783660a | 2015-04-12 00:32:40 +0900 | [diff] [blame] | 350 | path = self.test_logfile(test.name, prog_kind, self.testdir) |
Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 351 | if os.path.exists(path): |
| 352 | with logfile_open(path, 'r') as prog_fp: |
| 353 | print(prog_fp.read(), file=fp) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 354 | filename = title.replace(' ', '_') + '.log' |
Nobuaki Sukegawa | e68ccc2 | 2015-12-13 21:45:39 +0900 | [diff] [blame] | 355 | with logfile_open(os.path.join(self.logdir, filename), 'w+') as fp: |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 356 | for test in map(self._tests.__getitem__, indexes): |
| 357 | fp.write('TEST: [%s]\n' % test.name) |
| 358 | add_prog_log(fp, test, test.server.kind) |
| 359 | add_prog_log(fp, test, test.client.kind) |
| 360 | fp.write('**********************************************************************\n\n') |
Nobuaki Sukegawa | a6ab1f5 | 2015-11-28 15:04:39 +0900 | [diff] [blame] | 361 | print('%s are logged to test/%s/%s' % (title.capitalize(), LOG_DIR, filename)) |
Roger Meier | 41ad434 | 2015-03-24 22:30:40 +0100 | [diff] [blame] | 362 | |
| 363 | def end(self): |
| 364 | self._print_footer() |
| 365 | return len(self._unexpected_failure) == 0 |
| 366 | |
| 367 | def add_test(self, test_dict): |
| 368 | test = TestEntry(self.testdir, **test_dict) |
| 369 | self._lock.acquire() |
| 370 | try: |
| 371 | if not self.concurrent: |
| 372 | self.out.write(self._format_test(test, False)) |
| 373 | self.out.flush() |
| 374 | self._tests.append(test) |
| 375 | return len(self._tests) - 1 |
| 376 | finally: |
| 377 | self._lock.release() |
| 378 | |
| 379 | def add_result(self, index, returncode, expired): |
| 380 | self._lock.acquire() |
| 381 | try: |
| 382 | failed = returncode is None or returncode != 0 |
| 383 | test = self._tests[index] |
| 384 | known = test.name in self._known_failures |
| 385 | if failed: |
| 386 | if known: |
| 387 | self._log.debug('%s failed as expected' % test.name) |
| 388 | self._expected_failure.append(index) |
| 389 | else: |
| 390 | self._log.info('unexpected failure: %s' % test.name) |
| 391 | self._unexpected_failure.append(index) |
| 392 | elif known: |
| 393 | self._log.info('unexpected success: %s' % test.name) |
| 394 | self._unexpected_success.append(index) |
| 395 | test.success = not failed |
| 396 | test.returncode = returncode |
| 397 | test.expired = expired |
| 398 | test.as_expected = known == failed |
| 399 | if not self.concurrent: |
| 400 | result = 'success' if not failed else 'failure' |
| 401 | result_string = '%s(%d)' % (result, returncode) |
| 402 | self.out.write(result_string + '\n') |
| 403 | else: |
| 404 | self.out.write(self._format_test(test)) |
| 405 | finally: |
| 406 | self._lock.release() |