| 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 """A buildbot command for running and interpreting GTest tests.""" | 6 """A buildbot command for running and interpreting GTest tests.""" |
| 7 | 7 |
| 8 import fileinput | 8 import fileinput |
| 9 import re | 9 import re |
| 10 import sys | 10 import sys |
| 11 from buildbot.steps import shell | 11 from buildbot.steps import shell |
| 12 from buildbot.status import builder | 12 from buildbot.status import builder |
| 13 from buildbot.process import buildstep | 13 from buildbot.process import buildstep |
| 14 | 14 |
| 15 class TestObserver(buildstep.LogLineObserver): | 15 class TestObserver(buildstep.LogLineObserver): |
| 16 """This class knows how to understand GTest test output.""" | 16 """This class knows how to understand GTest test output.""" |
| 17 # TestAbbrFromTestID needs to be a member function. | 17 # TestAbbrFromTestID needs to be a member function. |
| 18 # pylint: disable=R0201 | 18 # pylint: disable=R0201 |
| 19 | 19 |
| 20 def __init__(self): | 20 def __init__(self): |
| 21 buildstep.LogLineObserver.__init__(self) | 21 buildstep.LogLineObserver.__init__(self) |
| 22 | 22 |
| 23 # State tracking for log parsing | 23 # State tracking for log parsing |
| 24 self._current_test = '' | 24 self._current_test = '' |
| 25 self._failure_description = [] | 25 self._failure_description = [] |
| 26 self._current_suppression_hash = '' | 26 self._current_suppression_hash = '' |
| 27 self._current_suppression = [] | 27 self._current_suppression = [] |
| 28 self._parsing_failures = False |
| 28 | 29 |
| 29 # Line number currently being processed. | 30 # Line number currently being processed. |
| 30 self._line_number = 0 | 31 self._line_number = 0 |
| 31 | 32 |
| 32 # List of parsing errors, as human-readable strings. | 33 # List of parsing errors, as human-readable strings. |
| 33 self.internal_error_lines = [] | 34 self.internal_error_lines = [] |
| 34 | 35 |
| 35 # Tests are stored here as 'test.name': (status, [description]). | 36 # Tests are stored here as 'test.name': (status, [description]). |
| 36 # The status should be one of ('started', 'OK', 'failed', 'timeout', | 37 # The status should be one of ('started', 'OK', 'failed', 'timeout', |
| 37 # 'warning'). Warning indicates that a test did not pass when run in | 38 # 'warning'). Warning indicates that a test did not pass when run in |
| 38 # parallel with other tests but passed when run alone. The description is | 39 # parallel with other tests but passed when run alone. The description is |
| 39 # a list of lines detailing the test's error, as reported in the log. | 40 # a list of lines detailing the test's error, as reported in the log. |
| 40 self._test_status = {} | 41 self._test_status = {} |
| 41 | 42 |
| 42 # Suppressions are stored here as 'hash': [suppression]. | 43 # Suppressions are stored here as 'hash': [suppression]. |
| 43 self._suppressions = {} | 44 self._suppressions = {} |
| 44 | 45 |
| 45 # This may be either text or a number. It will be used in the phrase | 46 # This may be either text or a number. It will be used in the phrase |
| 46 # '%s disabled' or '%s flaky' on the waterfall display. | 47 # '%s disabled' or '%s flaky' on the waterfall display. |
| 47 self.disabled_tests = 0 | 48 self.disabled_tests = 0 |
| 48 self.flaky_tests = 0 | 49 self.flaky_tests = 0 |
| 49 | 50 |
| 50 # Regular expressions for parsing GTest logs. Test names look like | 51 # Regular expressions for parsing GTest logs. Test names look like |
| 51 # SomeTestCase.SomeTest | 52 # SomeTestCase.SomeTest |
| 52 # SomeName/SomeTestCase.SomeTest/1 | 53 # SomeName/SomeTestCase.SomeTest/1 |
| 53 # This regexp also matches SomeName.SomeTest/1, which should be harmless. | 54 # This regexp also matches SomeName.SomeTest/1, which should be harmless. |
| 54 test_name_regexp = r'((\w+/)?\w+\.\w+(/\d+)?)' | 55 test_name_regexp = r'((\w+/)?\w+\.\w+(/\d+)?)' |
| 55 | 56 |
| 57 self._test_name = re.compile(test_name_regexp) |
| 56 self._test_start = re.compile('\[\s+RUN\s+\] ' + test_name_regexp) | 58 self._test_start = re.compile('\[\s+RUN\s+\] ' + test_name_regexp) |
| 57 self._test_ok = re.compile('\[\s+OK\s+\] ' + test_name_regexp) | 59 self._test_ok = re.compile('\[\s+OK\s+\] ' + test_name_regexp) |
| 58 self._test_fail = re.compile('\[\s+FAILED\s+\] ' + test_name_regexp) | 60 self._test_fail = re.compile('\[\s+FAILED\s+\] ' + test_name_regexp) |
| 59 self._test_timeout = re.compile( | 61 self._test_timeout = re.compile( |
| 60 'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp) | 62 'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp) |
| 61 self._disabled = re.compile(' YOU HAVE (\d+) DISABLED TEST') | 63 self._disabled = re.compile(' YOU HAVE (\d+) DISABLED TEST') |
| 62 self._flaky = re.compile(' YOU HAVE (\d+) FLAKY TEST') | 64 self._flaky = re.compile(' YOU HAVE (\d+) FLAKY TEST') |
| 63 | 65 |
| 64 self._suppression_start = re.compile( | 66 self._suppression_start = re.compile( |
| 65 'Suppression \(error hash=#([0-9A-F]+)#\):') | 67 'Suppression \(error hash=#([0-9A-F]+)#\):') |
| (...skipping 216 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 282 if self._current_suppression_hash: | 284 if self._current_suppression_hash: |
| 283 self._current_suppression.append(line) | 285 self._current_suppression.append(line) |
| 284 return | 286 return |
| 285 | 287 |
| 286 # Random line: if we're in a test, collect it for the failure description. | 288 # Random line: if we're in a test, collect it for the failure description. |
| 287 # Tests may run simultaneously, so this might be off, but it's worth a try. | 289 # Tests may run simultaneously, so this might be off, but it's worth a try. |
| 288 # This also won't work if a test times out before it begins running. | 290 # This also won't work if a test times out before it begins running. |
| 289 if self._current_test: | 291 if self._current_test: |
| 290 self._failure_description.append(line) | 292 self._failure_description.append(line) |
| 291 | 293 |
| 294 # Parse the "Failing tests:" list at the end of the output, and add any |
| 295 # additional failed tests to the list. For example, this includes tests |
| 296 # that crash after the OK line. |
| 297 if self._parsing_failures: |
| 298 results = self._test_name.search(line) |
| 299 if results: |
| 300 test_name = results.group(1) |
| 301 status = self._StatusOfTest(test_name) |
| 302 if status in ('not known', 'OK'): |
| 303 self._test_status[test_name] = ( |
| 304 'failed', ['Unknown error, see stdio log.']) |
| 305 else: |
| 306 self._parsing_failures = False |
| 307 elif line.startswith('Failing tests:'): |
| 308 self._parsing_failures = True |
| 292 | 309 |
| 293 class GTestCommand(shell.ShellCommand): | 310 class GTestCommand(shell.ShellCommand): |
| 294 """Buildbot command that knows how to display GTest output.""" | 311 """Buildbot command that knows how to display GTest output.""" |
| 295 # TestAbbrFromTestID needs to be a member function. | 312 # TestAbbrFromTestID needs to be a member function. |
| 296 # pylint: disable=R0201 | 313 # pylint: disable=R0201 |
| 297 | 314 |
| 298 _GTEST_DASHBOARD_BASE = ("http://test-results.appspot.com" | 315 _GTEST_DASHBOARD_BASE = ("http://test-results.appspot.com" |
| 299 "/dashboards/flakiness_dashboard.html") | 316 "/dashboards/flakiness_dashboard.html") |
| 300 | 317 |
| 301 def __init__(self, **kwargs): | 318 def __init__(self, **kwargs): |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 390 print 'Failed tests:\n' | 407 print 'Failed tests:\n' |
| 391 for failed_test in observer.FailedTests(True, True): | 408 for failed_test in observer.FailedTests(True, True): |
| 392 for fail_line in observer.FailureDescription(failed_test): | 409 for fail_line in observer.FailureDescription(failed_test): |
| 393 print fail_line.strip() | 410 print fail_line.strip() |
| 394 print '' | 411 print '' |
| 395 return 0 | 412 return 0 |
| 396 | 413 |
| 397 | 414 |
| 398 if '__main__' == __name__: | 415 if '__main__' == __name__: |
| 399 sys.exit(Main()) | 416 sys.exit(Main()) |
| OLD | NEW |