Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
1# SPDX-License-Identifier: GPL-2.0
2#
3# Parses KTAP test results from a kernel dmesg log and incrementally prints
4# results with reader-friendly format. Stores and returns test results in a
5# Test object.
6#
7# Copyright (C) 2019, Google LLC.
8# Author: Felix Guo <felixguoxiuping@gmail.com>
9# Author: Brendan Higgins <brendanhiggins@google.com>
10# Author: Rae Moar <rmoar@google.com>
11
12from __future__ import annotations
13import re
14
15from collections import namedtuple
16from datetime import datetime
17from enum import Enum, auto
18from functools import reduce
19from typing import Iterable, Iterator, List, Optional, Tuple
20
21TestResult = namedtuple('TestResult', ['status','test','log'])
22
23class Test(object):
24 """
25 A class to represent a test parsed from KTAP results. All KTAP
26 results within a test log are stored in a main Test object as
27 subtests.
28
29 Attributes:
30 status : TestStatus - status of the test
31 name : str - name of the test
32 expected_count : int - expected number of subtests (0 if single
33 test case and None if unknown expected number of subtests)
34 subtests : List[Test] - list of subtests
35 log : List[str] - log of KTAP lines that correspond to the test
36 counts : TestCounts - counts of the test statuses and errors of
37 subtests or of the test itself if the test is a single
38 test case.
39 """
40 def __init__(self) -> None:
41 """Creates Test object with default attributes."""
42 self.status = TestStatus.TEST_CRASHED
43 self.name = ''
44 self.expected_count = 0 # type: Optional[int]
45 self.subtests = [] # type: List[Test]
46 self.log = [] # type: List[str]
47 self.counts = TestCounts()
48
49 def __str__(self) -> str:
50 """Returns string representation of a Test class object."""
51 return ('Test(' + str(self.status) + ', ' + self.name +
52 ', ' + str(self.expected_count) + ', ' +
53 str(self.subtests) + ', ' + str(self.log) + ', ' +
54 str(self.counts) + ')')
55
56 def __repr__(self) -> str:
57 """Returns string representation of a Test class object."""
58 return str(self)
59
60 def add_error(self, error_message: str) -> None:
61 """Records an error that occurred while parsing this test."""
62 self.counts.errors += 1
63 print_error('Test ' + self.name + ': ' + error_message)
64
65class TestStatus(Enum):
66 """An enumeration class to represent the status of a test."""
67 SUCCESS = auto()
68 FAILURE = auto()
69 SKIPPED = auto()
70 TEST_CRASHED = auto()
71 NO_TESTS = auto()
72 FAILURE_TO_PARSE_TESTS = auto()
73
74class TestCounts:
75 """
76 Tracks the counts of statuses of all test cases and any errors within
77 a Test.
78
79 Attributes:
80 passed : int - the number of tests that have passed
81 failed : int - the number of tests that have failed
82 crashed : int - the number of tests that have crashed
83 skipped : int - the number of tests that have skipped
84 errors : int - the number of errors in the test and subtests
85 """
86 def __init__(self):
87 """Creates TestCounts object with counts of all test
88 statuses and test errors set to 0.
89 """
90 self.passed = 0
91 self.failed = 0
92 self.crashed = 0
93 self.skipped = 0
94 self.errors = 0
95
96 def __str__(self) -> str:
97 """Returns the string representation of a TestCounts object.
98 """
99 return ('Passed: ' + str(self.passed) +
100 ', Failed: ' + str(self.failed) +
101 ', Crashed: ' + str(self.crashed) +
102 ', Skipped: ' + str(self.skipped) +
103 ', Errors: ' + str(self.errors))
104
105 def total(self) -> int:
106 """Returns the total number of test cases within a test
107 object, where a test case is a test with no subtests.
108 """
109 return (self.passed + self.failed + self.crashed +
110 self.skipped)
111
112 def add_subtest_counts(self, counts: TestCounts) -> None:
113 """
114 Adds the counts of another TestCounts object to the current
115 TestCounts object. Used to add the counts of a subtest to the
116 parent test.
117
118 Parameters:
119 counts - a different TestCounts object whose counts
120 will be added to the counts of the TestCounts object
121 """
122 self.passed += counts.passed
123 self.failed += counts.failed
124 self.crashed += counts.crashed
125 self.skipped += counts.skipped
126 self.errors += counts.errors
127
128 def get_status(self) -> TestStatus:
129 """Returns the aggregated status of a Test using test
130 counts.
131 """
132 if self.total() == 0:
133 return TestStatus.NO_TESTS
134 elif self.crashed:
135 # If one of the subtests crash, the expected status
136 # of the Test is crashed.
137 return TestStatus.TEST_CRASHED
138 elif self.failed:
139 # Otherwise if one of the subtests fail, the
140 # expected status of the Test is failed.
141 return TestStatus.FAILURE
142 elif self.passed:
143 # Otherwise if one of the subtests pass, the
144 # expected status of the Test is passed.
145 return TestStatus.SUCCESS
146 else:
147 # Finally, if none of the subtests have failed,
148 # crashed, or passed, the expected status of the
149 # Test is skipped.
150 return TestStatus.SKIPPED
151
152 def add_status(self, status: TestStatus) -> None:
153 """
154 Increments count of inputted status.
155
156 Parameters:
157 status - status to be added to the TestCounts object
158 """
159 if status == TestStatus.SUCCESS:
160 self.passed += 1
161 elif status == TestStatus.FAILURE:
162 self.failed += 1
163 elif status == TestStatus.SKIPPED:
164 self.skipped += 1
165 elif status != TestStatus.NO_TESTS:
166 self.crashed += 1
167
168class LineStream:
169 """
170 A class to represent the lines of kernel output.
171 Provides a peek()/pop() interface over an iterator of
172 (line#, text).
173 """
174 _lines: Iterator[Tuple[int, str]]
175 _next: Tuple[int, str]
176 _done: bool
177
178 def __init__(self, lines: Iterator[Tuple[int, str]]):
179 """Creates a new LineStream that wraps the given iterator."""
180 self._lines = lines
181 self._done = False
182 self._next = (0, '')
183 self._get_next()
184
185 def _get_next(self) -> None:
186 """Advances the LineSteam to the next line."""
187 try:
188 self._next = next(self._lines)
189 except StopIteration:
190 self._done = True
191
192 def peek(self) -> str:
193 """Returns the current line, without advancing the LineStream.
194 """
195 return self._next[1]
196
197 def pop(self) -> str:
198 """Returns the current line and advances the LineStream to
199 the next line.
200 """
201 n = self._next
202 self._get_next()
203 return n[1]
204
205 def __bool__(self) -> bool:
206 """Returns True if stream has more lines."""
207 return not self._done
208
209 # Only used by kunit_tool_test.py.
210 def __iter__(self) -> Iterator[str]:
211 """Empties all lines stored in LineStream object into
212 Iterator object and returns the Iterator object.
213 """
214 while bool(self):
215 yield self.pop()
216
217 def line_number(self) -> int:
218 """Returns the line number of the current line."""
219 return self._next[0]
220
221# Parsing helper methods:
222
223KTAP_START = re.compile(r'KTAP version ([0-9]+)$')
224TAP_START = re.compile(r'TAP version ([0-9]+)$')
225KTAP_END = re.compile('(List of all partitions:|'
226 'Kernel panic - not syncing: VFS:|reboot: System halted)')
227
228def extract_tap_lines(kernel_output: Iterable[str]) -> LineStream:
229 """Extracts KTAP lines from the kernel output."""
230 def isolate_ktap_output(kernel_output: Iterable[str]) \
231 -> Iterator[Tuple[int, str]]:
232 line_num = 0
233 started = False
234 for line in kernel_output:
235 line_num += 1
236 line = line.rstrip() # remove trailing \n
237 if not started and KTAP_START.search(line):
238 # start extracting KTAP lines and set prefix
239 # to number of characters before version line
240 prefix_len = len(
241 line.split('KTAP version')[0])
242 started = True
243 yield line_num, line[prefix_len:]
244 elif not started and TAP_START.search(line):
245 # start extracting KTAP lines and set prefix
246 # to number of characters before version line
247 prefix_len = len(line.split('TAP version')[0])
248 started = True
249 yield line_num, line[prefix_len:]
250 elif started and KTAP_END.search(line):
251 # stop extracting KTAP lines
252 break
253 elif started:
254 # remove prefix and any indention and yield
255 # line with line number
256 line = line[prefix_len:].lstrip()
257 yield line_num, line
258 return LineStream(lines=isolate_ktap_output(kernel_output))
259
260KTAP_VERSIONS = [1]
261TAP_VERSIONS = [13, 14]
262
263def check_version(version_num: int, accepted_versions: List[int],
264 version_type: str, test: Test) -> None:
265 """
266 Adds error to test object if version number is too high or too
267 low.
268
269 Parameters:
270 version_num - The inputted version number from the parsed KTAP or TAP
271 header line
272 accepted_version - List of accepted KTAP or TAP versions
273 version_type - 'KTAP' or 'TAP' depending on the type of
274 version line.
275 test - Test object for current test being parsed
276 """
277 if version_num < min(accepted_versions):
278 test.add_error(version_type +
279 ' version lower than expected!')
280 elif version_num > max(accepted_versions):
281 test.add_error(
282 version_type + ' version higher than expected!')
283
284def parse_ktap_header(lines: LineStream, test: Test) -> bool:
285 """
286 Parses KTAP/TAP header line and checks version number.
287 Returns False if fails to parse KTAP/TAP header line.
288
289 Accepted formats:
290 - 'KTAP version [version number]'
291 - 'TAP version [version number]'
292
293 Parameters:
294 lines - LineStream of KTAP output to parse
295 test - Test object for current test being parsed
296
297 Return:
298 True if successfully parsed KTAP/TAP header line
299 """
300 ktap_match = KTAP_START.match(lines.peek())
301 tap_match = TAP_START.match(lines.peek())
302 if ktap_match:
303 version_num = int(ktap_match.group(1))
304 check_version(version_num, KTAP_VERSIONS, 'KTAP', test)
305 elif tap_match:
306 version_num = int(tap_match.group(1))
307 check_version(version_num, TAP_VERSIONS, 'TAP', test)
308 else:
309 return False
310 test.log.append(lines.pop())
311 return True
312
313TEST_HEADER = re.compile(r'^# Subtest: (.*)$')
314
315def parse_test_header(lines: LineStream, test: Test) -> bool:
316 """
317 Parses test header and stores test name in test object.
318 Returns False if fails to parse test header line.
319
320 Accepted format:
321 - '# Subtest: [test name]'
322
323 Parameters:
324 lines - LineStream of KTAP output to parse
325 test - Test object for current test being parsed
326
327 Return:
328 True if successfully parsed test header line
329 """
330 match = TEST_HEADER.match(lines.peek())
331 if not match:
332 return False
333 test.log.append(lines.pop())
334 test.name = match.group(1)
335 return True
336
337TEST_PLAN = re.compile(r'1\.\.([0-9]+)')
338
339def parse_test_plan(lines: LineStream, test: Test) -> bool:
340 """
341 Parses test plan line and stores the expected number of subtests in
342 test object. Reports an error if expected count is 0.
343 Returns False and reports missing test plan error if fails to parse
344 test plan.
345
346 Accepted format:
347 - '1..[number of subtests]'
348
349 Parameters:
350 lines - LineStream of KTAP output to parse
351 test - Test object for current test being parsed
352
353 Return:
354 True if successfully parsed test plan line
355 """
356 match = TEST_PLAN.match(lines.peek())
357 if not match:
358 test.expected_count = None
359 test.add_error('missing plan line!')
360 return False
361 test.log.append(lines.pop())
362 expected_count = int(match.group(1))
363 test.expected_count = expected_count
364 if expected_count == 0:
365 test.status = TestStatus.NO_TESTS
366 test.add_error('0 tests run!')
367 return True
368
369TEST_RESULT = re.compile(r'^(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
370
371TEST_RESULT_SKIP = re.compile(r'^(ok|not ok) ([0-9]+) (- )?(.*) # SKIP(.*)$')
372
373def peek_test_name_match(lines: LineStream, test: Test) -> bool:
374 """
375 Matches current line with the format of a test result line and checks
376 if the name matches the name of the current test.
377 Returns False if fails to match format or name.
378
379 Accepted format:
380 - '[ok|not ok] [test number] [-] [test name] [optional skip
381 directive]'
382
383 Parameters:
384 lines - LineStream of KTAP output to parse
385 test - Test object for current test being parsed
386
387 Return:
388 True if matched a test result line and the name matching the
389 expected test name
390 """
391 line = lines.peek()
392 match = TEST_RESULT.match(line)
393 if not match:
394 return False
395 name = match.group(4)
396 return (name == test.name)
397
398def parse_test_result(lines: LineStream, test: Test,
399 expected_num: int) -> bool:
400 """
401 Parses test result line and stores the status and name in the test
402 object. Reports an error if the test number does not match expected
403 test number.
404 Returns False if fails to parse test result line.
405
406 Note that the SKIP directive is the only direction that causes a
407 change in status.
408
409 Accepted format:
410 - '[ok|not ok] [test number] [-] [test name] [optional skip
411 directive]'
412
413 Parameters:
414 lines - LineStream of KTAP output to parse
415 test - Test object for current test being parsed
416 expected_num - expected test number for current test
417
418 Return:
419 True if successfully parsed a test result line.
420 """
421 line = lines.peek()
422 match = TEST_RESULT.match(line)
423 skip_match = TEST_RESULT_SKIP.match(line)
424
425 # Check if line matches test result line format
426 if not match:
427 return False
428 test.log.append(lines.pop())
429
430 # Set name of test object
431 if skip_match:
432 test.name = skip_match.group(4)
433 else:
434 test.name = match.group(4)
435
436 # Check test num
437 num = int(match.group(2))
438 if num != expected_num:
439 test.add_error('Expected test number ' +
440 str(expected_num) + ' but found ' + str(num))
441
442 # Set status of test object
443 status = match.group(1)
444 if skip_match:
445 test.status = TestStatus.SKIPPED
446 elif status == 'ok':
447 test.status = TestStatus.SUCCESS
448 else:
449 test.status = TestStatus.FAILURE
450 return True
451
452def parse_diagnostic(lines: LineStream) -> List[str]:
453 """
454 Parse lines that do not match the format of a test result line or
455 test header line and returns them in list.
456
457 Line formats that are not parsed:
458 - '# Subtest: [test name]'
459 - '[ok|not ok] [test number] [-] [test name] [optional skip
460 directive]'
461
462 Parameters:
463 lines - LineStream of KTAP output to parse
464
465 Return:
466 Log of diagnostic lines
467 """
468 log = [] # type: List[str]
469 while lines and not TEST_RESULT.match(lines.peek()) and not \
470 TEST_HEADER.match(lines.peek()):
471 log.append(lines.pop())
472 return log
473
474DIAGNOSTIC_CRASH_MESSAGE = re.compile(r'^# .*?: kunit test case crashed!$')
475
476def parse_crash_in_log(test: Test) -> bool:
477 """
478 Iterate through the lines of the log to parse for crash message.
479 If crash message found, set status to crashed and return True.
480 Otherwise return False.
481
482 Parameters:
483 test - Test object for current test being parsed
484
485 Return:
486 True if crash message found in log
487 """
488 for line in test.log:
489 if DIAGNOSTIC_CRASH_MESSAGE.match(line):
490 test.status = TestStatus.TEST_CRASHED
491 return True
492 return False
493
494
495# Printing helper methods:
496
497DIVIDER = '=' * 60
498
499RESET = '\033[0;0m'
500
501def red(text: str) -> str:
502 """Returns inputted string with red color code."""
503 return '\033[1;31m' + text + RESET
504
505def yellow(text: str) -> str:
506 """Returns inputted string with yellow color code."""
507 return '\033[1;33m' + text + RESET
508
509def green(text: str) -> str:
510 """Returns inputted string with green color code."""
511 return '\033[1;32m' + text + RESET
512
513ANSI_LEN = len(red(''))
514
515def print_with_timestamp(message: str) -> None:
516 """Prints message with timestamp at beginning."""
517 print('[%s] %s' % (datetime.now().strftime('%H:%M:%S'), message))
518
519def format_test_divider(message: str, len_message: int) -> str:
520 """
521 Returns string with message centered in fixed width divider.
522
523 Example:
524 '===================== message example ====================='
525
526 Parameters:
527 message - message to be centered in divider line
528 len_message - length of the message to be printed such that
529 any characters of the color codes are not counted
530
531 Return:
532 String containing message centered in fixed width divider
533 """
534 default_count = 3 # default number of dashes
535 len_1 = default_count
536 len_2 = default_count
537 difference = len(DIVIDER) - len_message - 2 # 2 spaces added
538 if difference > 0:
539 # calculate number of dashes for each side of the divider
540 len_1 = int(difference / 2)
541 len_2 = difference - len_1
542 return ('=' * len_1) + ' ' + message + ' ' + ('=' * len_2)
543
544def print_test_header(test: Test) -> None:
545 """
546 Prints test header with test name and optionally the expected number
547 of subtests.
548
549 Example:
550 '=================== example (2 subtests) ==================='
551
552 Parameters:
553 test - Test object representing current test being printed
554 """
555 message = test.name
556 if test.expected_count:
557 if test.expected_count == 1:
558 message += (' (' + str(test.expected_count) +
559 ' subtest)')
560 else:
561 message += (' (' + str(test.expected_count) +
562 ' subtests)')
563 print_with_timestamp(format_test_divider(message, len(message)))
564
565def print_log(log: Iterable[str]) -> None:
566 """
567 Prints all strings in saved log for test in yellow.
568
569 Parameters:
570 log - Iterable object with all strings saved in log for test
571 """
572 for m in log:
573 print_with_timestamp(yellow(m))
574
575def format_test_result(test: Test) -> str:
576 """
577 Returns string with formatted test result with colored status and test
578 name.
579
580 Example:
581 '[PASSED] example'
582
583 Parameters:
584 test - Test object representing current test being printed
585
586 Return:
587 String containing formatted test result
588 """
589 if test.status == TestStatus.SUCCESS:
590 return (green('[PASSED] ') + test.name)
591 elif test.status == TestStatus.SKIPPED:
592 return (yellow('[SKIPPED] ') + test.name)
593 elif test.status == TestStatus.TEST_CRASHED:
594 print_log(test.log)
595 return (red('[CRASHED] ') + test.name)
596 else:
597 print_log(test.log)
598 return (red('[FAILED] ') + test.name)
599
600def print_test_result(test: Test) -> None:
601 """
602 Prints result line with status of test.
603
604 Example:
605 '[PASSED] example'
606
607 Parameters:
608 test - Test object representing current test being printed
609 """
610 print_with_timestamp(format_test_result(test))
611
612def print_test_footer(test: Test) -> None:
613 """
614 Prints test footer with status of test.
615
616 Example:
617 '===================== [PASSED] example ====================='
618
619 Parameters:
620 test - Test object representing current test being printed
621 """
622 message = format_test_result(test)
623 print_with_timestamp(format_test_divider(message,
624 len(message) - ANSI_LEN))
625
626def print_summary_line(test: Test) -> None:
627 """
628 Prints summary line of test object. Color of line is dependent on
629 status of test. Color is green if test passes, yellow if test is
630 skipped, and red if the test fails or crashes. Summary line contains
631 counts of the statuses of the tests subtests or the test itself if it
632 has no subtests.
633
634 Example:
635 "Testing complete. Passed: 2, Failed: 0, Crashed: 0, Skipped: 0,
636 Errors: 0"
637
638 test - Test object representing current test being printed
639 """
640 if test.status == TestStatus.SUCCESS:
641 color = green
642 elif test.status == TestStatus.SKIPPED or test.status == TestStatus.NO_TESTS:
643 color = yellow
644 else:
645 color = red
646 counts = test.counts
647 print_with_timestamp(color('Testing complete. ' + str(counts)))
648
649def print_error(error_message: str) -> None:
650 """
651 Prints error message with error format.
652
653 Example:
654 "[ERROR] Test example: missing test plan!"
655
656 Parameters:
657 error_message - message describing error
658 """
659 print_with_timestamp(red('[ERROR] ') + error_message)
660
661# Other methods:
662
663def bubble_up_test_results(test: Test) -> None:
664 """
665 If the test has subtests, add the test counts of the subtests to the
666 test and check if any of the tests crashed and if so set the test
667 status to crashed. Otherwise if the test has no subtests add the
668 status of the test to the test counts.
669
670 Parameters:
671 test - Test object for current test being parsed
672 """
673 parse_crash_in_log(test)
674 subtests = test.subtests
675 counts = test.counts
676 status = test.status
677 for t in subtests:
678 counts.add_subtest_counts(t.counts)
679 if counts.total() == 0:
680 counts.add_status(status)
681 elif test.counts.get_status() == TestStatus.TEST_CRASHED:
682 test.status = TestStatus.TEST_CRASHED
683
684def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test:
685 """
686 Finds next test to parse in LineStream, creates new Test object,
687 parses any subtests of the test, populates Test object with all
688 information (status, name) about the test and the Test objects for
689 any subtests, and then returns the Test object. The method accepts
690 three formats of tests:
691
692 Accepted test formats:
693
694 - Main KTAP/TAP header
695
696 Example:
697
698 KTAP version 1
699 1..4
700 [subtests]
701
702 - Subtest header line
703
704 Example:
705
706 # Subtest: name
707 1..3
708 [subtests]
709 ok 1 name
710
711 - Test result line
712
713 Example:
714
715 ok 1 - test
716
717 Parameters:
718 lines - LineStream of KTAP output to parse
719 expected_num - expected test number for test to be parsed
720 log - list of strings containing any preceding diagnostic lines
721 corresponding to the current test
722
723 Return:
724 Test object populated with characteristics and any subtests
725 """
726 test = Test()
727 test.log.extend(log)
728 parent_test = False
729 main = parse_ktap_header(lines, test)
730 if main:
731 # If KTAP/TAP header is found, attempt to parse
732 # test plan
733 test.name = "main"
734 parse_test_plan(lines, test)
735 else:
736 # If KTAP/TAP header is not found, test must be subtest
737 # header or test result line so parse attempt to parser
738 # subtest header
739 parent_test = parse_test_header(lines, test)
740 if parent_test:
741 # If subtest header is found, attempt to parse
742 # test plan and print header
743 parse_test_plan(lines, test)
744 print_test_header(test)
745 expected_count = test.expected_count
746 subtests = []
747 test_num = 1
748 while expected_count is None or test_num <= expected_count:
749 # Loop to parse any subtests.
750 # Break after parsing expected number of tests or
751 # if expected number of tests is unknown break when test
752 # result line with matching name to subtest header is found
753 # or no more lines in stream.
754 sub_log = parse_diagnostic(lines)
755 sub_test = Test()
756 if not lines or (peek_test_name_match(lines, test) and
757 not main):
758 if expected_count and test_num <= expected_count:
759 # If parser reaches end of test before
760 # parsing expected number of subtests, print
761 # crashed subtest and record error
762 test.add_error('missing expected subtest!')
763 sub_test.log.extend(sub_log)
764 test.counts.add_status(
765 TestStatus.TEST_CRASHED)
766 print_test_result(sub_test)
767 else:
768 test.log.extend(sub_log)
769 break
770 else:
771 sub_test = parse_test(lines, test_num, sub_log)
772 subtests.append(sub_test)
773 test_num += 1
774 test.subtests = subtests
775 if not main:
776 # If not main test, look for test result line
777 test.log.extend(parse_diagnostic(lines))
778 if (parent_test and peek_test_name_match(lines, test)) or \
779 not parent_test:
780 parse_test_result(lines, test, expected_num)
781 else:
782 test.add_error('missing subtest result line!')
783 # Add statuses to TestCounts attribute in Test object
784 bubble_up_test_results(test)
785 if parent_test:
786 # If test has subtests and is not the main test object, print
787 # footer.
788 print_test_footer(test)
789 elif not main:
790 print_test_result(test)
791 return test
792
793def parse_run_tests(kernel_output: Iterable[str]) -> TestResult:
794 """
795 Using kernel output, extract KTAP lines, parse the lines for test
796 results and print condensed test results and summary line .
797
798 Parameters:
799 kernel_output - Iterable object contains lines of kernel output
800
801 Return:
802 TestResult - Tuple containg status of main test object, main test
803 object with all subtests, and log of all KTAP lines.
804 """
805 print_with_timestamp(DIVIDER)
806 lines = extract_tap_lines(kernel_output)
807 test = Test()
808 if not lines:
809 test.add_error('invalid KTAP input!')
810 test.status = TestStatus.FAILURE_TO_PARSE_TESTS
811 else:
812 test = parse_test(lines, 0, [])
813 if test.status != TestStatus.NO_TESTS:
814 test.status = test.counts.get_status()
815 print_with_timestamp(DIVIDER)
816 print_summary_line(test)
817 return TestResult(test.status, test, lines)