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