Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(281)

Side by Side Diff: Tools/AutoSheriff/gtest_utils.py

Issue 398823008: WIP: Add auto-sheriff.appspot.com code to Blink Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: Created 6 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 #!/usr/bin/env python
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
4 # found in the LICENSE file.
5
6 import json
7 import os
8 import re
9 import tempfile
10
11
12 def CompressList(lines, max_length, middle_replacement):
13 """Ensures that |lines| is no longer than |max_length|. If |lines| need to
14 be compressed then the middle items are replaced by |middle_replacement|.
15 """
16 if len(lines) <= max_length:
17 return lines
18 remove_from_start = max_length / 2
19 return (lines[:remove_from_start] +
20 [middle_replacement] +
21 lines[len(lines) - (max_length - remove_from_start):])
22
23
24 class GTestLogParser(object):
25 """This helper class process GTest test output."""
26
27 def __init__(self):
28 # State tracking for log parsing
29 self.completed = False
30 self._current_test = ''
31 self._failure_description = []
32 self._current_suppression_hash = ''
33 self._current_suppression = []
34 self._parsing_failures = False
35
36 # Line number currently being processed.
37 self._line_number = 0
38
39 # List of parsing errors, as human-readable strings.
40 self._internal_error_lines = []
41
42 # Tests are stored here as 'test.name': (status, [description]).
43 # The status should be one of ('started', 'OK', 'failed', 'timeout',
44 # 'warning'). Warning indicates that a test did not pass when run in
45 # parallel with other tests but passed when run alone. The description is
46 # a list of lines detailing the test's error, as reported in the log.
47 self._test_status = {}
48
49 # Suppressions are stored here as 'hash': [suppression].
50 self._suppressions = {}
51
52 # This may be either text or a number. It will be used in the phrase
53 # '%s disabled' or '%s flaky' on the waterfall display.
54 self._disabled_tests = 0
55 self._flaky_tests = 0
56
57 # Regular expressions for parsing GTest logs. Test names look like
58 # SomeTestCase.SomeTest
59 # SomeName/SomeTestCase.SomeTest/1
60 # This regexp also matches SomeName.SomeTest/1, which should be harmless.
61 test_name_regexp = r'((\w+/)?\w+\.\w+(/\d+)?)'
62
63 self._master_name_re = re.compile('\[Running for master: "([^"]*)"')
64 self.master_name = ''
65
66 self._test_name = re.compile(test_name_regexp)
67 self._test_start = re.compile('\[\s+RUN\s+\] ' + test_name_regexp)
68 self._test_ok = re.compile('\[\s+OK\s+\] ' + test_name_regexp)
69 self._test_fail = re.compile('\[\s+FAILED\s+\] ' + test_name_regexp)
70 self._test_passed = re.compile('\[\s+PASSED\s+\] \d+ tests?.')
71 self._run_test_cases_line = re.compile(
72 '\[\s*\d+\/\d+\]\s+[0-9\.]+s ' + test_name_regexp + ' .+')
73 self._test_timeout = re.compile(
74 'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp)
75 self._disabled = re.compile('\s*YOU HAVE (\d+) DISABLED TEST')
76 self._flaky = re.compile('\s*YOU HAVE (\d+) FLAKY TEST')
77
78 self._suppression_start = re.compile(
79 'Suppression \(error hash=#([0-9A-F]+)#\):')
80 self._suppression_end = re.compile('^}\s*$')
81
82 self._retry_message = re.compile('RETRYING FAILED TESTS:')
83 self.retrying_failed = False
84
85 def GetCurrentTest(self):
86 return self._current_test
87
88 def _StatusOfTest(self, test):
89 """Returns the status code for the given test, or 'not known'."""
90 test_status = self._test_status.get(test, ('not known', []))
91 return test_status[0]
92
93 def _TestsByStatus(self, status, include_fails, include_flaky):
94 """Returns list of tests with the given status.
95
96 Args:
97 include_fails: If False, tests containing 'FAILS_' anywhere in their
98 names will be excluded from the list.
99 include_flaky: If False, tests containing 'FLAKY_' anywhere in their
100 names will be excluded from the list.
101 """
102 test_list = [x[0] for x in self._test_status.items()
103 if self._StatusOfTest(x[0]) == status]
104
105 if not include_fails:
106 test_list = [x for x in test_list if x.find('FAILS_') == -1]
107 if not include_flaky:
108 test_list = [x for x in test_list if x.find('FLAKY_') == -1]
109
110 return test_list
111
112 def _RecordError(self, line, reason):
113 """Record a log line that produced a parsing error.
114
115 Args:
116 line: text of the line at which the error occurred
117 reason: a string describing the error
118 """
119 self._internal_error_lines.append('%s: %s [%s]' %
120 (self._line_number, line.strip(), reason))
121
122 def RunningTests(self):
123 """Returns list of tests that appear to be currently running."""
124 return self._TestsByStatus('started', True, True)
125
126 def ParsingErrors(self):
127 """Returns a list of lines that have caused parsing errors."""
128 return self._internal_error_lines
129
130 def ClearParsingErrors(self):
131 """Clears the currently stored parsing errors."""
132 self._internal_error_lines = ['Cleared.']
133
134 def PassedTests(self):
135 """Returns list of tests that passed."""
136 return self._TestsByStatus('OK', False, False)
137
138 def FailedTests(self, include_fails=False, include_flaky=False):
139 """Returns list of tests that failed, timed out, or didn't finish
140 (crashed).
141
142 This list will be incorrect until the complete log has been processed,
143 because it will show currently running tests as having failed.
144
145 Args:
146 include_fails: If true, all failing tests with FAILS_ in their names will
147 be included. Otherwise, they will only be included if they crashed or
148 timed out.
149 include_flaky: If true, all failing tests with FLAKY_ in their names will
150 be included. Otherwise, they will only be included if they crashed or
151 timed out.
152
153 """
154 return (self._TestsByStatus('failed', include_fails, include_flaky) +
155 self._TestsByStatus('timeout', True, True) +
156 self._TestsByStatus('warning', include_fails, include_flaky) +
157 self.RunningTests())
158
159 def DisabledTests(self):
160 """Returns the name of the disabled test (if there is only 1) or the number
161 of disabled tests.
162 """
163 return self._disabled_tests
164
165 def FlakyTests(self):
166 """Returns the name of the flaky test (if there is only 1) or the number
167 of flaky tests.
168 """
169 return self._flaky_tests
170
171 def FailureDescription(self, test):
172 """Returns a list containing the failure description for the given test.
173
174 If the test didn't fail or timeout, returns [].
175 """
176 test_status = self._test_status.get(test, ('', []))
177 return ['%s: ' % test] + test_status[1]
178
179 def SuppressionHashes(self):
180 """Returns list of suppression hashes found in the log."""
181 return self._suppressions.keys()
182
183 def Suppression(self, suppression_hash):
184 """Returns a list containing the suppression for a given hash.
185
186 If the suppression hash doesn't exist, returns [].
187 """
188 return self._suppressions.get(suppression_hash, [])
189
190 def CompletedWithoutFailure(self):
191 """Returns True if all tests completed and no tests failed unexpectedly."""
192 return self.completed and not self.FailedTests()
193
194 def ProcessLine(self, line):
195 """This is called once with each line of the test log."""
196
197 # Track line number for error messages.
198 self._line_number += 1
199
200 # Some tests (net_unittests in particular) run subprocesses which can write
201 # stuff to shared stdout buffer. Sometimes such output appears between new
202 # line and gtest directives ('[ RUN ]', etc) which breaks the parser.
203 # Code below tries to detect such cases and recognize a mixed line as two
204 # separate lines.
205
206 # List of regexps that parses expects to find at the start of a line but
207 # which can be somewhere in the middle.
208 gtest_regexps = [
209 self._test_start,
210 self._test_ok,
211 self._test_fail,
212 self._test_passed,
213 ]
214
215 for regexp in gtest_regexps:
216 match = regexp.search(line)
217 if match:
218 break
219
220 if not match or match.start() == 0:
221 self._ProcessLine(line)
222 else:
223 self._ProcessLine(line[:match.start()])
224 self._ProcessLine(line[match.start():])
225
226 def _ProcessLine(self, line):
227 """Parses the line and changes the state of parsed tests accordingly.
228
229 Will recognize newly started tests, OK or FAILED statuses, timeouts, etc.
230 """
231
232 # Note: When sharding, the number of disabled and flaky tests will be read
233 # multiple times, so this will only show the most recent values (but they
234 # should all be the same anyway).
235
236 # Is it a line listing the master name?
237 if not self.master_name:
238 results = self._master_name_re.match(line)
239 if results:
240 self.master_name = results.group(1)
241
242 results = self._run_test_cases_line.match(line)
243 if results:
244 # A run_test_cases.py output.
245 if self._current_test:
246 if self._test_status[self._current_test][0] == 'started':
247 self._test_status[self._current_test] = (
248 'timeout', self._failure_description)
249 self._current_test = ''
250 self._failure_description = []
251 return
252
253 # Is it a line declaring all tests passed?
254 results = self._test_passed.match(line)
255 if results:
256 self.completed = True
257 self._current_test = ''
258 return
259
260 # Is it a line reporting disabled tests?
261 results = self._disabled.match(line)
262 if results:
263 try:
264 disabled = int(results.group(1))
265 except ValueError:
266 disabled = 0
267 if disabled > 0 and isinstance(self._disabled_tests, int):
268 self._disabled_tests = disabled
269 else:
270 # If we can't parse the line, at least give a heads-up. This is a
271 # safety net for a case that shouldn't happen but isn't a fatal error.
272 self._disabled_tests = 'some'
273 return
274
275 # Is it a line reporting flaky tests?
276 results = self._flaky.match(line)
277 if results:
278 try:
279 flaky = int(results.group(1))
280 except ValueError:
281 flaky = 0
282 if flaky > 0 and isinstance(self._flaky_tests, int):
283 self._flaky_tests = flaky
284 else:
285 # If we can't parse the line, at least give a heads-up. This is a
286 # safety net for a case that shouldn't happen but isn't a fatal error.
287 self._flaky_tests = 'some'
288 return
289
290 # Is it the start of a test?
291 results = self._test_start.match(line)
292 if results:
293 if self._current_test:
294 if self._test_status[self._current_test][0] == 'started':
295 self._test_status[self._current_test] = (
296 'timeout', self._failure_description)
297 test_name = results.group(1)
298 self._test_status[test_name] = ('started', ['Did not complete.'])
299 self._current_test = test_name
300 if self.retrying_failed:
301 self._failure_description = self._test_status[test_name][1]
302 self._failure_description.extend(['', 'RETRY OUTPUT:', ''])
303 else:
304 self._failure_description = []
305 return
306
307 # Is it a test success line?
308 results = self._test_ok.match(line)
309 if results:
310 test_name = results.group(1)
311 status = self._StatusOfTest(test_name)
312 if status != 'started':
313 self._RecordError(line, 'success while in status %s' % status)
314 if self.retrying_failed:
315 self._test_status[test_name] = ('warning', self._failure_description)
316 else:
317 self._test_status[test_name] = ('OK', [])
318 self._failure_description = []
319 self._current_test = ''
320 return
321
322 # Is it a test failure line?
323 results = self._test_fail.match(line)
324 if results:
325 test_name = results.group(1)
326 status = self._StatusOfTest(test_name)
327 if status not in ('started', 'failed', 'timeout'):
328 self._RecordError(line, 'failure while in status %s' % status)
329 # Don't overwrite the failure description when a failing test is listed a
330 # second time in the summary, or if it was already recorded as timing
331 # out.
332 if status not in ('failed', 'timeout'):
333 self._test_status[test_name] = ('failed', self._failure_description)
334 self._failure_description = []
335 self._current_test = ''
336 return
337
338 # Is it a test timeout line?
339 results = self._test_timeout.search(line)
340 if results:
341 test_name = results.group(1)
342 status = self._StatusOfTest(test_name)
343 if status not in ('started', 'failed'):
344 self._RecordError(line, 'timeout while in status %s' % status)
345 self._test_status[test_name] = (
346 'timeout', self._failure_description + ['Killed (timed out).'])
347 self._failure_description = []
348 self._current_test = ''
349 return
350
351 # Is it the start of a new valgrind suppression?
352 results = self._suppression_start.match(line)
353 if results:
354 suppression_hash = results.group(1)
355 if suppression_hash in self._suppressions:
356 self._RecordError(line, 'suppression reported more than once')
357 self._suppressions[suppression_hash] = []
358 self._current_suppression_hash = suppression_hash
359 self._current_suppression = [line]
360 return
361
362 # Is it the end of a valgrind suppression?
363 results = self._suppression_end.match(line)
364 if results and self._current_suppression_hash:
365 self._current_suppression.append(line)
366 self._suppressions[self._current_suppression_hash] = (
367 self._current_suppression)
368 self._current_suppression_hash = ''
369 self._current_suppression = []
370 return
371
372 # Is it the start of the retry tests?
373 results = self._retry_message.match(line)
374 if results:
375 self.retrying_failed = True
376 return
377
378 # Random line: if we're in a suppression, collect it. Suppressions are
379 # generated after all tests are finished, so this should always belong to
380 # the current suppression hash.
381 if self._current_suppression_hash:
382 self._current_suppression.append(line)
383 return
384
385 # Random line: if we're in a test, collect it for the failure description.
386 # Tests may run simultaneously, so this might be off, but it's worth a try.
387 # This also won't work if a test times out before it begins running.
388 if self._current_test:
389 self._failure_description.append(line)
390
391 # Parse the "Failing tests:" list at the end of the output, and add any
392 # additional failed tests to the list. For example, this includes tests
393 # that crash after the OK line.
394 if self._parsing_failures:
395 results = self._test_name.match(line)
396 if results:
397 test_name = results.group(1)
398 status = self._StatusOfTest(test_name)
399 if status in ('not known', 'OK'):
400 self._test_status[test_name] = (
401 'failed', ['Unknown error, see stdio log.'])
402 else:
403 self._parsing_failures = False
404 elif line.startswith('Failing tests:'):
405 self._parsing_failures = True
406
407
408 class GTestJSONParser(object):
409 # Limit of output snippet lines. Avoids flooding the logs with amount
410 # of output that gums up the infrastructure.
411 OUTPUT_SNIPPET_LINES_LIMIT = 5000
412
413 def __init__(self):
414 self.json_file_path = None
415 self.delete_json_file = False
416
417 self.disabled_tests = set()
418 self.passed_tests = set()
419 self.failed_tests = set()
420 self.flaky_tests = set()
421 self.test_logs = {}
422
423 self.parsing_errors = []
424
425 self.master_name = None
426
427 def ProcessLine(self, line):
428 # Deliberately do nothing - we parse out-of-band JSON summary
429 # instead of in-band stdout.
430 pass
431
432 def PassedTests(self):
433 return sorted(self.passed_tests)
434
435 def FailedTests(self, include_fails=False, include_flaky=False):
436 return sorted(self.failed_tests)
437
438 def FailureDescription(self, test):
439 return self.test_logs.get(test, [])
440
441 @staticmethod
442 def SuppressionHashes():
443 return []
444
445 def ParsingErrors(self):
446 return self.parsing_errors
447
448 def ClearParsingErrors(self):
449 self.parsing_errors = ['Cleared.']
450
451 def DisabledTests(self):
452 return len(self.disabled_tests)
453
454 def FlakyTests(self):
455 return len(self.flaky_tests)
456
457 @staticmethod
458 def RunningTests():
459 return []
460
461 def PrepareJSONFile(self, cmdline_path):
462 if cmdline_path:
463 self.json_file_path = cmdline_path
464 # If the caller requested JSON summary, do not delete it.
465 self.delete_json_file = False
466 else:
467 fd, self.json_file_path = tempfile.mkstemp()
468 os.close(fd)
469 # When we create the file ourselves, delete it to avoid littering.
470 self.delete_json_file = True
471 return self.json_file_path
472
473 def ProcessJSONFile(self):
474 if not self.json_file_path:
475 return
476
477 with open(self.json_file_path) as json_file:
478 try:
479 json_output = json_file.read()
480 json_data = json.loads(json_output)
481 except ValueError:
482 # Only signal parsing error if the file is non-empty. Empty file
483 # most likely means the binary doesn't support JSON output.
484 if json_output:
485 self.parsing_errors = json_output.split('\n')
486 else:
487 self.ProcessJSONData(json_data)
488
489 if self.delete_json_file:
490 os.remove(self.json_file_path)
491
492 def ProcessJSONData(self, json_data):
493 # TODO(phajdan.jr): Require disabled_tests to be present (May 2014).
494 self.disabled_tests.update(json_data.get('disabled_tests', []))
495
496 for iteration_data in json_data['per_iteration_data']:
497 for test_name, test_runs in iteration_data.iteritems():
498 if test_runs[-1]['status'] == 'SUCCESS':
499 self.passed_tests.add(test_name)
500 else:
501 self.failed_tests.add(test_name)
502
503 if len(test_runs) > 1:
504 self.flaky_tests.add(test_name)
505
506 self.test_logs.setdefault(test_name, [])
507 for run_index, run_data in enumerate(test_runs, start=1):
508 run_lines = ['%s (run #%d):' % (test_name, run_index)]
509 # Make sure the annotations are ASCII to avoid character set related
510 # errors. They are mostly informational anyway, and more detailed
511 # info can be obtained from the original JSON output.
512 ascii_lines = run_data['output_snippet'].encode('ascii',
513 errors='replace')
514 decoded_lines = CompressList(
515 ascii_lines.decode('string_escape').split('\n'),
516 self.OUTPUT_SNIPPET_LINES_LIMIT,
517 '<truncated, full output is in gzipped JSON '
518 'output at end of step>')
519 run_lines.extend(decoded_lines)
520 self.test_logs[test_name].extend(run_lines)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698