xref: /aosp_15_r20/external/autotest/client/common_lib/gtest_parser.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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