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