blob: 1d8671696eb798b49505f9f278771666a45c705a [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) +
278 self._TestsByStatus('timeout', include_fails, include_flaky))
279
280 def FailureDescription(self, test):
281 """Returns a list containing the failure description for the given test.
282
283 If the test didn't fail or timeout, returns [].
284 Args:
285 test: Name to test to find failure reason.
286 Returns:
287 List of test name, and failure string.
288 """
289 test_status = self._test_status.get(test, ('', []))
Paul Pendlebury57593562011-06-15 10:45:49 -0700290 return test_status[1]
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -0700291
292 def SuppressionHashes(self):
293 """Returns list of suppression hashes found in the log."""
294 return self._suppressions.keys()
295
296 def Suppression(self, suppression_hash):
297 """Returns a list containing the suppression for a given hash.
298
299 If the suppression hash doesn't exist, returns [].
300
301 Args:
302 suppression_hash: name of hash.
303 Returns:
304 List of suppression for the hash.
305 """
306 return self._suppressions.get(suppression_hash, [])
307
308 def ProcessLogLine(self, line):
309 """This is called once with each line of the test log."""
310
311 # Track line number for error messages.
312 self._line_number += 1
313
314 if not self.master_name:
315 results = self._master_name_re.search(line)
316 if results:
317 self.master_name = results.group(1)
318
319 # Is it a line reporting disabled tests?
320 results = self._disabled.search(line)
321 if results:
322 try:
323 disabled = int(results.group(1))
324 except ValueError:
325 disabled = 0
326 if disabled > 0 and isinstance(self.disabled_tests, int):
327 self.disabled_tests += disabled
328 else:
329 # If we can't parse the line, at least give a heads-up. This is
330 # a safety net for a case that shouldn't happen but isn't a
331 # fatal error.
332 self.disabled_tests = 'some'
333 return
334
335 # Is it a line reporting flaky tests?
336 results = self._flaky.search(line)
337 if results:
338 try:
339 flaky = int(results.group(1))
340 except ValueError:
341 flaky = 0
342 if flaky > 0 and isinstance(self.flaky_tests, int):
343 self.flaky_tests = flaky
344 else:
345 # If we can't parse the line, at least give a heads-up. This is
346 # a safety net for a case that shouldn't happen but isn't a
347 # fatal error.
348 self.flaky_tests = 'some'
349 return
350
351 # Is it the start of a test?
352 results = self._test_start.search(line)
353 if results:
354 test_name = results.group(1)
355 if test_name in self._test_status:
356 self._RecordError(line, 'test started more than once')
357 return
358 if self._current_test:
359 status = self._StatusOfTest(self._current_test)
360 if status in ('OK', 'failed', 'timeout'):
361 self._RecordError(line, 'start while in status %s' % status)
362 return
363 if status not in ('failed', 'timeout'):
364 self._test_status[self._current_test] = (
365 'failed', self._failure_description)
366 self._test_status[test_name] = ('started', ['Did not complete.'])
367 self._current_test = test_name
368 self._failure_description = []
369 return
370
371 # Is it a test success line?
372 results = self._test_ok.search(line)
373 if results:
374 test_name = results.group(1)
375 status = self._StatusOfTest(test_name)
376 if status != 'started':
377 self._RecordError(line, 'success while in status %s' % status)
378 return
379 self._test_status[test_name] = ('OK', [])
380 self._failure_description = []
381 self._current_test = ''
382 return
383
384 # Is it a test failure line?
385 results = self._test_fail.search(line)
386 if results:
387 test_name = results.group(1)
388 status = self._StatusOfTest(test_name)
389 if status not in ('started', 'failed', 'timeout'):
390 self._RecordError(line, 'failure while in status %s' % status)
391 return
392 # Don't overwrite the failure description when a failing test is
393 # listed a second time in the summary, or if it was already
394 # recorded as timing out.
395 if status not in ('failed', 'timeout'):
396 self._test_status[test_name] = ('failed',
397 self._failure_description)
398 self._failure_description = []
399 self._current_test = ''
400 return
401
402 # Is it a test timeout line?
403 results = self._test_timeout.search(line)
404 if results:
405 test_name = results.group(1)
406 status = self._StatusOfTest(test_name)
407 if status not in ('started', 'failed'):
408 self._RecordError(line, 'timeout while in status %s' % status)
409 return
410 self._test_status[test_name] = (
411 'timeout', self._failure_description + ['Killed (timed out).'])
412 self._failure_description = []
413 self._current_test = ''
414 return
415
416 # Is it the start of a new valgrind suppression?
417 results = self._suppression_start.search(line)
418 if results:
419 suppression_hash = results.group(1)
420 if suppression_hash in self._suppressions:
421 self._RecordError(line, 'suppression reported more than once')
422 return
423 self._suppressions[suppression_hash] = []
424 self._current_suppression_hash = suppression_hash
425 self._current_suppression = [line]
426 return
427
428 # Is it the end of a valgrind suppression?
429 results = self._suppression_end.search(line)
430 if results and self._current_suppression_hash:
431 self._current_suppression.append(line)
432 self._suppressions[self._current_suppression_hash] = (
433 self._current_suppression)
434 self._current_suppression_hash = ''
435 self._current_suppression = []
436 return
437
Paul Pendlebury699d64e2011-06-22 12:03:56 -0700438 # Is it the start of a test summary error message?
439 results = self._error_logging_test_name_re.search(line)
440 if results:
441 test_name = results.group(1)
442 self._test_status[test_name] = ('failed', ['Output not found.'])
443 self._current_test = test_name
444 self._failure_description = []
445 self._error_logging_first_dash_found = False
446 return
447
448 # Is it the start of the next test summary signaling the end
449 # of the previous message?
450 results = self._error_logging_start_re.search(line)
451 if results and self._current_test:
452 self._test_status[self._current_test] = ('failed',
453 self._failure_description)
454 self._failure_description = []
455 self._current_test = ''
456 return
457
458 # Is it the end of the extra test failure summaries?
459 results = self._error_logging_end_re.search(line)
460 if results and self._current_test:
461 if self._error_logging_first_dash_found:
462 self._test_status[self._current_test] = (
463 'failed', self._failure_description)
464 self._failure_description = []
465 self._current_test = ''
466 self._error_logging_first_dash_found = True
467 return
468
Paul Pendlebury7c1fdcf2011-05-04 12:39:15 -0700469 # Random line: if we're in a suppression, collect it. Suppressions are
470 # generated after all tests are finished, so this should always belong
471 # to the current suppression hash.
472 if self._current_suppression_hash:
473 self._current_suppression.append(line)
474 return
475
476 # Random line: if we're in a test, collect it for the failure
477 # description. Tests may run simultaneously, so this might be off, but
478 # it's worth a try.
479 if self._current_test:
480 self._failure_description.append(line)