Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """A buildbot command for running and interpreting GTest tests.""" | 5 """A buildbot command for running and interpreting GTest tests.""" |
| 6 | 6 |
| 7 import re | 7 import re |
| 8 from buildbot.steps import shell | 8 from buildbot.steps import shell |
| 9 from buildbot.status import builder | 9 from buildbot.status import builder |
| 10 from buildbot.process import buildstep | 10 from buildbot.process import buildstep |
| 11 | 11 |
| 12 class TestObserver(buildstep.LogLineObserver): | 12 class TestObserver(buildstep.LogLineObserver): |
| 13 """This class knows how to understand GTest test output.""" | 13 """This class knows how to understand GTest test output.""" |
| 14 # TestAbbrFromTestID needs to be a member function. | 14 # TestAbbrFromTestID needs to be a member function. |
| 15 # pylint: disable=R0201 | 15 # pylint: disable=R0201 |
| 16 | 16 |
| 17 def __init__(self): | 17 def __init__(self): |
| 18 buildstep.LogLineObserver.__init__(self) | 18 buildstep.LogLineObserver.__init__(self) |
| 19 | 19 |
| 20 # State tracking for log parsing | 20 # State tracking for log parsing |
| 21 self._current_test = '' | 21 self._current_test = '' |
| 22 self._failure_description = [] | 22 self._failure_description = [] |
| 23 self._current_suppression_hash = '' | 23 self._current_suppression_hash = '' |
| 24 self._current_suppression = [] | 24 self._current_suppression = [] |
| 25 self._parsing_failures = False | |
| 25 | 26 |
| 26 # Line number currently being processed. | 27 # Line number currently being processed. |
| 27 self._line_number = 0 | 28 self._line_number = 0 |
| 28 | 29 |
| 29 # List of parsing errors, as human-readable strings. | 30 # List of parsing errors, as human-readable strings. |
| 30 self.internal_error_lines = [] | 31 self.internal_error_lines = [] |
| 31 | 32 |
| 32 # Tests are stored here as 'test.name': (status, [description]). | 33 # Tests are stored here as 'test.name': (status, [description]). |
| 33 # The status should be one of ('started', 'OK', 'failed', 'timeout', | 34 # The status should be one of ('started', 'OK', 'failed', 'timeout', |
| 34 # 'warning'). Warning indicates that a test did not pass when run in | 35 # 'warning'). Warning indicates that a test did not pass when run in |
| 35 # parallel with other tests but passed when run alone. The description is | 36 # parallel with other tests but passed when run alone. The description is |
| 36 # a list of lines detailing the test's error, as reported in the log. | 37 # a list of lines detailing the test's error, as reported in the log. |
| 37 self._test_status = {} | 38 self._test_status = {} |
| 38 | 39 |
| 39 # Suppressions are stored here as 'hash': [suppression]. | 40 # Suppressions are stored here as 'hash': [suppression]. |
| 40 self._suppressions = {} | 41 self._suppressions = {} |
| 41 | 42 |
| 42 # This may be either text or a number. It will be used in the phrase | 43 # This may be either text or a number. It will be used in the phrase |
| 43 # '%s disabled' or '%s flaky' on the waterfall display. | 44 # '%s disabled' or '%s flaky' on the waterfall display. |
| 44 self.disabled_tests = 0 | 45 self.disabled_tests = 0 |
| 45 self.flaky_tests = 0 | 46 self.flaky_tests = 0 |
| 46 | 47 |
| 47 # Regular expressions for parsing GTest logs. Test names look like | 48 # Regular expressions for parsing GTest logs. Test names look like |
| 48 # SomeTestCase.SomeTest | 49 # SomeTestCase.SomeTest |
| 49 # SomeName/SomeTestCase.SomeTest/1 | 50 # SomeName/SomeTestCase.SomeTest/1 |
| 50 # This regexp also matches SomeName.SomeTest/1, which should be harmless. | 51 # This regexp also matches SomeName.SomeTest/1, which should be harmless. |
| 51 test_name_regexp = r'((\w+/)?\w+\.\w+(/\d+)?)' | 52 test_name_regexp = r'((\w+/)?\w+\.\w+(/\d+)?)' |
| 52 | 53 |
| 54 self._test_name = re.compile(test_name_regexp) | |
| 53 self._test_start = re.compile('\[\s+RUN\s+\] ' + test_name_regexp) | 55 self._test_start = re.compile('\[\s+RUN\s+\] ' + test_name_regexp) |
| 54 self._test_ok = re.compile('\[\s+OK\s+\] ' + test_name_regexp) | 56 self._test_ok = re.compile('\[\s+OK\s+\] ' + test_name_regexp) |
| 55 self._test_fail = re.compile('\[\s+FAILED\s+\] ' + test_name_regexp) | 57 self._test_fail = re.compile('\[\s+FAILED\s+\] ' + test_name_regexp) |
| 56 self._test_timeout = re.compile( | 58 self._test_timeout = re.compile( |
| 57 'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp) | 59 'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp) |
| 58 self._disabled = re.compile(' YOU HAVE (\d+) DISABLED TEST') | 60 self._disabled = re.compile(' YOU HAVE (\d+) DISABLED TEST') |
| 59 self._flaky = re.compile(' YOU HAVE (\d+) FLAKY TEST') | 61 self._flaky = re.compile(' YOU HAVE (\d+) FLAKY TEST') |
| 60 | 62 |
| 61 self._suppression_start = re.compile( | 63 self._suppression_start = re.compile( |
| 62 'Suppression \(error hash=#([0-9A-F]+)#\):') | 64 'Suppression \(error hash=#([0-9A-F]+)#\):') |
| (...skipping 216 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 279 if self._current_suppression_hash: | 281 if self._current_suppression_hash: |
| 280 self._current_suppression.append(line) | 282 self._current_suppression.append(line) |
| 281 return | 283 return |
| 282 | 284 |
| 283 # Random line: if we're in a test, collect it for the failure description. | 285 # Random line: if we're in a test, collect it for the failure description. |
| 284 # Tests may run simultaneously, so this might be off, but it's worth a try. | 286 # Tests may run simultaneously, so this might be off, but it's worth a try. |
| 285 # This also won't work if a test times out before it begins running. | 287 # This also won't work if a test times out before it begins running. |
| 286 if self._current_test: | 288 if self._current_test: |
| 287 self._failure_description.append(line) | 289 self._failure_description.append(line) |
| 288 | 290 |
| 291 # Parse the "Failing tests:" list at the end of the output, and add any | |
| 292 # additional failed tests to the list. For example, this includes tests | |
| 293 # that crash after the OK line. | |
| 294 if self._parsing_failures: | |
| 295 results = self._test_name.search(line) | |
| 296 if results: | |
| 297 test_name = results.group(1) | |
| 298 if self._StatusOfTest(test_name) == 'OK': | |
| 299 self._test_status[test_name] = ( | |
| 300 'failed', ['Unknown error, see stdio log.']) | |
| 301 else: | |
| 302 self._parsing_failures = False | |
| 303 elif line.find('Failing tests:') != -1: | |
|
M-A Ruel
2012/01/06 17:07:36
elif 'Failing tests:' in line:
Alexei Svitkine (slow)
2012/01/06 18:08:03
Changed to startswith(), which is more precise.
| |
| 304 self._parsing_failures = True | |
| 289 | 305 |
| 290 class GTestCommand(shell.ShellCommand): | 306 class GTestCommand(shell.ShellCommand): |
| 291 """Buildbot command that knows how to display GTest output.""" | 307 """Buildbot command that knows how to display GTest output.""" |
| 292 # TestAbbrFromTestID needs to be a member function. | 308 # TestAbbrFromTestID needs to be a member function. |
| 293 # pylint: disable=R0201 | 309 # pylint: disable=R0201 |
| 294 | 310 |
| 295 _GTEST_DASHBOARD_BASE = ("http://test-results.appspot.com" | 311 _GTEST_DASHBOARD_BASE = ("http://test-results.appspot.com" |
| 296 "/dashboards/flakiness_dashboard.html") | 312 "/dashboards/flakiness_dashboard.html") |
| 297 | 313 |
| 298 def __init__(self, **kwargs): | 314 def __init__(self, **kwargs): |
| (...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 371 self.addCompleteLog(suppression_hash, | 387 self.addCompleteLog(suppression_hash, |
| 372 '\n'.join(observer.Suppression(suppression_hash))) | 388 '\n'.join(observer.Suppression(suppression_hash))) |
| 373 | 389 |
| 374 | 390 |
| 375 class GTestFullCommand(GTestCommand): | 391 class GTestFullCommand(GTestCommand): |
| 376 def TestAbbrFromTestID(self, testid): | 392 def TestAbbrFromTestID(self, testid): |
| 377 """ | 393 """ |
| 378 Return the full TestCase.TestName ID. | 394 Return the full TestCase.TestName ID. |
| 379 """ | 395 """ |
| 380 return testid | 396 return testid |
| OLD | NEW |