blob: 5efc0fb7e32edcae12489a67ff532a5e33556277 [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
15from autotest_lib.server import server_job_utils
16
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
40 server_job_utils.test_item class.
41 machine: Name (IP) if remote host to run tests on.
42 work_dir: Local directory to run tests in.
43
44 """
45 self._gtest = server_job_utils.test_item(*gtest_entry)
46 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)
56 self._gtest.run_test(client_autotest, self._results_dir)
57 self.parse()
58 else:
59 self.record_failed_test(self._gtest.test_name,
60 'No machines found for: ' + self._gtest)
61
62 def parse(self):
63 """Parse the gtest output recording individual test results.
64
65 Uses gtest_parser to pull the test results out of the gtest log file.
66 Then creates entries in status.log file for each test.
67 """
68 parser = gtest_parser()
69
70 # Find gtest log files from the autotest client run.
71 log_path = os.path.join(self._results_dir, self._gtest.test_name,
72 'debug', self._gtest.test_name + '.DEBUG')
73
74 # Read the log file line-by-line, passing each line into the parser.
75 with open(log_path, 'r') as log_file:
76 for log_line in log_file:
77 parser.ProcessLogLine(log_line)
78
79 logging.info('gtest_runner found %d tests.', parser.TotalTests())
80
81 # Record each failed test.
82 for failed in parser.FailedTests():
83 fail_description = parser.FailureDescription(failed)
84 self.record_failed_test(failed, fail_description[1].strip(),
85 ''.join(fail_description[1:]))
86
87 # Finally record each successful test.
88 for passed in parser.PassedTests():
89 self.record_passed_test(passed)
90
91 def record_failed_test(self, failed_test, message, error_lines=None):
92 """Insert a failure record into status.log for this test.
93
94 Args:
95 failed_test: Name of test that failed.
96 message: Reason test failed, will be put in status.log file.
97 error_lines: Additional failure info, will be put in ERROR log.
98 """
99 self._host.record('START', None, failed_test)
100 self._host.record('INFO', None, 'FAILED: ' + failed_test)
101 self._host.record('END FAIL', None, failed_test, message)
102
103 # If we have additional information on the failure, create an error log
104 # file for this test in the location a normal autotest would have left
105 # it so the frontend knows where to find it.
106 if error_lines is not None:
107 fail_log_dir = os.path.join(self._results_dir, failed_test, 'debug')
108 fail_log_path = os.path.join(fail_log_dir, failed_test + '.ERROR')
109
110 if not os.path.exists(fail_log_dir):
111 try:
112 os.makedirs(fail_log_dir)
113 except OSError:
114 logging.exception('Failed to created log directory: %s',
115 fail_log_dir)
116 return
117 try:
118 with open(fail_log_path, 'w') as fail_log:
119 fail_log.write(error_lines)
120 except IOError:
121 logging.exception('Failed to open log file: %s', fail_log_path)
122
123 def record_passed_test(self, passed_test):
124 """Insert a failure record into status.log for this test.
125
126 Args:
127 passed_test: Name of test that passed.
128 """
129 self._host.record('START', None, passed_test)
130 self._host.record('INFO', None, 'PASSED: ' + passed_test)
131 self._host.record('END GOOD', None, passed_test)
132
133
134class gtest_parser(object):
135 """This class knows how to understand GTest test output.
136
137 The code was borrowed with minor changes from chrome utility gtest_command.
138 http://src.chromium.org/viewvc/chrome/trunk/tools/build/scripts/master/
139 log_parser/gtest_command.py?view=markup
140 """
141
142 def __init__(self):
143 # State tracking for log parsing
144 self._current_test = ''
145 self._failure_description = []
146 self._current_suppression_hash = ''
147 self._current_suppression = []
148
149 # Line number currently being processed.
150 self._line_number = 0
151
152 # List of parsing errors, as human-readable strings.
153 self.internal_error_lines = []
154
155 # Tests are stored here as 'test.name': (status, [description]).
156 # The status should be one of ('started', 'OK', 'failed', 'timeout').
157 # The description is a list of lines detailing the test's error, as
158 # reported in the log.
159 self._test_status = {}
160
161 # Suppressions are stored here as 'hash': [suppression].
162 self._suppressions = {}
163
164 # This may be either text or a number. It will be used in the phrase
165 # '%s disabled' or '%s flaky' on the waterfall display.
166 self.disabled_tests = 0
167 self.flaky_tests = 0
168
169 # Regular expressions for parsing GTest logs. Test names look like
170 # SomeTestCase.SomeTest
171 # SomeName/SomeTestCase.SomeTest/1
172 # This regexp also matches SomeName.SomeTest/1, which should be
173 # harmless.
174 test_name_regexp = r'((\w+/)?\w+\.\w+(\.\w+)?(/\d+)?)'
175 self._test_start = re.compile('\[\s+RUN\s+\] ' + test_name_regexp)
176 self._test_ok = re.compile('\[\s+OK\s+\] ' + test_name_regexp)
177 self._test_fail = re.compile('\[\s+FAILED\s+\] ' + test_name_regexp)
178 self._test_timeout = re.compile(
179 'Test timeout \([0-9]+ ms\) exceeded for ' + test_name_regexp)
180 self._disabled = re.compile(' YOU HAVE (\d+) DISABLED TEST')
181 self._flaky = re.compile(' YOU HAVE (\d+) FLAKY TEST')
182
183 self._suppression_start = re.compile(
184 'Suppression \(error hash=#([0-9A-F]+)#\):')
185 self._suppression_end = re.compile('^}\s*$')
186
187 self._master_name_re = re.compile('\[Running for master: "([^"]*)"')
188 self.master_name = ''
189
190 def _TestsByStatus(self, status, include_fails, include_flaky):
191 """Returns list of tests with the given status.
192
193 Args:
194 status: test results status to search for.
195 include_fails: If False, tests containing 'FAILS_' anywhere in
196 their names will be excluded from the list.
197 include_flaky: If False, tests containing 'FLAKY_' anywhere in
198 their names will be excluded from the list.
199 Returns:
200 List of tests with the status.
201 """
202 test_list = [x[0] for x in self._test_status.items()
203 if self._StatusOfTest(x[0]) == status]
204
205 if not include_fails:
206 test_list = [x for x in test_list if x.find('FAILS_') == -1]
207 if not include_flaky:
208 test_list = [x for x in test_list if x.find('FLAKY_') == -1]
209
210 return test_list
211
212 def _StatusOfTest(self, test):
213 """Returns the status code for the given test, or 'not known'."""
214 test_status = self._test_status.get(test, ('not known', []))
215 return test_status[0]
216
217 def _RecordError(self, line, reason):
218 """Record a log line that produced a parsing error.
219
220 Args:
221 line: text of the line at which the error occurred.
222 reason: a string describing the error.
223 """
224 self.internal_error_lines.append("%s: %s [%s]" % (self._line_number,
225 line.strip(),
226 reason))
227
228 def TotalTests(self):
229 """Returns the number of parsed tests."""
230 return len(self._test_status)
231
232 def PassedTests(self):
233 """Returns list of tests that passed."""
234 return self._TestsByStatus('OK', False, False)
235
236 def FailedTests(self, include_fails=False, include_flaky=False):
237 """Returns list of tests that failed, timed out, or didn't finish.
238
239 This list will be incorrect until the complete log has been processed,
240 because it will show currently running tests as having failed.
241
242 Args:
243 include_fails: If true, all failing tests with FAILS_ in their
244 names will be included. Otherwise, they will only be included
245 if they crashed.
246 include_flaky: If true, all failing tests with FLAKY_ in their
247 names will be included. Otherwise, they will only be included
248 if they crashed.
249 Returns:
250 List of failed tests.
251 """
252 return (self._TestsByStatus('failed', include_fails, include_flaky) +
253 self._TestsByStatus('timeout', include_fails, include_flaky))
254
255 def FailureDescription(self, test):
256 """Returns a list containing the failure description for the given test.
257
258 If the test didn't fail or timeout, returns [].
259 Args:
260 test: Name to test to find failure reason.
261 Returns:
262 List of test name, and failure string.
263 """
264 test_status = self._test_status.get(test, ('', []))
265 return ["%s: " % test] + test_status[1]
266
267 def SuppressionHashes(self):
268 """Returns list of suppression hashes found in the log."""
269 return self._suppressions.keys()
270
271 def Suppression(self, suppression_hash):
272 """Returns a list containing the suppression for a given hash.
273
274 If the suppression hash doesn't exist, returns [].
275
276 Args:
277 suppression_hash: name of hash.
278 Returns:
279 List of suppression for the hash.
280 """
281 return self._suppressions.get(suppression_hash, [])
282
283 def ProcessLogLine(self, line):
284 """This is called once with each line of the test log."""
285
286 # Track line number for error messages.
287 self._line_number += 1
288
289 if not self.master_name:
290 results = self._master_name_re.search(line)
291 if results:
292 self.master_name = results.group(1)
293
294 # Is it a line reporting disabled tests?
295 results = self._disabled.search(line)
296 if results:
297 try:
298 disabled = int(results.group(1))
299 except ValueError:
300 disabled = 0
301 if disabled > 0 and isinstance(self.disabled_tests, int):
302 self.disabled_tests += disabled
303 else:
304 # If we can't parse the line, at least give a heads-up. This is
305 # a safety net for a case that shouldn't happen but isn't a
306 # fatal error.
307 self.disabled_tests = 'some'
308 return
309
310 # Is it a line reporting flaky tests?
311 results = self._flaky.search(line)
312 if results:
313 try:
314 flaky = int(results.group(1))
315 except ValueError:
316 flaky = 0
317 if flaky > 0 and isinstance(self.flaky_tests, int):
318 self.flaky_tests = flaky
319 else:
320 # If we can't parse the line, at least give a heads-up. This is
321 # a safety net for a case that shouldn't happen but isn't a
322 # fatal error.
323 self.flaky_tests = 'some'
324 return
325
326 # Is it the start of a test?
327 results = self._test_start.search(line)
328 if results:
329 test_name = results.group(1)
330 if test_name in self._test_status:
331 self._RecordError(line, 'test started more than once')
332 return
333 if self._current_test:
334 status = self._StatusOfTest(self._current_test)
335 if status in ('OK', 'failed', 'timeout'):
336 self._RecordError(line, 'start while in status %s' % status)
337 return
338 if status not in ('failed', 'timeout'):
339 self._test_status[self._current_test] = (
340 'failed', self._failure_description)
341 self._test_status[test_name] = ('started', ['Did not complete.'])
342 self._current_test = test_name
343 self._failure_description = []
344 return
345
346 # Is it a test success line?
347 results = self._test_ok.search(line)
348 if results:
349 test_name = results.group(1)
350 status = self._StatusOfTest(test_name)
351 if status != 'started':
352 self._RecordError(line, 'success while in status %s' % status)
353 return
354 self._test_status[test_name] = ('OK', [])
355 self._failure_description = []
356 self._current_test = ''
357 return
358
359 # Is it a test failure line?
360 results = self._test_fail.search(line)
361 if results:
362 test_name = results.group(1)
363 status = self._StatusOfTest(test_name)
364 if status not in ('started', 'failed', 'timeout'):
365 self._RecordError(line, 'failure while in status %s' % status)
366 return
367 # Don't overwrite the failure description when a failing test is
368 # listed a second time in the summary, or if it was already
369 # recorded as timing out.
370 if status not in ('failed', 'timeout'):
371 self._test_status[test_name] = ('failed',
372 self._failure_description)
373 self._failure_description = []
374 self._current_test = ''
375 return
376
377 # Is it a test timeout line?
378 results = self._test_timeout.search(line)
379 if results:
380 test_name = results.group(1)
381 status = self._StatusOfTest(test_name)
382 if status not in ('started', 'failed'):
383 self._RecordError(line, 'timeout while in status %s' % status)
384 return
385 self._test_status[test_name] = (
386 'timeout', self._failure_description + ['Killed (timed out).'])
387 self._failure_description = []
388 self._current_test = ''
389 return
390
391 # Is it the start of a new valgrind suppression?
392 results = self._suppression_start.search(line)
393 if results:
394 suppression_hash = results.group(1)
395 if suppression_hash in self._suppressions:
396 self._RecordError(line, 'suppression reported more than once')
397 return
398 self._suppressions[suppression_hash] = []
399 self._current_suppression_hash = suppression_hash
400 self._current_suppression = [line]
401 return
402
403 # Is it the end of a valgrind suppression?
404 results = self._suppression_end.search(line)
405 if results and self._current_suppression_hash:
406 self._current_suppression.append(line)
407 self._suppressions[self._current_suppression_hash] = (
408 self._current_suppression)
409 self._current_suppression_hash = ''
410 self._current_suppression = []
411 return
412
413 # Random line: if we're in a suppression, collect it. Suppressions are
414 # generated after all tests are finished, so this should always belong
415 # to the current suppression hash.
416 if self._current_suppression_hash:
417 self._current_suppression.append(line)
418 return
419
420 # Random line: if we're in a test, collect it for the failure
421 # description. Tests may run simultaneously, so this might be off, but
422 # it's worth a try.
423 if self._current_test:
424 self._failure_description.append(line)