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 sets expected_count to None if there is no valid test 344 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 return False 360 test.log.append(lines.pop()) 361 expected_count = int(match.group(1)) 362 test.expected_count = expected_count 363 return True 364 365TEST_RESULT = re.compile(r'^(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$') 366 367TEST_RESULT_SKIP = re.compile(r'^(ok|not ok) ([0-9]+) (- )?(.*) # SKIP(.*)$') 368 369def peek_test_name_match(lines: LineStream, test: Test) -> bool: 370 """ 371 Matches current line with the format of a test result line and checks 372 if the name matches the name of the current test. 373 Returns False if fails to match format or name. 374 375 Accepted format: 376 - '[ok|not ok] [test number] [-] [test name] [optional skip 377 directive]' 378 379 Parameters: 380 lines - LineStream of KTAP output to parse 381 test - Test object for current test being parsed 382 383 Return: 384 True if matched a test result line and the name matching the 385 expected test name 386 """ 387 line = lines.peek() 388 match = TEST_RESULT.match(line) 389 if not match: 390 return False 391 name = match.group(4) 392 return (name == test.name) 393 394def parse_test_result(lines: LineStream, test: Test, 395 expected_num: int) -> bool: 396 """ 397 Parses test result line and stores the status and name in the test 398 object. Reports an error if the test number does not match expected 399 test number. 400 Returns False if fails to parse test result line. 401 402 Note that the SKIP directive is the only direction that causes a 403 change in status. 404 405 Accepted format: 406 - '[ok|not ok] [test number] [-] [test name] [optional skip 407 directive]' 408 409 Parameters: 410 lines - LineStream of KTAP output to parse 411 test - Test object for current test being parsed 412 expected_num - expected test number for current test 413 414 Return: 415 True if successfully parsed a test result line. 416 """ 417 line = lines.peek() 418 match = TEST_RESULT.match(line) 419 skip_match = TEST_RESULT_SKIP.match(line) 420 421 # Check if line matches test result line format 422 if not match: 423 return False 424 test.log.append(lines.pop()) 425 426 # Set name of test object 427 if skip_match: 428 test.name = skip_match.group(4) 429 else: 430 test.name = match.group(4) 431 432 # Check test num 433 num = int(match.group(2)) 434 if num != expected_num: 435 test.add_error('Expected test number ' + 436 str(expected_num) + ' but found ' + str(num)) 437 438 # Set status of test object 439 status = match.group(1) 440 if skip_match: 441 test.status = TestStatus.SKIPPED 442 elif status == 'ok': 443 test.status = TestStatus.SUCCESS 444 else: 445 test.status = TestStatus.FAILURE 446 return True 447 448def parse_diagnostic(lines: LineStream) -> List[str]: 449 """ 450 Parse lines that do not match the format of a test result line or 451 test header line and returns them in list. 452 453 Line formats that are not parsed: 454 - '# Subtest: [test name]' 455 - '[ok|not ok] [test number] [-] [test name] [optional skip 456 directive]' 457 458 Parameters: 459 lines - LineStream of KTAP output to parse 460 461 Return: 462 Log of diagnostic lines 463 """ 464 log = [] # type: List[str] 465 while lines and not TEST_RESULT.match(lines.peek()) and not \ 466 TEST_HEADER.match(lines.peek()): 467 log.append(lines.pop()) 468 return log 469 470DIAGNOSTIC_CRASH_MESSAGE = re.compile(r'^# .*?: kunit test case crashed!$') 471 472def parse_crash_in_log(test: Test) -> bool: 473 """ 474 Iterate through the lines of the log to parse for crash message. 475 If crash message found, set status to crashed and return True. 476 Otherwise return False. 477 478 Parameters: 479 test - Test object for current test being parsed 480 481 Return: 482 True if crash message found in log 483 """ 484 for line in test.log: 485 if DIAGNOSTIC_CRASH_MESSAGE.match(line): 486 test.status = TestStatus.TEST_CRASHED 487 return True 488 return False 489 490 491# Printing helper methods: 492 493DIVIDER = '=' * 60 494 495RESET = '\033[0;0m' 496 497def red(text: str) -> str: 498 """Returns inputted string with red color code.""" 499 return '\033[1;31m' + text + RESET 500 501def yellow(text: str) -> str: 502 """Returns inputted string with yellow color code.""" 503 return '\033[1;33m' + text + RESET 504 505def green(text: str) -> str: 506 """Returns inputted string with green color code.""" 507 return '\033[1;32m' + text + RESET 508 509ANSI_LEN = len(red('')) 510 511def print_with_timestamp(message: str) -> None: 512 """Prints message with timestamp at beginning.""" 513 print('[%s] %s' % (datetime.now().strftime('%H:%M:%S'), message)) 514 515def format_test_divider(message: str, len_message: int) -> str: 516 """ 517 Returns string with message centered in fixed width divider. 518 519 Example: 520 '===================== message example =====================' 521 522 Parameters: 523 message - message to be centered in divider line 524 len_message - length of the message to be printed such that 525 any characters of the color codes are not counted 526 527 Return: 528 String containing message centered in fixed width divider 529 """ 530 default_count = 3 # default number of dashes 531 len_1 = default_count 532 len_2 = default_count 533 difference = len(DIVIDER) - len_message - 2 # 2 spaces added 534 if difference > 0: 535 # calculate number of dashes for each side of the divider 536 len_1 = int(difference / 2) 537 len_2 = difference - len_1 538 return ('=' * len_1) + ' ' + message + ' ' + ('=' * len_2) 539 540def print_test_header(test: Test) -> None: 541 """ 542 Prints test header with test name and optionally the expected number 543 of subtests. 544 545 Example: 546 '=================== example (2 subtests) ===================' 547 548 Parameters: 549 test - Test object representing current test being printed 550 """ 551 message = test.name 552 if test.expected_count: 553 if test.expected_count == 1: 554 message += (' (' + str(test.expected_count) + 555 ' subtest)') 556 else: 557 message += (' (' + str(test.expected_count) + 558 ' subtests)') 559 print_with_timestamp(format_test_divider(message, len(message))) 560 561def print_log(log: Iterable[str]) -> None: 562 """ 563 Prints all strings in saved log for test in yellow. 564 565 Parameters: 566 log - Iterable object with all strings saved in log for test 567 """ 568 for m in log: 569 print_with_timestamp(yellow(m)) 570 571def format_test_result(test: Test) -> str: 572 """ 573 Returns string with formatted test result with colored status and test 574 name. 575 576 Example: 577 '[PASSED] example' 578 579 Parameters: 580 test - Test object representing current test being printed 581 582 Return: 583 String containing formatted test result 584 """ 585 if test.status == TestStatus.SUCCESS: 586 return (green('[PASSED] ') + test.name) 587 elif test.status == TestStatus.SKIPPED: 588 return (yellow('[SKIPPED] ') + test.name) 589 elif test.status == TestStatus.NO_TESTS: 590 return (yellow('[NO TESTS RUN] ') + test.name) 591 elif test.status == TestStatus.TEST_CRASHED: 592 print_log(test.log) 593 return (red('[CRASHED] ') + test.name) 594 else: 595 print_log(test.log) 596 return (red('[FAILED] ') + test.name) 597 598def print_test_result(test: Test) -> None: 599 """ 600 Prints result line with status of test. 601 602 Example: 603 '[PASSED] example' 604 605 Parameters: 606 test - Test object representing current test being printed 607 """ 608 print_with_timestamp(format_test_result(test)) 609 610def print_test_footer(test: Test) -> None: 611 """ 612 Prints test footer with status of test. 613 614 Example: 615 '===================== [PASSED] example =====================' 616 617 Parameters: 618 test - Test object representing current test being printed 619 """ 620 message = format_test_result(test) 621 print_with_timestamp(format_test_divider(message, 622 len(message) - ANSI_LEN)) 623 624def print_summary_line(test: Test) -> None: 625 """ 626 Prints summary line of test object. Color of line is dependent on 627 status of test. Color is green if test passes, yellow if test is 628 skipped, and red if the test fails or crashes. Summary line contains 629 counts of the statuses of the tests subtests or the test itself if it 630 has no subtests. 631 632 Example: 633 "Testing complete. Passed: 2, Failed: 0, Crashed: 0, Skipped: 0, 634 Errors: 0" 635 636 test - Test object representing current test being printed 637 """ 638 if test.status == TestStatus.SUCCESS: 639 color = green 640 elif test.status == TestStatus.SKIPPED or test.status == TestStatus.NO_TESTS: 641 color = yellow 642 else: 643 color = red 644 counts = test.counts 645 print_with_timestamp(color('Testing complete. ' + str(counts))) 646 647def print_error(error_message: str) -> None: 648 """ 649 Prints error message with error format. 650 651 Example: 652 "[ERROR] Test example: missing test plan!" 653 654 Parameters: 655 error_message - message describing error 656 """ 657 print_with_timestamp(red('[ERROR] ') + error_message) 658 659# Other methods: 660 661def bubble_up_test_results(test: Test) -> None: 662 """ 663 If the test has subtests, add the test counts of the subtests to the 664 test and check if any of the tests crashed and if so set the test 665 status to crashed. Otherwise if the test has no subtests add the 666 status of the test to the test counts. 667 668 Parameters: 669 test - Test object for current test being parsed 670 """ 671 parse_crash_in_log(test) 672 subtests = test.subtests 673 counts = test.counts 674 status = test.status 675 for t in subtests: 676 counts.add_subtest_counts(t.counts) 677 if counts.total() == 0: 678 counts.add_status(status) 679 elif test.counts.get_status() == TestStatus.TEST_CRASHED: 680 test.status = TestStatus.TEST_CRASHED 681 682def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: 683 """ 684 Finds next test to parse in LineStream, creates new Test object, 685 parses any subtests of the test, populates Test object with all 686 information (status, name) about the test and the Test objects for 687 any subtests, and then returns the Test object. The method accepts 688 three formats of tests: 689 690 Accepted test formats: 691 692 - Main KTAP/TAP header 693 694 Example: 695 696 KTAP version 1 697 1..4 698 [subtests] 699 700 - Subtest header line 701 702 Example: 703 704 # Subtest: name 705 1..3 706 [subtests] 707 ok 1 name 708 709 - Test result line 710 711 Example: 712 713 ok 1 - test 714 715 Parameters: 716 lines - LineStream of KTAP output to parse 717 expected_num - expected test number for test to be parsed 718 log - list of strings containing any preceding diagnostic lines 719 corresponding to the current test 720 721 Return: 722 Test object populated with characteristics and any subtests 723 """ 724 test = Test() 725 test.log.extend(log) 726 parent_test = False 727 main = parse_ktap_header(lines, test) 728 if main: 729 # If KTAP/TAP header is found, attempt to parse 730 # test plan 731 test.name = "main" 732 parse_test_plan(lines, test) 733 parent_test = True 734 else: 735 # If KTAP/TAP header is not found, test must be subtest 736 # header or test result line so parse attempt to parser 737 # subtest header 738 parent_test = parse_test_header(lines, test) 739 if parent_test: 740 # If subtest header is found, attempt to parse 741 # test plan and print header 742 parse_test_plan(lines, test) 743 print_test_header(test) 744 expected_count = test.expected_count 745 subtests = [] 746 test_num = 1 747 while parent_test and (expected_count is None or test_num <= expected_count): 748 # Loop to parse any subtests. 749 # Break after parsing expected number of tests or 750 # if expected number of tests is unknown break when test 751 # result line with matching name to subtest header is found 752 # or no more lines in stream. 753 sub_log = parse_diagnostic(lines) 754 sub_test = Test() 755 if not lines or (peek_test_name_match(lines, test) and 756 not main): 757 if expected_count and test_num <= expected_count: 758 # If parser reaches end of test before 759 # parsing expected number of subtests, print 760 # crashed subtest and record error 761 test.add_error('missing expected subtest!') 762 sub_test.log.extend(sub_log) 763 test.counts.add_status( 764 TestStatus.TEST_CRASHED) 765 print_test_result(sub_test) 766 else: 767 test.log.extend(sub_log) 768 break 769 else: 770 sub_test = parse_test(lines, test_num, sub_log) 771 subtests.append(sub_test) 772 test_num += 1 773 test.subtests = subtests 774 if not main: 775 # If not main test, look for test result line 776 test.log.extend(parse_diagnostic(lines)) 777 if (parent_test and peek_test_name_match(lines, test)) or \ 778 not parent_test: 779 parse_test_result(lines, test, expected_num) 780 else: 781 test.add_error('missing subtest result line!') 782 783 # Check for there being no tests 784 if parent_test and len(subtests) == 0: 785 test.status = TestStatus.NO_TESTS 786 test.add_error('0 tests run!') 787 788 # Add statuses to TestCounts attribute in Test object 789 bubble_up_test_results(test) 790 if parent_test and not main: 791 # If test has subtests and is not the main test object, print 792 # footer. 793 print_test_footer(test) 794 elif not main: 795 print_test_result(test) 796 return test 797 798def parse_run_tests(kernel_output: Iterable[str]) -> TestResult: 799 """ 800 Using kernel output, extract KTAP lines, parse the lines for test 801 results and print condensed test results and summary line . 802 803 Parameters: 804 kernel_output - Iterable object contains lines of kernel output 805 806 Return: 807 TestResult - Tuple containg status of main test object, main test 808 object with all subtests, and log of all KTAP lines. 809 """ 810 print_with_timestamp(DIVIDER) 811 lines = extract_tap_lines(kernel_output) 812 test = Test() 813 if not lines: 814 test.add_error('invalid KTAP input!') 815 test.status = TestStatus.FAILURE_TO_PARSE_TESTS 816 else: 817 test = parse_test(lines, 0, []) 818 if test.status != TestStatus.NO_TESTS: 819 test.status = test.counts.get_status() 820 print_with_timestamp(DIVIDER) 821 print_summary_line(test) 822 return TestResult(test.status, test, lines) 823