OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
| 6 from common import chromium_utils |
6 import json | 7 import json |
7 import os | 8 import os |
8 import re | 9 import re |
9 import tempfile | 10 import tempfile |
10 | 11 |
11 | 12 |
12 def CompressList(lines, max_length, middle_replacement): | 13 def CompressList(lines, max_length, middle_replacement): |
13 """Ensures that |lines| is no longer than |max_length|. If |lines| need to | 14 """Ensures that |lines| is no longer than |max_length|. If |lines| need to |
14 be compressed then the middle items are replaced by |middle_replacement|. | 15 be compressed then the middle items are replaced by |middle_replacement|. |
15 """ | 16 """ |
(...skipping 396 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
412 | 413 |
413 def __init__(self): | 414 def __init__(self): |
414 self.json_file_path = None | 415 self.json_file_path = None |
415 self.delete_json_file = False | 416 self.delete_json_file = False |
416 | 417 |
417 self.disabled_tests = set() | 418 self.disabled_tests = set() |
418 self.passed_tests = set() | 419 self.passed_tests = set() |
419 self.failed_tests = set() | 420 self.failed_tests = set() |
420 self.flaky_tests = set() | 421 self.flaky_tests = set() |
421 self.test_logs = {} | 422 self.test_logs = {} |
| 423 self.ignored_failed_tests = set() |
422 | 424 |
423 self.parsing_errors = [] | 425 self.parsing_errors = [] |
424 | 426 |
425 self.master_name = None | 427 self.master_name = None |
426 | 428 |
427 def ProcessLine(self, line): | 429 def ProcessLine(self, line): |
428 # Deliberately do nothing - we parse out-of-band JSON summary | 430 # Deliberately do nothing - we parse out-of-band JSON summary |
429 # instead of in-band stdout. | 431 # instead of in-band stdout. |
430 pass | 432 pass |
431 | 433 |
432 def PassedTests(self): | 434 def PassedTests(self): |
433 return sorted(self.passed_tests) | 435 return sorted(self.passed_tests) |
434 | 436 |
435 def FailedTests(self, include_fails=False, include_flaky=False): | 437 def FailedTests(self, include_fails=False, include_flaky=False): |
436 return sorted(self.failed_tests) | 438 return sorted(self.failed_tests - self.ignored_failed_tests) |
437 | 439 |
438 def FailureDescription(self, test): | 440 def FailureDescription(self, test): |
439 return self.test_logs.get(test, []) | 441 return self.test_logs.get(test, []) |
440 | 442 |
| 443 def IgnoredFailedTests(self): |
| 444 return sorted(self.ignored_failed_tests) |
| 445 |
441 @staticmethod | 446 @staticmethod |
442 def SuppressionHashes(): | 447 def SuppressionHashes(): |
443 return [] | 448 return [] |
444 | 449 |
445 def ParsingErrors(self): | 450 def ParsingErrors(self): |
446 return self.parsing_errors | 451 return self.parsing_errors |
447 | 452 |
448 def ClearParsingErrors(self): | 453 def ClearParsingErrors(self): |
449 self.parsing_errors = ['Cleared.'] | 454 self.parsing_errors = ['Cleared.'] |
450 | 455 |
(...skipping 12 matching lines...) Expand all Loading... |
463 self.json_file_path = cmdline_path | 468 self.json_file_path = cmdline_path |
464 # If the caller requested JSON summary, do not delete it. | 469 # If the caller requested JSON summary, do not delete it. |
465 self.delete_json_file = False | 470 self.delete_json_file = False |
466 else: | 471 else: |
467 fd, self.json_file_path = tempfile.mkstemp() | 472 fd, self.json_file_path = tempfile.mkstemp() |
468 os.close(fd) | 473 os.close(fd) |
469 # When we create the file ourselves, delete it to avoid littering. | 474 # When we create the file ourselves, delete it to avoid littering. |
470 self.delete_json_file = True | 475 self.delete_json_file = True |
471 return self.json_file_path | 476 return self.json_file_path |
472 | 477 |
473 def ProcessJSONFile(self): | 478 def ProcessJSONFile(self, build_dir): |
474 if not self.json_file_path: | 479 if not self.json_file_path: |
475 return | 480 return |
476 | 481 |
477 with open(self.json_file_path) as json_file: | 482 with open(self.json_file_path) as json_file: |
478 try: | 483 try: |
479 json_output = json_file.read() | 484 json_output = json_file.read() |
480 json_data = json.loads(json_output) | 485 json_data = json.loads(json_output) |
481 except ValueError: | 486 except ValueError: |
482 # Only signal parsing error if the file is non-empty. Empty file | 487 # Only signal parsing error if the file is non-empty. Empty file |
483 # most likely means the binary doesn't support JSON output. | 488 # most likely means the binary doesn't support JSON output. |
484 if json_output: | 489 if json_output: |
485 self.parsing_errors = json_output.split('\n') | 490 self.parsing_errors = json_output.split('\n') |
486 else: | 491 else: |
487 self.ProcessJSONData(json_data) | 492 self.ProcessJSONData(json_data, build_dir) |
488 | 493 |
489 if self.delete_json_file: | 494 if self.delete_json_file: |
490 os.remove(self.json_file_path) | 495 os.remove(self.json_file_path) |
491 | 496 |
492 def ProcessJSONData(self, json_data): | 497 def _ParseIngoredFailedTestsSpec(self, build_dir, platform_flags): |
| 498 if not build_dir: |
| 499 return |
| 500 |
| 501 try: |
| 502 ignored_failed_tests_path = chromium_utils.FindUpward( |
| 503 os.path.abspath(build_dir), 'tools', 'ignorer_bot', |
| 504 'ignored_failed_tests.txt') |
| 505 except chromium_utils.PathNotFound: |
| 506 return |
| 507 |
| 508 with open(ignored_failed_tests_path) as ignored_failed_tests_file: |
| 509 ignored_failed_tests_spec = ignored_failed_tests_file.readlines() |
| 510 |
| 511 for spec_line in ignored_failed_tests_spec: |
| 512 spec_line = spec_line.strip() |
| 513 if spec_line.startswith('#') or not spec_line: |
| 514 continue |
| 515 |
| 516 # Any number of flags separated by whitespace with optional trailing |
| 517 # and/or leading whitespace. |
| 518 platform_spec_regexp = r'\s*\w+(?:\s+\w+)*\s*' |
| 519 |
| 520 match = re.match( |
| 521 r'^http://crbug.com/\d+' # Issue URL. |
| 522 r'\s+' # Some whitespace. |
| 523 r'\[(' + # Opening square bracket '['. |
| 524 platform_spec_regexp + # At least one platform, and... |
| 525 r'(?:,' + # ...separated by commas... |
| 526 platform_spec_regexp + # ...any number of additional... |
| 527 r')*' # ...platforms. |
| 528 r')\]' # Closing square bracket ']'. |
| 529 r'\s+' # Some whitespace. |
| 530 r'(\S+)$', spec_line) # Test name. |
| 531 |
| 532 if not match: |
| 533 continue |
| 534 |
| 535 platform_specs = match.group(1).strip() |
| 536 test_name = match.group(2).strip() |
| 537 |
| 538 platform_flags = set(platform_flags) |
| 539 for platform_spec in platform_specs.split(','): |
| 540 required_platform_flags = set(platform_spec.split()) |
| 541 if required_platform_flags.issubset(platform_flags): |
| 542 self.ignored_failed_tests.add(test_name) |
| 543 break |
| 544 |
| 545 def ProcessJSONData(self, json_data, build_dir=None): |
493 # TODO(phajdan.jr): Require disabled_tests to be present (May 2014). | 546 # TODO(phajdan.jr): Require disabled_tests to be present (May 2014). |
494 self.disabled_tests.update(json_data.get('disabled_tests', [])) | 547 self.disabled_tests.update(json_data.get('disabled_tests', [])) |
| 548 self._ParseIngoredFailedTestsSpec(build_dir, |
| 549 json_data.get('global_tags', [])) |
495 | 550 |
496 for iteration_data in json_data['per_iteration_data']: | 551 for iteration_data in json_data['per_iteration_data']: |
497 for test_name, test_runs in iteration_data.iteritems(): | 552 for test_name, test_runs in iteration_data.iteritems(): |
498 if test_runs[-1]['status'] == 'SUCCESS': | 553 if test_runs[-1]['status'] == 'SUCCESS': |
499 self.passed_tests.add(test_name) | 554 self.passed_tests.add(test_name) |
500 else: | 555 else: |
501 self.failed_tests.add(test_name) | 556 self.failed_tests.add(test_name) |
502 | 557 |
503 if len(test_runs) > 1: | 558 if len(test_runs) > 1: |
504 self.flaky_tests.add(test_name) | 559 self.flaky_tests.add(test_name) |
505 | 560 |
506 self.test_logs.setdefault(test_name, []) | 561 self.test_logs.setdefault(test_name, []) |
507 for run_index, run_data in enumerate(test_runs, start=1): | 562 for run_index, run_data in enumerate(test_runs, start=1): |
508 run_lines = ['%s (run #%d):' % (test_name, run_index)] | 563 run_lines = ['%s (run #%d):' % (test_name, run_index)] |
509 # Make sure the annotations are ASCII to avoid character set related | 564 # Make sure the annotations are ASCII to avoid character set related |
510 # errors. They are mostly informational anyway, and more detailed | 565 # errors. They are mostly informational anyway, and more detailed |
511 # info can be obtained from the original JSON output. | 566 # info can be obtained from the original JSON output. |
512 ascii_lines = run_data['output_snippet'].encode('ascii', | 567 ascii_lines = run_data['output_snippet'].encode('ascii', |
513 errors='replace') | 568 errors='replace') |
514 decoded_lines = CompressList( | 569 decoded_lines = CompressList( |
515 ascii_lines.decode('string_escape').split('\n'), | 570 ascii_lines.decode('string_escape').split('\n'), |
516 self.OUTPUT_SNIPPET_LINES_LIMIT, | 571 self.OUTPUT_SNIPPET_LINES_LIMIT, |
517 '<truncated, full output is in gzipped JSON ' | 572 '<truncated, full output is in gzipped JSON ' |
518 'output at end of step>') | 573 'output at end of step>') |
519 run_lines.extend(decoded_lines) | 574 run_lines.extend(decoded_lines) |
520 self.test_logs[test_name].extend(run_lines) | 575 self.test_logs[test_name].extend(run_lines) |
OLD | NEW |