blob: c1df4098cf3964c389413e01fc73aad7e46acf52 [file] [log] [blame]
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -07001# Copyright (c) 2011 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 classes used to run and parse a gest suite in autotest.
6
7gtest_runner: runs a gtest suite parsing individual tests.
8
9gtest_parser: understands the output of a gtest run.
10"""
11
12
13import logging, os, re
14from autotest_lib.server import autotest, hosts, host_attributes
Paul Pendlebury57593562011-06-15 10:45:49 -070015from autotest_lib.server import site_server_job_utils
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -070016
17
18class gtest_runner(object):
19 """Run a gtest test suite and evaluate the individual tests."""
20
21 def __init__(self):
22 """Creates an instance of gtest_runner to run tests on a remote host."""
23 self._results_dir = ''
24 self._gtest = None
25 self._host = None
26
27 def run(self, gtest_entry, machine, work_dir='.'):
28 """Run the gtest suite on a remote host, then parse the results.
29
30 Like machine_worker, gtest_runner honors include/exclude attributes on
31 the test item and will only run the test if the supplied host meets the
32 test requirements.
33
34 Note: This method takes a test and a machine as arguments, not a list
35 of tests and a list of machines like the parallel and distribute
36 methods do.
37
38 Args:
39 gtest_entry: Test tuple from control file. See documentation in
Nirnimesh717b0922011-11-09 12:03:48 -080040 site_server_job_utils.test_item class.
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -070041 machine: Name (IP) if remote host to run tests on.
42 work_dir: Local directory to run tests in.
43
44 """
Paul Pendlebury57593562011-06-15 10:45:49 -070045 self._gtest = site_server_job_utils.test_item(*gtest_entry)
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -070046 self._host = hosts.create_host(machine)
47 self._results_dir = work_dir
48
49 client_autotest = autotest.Autotest(self._host)
50 client_attributes = host_attributes.host_attributes(machine)
51 attribute_set = set(client_attributes.get_attributes())
52
53 if self._gtest.validate(attribute_set):
54 logging.info('%s %s Running %s', self._host,
55 [a for a in attribute_set], self._gtest)
Paul Pendlebury9af083b2011-05-17 13:38:58 -070056 try:
57 self._gtest.run_test(client_autotest, self._results_dir)
58 finally:
59 self.parse()
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -070060 else:
61 self.record_failed_test(self._gtest.test_name,
62 'No machines found for: ' + self._gtest)
63
64 def parse(self):
65 """Parse the gtest output recording individual test results.
66
67 Uses gtest_parser to pull the test results out of the gtest log file.
68 Then creates entries in status.log file for each test.
69 """
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -070070 # Find gtest log files from the autotest client run.
Nirnimesh717b0922011-11-09 12:03:48 -080071 log_path = os.path.join(
72 self._results_dir, self._gtest.tagged_test_name,
73 'debug', self._gtest.tagged_test_name + '.DEBUG')
Paul Pendlebury57593562011-06-15 10:45:49 -070074 if not os.path.exists(log_path):
75 logging.error('gtest log file "%s" is missing.', log_path)
76 return
77
78 parser = gtest_parser()
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -070079
80 # Read the log file line-by-line, passing each line into the parser.
81 with open(log_path, 'r') as log_file:
82 for log_line in log_file:
83 parser.ProcessLogLine(log_line)
84
85 logging.info('gtest_runner found %d tests.', parser.TotalTests())
86
87 # Record each failed test.
88 for failed in parser.FailedTests():
89 fail_description = parser.FailureDescription(failed)
Paul Pendlebury57593562011-06-15 10:45:49 -070090 if fail_description:
91 self.record_failed_test(failed, fail_description[0].strip(),
92 ''.join(fail_description))
Paul Pendlebury699d64e2011-06-22 12:03:56 -070093 else:
Paul Pendlebury57593562011-06-15 10:45:49 -070094 self.record_failed_test(failed, 'NO ERROR LINES FOUND.')
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -070095
96 # Finally record each successful test.
97 for passed in parser.PassedTests():
98 self.record_passed_test(passed)
99
100 def record_failed_test(self, failed_test, message, error_lines=None):
101 """Insert a failure record into status.log for this test.
102
103 Args:
104 failed_test: Name of test that failed.
105 message: Reason test failed, will be put in status.log file.
106 error_lines: Additional failure info, will be put in ERROR log.
107 """
Paul Pendleburydf3124a2011-05-12 14:00:14 -0700108 # Create a test name subdirectory to hold the test status.log file.
109 test_dir = os.path.join(self._results_dir, failed_test)
110 if not os.path.exists(test_dir):
111 try:
112 os.makedirs(test_dir)
113 except OSError:
114 logging.exception('Failed to created test directory: %s',
115 test_dir)
116
117 # Record failure into the global job and test specific status files.
118 self._host.record('START', failed_test, failed_test)
119 self._host.record('INFO', failed_test, 'FAILED: ' + failed_test)
120 self._host.record('END FAIL', failed_test, failed_test, message)
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -0700121
122 # If we have additional information on the failure, create an error log
123 # file for this test in the location a normal autotest would have left
124 # it so the frontend knows where to find it.
125 if error_lines is not None:
Paul Pendleburydf3124a2011-05-12 14:00:14 -0700126 fail_log_dir = os.path.join(test_dir, 'debug')
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -0700127 fail_log_path = os.path.join(fail_log_dir, failed_test + '.ERROR')
128
129 if not os.path.exists(fail_log_dir):
130 try:
131 os.makedirs(fail_log_dir)
132 except OSError:
133 logging.exception('Failed to created log directory: %s',
134 fail_log_dir)
135 return
136 try:
137 with open(fail_log_path, 'w') as fail_log:
138 fail_log.write(error_lines)
139 except IOError:
140 logging.exception('Failed to open log file: %s', fail_log_path)
141
142 def record_passed_test(self, passed_test):
143 """Insert a failure record into status.log for this test.
144
145 Args:
146 passed_test: Name of test that passed.
147 """
148 self._host.record('START', None, passed_test)
149 self._host.record('INFO', None, 'PASSED: ' + passed_test)
150 self._host.record('END GOOD', None, passed_test)
151
152
153class gtest_parser(object):
154 """This class knows how to understand GTest test output.
155
156 The code was borrowed with minor changes from chrome utility gtest_command.
157 http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/master/
158 log_parser/gtest_command.py?view=markup
159 """
160
161 def __init__(self):
162 # State tracking for log parsing
163 self._current_test = ''
164 self._failure_description = []
165 self._current_suppression_hash = ''
166 self._current_suppression = []
167
168 # Line number currently being processed.
169 self._line_number = 0
170
171 # List of parsing errors, as human-readable strings.
172 self.internal_error_lines = []
173
174 # Tests are stored here as 'test.name': (status, [description]).
175 # The status should be one of ('started', 'OK', 'failed', 'timeout').
176 # The description is a list of lines detailing the test's error, as
177 # reported in the log.
178 self._test_status = {}
179
180 # Suppressions are stored here as 'hash': [suppression].
181 self._suppressions = {}
182
183 # This may be either text or a number. It will be used in the phrase
184 # '%s disabled' or '%s flaky' on the waterfall display.
185 self.disabled_tests = 0
186 self.flaky_tests = 0
187
188 # Regular expressions for parsing GTest logs. Test names look like
189 # SomeTestCase.SomeTest
190 # SomeName/SomeTestCase.SomeTest/1
191 # This regexp also matches SomeName.SomeTest/1, which should be
192 # harmless.
193 test_name_regexp = r'((\w+/)?\w+\.\w+(\.\w+)?(/\d+)?)'
194 self._test_start = re.compile('\[\s+RUN\s+\] ' + test_name_regexp)
195 self._test_ok = re.compile('\[\s+OK\s+\] ' + test_name_regexp)
196 self._test_fail = re.compile('\[\s+FAILED\s+\] ' + test_name_regexp)
197 self._test_timeout = re.compile(
198 'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp)
199 self._disabled = re.compile(' YOU HAVE (\d+) DISABLED TEST')
200 self._flaky = re.compile(' YOU HAVE (\d+) FLAKY TEST')
201
202 self._suppression_start = re.compile(
203 'Suppression \(error hash=#([0-9A-F]+)#\):')
204 self._suppression_end = re.compile('^}\s*$')
205
206 self._master_name_re = re.compile('\[Running for master: "([^"]*)"')
207 self.master_name = ''
208
Paul Pendlebury699d64e2011-06-22 12:03:56 -0700209 self._error_logging_start_re = re.compile('=' * 70)
210 self._error_logging_test_name_re = re.compile(
211 '[FAIL|ERROR]: ' + test_name_regexp)
212 self._error_logging_end_re = re.compile('-' * 70)
213 self._error_logging_first_dash_found = False
214
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -0700215 def _TestsByStatus(self, status, include_fails, include_flaky):
216 """Returns list of tests with the given status.
217
218 Args:
219 status: test results status to search for.
220 include_fails: If False, tests containing 'FAILS_' anywhere in
221 their names will be excluded from the list.
222 include_flaky: If False, tests containing 'FLAKY_' anywhere in
223 their names will be excluded from the list.
224 Returns:
225 List of tests with the status.
226 """
227 test_list = [x[0] for x in self._test_status.items()
228 if self._StatusOfTest(x[0]) == status]
229
230 if not include_fails:
231 test_list = [x for x in test_list if x.find('FAILS_') == -1]
232 if not include_flaky:
233 test_list = [x for x in test_list if x.find('FLAKY_') == -1]
234
235 return test_list
236
237 def _StatusOfTest(self, test):
238 """Returns the status code for the given test, or 'not known'."""
239 test_status = self._test_status.get(test, ('not known', []))
240 return test_status[0]
241
242 def _RecordError(self, line, reason):
243 """Record a log line that produced a parsing error.
244
245 Args:
246 line: text of the line at which the error occurred.
247 reason: a string describing the error.
248 """
249 self.internal_error_lines.append("%s: %s [%s]" % (self._line_number,
250 line.strip(),
251 reason))
252
253 def TotalTests(self):
254 """Returns the number of parsed tests."""
255 return len(self._test_status)
256
257 def PassedTests(self):
258 """Returns list of tests that passed."""
259 return self._TestsByStatus('OK', False, False)
260
261 def FailedTests(self, include_fails=False, include_flaky=False):
262 """Returns list of tests that failed, timed out, or didn't finish.
263
264 This list will be incorrect until the complete log has been processed,
265 because it will show currently running tests as having failed.
266
267 Args:
268 include_fails: If true, all failing tests with FAILS_ in their
269 names will be included. Otherwise, they will only be included
270 if they crashed.
271 include_flaky: If true, all failing tests with FLAKY_ in their
272 names will be included. Otherwise, they will only be included
273 if they crashed.
274 Returns:
275 List of failed tests.
276 """
277 return (self._TestsByStatus('failed', include_fails, include_flaky) +
Alex Deymo4a735642016-03-01 10:17:08 -0800278 self._TestsByStatus('timeout', include_fails, include_flaky) +
279 self._TestsByStatus('started', include_fails, include_flaky))
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -0700280
281 def FailureDescription(self, test):
282 """Returns a list containing the failure description for the given test.
283
284 If the test didn't fail or timeout, returns [].
285 Args:
286 test: Name to test to find failure reason.
287 Returns:
288 List of test name, and failure string.
289 """
290 test_status = self._test_status.get(test, ('', []))
Paul Pendlebury57593562011-06-15 10:45:49 -0700291 return test_status[1]
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -0700292
293 def SuppressionHashes(self):
294 """Returns list of suppression hashes found in the log."""
295 return self._suppressions.keys()
296
297 def Suppression(self, suppression_hash):
298 """Returns a list containing the suppression for a given hash.
299
300 If the suppression hash doesn't exist, returns [].
301
302 Args:
303 suppression_hash: name of hash.
304 Returns:
305 List of suppression for the hash.
306 """
307 return self._suppressions.get(suppression_hash, [])
308
309 def ProcessLogLine(self, line):
310 """This is called once with each line of the test log."""
311
312 # Track line number for error messages.
313 self._line_number += 1
314
315 if not self.master_name:
316 results = self._master_name_re.search(line)
317 if results:
318 self.master_name = results.group(1)
319
320 # Is it a line reporting disabled tests?
321 results = self._disabled.search(line)
322 if results:
323 try:
324 disabled = int(results.group(1))
325 except ValueError:
326 disabled = 0
327 if disabled > 0 and isinstance(self.disabled_tests, int):
328 self.disabled_tests += disabled
329 else:
330 # If we can't parse the line, at least give a heads-up. This is
331 # a safety net for a case that shouldn't happen but isn't a
332 # fatal error.
333 self.disabled_tests = 'some'
334 return
335
336 # Is it a line reporting flaky tests?
337 results = self._flaky.search(line)
338 if results:
339 try:
340 flaky = int(results.group(1))
341 except ValueError:
342 flaky = 0
343 if flaky > 0 and isinstance(self.flaky_tests, int):
344 self.flaky_tests = flaky
345 else:
346 # If we can't parse the line, at least give a heads-up. This is
347 # a safety net for a case that shouldn't happen but isn't a
348 # fatal error.
349 self.flaky_tests = 'some'
350 return
351
352 # Is it the start of a test?
353 results = self._test_start.search(line)
354 if results:
355 test_name = results.group(1)
356 if test_name in self._test_status:
357 self._RecordError(line, 'test started more than once')
358 return
359 if self._current_test:
360 status = self._StatusOfTest(self._current_test)
361 if status in ('OK', 'failed', 'timeout'):
362 self._RecordError(line, 'start while in status %s' % status)
363 return
364 if status not in ('failed', 'timeout'):
365 self._test_status[self._current_test] = (
366 'failed', self._failure_description)
367 self._test_status[test_name] = ('started', ['Did not complete.'])
368 self._current_test = test_name
369 self._failure_description = []
370 return
371
372 # Is it a test success line?
373 results = self._test_ok.search(line)
374 if results:
375 test_name = results.group(1)
376 status = self._StatusOfTest(test_name)
377 if status != 'started':
378 self._RecordError(line, 'success while in status %s' % status)
379 return
380 self._test_status[test_name] = ('OK', [])
381 self._failure_description = []
382 self._current_test = ''
383 return
384
385 # Is it a test failure line?
386 results = self._test_fail.search(line)
387 if results:
388 test_name = results.group(1)
389 status = self._StatusOfTest(test_name)
390 if status not in ('started', 'failed', 'timeout'):
391 self._RecordError(line, 'failure while in status %s' % status)
392 return
393 # Don't overwrite the failure description when a failing test is
394 # listed a second time in the summary, or if it was already
395 # recorded as timing out.
396 if status not in ('failed', 'timeout'):
397 self._test_status[test_name] = ('failed',
398 self._failure_description)
399 self._failure_description = []
400 self._current_test = ''
401 return
402
403 # Is it a test timeout line?
404 results = self._test_timeout.search(line)
405 if results:
406 test_name = results.group(1)
407 status = self._StatusOfTest(test_name)
408 if status not in ('started', 'failed'):
409 self._RecordError(line, 'timeout while in status %s' % status)
410 return
411 self._test_status[test_name] = (
412 'timeout', self._failure_description + ['Killed (timed out).'])
413 self._failure_description = []
414 self._current_test = ''
415 return
416
417 # Is it the start of a new valgrind suppression?
418 results = self._suppression_start.search(line)
419 if results:
420 suppression_hash = results.group(1)
421 if suppression_hash in self._suppressions:
422 self._RecordError(line, 'suppression reported more than once')
423 return
424 self._suppressions[suppression_hash] = []
425 self._current_suppression_hash = suppression_hash
426 self._current_suppression = [line]
427 return
428
429 # Is it the end of a valgrind suppression?
430 results = self._suppression_end.search(line)
431 if results and self._current_suppression_hash:
432 self._current_suppression.append(line)
433 self._suppressions[self._current_suppression_hash] = (
434 self._current_suppression)
435 self._current_suppression_hash = ''
436 self._current_suppression = []
437 return
438
Paul Pendlebury699d64e2011-06-22 12:03:56 -0700439 # Is it the start of a test summary error message?
440 results = self._error_logging_test_name_re.search(line)
441 if results:
442 test_name = results.group(1)
443 self._test_status[test_name] = ('failed', ['Output not found.'])
444 self._current_test = test_name
445 self._failure_description = []
446 self._error_logging_first_dash_found = False
447 return
448
449 # Is it the start of the next test summary signaling the end
450 # of the previous message?
451 results = self._error_logging_start_re.search(line)
452 if results and self._current_test:
453 self._test_status[self._current_test] = ('failed',
454 self._failure_description)
455 self._failure_description = []
456 self._current_test = ''
457 return
458
459 # Is it the end of the extra test failure summaries?
460 results = self._error_logging_end_re.search(line)
461 if results and self._current_test:
462 if self._error_logging_first_dash_found:
463 self._test_status[self._current_test] = (
464 'failed', self._failure_description)
465 self._failure_description = []
466 self._current_test = ''
467 self._error_logging_first_dash_found = True
468 return
469
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -0700470 # Random line: if we're in a suppression, collect it. Suppressions are
471 # generated after all tests are finished, so this should always belong
472 # to the current suppression hash.
473 if self._current_suppression_hash:
474 self._current_suppression.append(line)
475 return
476
477 # Random line: if we're in a test, collect it for the failure
478 # description. Tests may run simultaneously, so this might be off, but
479 # it's worth a try.
480 if self._current_test:
481 self._failure_description.append(line)