OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 from common import chromium_utils |
| 7 import json |
| 8 import os |
| 9 import re |
| 10 import tempfile |
| 11 |
| 12 |
| 13 # These labels should match the ones output by gtest's JSON. |
| 14 TEST_UNKNOWN_LABEL = 'UNKNOWN' |
| 15 TEST_SUCCESS_LABEL = 'SUCCESS' |
| 16 TEST_FAILURE_LABEL = 'FAILURE' |
| 17 TEST_FAILURE_ON_EXIT_LABEL = 'FAILURE_ON_EXIT' |
| 18 TEST_CRASH_LABEL = 'CRASH' |
| 19 TEST_TIMEOUT_LABEL = 'TIMEOUT' |
| 20 TEST_SKIPPED_LABEL = 'SKIPPED' |
| 21 TEST_WARNING_LABEL = 'WARNING' |
| 22 |
| 23 FULL_RESULTS_FILENAME = 'full_results.json' |
| 24 TIMES_MS_FILENAME = 'times_ms.json' |
| 25 |
| 26 def CompressList(lines, max_length, middle_replacement): |
| 27 """Ensures that |lines| is no longer than |max_length|. If |lines| need to |
| 28 be compressed then the middle items are replaced by |middle_replacement|. |
| 29 """ |
| 30 if len(lines) <= max_length: |
| 31 return lines |
| 32 remove_from_start = max_length / 2 |
| 33 return (lines[:remove_from_start] + |
| 34 [middle_replacement] + |
| 35 lines[len(lines) - (max_length - remove_from_start):]) |
| 36 |
| 37 |
| 38 class GTestLogParser(object): |
| 39 """This helper class process GTest test output.""" |
| 40 |
| 41 def __init__(self): |
| 42 # State tracking for log parsing |
| 43 self.completed = False |
| 44 self._current_test = '' |
| 45 self._failure_description = [] |
| 46 self._current_report_hash = '' |
| 47 self._current_report = [] |
| 48 self._parsing_failures = False |
| 49 |
| 50 # Line number currently being processed. |
| 51 self._line_number = 0 |
| 52 |
| 53 # List of parsing errors, as human-readable strings. |
| 54 self._internal_error_lines = [] |
| 55 |
| 56 # Tests are stored here as 'test.name': (status, [description]). |
| 57 # The status should be one of ('started', 'OK', 'failed', 'timeout', |
| 58 # 'warning'). Warning indicates that a test did not pass when run in |
| 59 # parallel with other tests but passed when run alone. The description is |
| 60 # a list of lines detailing the test's error, as reported in the log. |
| 61 self._test_status = {} |
| 62 |
| 63 # Reports are stored here as 'hash': [report]. |
| 64 self._memory_tool_reports = {} |
| 65 |
| 66 # This may be either text or a number. It will be used in the phrase |
| 67 # '%s disabled' or '%s flaky' on the waterfall display. |
| 68 self._disabled_tests = 0 |
| 69 self._flaky_tests = 0 |
| 70 |
| 71 # Regular expressions for parsing GTest logs. Test names look like |
| 72 # SomeTestCase.SomeTest |
| 73 # SomeName/SomeTestCase.SomeTest/1 |
| 74 # This regexp also matches SomeName.SomeTest/1, which should be harmless. |
| 75 test_name_regexp = r'((\w+/)?\w+\.\w+(/\d+)?)' |
| 76 |
| 77 self._master_name_re = re.compile(r'\[Running for master: "([^"]*)"') |
| 78 self.master_name = '' |
| 79 |
| 80 self._test_name = re.compile(test_name_regexp) |
| 81 self._test_start = re.compile(r'\[\s+RUN\s+\] ' + test_name_regexp) |
| 82 self._test_ok = re.compile(r'\[\s+OK\s+\] ' + test_name_regexp) |
| 83 self._test_fail = re.compile(r'\[\s+FAILED\s+\] ' + test_name_regexp) |
| 84 self._test_passed = re.compile(r'\[\s+PASSED\s+\] \d+ tests?.') |
| 85 self._run_test_cases_line = re.compile( |
| 86 r'\[\s*\d+\/\d+\]\s+[0-9\.]+s ' + test_name_regexp + ' .+') |
| 87 self._test_timeout = re.compile( |
| 88 r'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp) |
| 89 self._disabled = re.compile(r'\s*YOU HAVE (\d+) DISABLED TEST') |
| 90 self._flaky = re.compile(r'\s*YOU HAVE (\d+) FLAKY TEST') |
| 91 |
| 92 self._report_start = re.compile( |
| 93 r'### BEGIN MEMORY TOOL REPORT \(error hash=#([0-9A-F]+)#\)') |
| 94 self._report_end = re.compile( |
| 95 r'### END MEMORY TOOL REPORT \(error hash=#([0-9A-F]+)#\)') |
| 96 |
| 97 self._retry_message = re.compile('RETRYING FAILED TESTS:') |
| 98 self.retrying_failed = False |
| 99 |
| 100 self.TEST_STATUS_MAP = { |
| 101 'OK': TEST_SUCCESS_LABEL, |
| 102 'failed': TEST_FAILURE_LABEL, |
| 103 'timeout': TEST_TIMEOUT_LABEL, |
| 104 'warning': TEST_WARNING_LABEL |
| 105 } |
| 106 |
| 107 def GetCurrentTest(self): |
| 108 return self._current_test |
| 109 |
| 110 def _StatusOfTest(self, test): |
| 111 """Returns the status code for the given test, or 'not known'.""" |
| 112 test_status = self._test_status.get(test, ('not known', [])) |
| 113 return test_status[0] |
| 114 |
| 115 def _TestsByStatus(self, status, include_fails, include_flaky): |
| 116 """Returns list of tests with the given status. |
| 117 |
| 118 Args: |
| 119 include_fails: If False, tests containing 'FAILS_' anywhere in their |
| 120 names will be excluded from the list. |
| 121 include_flaky: If False, tests containing 'FLAKY_' anywhere in their |
| 122 names will be excluded from the list. |
| 123 """ |
| 124 test_list = [x[0] for x in self._test_status.items() |
| 125 if self._StatusOfTest(x[0]) == status] |
| 126 |
| 127 if not include_fails: |
| 128 test_list = [x for x in test_list if x.find('FAILS_') == -1] |
| 129 if not include_flaky: |
| 130 test_list = [x for x in test_list if x.find('FLAKY_') == -1] |
| 131 |
| 132 return test_list |
| 133 |
| 134 def _RecordError(self, line, reason): |
| 135 """Record a log line that produced a parsing error. |
| 136 |
| 137 Args: |
| 138 line: text of the line at which the error occurred |
| 139 reason: a string describing the error |
| 140 """ |
| 141 self._internal_error_lines.append('%s: %s [%s]' % |
| 142 (self._line_number, line.strip(), reason)) |
| 143 |
| 144 def RunningTests(self): |
| 145 """Returns list of tests that appear to be currently running.""" |
| 146 return self._TestsByStatus('started', True, True) |
| 147 |
| 148 def ParsingErrors(self): |
| 149 """Returns a list of lines that have caused parsing errors.""" |
| 150 return self._internal_error_lines |
| 151 |
| 152 def ClearParsingErrors(self): |
| 153 """Clears the currently stored parsing errors.""" |
| 154 self._internal_error_lines = ['Cleared.'] |
| 155 |
| 156 def PassedTests(self, include_fails=False, include_flaky=False): |
| 157 """Returns list of tests that passed.""" |
| 158 return self._TestsByStatus('OK', include_fails, include_flaky) |
| 159 |
| 160 def FailedTests(self, include_fails=False, include_flaky=False): |
| 161 """Returns list of tests that failed, timed out, or didn't finish |
| 162 (crashed). |
| 163 |
| 164 This list will be incorrect until the complete log has been processed, |
| 165 because it will show currently running tests as having failed. |
| 166 |
| 167 Args: |
| 168 include_fails: If true, all failing tests with FAILS_ in their names will |
| 169 be included. Otherwise, they will only be included if they crashed or |
| 170 timed out. |
| 171 include_flaky: If true, all failing tests with FLAKY_ in their names will |
| 172 be included. Otherwise, they will only be included if they crashed or |
| 173 timed out. |
| 174 |
| 175 """ |
| 176 return (self._TestsByStatus('failed', include_fails, include_flaky) + |
| 177 self._TestsByStatus('timeout', True, True) + |
| 178 self._TestsByStatus('warning', include_fails, include_flaky) + |
| 179 self.RunningTests()) |
| 180 |
| 181 def TriesForTest(self, test): |
| 182 """Returns a list containing the state for all tries of the given test. |
| 183 This parser doesn't support retries so a single result is returned.""" |
| 184 return [self.TEST_STATUS_MAP.get(self._StatusOfTest(test), |
| 185 TEST_UNKNOWN_LABEL)] |
| 186 |
| 187 def DisabledTests(self): |
| 188 """Returns the name of the disabled test (if there is only 1) or the number |
| 189 of disabled tests. |
| 190 """ |
| 191 return self._disabled_tests |
| 192 |
| 193 def FlakyTests(self): |
| 194 """Returns the name of the flaky test (if there is only 1) or the number |
| 195 of flaky tests. |
| 196 """ |
| 197 return self._flaky_tests |
| 198 |
| 199 def FailureDescription(self, test): |
| 200 """Returns a list containing the failure description for the given test. |
| 201 |
| 202 If the test didn't fail or timeout, returns []. |
| 203 """ |
| 204 test_status = self._test_status.get(test, ('', [])) |
| 205 return ['%s: ' % test] + test_status[1] |
| 206 |
| 207 def MemoryToolReportHashes(self): |
| 208 """Returns list of report hashes found in the log.""" |
| 209 return self._memory_tool_reports.keys() |
| 210 |
| 211 def MemoryToolReport(self, report_hash): |
| 212 """Returns a list containing the report for a given hash. |
| 213 |
| 214 If the report hash doesn't exist, returns []. |
| 215 """ |
| 216 return self._memory_tool_reports.get(report_hash, []) |
| 217 |
| 218 def CompletedWithoutFailure(self): |
| 219 """Returns True if all tests completed and no tests failed unexpectedly.""" |
| 220 return self.completed and not self.FailedTests() |
| 221 |
| 222 def ProcessLine(self, line): |
| 223 """This is called once with each line of the test log.""" |
| 224 |
| 225 # Track line number for error messages. |
| 226 self._line_number += 1 |
| 227 |
| 228 # Some tests (net_unittests in particular) run subprocesses which can write |
| 229 # stuff to shared stdout buffer. Sometimes such output appears between new |
| 230 # line and gtest directives ('[ RUN ]', etc) which breaks the parser. |
| 231 # Code below tries to detect such cases and recognize a mixed line as two |
| 232 # separate lines. |
| 233 |
| 234 # List of regexps that parses expects to find at the start of a line but |
| 235 # which can be somewhere in the middle. |
| 236 gtest_regexps = [ |
| 237 self._test_start, |
| 238 self._test_ok, |
| 239 self._test_fail, |
| 240 self._test_passed, |
| 241 ] |
| 242 |
| 243 for regexp in gtest_regexps: |
| 244 match = regexp.search(line) |
| 245 if match: |
| 246 break |
| 247 |
| 248 if not match or match.start() == 0: |
| 249 self._ProcessLine(line) |
| 250 else: |
| 251 self._ProcessLine(line[:match.start()]) |
| 252 self._ProcessLine(line[match.start():]) |
| 253 |
| 254 def _ProcessLine(self, line): |
| 255 """Parses the line and changes the state of parsed tests accordingly. |
| 256 |
| 257 Will recognize newly started tests, OK or FAILED statuses, timeouts, etc. |
| 258 """ |
| 259 |
| 260 # Note: When sharding, the number of disabled and flaky tests will be read |
| 261 # multiple times, so this will only show the most recent values (but they |
| 262 # should all be the same anyway). |
| 263 |
| 264 # Is it a line listing the master name? |
| 265 if not self.master_name: |
| 266 results = self._master_name_re.match(line) |
| 267 if results: |
| 268 self.master_name = results.group(1) |
| 269 |
| 270 results = self._run_test_cases_line.match(line) |
| 271 if results: |
| 272 # A run_test_cases.py output. |
| 273 if self._current_test: |
| 274 if self._test_status[self._current_test][0] == 'started': |
| 275 self._test_status[self._current_test] = ( |
| 276 'timeout', self._failure_description) |
| 277 self._current_test = '' |
| 278 self._failure_description = [] |
| 279 return |
| 280 |
| 281 # Is it a line declaring all tests passed? |
| 282 results = self._test_passed.match(line) |
| 283 if results: |
| 284 self.completed = True |
| 285 self._current_test = '' |
| 286 return |
| 287 |
| 288 # Is it a line reporting disabled tests? |
| 289 results = self._disabled.match(line) |
| 290 if results: |
| 291 try: |
| 292 disabled = int(results.group(1)) |
| 293 except ValueError: |
| 294 disabled = 0 |
| 295 if disabled > 0 and isinstance(self._disabled_tests, int): |
| 296 self._disabled_tests = disabled |
| 297 else: |
| 298 # If we can't parse the line, at least give a heads-up. This is a |
| 299 # safety net for a case that shouldn't happen but isn't a fatal error. |
| 300 self._disabled_tests = 'some' |
| 301 return |
| 302 |
| 303 # Is it a line reporting flaky tests? |
| 304 results = self._flaky.match(line) |
| 305 if results: |
| 306 try: |
| 307 flaky = int(results.group(1)) |
| 308 except ValueError: |
| 309 flaky = 0 |
| 310 if flaky > 0 and isinstance(self._flaky_tests, int): |
| 311 self._flaky_tests = flaky |
| 312 else: |
| 313 # If we can't parse the line, at least give a heads-up. This is a |
| 314 # safety net for a case that shouldn't happen but isn't a fatal error. |
| 315 self._flaky_tests = 'some' |
| 316 return |
| 317 |
| 318 # Is it the start of a test? |
| 319 results = self._test_start.match(line) |
| 320 if results: |
| 321 if self._current_test: |
| 322 if self._test_status[self._current_test][0] == 'started': |
| 323 self._test_status[self._current_test] = ( |
| 324 'timeout', self._failure_description) |
| 325 test_name = results.group(1) |
| 326 self._test_status[test_name] = ('started', ['Did not complete.']) |
| 327 self._current_test = test_name |
| 328 if self.retrying_failed: |
| 329 self._failure_description = self._test_status[test_name][1] |
| 330 self._failure_description.extend(['', 'RETRY OUTPUT:', '']) |
| 331 else: |
| 332 self._failure_description = [] |
| 333 return |
| 334 |
| 335 # Is it a test success line? |
| 336 results = self._test_ok.match(line) |
| 337 if results: |
| 338 test_name = results.group(1) |
| 339 status = self._StatusOfTest(test_name) |
| 340 if status != 'started': |
| 341 self._RecordError(line, 'success while in status %s' % status) |
| 342 if self.retrying_failed: |
| 343 self._test_status[test_name] = ('warning', self._failure_description) |
| 344 else: |
| 345 self._test_status[test_name] = ('OK', []) |
| 346 self._failure_description = [] |
| 347 self._current_test = '' |
| 348 return |
| 349 |
| 350 # Is it a test failure line? |
| 351 results = self._test_fail.match(line) |
| 352 if results: |
| 353 test_name = results.group(1) |
| 354 status = self._StatusOfTest(test_name) |
| 355 if status not in ('started', 'failed', 'timeout'): |
| 356 self._RecordError(line, 'failure while in status %s' % status) |
| 357 # Don't overwrite the failure description when a failing test is listed a |
| 358 # second time in the summary, or if it was already recorded as timing |
| 359 # out. |
| 360 if status not in ('failed', 'timeout'): |
| 361 self._test_status[test_name] = ('failed', self._failure_description) |
| 362 self._failure_description = [] |
| 363 self._current_test = '' |
| 364 return |
| 365 |
| 366 # Is it a test timeout line? |
| 367 results = self._test_timeout.search(line) |
| 368 if results: |
| 369 test_name = results.group(1) |
| 370 status = self._StatusOfTest(test_name) |
| 371 if status not in ('started', 'failed'): |
| 372 self._RecordError(line, 'timeout while in status %s' % status) |
| 373 self._test_status[test_name] = ( |
| 374 'timeout', self._failure_description + ['Killed (timed out).']) |
| 375 self._failure_description = [] |
| 376 self._current_test = '' |
| 377 return |
| 378 |
| 379 # Is it the start of a new memory tool report? |
| 380 results = self._report_start.match(line) |
| 381 if results: |
| 382 report_hash = results.group(1) |
| 383 if report_hash in self._memory_tool_reports: |
| 384 self._RecordError(line, 'multiple reports for this hash') |
| 385 self._memory_tool_reports[report_hash] = [] |
| 386 self._current_report_hash = report_hash |
| 387 self._current_report = [] |
| 388 return |
| 389 |
| 390 # Is it the end of a memory tool report? |
| 391 results = self._report_end.match(line) |
| 392 if results: |
| 393 report_hash = results.group(1) |
| 394 if not self._current_report_hash: |
| 395 self._RecordError(line, 'no BEGIN matches this END') |
| 396 elif report_hash != self._current_report_hash: |
| 397 self._RecordError(line, 'expected (error hash=#%s#)' % |
| 398 self._current_report_hash) |
| 399 else: |
| 400 self._memory_tool_reports[self._current_report_hash] = ( |
| 401 self._current_report) |
| 402 self._current_report_hash = '' |
| 403 self._current_report = [] |
| 404 return |
| 405 |
| 406 # Is it the start of the retry tests? |
| 407 results = self._retry_message.match(line) |
| 408 if results: |
| 409 self.retrying_failed = True |
| 410 return |
| 411 |
| 412 # Random line: if we're in a report, collect it. Reports are |
| 413 # generated after all tests are finished, so this should always belong to |
| 414 # the current report hash. |
| 415 if self._current_report_hash: |
| 416 self._current_report.append(line) |
| 417 return |
| 418 |
| 419 # Random line: if we're in a test, collect it for the failure description. |
| 420 # Tests may run simultaneously, so this might be off, but it's worth a try. |
| 421 # This also won't work if a test times out before it begins running. |
| 422 if self._current_test: |
| 423 self._failure_description.append(line) |
| 424 |
| 425 # Parse the "Failing tests:" list at the end of the output, and add any |
| 426 # additional failed tests to the list. For example, this includes tests |
| 427 # that crash after the OK line. |
| 428 if self._parsing_failures: |
| 429 results = self._test_name.match(line) |
| 430 if results: |
| 431 test_name = results.group(1) |
| 432 status = self._StatusOfTest(test_name) |
| 433 if status in ('not known', 'OK'): |
| 434 self._test_status[test_name] = ( |
| 435 'failed', ['Unknown error, see stdio log.']) |
| 436 else: |
| 437 self._parsing_failures = False |
| 438 elif line.startswith('Failing tests:'): |
| 439 self._parsing_failures = True |
| 440 |
| 441 |
| 442 class GTestJSONParser(object): |
| 443 # Limit of output snippet lines. Avoids flooding the logs with amount |
| 444 # of output that gums up the infrastructure. |
| 445 OUTPUT_SNIPPET_LINES_LIMIT = 5000 |
| 446 |
| 447 def __init__(self, mastername=None): |
| 448 self.json_file_path = None |
| 449 self.delete_json_file = False |
| 450 |
| 451 self.disabled_tests = set() |
| 452 self.passed_tests = set() |
| 453 self.failed_tests = set() |
| 454 self.flaky_tests = set() |
| 455 self.test_logs = {} |
| 456 self.run_results = {} |
| 457 self.ignored_failed_tests = set() |
| 458 |
| 459 self.parsing_errors = [] |
| 460 |
| 461 self.master_name = mastername |
| 462 |
| 463 # List our labels that match the ones output by gtest JSON. |
| 464 self.SUPPORTED_LABELS = (TEST_UNKNOWN_LABEL, |
| 465 TEST_SUCCESS_LABEL, |
| 466 TEST_FAILURE_LABEL, |
| 467 TEST_FAILURE_ON_EXIT_LABEL, |
| 468 TEST_CRASH_LABEL, |
| 469 TEST_TIMEOUT_LABEL, |
| 470 TEST_SKIPPED_LABEL) |
| 471 |
| 472 def ProcessLine(self, line): |
| 473 # Deliberately do nothing - we parse out-of-band JSON summary |
| 474 # instead of in-band stdout. |
| 475 pass |
| 476 |
| 477 def PassedTests(self): |
| 478 return sorted(self.passed_tests) |
| 479 |
| 480 def FailedTests(self, include_fails=False, include_flaky=False): |
| 481 return sorted(self.failed_tests - self.ignored_failed_tests) |
| 482 |
| 483 def TriesForTest(self, test): |
| 484 """Returns a list containing the state for all tries of the given test.""" |
| 485 return self.run_results.get(test, [TEST_UNKNOWN_LABEL]) |
| 486 |
| 487 def FailureDescription(self, test): |
| 488 return self.test_logs.get(test, []) |
| 489 |
| 490 def IgnoredFailedTests(self): |
| 491 return sorted(self.ignored_failed_tests) |
| 492 |
| 493 @staticmethod |
| 494 def MemoryToolReportHashes(): |
| 495 return [] |
| 496 |
| 497 def ParsingErrors(self): |
| 498 return self.parsing_errors |
| 499 |
| 500 def ClearParsingErrors(self): |
| 501 self.parsing_errors = ['Cleared.'] |
| 502 |
| 503 def DisabledTests(self): |
| 504 return len(self.disabled_tests) |
| 505 |
| 506 def FlakyTests(self): |
| 507 return len(self.flaky_tests) |
| 508 |
| 509 @staticmethod |
| 510 def RunningTests(): |
| 511 return [] |
| 512 |
| 513 def PrepareJSONFile(self, cmdline_path): |
| 514 if cmdline_path: |
| 515 self.json_file_path = cmdline_path |
| 516 # If the caller requested JSON summary, do not delete it. |
| 517 self.delete_json_file = False |
| 518 else: |
| 519 fd, self.json_file_path = tempfile.mkstemp() |
| 520 os.close(fd) |
| 521 # When we create the file ourselves, delete it to avoid littering. |
| 522 self.delete_json_file = True |
| 523 return self.json_file_path |
| 524 |
| 525 def ProcessJSONFile(self, build_dir): |
| 526 if not self.json_file_path: |
| 527 return |
| 528 |
| 529 with open(self.json_file_path) as json_file: |
| 530 try: |
| 531 json_output = json_file.read() |
| 532 json_data = json.loads(json_output) |
| 533 except ValueError: |
| 534 # Only signal parsing error if the file is non-empty. Empty file |
| 535 # most likely means the binary doesn't support JSON output. |
| 536 if json_output: |
| 537 self.parsing_errors = json_output.split('\n') |
| 538 else: |
| 539 self.ProcessJSONData(json_data, build_dir) |
| 540 |
| 541 if self.delete_json_file: |
| 542 os.remove(self.json_file_path) |
| 543 |
| 544 @staticmethod |
| 545 def ParseIgnoredFailedTestSpec(dir_in_chrome): |
| 546 """Returns parsed ignored failed test spec. |
| 547 |
| 548 Args: |
| 549 dir_in_chrome: Any directory within chrome checkout to be used as a |
| 550 reference to find ignored failed test spec file. |
| 551 |
| 552 Returns: |
| 553 A list of tuples (test_name, platforms), where platforms is a list of sets |
| 554 of platform flags. For example: |
| 555 |
| 556 [('MyTest.TestOne', [set('OS_WIN', 'CPU_32_BITS', 'MODE_RELEASE'), |
| 557 set('OS_LINUX', 'CPU_64_BITS', 'MODE_DEBUG')]), |
| 558 ('MyTest.TestTwo', [set('OS_MACOSX', 'CPU_64_BITS', 'MODE_RELEASE'), |
| 559 set('CPU_32_BITS')]), |
| 560 ('MyTest.TestThree', [set()]] |
| 561 """ |
| 562 |
| 563 try: |
| 564 ignored_failed_tests_path = chromium_utils.FindUpward( |
| 565 os.path.abspath(dir_in_chrome), 'tools', 'ignorer_bot', |
| 566 'ignored_failed_tests.txt') |
| 567 except chromium_utils.PathNotFound: |
| 568 return |
| 569 |
| 570 with open(ignored_failed_tests_path) as ignored_failed_tests_file: |
| 571 ignored_failed_tests_spec = ignored_failed_tests_file.readlines() |
| 572 |
| 573 parsed_spec = [] |
| 574 for spec_line in ignored_failed_tests_spec: |
| 575 spec_line = spec_line.strip() |
| 576 if spec_line.startswith('#') or not spec_line: |
| 577 continue |
| 578 |
| 579 # Any number of platform flags identifiers separated by whitespace. |
| 580 platform_spec_regexp = r'[A-Za-z0-9_\s]*' |
| 581 |
| 582 match = re.match( |
| 583 r'^crbug.com/\d+' # Issue URL. |
| 584 r'\s+' # Some whitespace. |
| 585 r'\[(' + # Opening square bracket '['. |
| 586 platform_spec_regexp + # At least one platform, and... |
| 587 r'(?:,' + # ...separated by commas... |
| 588 platform_spec_regexp + # ...any number of additional... |
| 589 r')*' # ...platforms. |
| 590 r')\]' # Closing square bracket ']'. |
| 591 r'\s+' # Some whitespace. |
| 592 r'(\S+)$', spec_line) # Test name. |
| 593 |
| 594 if not match: |
| 595 continue |
| 596 |
| 597 platform_specs = match.group(1).strip() |
| 598 test_name = match.group(2).strip() |
| 599 |
| 600 platforms = [set(platform.split()) |
| 601 for platform in platform_specs.split(',')] |
| 602 |
| 603 parsed_spec.append((test_name, platforms)) |
| 604 |
| 605 return parsed_spec |
| 606 |
| 607 |
| 608 def _RetrieveIgnoredFailuresForPlatform(self, build_dir, platform_flags): |
| 609 """Parses the ignored failed tests spec into self.ignored_failed_tests.""" |
| 610 if not build_dir: |
| 611 return |
| 612 |
| 613 platform_flags = set(platform_flags) |
| 614 parsed_spec = self.ParseIgnoredFailedTestSpec(build_dir) |
| 615 |
| 616 if not parsed_spec: |
| 617 return |
| 618 |
| 619 for test_name, platforms in parsed_spec: |
| 620 for required_platform_flags in platforms: |
| 621 if required_platform_flags.issubset(platform_flags): |
| 622 self.ignored_failed_tests.add(test_name) |
| 623 break |
| 624 |
| 625 def ProcessJSONData(self, json_data, build_dir=None): |
| 626 self.disabled_tests.update(json_data['disabled_tests']) |
| 627 self._RetrieveIgnoredFailuresForPlatform( |
| 628 build_dir, json_data['global_tags']) |
| 629 |
| 630 for iteration_data in json_data['per_iteration_data']: |
| 631 for test_name, test_runs in iteration_data.iteritems(): |
| 632 if test_runs[-1]['status'] == 'SUCCESS': |
| 633 self.passed_tests.add(test_name) |
| 634 else: |
| 635 self.failed_tests.add(test_name) |
| 636 |
| 637 self.run_results[test_name] = [] |
| 638 self.test_logs.setdefault(test_name, []) |
| 639 for run_index, run_data in enumerate(test_runs, start=1): |
| 640 # Mark as flaky if the run result differs. |
| 641 if run_data['status'] != test_runs[0]['status']: |
| 642 self.flaky_tests.add(test_name) |
| 643 if run_data['status'] in self.SUPPORTED_LABELS: |
| 644 self.run_results[test_name].append(run_data['status']) |
| 645 else: |
| 646 self.run_results[test_name].append(TEST_UNKNOWN_LABEL) |
| 647 run_lines = ['%s (run #%d):' % (test_name, run_index)] |
| 648 # Make sure the annotations are ASCII to avoid character set related |
| 649 # errors. They are mostly informational anyway, and more detailed |
| 650 # info can be obtained from the original JSON output. |
| 651 ascii_lines = run_data['output_snippet'].encode('ascii', |
| 652 errors='replace') |
| 653 decoded_lines = CompressList( |
| 654 ascii_lines.decode('string_escape').split('\n'), |
| 655 self.OUTPUT_SNIPPET_LINES_LIMIT, |
| 656 '<truncated, full output is in gzipped JSON ' |
| 657 'output at end of step>') |
| 658 run_lines.extend(decoded_lines) |
| 659 self.test_logs[test_name].extend(run_lines) |
OLD | NEW |