Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

kunit: tool: parse KTAP compliant test output

Change the KUnit parser to be able to parse test output that complies with
the KTAP version 1 specification format found here:
https://kernel.org/doc/html/latest/dev-tools/ktap.html. Ensure the parser
is able to parse tests with the original KUnit test output format as
well.

KUnit parser now accepts any of the following test output formats:

Original KUnit test output format:

TAP version 14
1..1
# Subtest: kunit-test-suite
1..3
ok 1 - kunit_test_1
ok 2 - kunit_test_2
ok 3 - kunit_test_3
# kunit-test-suite: pass:3 fail:0 skip:0 total:3
# Totals: pass:3 fail:0 skip:0 total:3
ok 1 - kunit-test-suite

KTAP version 1 test output format:

KTAP version 1
1..1
KTAP version 1
1..3
ok 1 kunit_test_1
ok 2 kunit_test_2
ok 3 kunit_test_3
ok 1 kunit-test-suite

New KUnit test output format (changes made in the next patch of
this series):

KTAP version 1
1..1
KTAP version 1
# Subtest: kunit-test-suite
1..3
ok 1 kunit_test_1
ok 2 kunit_test_2
ok 3 kunit_test_3
# kunit-test-suite: pass:3 fail:0 skip:0 total:3
# Totals: pass:3 fail:0 skip:0 total:3
ok 1 kunit-test-suite

Signed-off-by: Rae Moar <rmoar@google.com>
Reviewed-by: Daniel Latypov <dlatypov@google.com>
Reviewed-by: David Gow <davidgow@google.com>
Signed-off-by: Shuah Khan <skhan@linuxfoundation.org>

authored by

Rae Moar and committed by
Shuah Khan
434498a6 909c6475

+80 -28
+51 -28
tools/testing/kunit/kunit_parser.py
··· 441 441 - '# Subtest: [test name]' 442 442 - '[ok|not ok] [test number] [-] [test name] [optional skip 443 443 directive]' 444 + - 'KTAP version [version number]' 444 445 445 446 Parameters: 446 447 lines - LineStream of KTAP output to parse ··· 450 449 Log of diagnostic lines 451 450 """ 452 451 log = [] # type: List[str] 453 - while lines and not TEST_RESULT.match(lines.peek()) and not \ 454 - TEST_HEADER.match(lines.peek()): 452 + non_diagnostic_lines = [TEST_RESULT, TEST_HEADER, KTAP_START] 453 + while lines and not any(re.match(lines.peek()) 454 + for re in non_diagnostic_lines): 455 455 log.append(lines.pop()) 456 456 return log 457 457 ··· 498 496 test - Test object representing current test being printed 499 497 """ 500 498 message = test.name 499 + if message != "": 500 + # Add a leading space before the subtest counts only if a test name 501 + # is provided using a "# Subtest" header line. 502 + message += " " 501 503 if test.expected_count: 502 504 if test.expected_count == 1: 503 - message += ' (1 subtest)' 505 + message += '(1 subtest)' 504 506 else: 505 - message += f' ({test.expected_count} subtests)' 507 + message += f'({test.expected_count} subtests)' 506 508 stdout.print_with_timestamp(format_test_divider(message, len(message))) 507 509 508 510 def print_log(log: Iterable[str]) -> None: ··· 653 647 elif test.counts.get_status() == TestStatus.TEST_CRASHED: 654 648 test.status = TestStatus.TEST_CRASHED 655 649 656 - def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: 650 + def parse_test(lines: LineStream, expected_num: int, log: List[str], is_subtest: bool) -> Test: 657 651 """ 658 652 Finds next test to parse in LineStream, creates new Test object, 659 653 parses any subtests of the test, populates Test object with all ··· 671 665 1..4 672 666 [subtests] 673 667 674 - - Subtest header line 668 + - Subtest header (must include either the KTAP version line or 669 + "# Subtest" header line) 675 670 676 - Example: 671 + Example (preferred format with both KTAP version line and 672 + "# Subtest" line): 673 + 674 + KTAP version 1 675 + # Subtest: name 676 + 1..3 677 + [subtests] 678 + ok 1 name 679 + 680 + Example (only "# Subtest" line): 677 681 678 682 # Subtest: name 683 + 1..3 684 + [subtests] 685 + ok 1 name 686 + 687 + Example (only KTAP version line, compliant with KTAP v1 spec): 688 + 689 + KTAP version 1 679 690 1..3 680 691 [subtests] 681 692 ok 1 name ··· 708 685 expected_num - expected test number for test to be parsed 709 686 log - list of strings containing any preceding diagnostic lines 710 687 corresponding to the current test 688 + is_subtest - boolean indicating whether test is a subtest 711 689 712 690 Return: 713 691 Test object populated with characteristics and any subtests 714 692 """ 715 693 test = Test() 716 694 test.log.extend(log) 717 - parent_test = False 718 - main = parse_ktap_header(lines, test) 719 - if main: 720 - # If KTAP/TAP header is found, attempt to parse 695 + if not is_subtest: 696 + # If parsing the main/top-level test, parse KTAP version line and 721 697 # test plan 722 698 test.name = "main" 699 + ktap_line = parse_ktap_header(lines, test) 723 700 parse_test_plan(lines, test) 724 701 parent_test = True 725 702 else: 726 - # If KTAP/TAP header is not found, test must be subtest 727 - # header or test result line so parse attempt to parser 728 - # subtest header 729 - parent_test = parse_test_header(lines, test) 703 + # If not the main test, attempt to parse a test header containing 704 + # the KTAP version line and/or subtest header line 705 + ktap_line = parse_ktap_header(lines, test) 706 + subtest_line = parse_test_header(lines, test) 707 + parent_test = (ktap_line or subtest_line) 730 708 if parent_test: 731 - # If subtest header is found, attempt to parse 732 - # test plan and print header 709 + # If KTAP version line and/or subtest header is found, attempt 710 + # to parse test plan and print test header 733 711 parse_test_plan(lines, test) 734 712 print_test_header(test) 735 713 expected_count = test.expected_count ··· 745 721 sub_log = parse_diagnostic(lines) 746 722 sub_test = Test() 747 723 if not lines or (peek_test_name_match(lines, test) and 748 - not main): 724 + is_subtest): 749 725 if expected_count and test_num <= expected_count: 750 726 # If parser reaches end of test before 751 727 # parsing expected number of subtests, print ··· 759 735 test.log.extend(sub_log) 760 736 break 761 737 else: 762 - sub_test = parse_test(lines, test_num, sub_log) 738 + sub_test = parse_test(lines, test_num, sub_log, True) 763 739 subtests.append(sub_test) 764 740 test_num += 1 765 741 test.subtests = subtests 766 - if not main: 742 + if is_subtest: 767 743 # If not main test, look for test result line 768 744 test.log.extend(parse_diagnostic(lines)) 769 - if (parent_test and peek_test_name_match(lines, test)) or \ 770 - not parent_test: 771 - parse_test_result(lines, test, expected_num) 772 - else: 745 + if test.name != "" and not peek_test_name_match(lines, test): 773 746 test.add_error('missing subtest result line!') 747 + else: 748 + parse_test_result(lines, test, expected_num) 774 749 775 - # Check for there being no tests 750 + # Check for there being no subtests within parent test 776 751 if parent_test and len(subtests) == 0: 777 752 # Don't override a bad status if this test had one reported. 778 753 # Assumption: no subtests means CRASHED is from Test.__init__() ··· 781 758 782 759 # Add statuses to TestCounts attribute in Test object 783 760 bubble_up_test_results(test) 784 - if parent_test and not main: 761 + if parent_test and is_subtest: 785 762 # If test has subtests and is not the main test object, print 786 763 # footer. 787 764 print_test_footer(test) 788 - elif not main: 765 + elif is_subtest: 789 766 print_test_result(test) 790 767 return test 791 768 ··· 808 785 test.add_error('Could not find any KTAP output. Did any KUnit tests run?') 809 786 test.status = TestStatus.FAILURE_TO_PARSE_TESTS 810 787 else: 811 - test = parse_test(lines, 0, []) 788 + test = parse_test(lines, 0, [], False) 812 789 if test.status != TestStatus.NO_TESTS: 813 790 test.status = test.counts.get_status() 814 791 stdout.print_with_timestamp(DIVIDER)
+14
tools/testing/kunit/kunit_tool_test.py
··· 312 312 self.assertEqual(kunit_parser._summarize_failed_tests(result), 313 313 'Failures: all_failed_suite, some_failed_suite.test2') 314 314 315 + def test_ktap_format(self): 316 + ktap_log = test_data_path('test_parse_ktap_output.log') 317 + with open(ktap_log) as file: 318 + result = kunit_parser.parse_run_tests(file.readlines()) 319 + self.assertEqual(result.counts, kunit_parser.TestCounts(passed=3)) 320 + self.assertEqual('suite', result.subtests[0].name) 321 + self.assertEqual('case_1', result.subtests[0].subtests[0].name) 322 + self.assertEqual('case_2', result.subtests[0].subtests[1].name) 323 + 324 + def test_parse_subtest_header(self): 325 + ktap_log = test_data_path('test_parse_subtest_header.log') 326 + with open(ktap_log) as file: 327 + result = kunit_parser.parse_run_tests(file.readlines()) 328 + self.print_mock.assert_any_call(StrContains('suite (1 subtest)')) 315 329 316 330 def line_stream_from_strs(strs: Iterable[str]) -> kunit_parser.LineStream: 317 331 return kunit_parser.LineStream(enumerate(strs, start=1))
+8
tools/testing/kunit/test_data/test_parse_ktap_output.log
··· 1 + KTAP version 1 2 + 1..1 3 + KTAP version 1 4 + 1..3 5 + ok 1 case_1 6 + ok 2 case_2 7 + ok 3 case_3 8 + ok 1 suite
+7
tools/testing/kunit/test_data/test_parse_subtest_header.log
··· 1 + KTAP version 1 2 + 1..1 3 + KTAP version 1 4 + # Subtest: suite 5 + 1..1 6 + ok 1 test 7 + ok 1 suite