1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2017 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li"""Utility class to parse the output of a gtest suite run.""" 7*9c5db199SXin Li 8*9c5db199SXin Liimport re 9*9c5db199SXin Li 10*9c5db199SXin Li 11*9c5db199SXin Liclass gtest_parser(object): 12*9c5db199SXin Li """This class knows how to understand GTest test output. 13*9c5db199SXin Li 14*9c5db199SXin Li The code was borrowed with minor changes from chrome utility gtest_command. 15*9c5db199SXin Li """ 16*9c5db199SXin Li 17*9c5db199SXin Li def __init__(self): 18*9c5db199SXin Li # State tracking for log parsing 19*9c5db199SXin Li self._current_test = '' 20*9c5db199SXin Li self._failure_description = [] 21*9c5db199SXin Li self._current_suppression_hash = '' 22*9c5db199SXin Li self._current_suppression = [] 23*9c5db199SXin Li 24*9c5db199SXin Li # Line number currently being processed. 25*9c5db199SXin Li self._line_number = 0 26*9c5db199SXin Li 27*9c5db199SXin Li # List of parsing errors, as human-readable strings. 28*9c5db199SXin Li self.internal_error_lines = [] 29*9c5db199SXin Li 30*9c5db199SXin Li # Tests are stored here as 'test.name': (status, [description]). 31*9c5db199SXin Li # The status should be one of ('started', 'OK', 'failed', 'timeout'). 32*9c5db199SXin Li # The description is a list of lines detailing the test's error, as 33*9c5db199SXin Li # reported in the log. 34*9c5db199SXin Li self._test_status = {} 35*9c5db199SXin Li 36*9c5db199SXin Li # Suppressions are stored here as 'hash': [suppression]. 37*9c5db199SXin Li self._suppressions = {} 38*9c5db199SXin Li 39*9c5db199SXin Li # This may be either text or a number. It will be used in the phrase 40*9c5db199SXin Li # '%s disabled' or '%s flaky' on the waterfall display. 41*9c5db199SXin Li self.disabled_tests = 0 42*9c5db199SXin Li self.flaky_tests = 0 43*9c5db199SXin Li 44*9c5db199SXin Li # Regular expressions for parsing GTest logs. Test names look like 45*9c5db199SXin Li # SomeTestCase.SomeTest 46*9c5db199SXin Li # SomeName/SomeTestCase.SomeTest/1 47*9c5db199SXin Li # This regexp also matches SomeName.SomeTest/1, which should be 48*9c5db199SXin Li # harmless. 49*9c5db199SXin Li test_name_regexp = r'((\w+/)?\w+\.\w+(\.\w+)?(/\d+)?)' 50*9c5db199SXin Li self._test_start = re.compile(r'\[\s+RUN\s+\] ' + test_name_regexp) 51*9c5db199SXin Li self._test_ok = re.compile(r'\[\s+OK\s+\] ' + test_name_regexp) 52*9c5db199SXin Li self._test_fail = re.compile(r'\[\s+FAILED\s+\] ' + test_name_regexp) 53*9c5db199SXin Li self._test_timeout = re.compile( 54*9c5db199SXin Li r'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp) 55*9c5db199SXin Li self._disabled = re.compile(r' YOU HAVE (\d+) DISABLED TEST') 56*9c5db199SXin Li self._flaky = re.compile(r' YOU HAVE (\d+) FLAKY TEST') 57*9c5db199SXin Li 58*9c5db199SXin Li self._suppression_start = re.compile( 59*9c5db199SXin Li r'Suppression \(error hash=#([0-9A-F]+)#\):') 60*9c5db199SXin Li self._suppression_end = re.compile(r'^}\s*$') 61*9c5db199SXin Li 62*9c5db199SXin Li # TODO b:169251326 terms below are set outside of this codebase 63*9c5db199SXin Li # and should be updated when possible. ("master" -> "main") 64*9c5db199SXin Li self._main_name_re = re.compile( 65*9c5db199SXin Li r'\[Running for master: "([^"]*)"') # nocheck 66*9c5db199SXin Li self.main_name = '' 67*9c5db199SXin Li 68*9c5db199SXin Li self._error_logging_start_re = re.compile('=' * 70) 69*9c5db199SXin Li self._error_logging_test_name_re = re.compile( 70*9c5db199SXin Li '[FAIL|ERROR]: ' + test_name_regexp) 71*9c5db199SXin Li self._error_logging_end_re = re.compile('-' * 70) 72*9c5db199SXin Li self._error_logging_first_dash_found = False 73*9c5db199SXin Li 74*9c5db199SXin Li def _TestsByStatus(self, status, include_fails, include_flaky): 75*9c5db199SXin Li """Returns list of tests with the given status. 76*9c5db199SXin Li 77*9c5db199SXin Li Args: 78*9c5db199SXin Li status: test results status to search for. 79*9c5db199SXin Li include_fails: If False, tests containing 'FAILS_' anywhere in 80*9c5db199SXin Li their names will be excluded from the list. 81*9c5db199SXin Li include_flaky: If False, tests containing 'FLAKY_' anywhere in 82*9c5db199SXin Li their names will be excluded from the list. 83*9c5db199SXin Li Returns: 84*9c5db199SXin Li List of tests with the status. 85*9c5db199SXin Li """ 86*9c5db199SXin Li test_list = [x[0] for x in self._test_status.items() 87*9c5db199SXin Li if self._StatusOfTest(x[0]) == status] 88*9c5db199SXin Li 89*9c5db199SXin Li if not include_fails: 90*9c5db199SXin Li test_list = [x for x in test_list if x.find('FAILS_') == -1] 91*9c5db199SXin Li if not include_flaky: 92*9c5db199SXin Li test_list = [x for x in test_list if x.find('FLAKY_') == -1] 93*9c5db199SXin Li 94*9c5db199SXin Li return test_list 95*9c5db199SXin Li 96*9c5db199SXin Li def _StatusOfTest(self, test): 97*9c5db199SXin Li """Returns the status code for the given test, or 'not known'.""" 98*9c5db199SXin Li test_status = self._test_status.get(test, ('not known', [])) 99*9c5db199SXin Li return test_status[0] 100*9c5db199SXin Li 101*9c5db199SXin Li def _RecordError(self, line, reason): 102*9c5db199SXin Li """Record a log line that produced a parsing error. 103*9c5db199SXin Li 104*9c5db199SXin Li Args: 105*9c5db199SXin Li line: text of the line at which the error occurred. 106*9c5db199SXin Li reason: a string describing the error. 107*9c5db199SXin Li """ 108*9c5db199SXin Li self.internal_error_lines.append("%s: %s [%s]" % (self._line_number, 109*9c5db199SXin Li line.strip(), 110*9c5db199SXin Li reason)) 111*9c5db199SXin Li 112*9c5db199SXin Li def TotalTests(self): 113*9c5db199SXin Li """Returns the number of parsed tests.""" 114*9c5db199SXin Li return len(self._test_status) 115*9c5db199SXin Li 116*9c5db199SXin Li def PassedTests(self): 117*9c5db199SXin Li """Returns list of tests that passed.""" 118*9c5db199SXin Li return self._TestsByStatus('OK', False, False) 119*9c5db199SXin Li 120*9c5db199SXin Li def FailedTests(self, include_fails=False, include_flaky=False): 121*9c5db199SXin Li """Returns list of tests that failed, timed out, or didn't finish. 122*9c5db199SXin Li 123*9c5db199SXin Li This list will be incorrect until the complete log has been processed, 124*9c5db199SXin Li because it will show currently running tests as having failed. 125*9c5db199SXin Li 126*9c5db199SXin Li Args: 127*9c5db199SXin Li include_fails: If true, all failing tests with FAILS_ in their 128*9c5db199SXin Li names will be included. Otherwise, they will only be included 129*9c5db199SXin Li if they crashed. 130*9c5db199SXin Li include_flaky: If true, all failing tests with FLAKY_ in their 131*9c5db199SXin Li names will be included. Otherwise, they will only be included 132*9c5db199SXin Li if they crashed. 133*9c5db199SXin Li Returns: 134*9c5db199SXin Li List of failed tests. 135*9c5db199SXin Li """ 136*9c5db199SXin Li return (self._TestsByStatus('failed', include_fails, include_flaky) + 137*9c5db199SXin Li self._TestsByStatus('timeout', include_fails, include_flaky) + 138*9c5db199SXin Li self._TestsByStatus('started', include_fails, include_flaky)) 139*9c5db199SXin Li 140*9c5db199SXin Li def FailureDescription(self, test): 141*9c5db199SXin Li """Returns a list containing the failure description for the given test. 142*9c5db199SXin Li 143*9c5db199SXin Li If the test didn't fail or timeout, returns []. 144*9c5db199SXin Li Args: 145*9c5db199SXin Li test: Name to test to find failure reason. 146*9c5db199SXin Li Returns: 147*9c5db199SXin Li List of test name, and failure string. 148*9c5db199SXin Li """ 149*9c5db199SXin Li test_status = self._test_status.get(test, ('', [])) 150*9c5db199SXin Li return test_status[1] 151*9c5db199SXin Li 152*9c5db199SXin Li def SuppressionHashes(self): 153*9c5db199SXin Li """Returns list of suppression hashes found in the log.""" 154*9c5db199SXin Li return list(self._suppressions.keys()) 155*9c5db199SXin Li 156*9c5db199SXin Li def Suppression(self, suppression_hash): 157*9c5db199SXin Li """Returns a list containing the suppression for a given hash. 158*9c5db199SXin Li 159*9c5db199SXin Li If the suppression hash doesn't exist, returns []. 160*9c5db199SXin Li 161*9c5db199SXin Li Args: 162*9c5db199SXin Li suppression_hash: name of hash. 163*9c5db199SXin Li Returns: 164*9c5db199SXin Li List of suppression for the hash. 165*9c5db199SXin Li """ 166*9c5db199SXin Li return self._suppressions.get(suppression_hash, []) 167*9c5db199SXin Li 168*9c5db199SXin Li def ProcessLogLine(self, line): 169*9c5db199SXin Li """This is called once with each line of the test log.""" 170*9c5db199SXin Li 171*9c5db199SXin Li # Track line number for error messages. 172*9c5db199SXin Li self._line_number += 1 173*9c5db199SXin Li 174*9c5db199SXin Li if not self.main_name: 175*9c5db199SXin Li results = self._main_name_re.search(line) 176*9c5db199SXin Li if results: 177*9c5db199SXin Li self.main_name = results.group(1) 178*9c5db199SXin Li 179*9c5db199SXin Li # Is it a line reporting disabled tests? 180*9c5db199SXin Li results = self._disabled.search(line) 181*9c5db199SXin Li if results: 182*9c5db199SXin Li try: 183*9c5db199SXin Li disabled = int(results.group(1)) 184*9c5db199SXin Li except ValueError: 185*9c5db199SXin Li disabled = 0 186*9c5db199SXin Li if disabled > 0 and isinstance(self.disabled_tests, int): 187*9c5db199SXin Li self.disabled_tests += disabled 188*9c5db199SXin Li else: 189*9c5db199SXin Li # If we can't parse the line, at least give a heads-up. This is 190*9c5db199SXin Li # a safety net for a case that shouldn't happen but isn't a 191*9c5db199SXin Li # fatal error. 192*9c5db199SXin Li self.disabled_tests = 'some' 193*9c5db199SXin Li return 194*9c5db199SXin Li 195*9c5db199SXin Li # Is it a line reporting flaky tests? 196*9c5db199SXin Li results = self._flaky.search(line) 197*9c5db199SXin Li if results: 198*9c5db199SXin Li try: 199*9c5db199SXin Li flaky = int(results.group(1)) 200*9c5db199SXin Li except ValueError: 201*9c5db199SXin Li flaky = 0 202*9c5db199SXin Li if flaky > 0 and isinstance(self.flaky_tests, int): 203*9c5db199SXin Li self.flaky_tests = flaky 204*9c5db199SXin Li else: 205*9c5db199SXin Li # If we can't parse the line, at least give a heads-up. This is 206*9c5db199SXin Li # a safety net for a case that shouldn't happen but isn't a 207*9c5db199SXin Li # fatal error. 208*9c5db199SXin Li self.flaky_tests = 'some' 209*9c5db199SXin Li return 210*9c5db199SXin Li 211*9c5db199SXin Li # Is it the start of a test? 212*9c5db199SXin Li results = self._test_start.search(line) 213*9c5db199SXin Li if results: 214*9c5db199SXin Li test_name = results.group(1) 215*9c5db199SXin Li if test_name in self._test_status: 216*9c5db199SXin Li self._RecordError(line, 'test started more than once') 217*9c5db199SXin Li return 218*9c5db199SXin Li if self._current_test: 219*9c5db199SXin Li status = self._StatusOfTest(self._current_test) 220*9c5db199SXin Li if status in ('OK', 'failed', 'timeout'): 221*9c5db199SXin Li self._RecordError(line, 'start while in status %s' % status) 222*9c5db199SXin Li return 223*9c5db199SXin Li if status not in ('failed', 'timeout'): 224*9c5db199SXin Li self._test_status[self._current_test] = ( 225*9c5db199SXin Li 'failed', self._failure_description) 226*9c5db199SXin Li self._test_status[test_name] = ('started', ['Did not complete.']) 227*9c5db199SXin Li self._current_test = test_name 228*9c5db199SXin Li self._failure_description = [] 229*9c5db199SXin Li return 230*9c5db199SXin Li 231*9c5db199SXin Li # Is it a test success line? 232*9c5db199SXin Li results = self._test_ok.search(line) 233*9c5db199SXin Li if results: 234*9c5db199SXin Li test_name = results.group(1) 235*9c5db199SXin Li status = self._StatusOfTest(test_name) 236*9c5db199SXin Li if status != 'started': 237*9c5db199SXin Li self._RecordError(line, 'success while in status %s' % status) 238*9c5db199SXin Li return 239*9c5db199SXin Li self._test_status[test_name] = ('OK', []) 240*9c5db199SXin Li self._failure_description = [] 241*9c5db199SXin Li self._current_test = '' 242*9c5db199SXin Li return 243*9c5db199SXin Li 244*9c5db199SXin Li # Is it a test failure line? 245*9c5db199SXin Li results = self._test_fail.search(line) 246*9c5db199SXin Li if results: 247*9c5db199SXin Li test_name = results.group(1) 248*9c5db199SXin Li status = self._StatusOfTest(test_name) 249*9c5db199SXin Li if status not in ('started', 'failed', 'timeout'): 250*9c5db199SXin Li self._RecordError(line, 'failure while in status %s' % status) 251*9c5db199SXin Li return 252*9c5db199SXin Li # Don't overwrite the failure description when a failing test is 253*9c5db199SXin Li # listed a second time in the summary, or if it was already 254*9c5db199SXin Li # recorded as timing out. 255*9c5db199SXin Li if status not in ('failed', 'timeout'): 256*9c5db199SXin Li self._test_status[test_name] = ('failed', 257*9c5db199SXin Li self._failure_description) 258*9c5db199SXin Li self._failure_description = [] 259*9c5db199SXin Li self._current_test = '' 260*9c5db199SXin Li return 261*9c5db199SXin Li 262*9c5db199SXin Li # Is it a test timeout line? 263*9c5db199SXin Li results = self._test_timeout.search(line) 264*9c5db199SXin Li if results: 265*9c5db199SXin Li test_name = results.group(1) 266*9c5db199SXin Li status = self._StatusOfTest(test_name) 267*9c5db199SXin Li if status not in ('started', 'failed'): 268*9c5db199SXin Li self._RecordError(line, 'timeout while in status %s' % status) 269*9c5db199SXin Li return 270*9c5db199SXin Li self._test_status[test_name] = ( 271*9c5db199SXin Li 'timeout', self._failure_description + ['Killed (timed out).']) 272*9c5db199SXin Li self._failure_description = [] 273*9c5db199SXin Li self._current_test = '' 274*9c5db199SXin Li return 275*9c5db199SXin Li 276*9c5db199SXin Li # Is it the start of a new valgrind suppression? 277*9c5db199SXin Li results = self._suppression_start.search(line) 278*9c5db199SXin Li if results: 279*9c5db199SXin Li suppression_hash = results.group(1) 280*9c5db199SXin Li if suppression_hash in self._suppressions: 281*9c5db199SXin Li self._RecordError(line, 'suppression reported more than once') 282*9c5db199SXin Li return 283*9c5db199SXin Li self._suppressions[suppression_hash] = [] 284*9c5db199SXin Li self._current_suppression_hash = suppression_hash 285*9c5db199SXin Li self._current_suppression = [line] 286*9c5db199SXin Li return 287*9c5db199SXin Li 288*9c5db199SXin Li # Is it the end of a valgrind suppression? 289*9c5db199SXin Li results = self._suppression_end.search(line) 290*9c5db199SXin Li if results and self._current_suppression_hash: 291*9c5db199SXin Li self._current_suppression.append(line) 292*9c5db199SXin Li self._suppressions[self._current_suppression_hash] = ( 293*9c5db199SXin Li self._current_suppression) 294*9c5db199SXin Li self._current_suppression_hash = '' 295*9c5db199SXin Li self._current_suppression = [] 296*9c5db199SXin Li return 297*9c5db199SXin Li 298*9c5db199SXin Li # Is it the start of a test summary error message? 299*9c5db199SXin Li results = self._error_logging_test_name_re.search(line) 300*9c5db199SXin Li if results: 301*9c5db199SXin Li test_name = results.group(1) 302*9c5db199SXin Li self._test_status[test_name] = ('failed', ['Output not found.']) 303*9c5db199SXin Li self._current_test = test_name 304*9c5db199SXin Li self._failure_description = [] 305*9c5db199SXin Li self._error_logging_first_dash_found = False 306*9c5db199SXin Li return 307*9c5db199SXin Li 308*9c5db199SXin Li # Is it the start of the next test summary signaling the end 309*9c5db199SXin Li # of the previous message? 310*9c5db199SXin Li results = self._error_logging_start_re.search(line) 311*9c5db199SXin Li if results and self._current_test: 312*9c5db199SXin Li self._test_status[self._current_test] = ('failed', 313*9c5db199SXin Li self._failure_description) 314*9c5db199SXin Li self._failure_description = [] 315*9c5db199SXin Li self._current_test = '' 316*9c5db199SXin Li return 317*9c5db199SXin Li 318*9c5db199SXin Li # Is it the end of the extra test failure summaries? 319*9c5db199SXin Li results = self._error_logging_end_re.search(line) 320*9c5db199SXin Li if results and self._current_test: 321*9c5db199SXin Li if self._error_logging_first_dash_found: 322*9c5db199SXin Li self._test_status[self._current_test] = ( 323*9c5db199SXin Li 'failed', self._failure_description) 324*9c5db199SXin Li self._failure_description = [] 325*9c5db199SXin Li self._current_test = '' 326*9c5db199SXin Li self._error_logging_first_dash_found = True 327*9c5db199SXin Li return 328*9c5db199SXin Li 329*9c5db199SXin Li # Random line: if we're in a suppression, collect it. Suppressions are 330*9c5db199SXin Li # generated after all tests are finished, so this should always belong 331*9c5db199SXin Li # to the current suppression hash. 332*9c5db199SXin Li if self._current_suppression_hash: 333*9c5db199SXin Li self._current_suppression.append(line) 334*9c5db199SXin Li return 335*9c5db199SXin Li 336*9c5db199SXin Li # Random line: if we're in a test, collect it for the failure 337*9c5db199SXin Li # description. Tests may run simultaneously, so this might be off, but 338*9c5db199SXin Li # it's worth a try. 339*9c5db199SXin Li if self._current_test: 340*9c5db199SXin Li self._failure_description.append(line) 341