Index: ios/build/bots/scripts/xctest_utils.py |
diff --git a/ios/build/bots/scripts/gtest_utils.py b/ios/build/bots/scripts/xctest_utils.py |
old mode 100755 |
new mode 100644 |
similarity index 52% |
copy from ios/build/bots/scripts/gtest_utils.py |
copy to ios/build/bots/scripts/xctest_utils.py |
index 891939de6737b8311658475c5b083398e2658a6c..1d9d35ffe653620f100613c3a57b327bebf8a729 |
--- a/ios/build/bots/scripts/gtest_utils.py |
+++ b/ios/build/bots/scripts/xctest_utils.py |
@@ -1,136 +1,32 @@ |
-#!/usr/bin/env python |
# Copyright (c) 2016 The Chromium Authors. All rights reserved. |
# Use of this source code is governed by a BSD-style license that can be |
# found in the LICENSE file. |
-import collections |
-import copy |
+import json |
+import os |
import re |
+import tempfile |
# These labels should match the ones output by gtest's JSON. |
TEST_UNKNOWN_LABEL = 'UNKNOWN' |
TEST_SUCCESS_LABEL = 'SUCCESS' |
TEST_FAILURE_LABEL = 'FAILURE' |
+TEST_CRASH_LABEL = 'CRASH' |
TEST_TIMEOUT_LABEL = 'TIMEOUT' |
TEST_WARNING_LABEL = 'WARNING' |
-class GTestResult(object): |
- """A result of gtest. |
- |
- Properties: |
- command: The command argv. |
- crashed: Whether or not the test crashed. |
- crashed_test: The name of the test during which execution crashed, or |
- None if a particular test didn't crash. |
- failed_tests: A dict mapping the names of failed tests to a list of |
- lines of output from those tests. |
- flaked_tests: A dict mapping the names of failed flaky tests to a list |
- of lines of output from those tests. |
- passed_tests: A list of passed tests. |
- perf_links: A dict mapping the names of perf data points collected |
- to links to view those graphs. |
- return_code: The return code of the command. |
- success: Whether or not this run of the command was considered a |
- successful GTest execution. |
- """ |
- @property |
- def crashed(self): |
- return self._crashed |
- |
- @property |
- def crashed_test(self): |
- return self._crashed_test |
- |
- @property |
- def command(self): |
- return self._command |
- |
- @property |
- def failed_tests(self): |
- if self.__finalized: |
- return copy.deepcopy(self._failed_tests) |
- return self._failed_tests |
- |
- @property |
- def flaked_tests(self): |
- if self.__finalized: |
- return copy.deepcopy(self._flaked_tests) |
- return self._flaked_tests |
- |
- @property |
- def passed_tests(self): |
- if self.__finalized: |
- return copy.deepcopy(self._passed_tests) |
- return self._passed_tests |
- |
- @property |
- def perf_links(self): |
- if self.__finalized: |
- return copy.deepcopy(self._perf_links) |
- return self._perf_links |
- |
- @property |
- def return_code(self): |
- return self._return_code |
- |
- @property |
- def success(self): |
- return self._success |
- |
- def __init__(self, command): |
- if not isinstance(command, collections.Iterable): |
- raise ValueError('Expected an iterable of command arguments.', command) |
- |
- if not command: |
- raise ValueError('Expected a non-empty command.', command) |
- |
- self._command = tuple(command) |
- self._crashed = False |
- self._crashed_test = None |
- self._failed_tests = collections.OrderedDict() |
- self._flaked_tests = collections.OrderedDict() |
- self._passed_tests = [] |
- self._perf_links = collections.OrderedDict() |
- self._return_code = None |
- self._success = None |
- self.__finalized = False |
- |
- def finalize(self, return_code, success): |
- self._return_code = return_code |
- self._success = success |
- |
- # If the test was not considered to be a GTest success, but had no |
- # failing tests, conclude that it must have crashed. |
- if not self._success and not self._failed_tests and not self._flaked_tests: |
- self._crashed = True |
- |
- # At most one test can crash the entire app in a given parsing. |
- for test, log_lines in self._failed_tests.iteritems(): |
- # A test with no output would have crashed. No output is replaced |
- # by the GTestLogParser by a sentence indicating non-completion. |
- if 'Did not complete.' in log_lines: |
- self._crashed = True |
- self._crashed_test = test |
- |
- # A test marked as flaky may also have crashed the app. |
- for test, log_lines in self._flaked_tests.iteritems(): |
- if 'Did not complete.' in log_lines: |
- self._crashed = True |
- self._crashed_test = test |
- |
- self.__finalized = True |
- |
- |
-class GTestLogParser(object): |
- """This helper class process GTest test output.""" |
+class XCTestLogParser(object): |
+ """This helper class process XCTest test output.""" |
def __init__(self): |
# State tracking for log parsing |
self.completed = False |
self._current_test = '' |
self._failure_description = [] |
+ self._current_report_hash = '' |
+ self._current_report = [] |
self._parsing_failures = False |
# Line number currently being processed. |
@@ -151,29 +47,17 @@ class GTestLogParser(object): |
self._disabled_tests = 0 |
self._flaky_tests = 0 |
- # Regular expressions for parsing GTest logs. Test names look like |
- # "x.y", with 0 or more "w/" prefixes and 0 or more "/z" suffixes. |
- # e.g.: |
- # SomeName/SomeTestCase.SomeTest/1 |
- # SomeName/SomeTestCase/1.SomeTest |
- # SomeName/SomeTestCase/1.SomeTest/SomeModifider |
- test_name_regexp = r'((\w+/)*\w+\.\w+(/\w+)*)' |
- |
- self._master_name_re = re.compile(r'\[Running for master: "([^"]*)"') |
- self.master_name = '' |
- |
+ test_name_regexp = r'\-\[(\w+)\s(\w+)\]' |
self._test_name = re.compile(test_name_regexp) |
- self._test_start = re.compile(r'\[\s+RUN\s+\] ' + test_name_regexp) |
- self._test_ok = re.compile(r'\[\s+OK\s+\] ' + test_name_regexp) |
- self._test_fail = re.compile(r'\[\s+FAILED\s+\] ' + test_name_regexp) |
- self._test_passed = re.compile(r'\[\s+PASSED\s+\] \d+ tests?.') |
- self._run_test_cases_line = re.compile( |
- r'\[\s*\d+\/\d+\]\s+[0-9\.]+s ' + test_name_regexp + ' .+') |
- self._test_timeout = re.compile( |
- r'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp) |
- self._disabled = re.compile(r'\s*YOU HAVE (\d+) DISABLED TEST') |
- self._flaky = re.compile(r'\s*YOU HAVE (\d+) FLAKY TEST') |
- |
+ self._test_start = re.compile( |
+ r'Test Case \'' + test_name_regexp + '\' started\.') |
+ self._test_ok = re.compile( |
+ r'Test Case \'' + test_name_regexp + |
+ '\' passed\s+\(\d+\.\d+\s+seconds\)?.') |
+ self._test_fail = re.compile( |
+ r'Test Case \'' + test_name_regexp + |
+ '\' failed\s+\(\d+\.\d+\s+seconds\)?.') |
+ self._test_passed = re.compile(r'\*\*\s+TEST\s+EXECUTE\s+SUCCEEDED\s+\*\*') |
self._retry_message = re.compile('RETRYING FAILED TESTS:') |
self.retrying_failed = False |
@@ -264,18 +148,6 @@ class GTestLogParser(object): |
return [self.TEST_STATUS_MAP.get(self._StatusOfTest(test), |
TEST_UNKNOWN_LABEL)] |
- def DisabledTests(self): |
- """Returns the name of the disabled test (if there is only 1) or the number |
- of disabled tests. |
- """ |
- return self._disabled_tests |
- |
- def FlakyTests(self): |
- """Returns the name of the flaky test (if there is only 1) or the number |
- of flaky tests. |
- """ |
- return self._flaky_tests |
- |
def FailureDescription(self, test): |
"""Returns a list containing the failure description for the given test. |
@@ -286,7 +158,7 @@ class GTestLogParser(object): |
def CompletedWithoutFailure(self): |
"""Returns True if all tests completed and no tests failed unexpectedly.""" |
- return self.completed and not self.FailedTests() |
+ return self.completed |
def ProcessLine(self, line): |
"""This is called once with each line of the test log.""" |
@@ -326,27 +198,6 @@ class GTestLogParser(object): |
Will recognize newly started tests, OK or FAILED statuses, timeouts, etc. |
""" |
- # Note: When sharding, the number of disabled and flaky tests will be read |
- # multiple times, so this will only show the most recent values (but they |
- # should all be the same anyway). |
- |
- # Is it a line listing the master name? |
- if not self.master_name: |
- results = self._master_name_re.match(line) |
- if results: |
- self.master_name = results.group(1) |
- |
- results = self._run_test_cases_line.match(line) |
- if results: |
- # A run_test_cases.py output. |
- if self._current_test: |
- if self._test_status[self._current_test][0] == 'started': |
- self._test_status[self._current_test] = ( |
- 'timeout', self._failure_description) |
- self._current_test = '' |
- self._failure_description = [] |
- return |
- |
# Is it a line declaring all tests passed? |
results = self._test_passed.match(line) |
if results: |
@@ -354,36 +205,6 @@ class GTestLogParser(object): |
self._current_test = '' |
return |
- # Is it a line reporting disabled tests? |
- results = self._disabled.match(line) |
- if results: |
- try: |
- disabled = int(results.group(1)) |
- except ValueError: |
- disabled = 0 |
- if disabled > 0 and isinstance(self._disabled_tests, int): |
- self._disabled_tests = disabled |
- else: |
- # If we can't parse the line, at least give a heads-up. This is a |
- # safety net for a case that shouldn't happen but isn't a fatal error. |
- self._disabled_tests = 'some' |
- return |
- |
- # Is it a line reporting flaky tests? |
- results = self._flaky.match(line) |
- if results: |
- try: |
- flaky = int(results.group(1)) |
- except ValueError: |
- flaky = 0 |
- if flaky > 0 and isinstance(self._flaky_tests, int): |
- self._flaky_tests = flaky |
- else: |
- # If we can't parse the line, at least give a heads-up. This is a |
- # safety net for a case that shouldn't happen but isn't a fatal error. |
- self._flaky_tests = 'some' |
- return |
- |
# Is it the start of a test? |
results = self._test_start.match(line) |
if results: |
@@ -391,7 +212,7 @@ class GTestLogParser(object): |
if self._test_status[self._current_test][0] == 'started': |
self._test_status[self._current_test] = ( |
'timeout', self._failure_description) |
- test_name = results.group(1) |
+ test_name = '%s.%s' % (results.group(1), results.group(2)) |
self._test_status[test_name] = ('started', ['Did not complete.']) |
self._current_test = test_name |
if self.retrying_failed: |
@@ -404,7 +225,7 @@ class GTestLogParser(object): |
# Is it a test success line? |
results = self._test_ok.match(line) |
if results: |
- test_name = results.group(1) |
+ test_name = '%s.%s' % (results.group(1), results.group(2)) |
status = self._StatusOfTest(test_name) |
if status != 'started': |
self._RecordError(line, 'success while in status %s' % status) |
@@ -419,7 +240,7 @@ class GTestLogParser(object): |
# Is it a test failure line? |
results = self._test_fail.match(line) |
if results: |
- test_name = results.group(1) |
+ test_name = '%s.%s' % (results.group(1), results.group(2)) |
status = self._StatusOfTest(test_name) |
if status not in ('started', 'failed', 'timeout'): |
self._RecordError(line, 'failure while in status %s' % status) |
@@ -432,19 +253,6 @@ class GTestLogParser(object): |
self._current_test = '' |
return |
- # Is it a test timeout line? |
- results = self._test_timeout.search(line) |
- if results: |
- test_name = results.group(1) |
- status = self._StatusOfTest(test_name) |
- if status not in ('started', 'failed'): |
- self._RecordError(line, 'timeout while in status %s' % status) |
- self._test_status[test_name] = ( |
- 'timeout', self._failure_description + ['Killed (timed out).']) |
- self._failure_description = [] |
- self._current_test = '' |
- return |
- |
# Is it the start of the retry tests? |
results = self._retry_message.match(line) |
if results: |
@@ -456,19 +264,3 @@ class GTestLogParser(object): |
# This also won't work if a test times out before it begins running. |
if self._current_test: |
self._failure_description.append(line) |
- |
- # Parse the "Failing tests:" list at the end of the output, and add any |
- # additional failed tests to the list. For example, this includes tests |
- # that crash after the OK line. |
- if self._parsing_failures: |
- results = self._test_name.match(line) |
- if results: |
- test_name = results.group(1) |
- status = self._StatusOfTest(test_name) |
- if status in ('not known', 'OK'): |
- self._test_status[test_name] = ( |
- 'failed', ['Unknown error, see stdio log.']) |
- else: |
- self._parsing_failures = False |
- elif line.startswith('Failing tests:'): |
- self._parsing_failures = True |