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