kunit: test: add test plan to KUnit TAP format

TAP 14 allows an optional test plan to be emitted before the start of
the start of testing[1]; this is valuable because it makes it possible
for a test harness to detect whether the number of tests run matches the
number of tests expected to be run, ensuring that no tests silently
failed.

Link[1]: https://github.com/isaacs/testanything.github.io/blob/tap14/tap-version-14-specification.md#the-plan
Signed-off-by: Brendan Higgins <brendanhiggins@google.com>
Reviewed-by: Stephen Boyd <sboyd@kernel.org>
Signed-off-by: Shuah Khan <skhan@linuxfoundation.org>

authored by Brendan Higgins and committed by Shuah Khan 45dcbb6f 8c0d8849

+82 -25
+17
lib/kunit/executor.c
··· 11 12 #if IS_BUILTIN(CONFIG_KUNIT) 13 14 int kunit_run_all_tests(void) 15 { 16 struct kunit_suite * const * const *suites; 17 18 for (suites = __kunit_suites_start; 19 suites < __kunit_suites_end;
··· 11 12 #if IS_BUILTIN(CONFIG_KUNIT) 13 14 + static void kunit_print_tap_header(void) 15 + { 16 + struct kunit_suite * const * const *suites, * const *subsuite; 17 + int num_of_suites = 0; 18 + 19 + for (suites = __kunit_suites_start; 20 + suites < __kunit_suites_end; 21 + suites++) 22 + for (subsuite = *suites; *subsuite != NULL; subsuite++) 23 + num_of_suites++; 24 + 25 + pr_info("TAP version 14\n"); 26 + pr_info("1..%d\n", num_of_suites); 27 + } 28 + 29 int kunit_run_all_tests(void) 30 { 31 struct kunit_suite * const * const *suites; 32 + 33 + kunit_print_tap_header(); 34 35 for (suites = __kunit_suites_start; 36 suites < __kunit_suites_end;
-11
lib/kunit/test.c
··· 20 WRITE_ONCE(test->success, false); 21 } 22 23 - static void kunit_print_tap_version(void) 24 - { 25 - static bool kunit_has_printed_tap_version; 26 - 27 - if (!kunit_has_printed_tap_version) { 28 - pr_info("TAP version 14\n"); 29 - kunit_has_printed_tap_version = true; 30 - } 31 - } 32 - 33 /* 34 * Append formatted message to log, size of which is limited to 35 * KUNIT_LOG_SIZE bytes (including null terminating byte). ··· 59 60 static void kunit_print_subtest_start(struct kunit_suite *suite) 61 { 62 - kunit_print_tap_version(); 63 kunit_log(KERN_INFO, suite, KUNIT_SUBTEST_INDENT "# Subtest: %s", 64 suite->name); 65 kunit_log(KERN_INFO, suite, KUNIT_SUBTEST_INDENT "1..%zd",
··· 20 WRITE_ONCE(test->success, false); 21 } 22 23 /* 24 * Append formatted message to log, size of which is limited to 25 * KUNIT_LOG_SIZE bytes (including null terminating byte). ··· 69 70 static void kunit_print_subtest_start(struct kunit_suite *suite) 71 { 72 kunit_log(KERN_INFO, suite, KUNIT_SUBTEST_INDENT "# Subtest: %s", 73 suite->name); 74 kunit_log(KERN_INFO, suite, KUNIT_SUBTEST_INDENT "1..%zd",
+62 -14
tools/testing/kunit/kunit_parser.py
··· 45 FAILURE = auto() 46 TEST_CRASHED = auto() 47 NO_TESTS = auto() 48 49 kunit_start_re = re.compile(r'TAP version [0-9]+$') 50 kunit_end_re = re.compile('(List of all partitions:|' 51 - 'Kernel panic - not syncing: VFS:|reboot: System halted)') 52 53 def isolate_kunit_output(kernel_output): 54 started = False ··· 110 111 OK_NOT_OK_SUBTEST = re.compile(r'^[\s]+(ok|not ok) [0-9]+ - (.*)$') 112 113 - OK_NOT_OK_MODULE = re.compile(r'^(ok|not ok) [0-9]+ - (.*)$') 114 115 def parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool: 116 save_non_diagnositic(lines, test_case) ··· 198 else: 199 return TestStatus.SUCCESS 200 201 - def parse_ok_not_ok_test_suite(lines: List[str], test_suite: TestSuite) -> bool: 202 consume_non_diagnositic(lines) 203 if not lines: 204 test_suite.status = TestStatus.TEST_CRASHED ··· 213 test_suite.status = TestStatus.SUCCESS 214 else: 215 test_suite.status = TestStatus.FAILURE 216 return True 217 else: 218 return False ··· 231 max_test_case_status = bubble_up_errors(lambda x: x.status, test_suite.cases) 232 return max_status(max_test_case_status, test_suite.status) 233 234 - def parse_test_suite(lines: List[str]) -> TestSuite: 235 if not lines: 236 return None 237 consume_non_diagnositic(lines) ··· 250 break 251 test_suite.cases.append(test_case) 252 expected_test_case_num -= 1 253 - if parse_ok_not_ok_test_suite(lines, test_suite): 254 test_suite.status = bubble_up_test_case_errors(test_suite) 255 return test_suite 256 elif not lines: ··· 270 else: 271 return False 272 273 def bubble_up_suite_errors(test_suite_list: List[TestSuite]) -> TestStatus: 274 return bubble_up_errors(lambda x: x.status, test_suite_list) 275 ··· 288 consume_non_diagnositic(lines) 289 if not lines or not parse_tap_header(lines): 290 return TestResult(TestStatus.NO_TESTS, [], lines) 291 test_suites = [] 292 - test_suite = parse_test_suite(lines) 293 - while test_suite: 294 - test_suites.append(test_suite) 295 - test_suite = parse_test_suite(lines) 296 - return TestResult(bubble_up_suite_errors(test_suites), test_suites, lines) 297 298 - def parse_run_tests(kernel_output) -> TestResult: 299 total_tests = 0 300 failed_tests = 0 301 crashed_tests = 0 302 - test_result = parse_test_result(list(isolate_kunit_output(kernel_output))) 303 - if test_result.status == TestStatus.NO_TESTS: 304 - print_with_timestamp(red('[ERROR] ') + 'no kunit output detected') 305 for test_suite in test_result.suites: 306 if test_suite.status == TestStatus.SUCCESS: 307 print_suite_divider(green('[PASSED] ') + test_suite.name) ··· 336 print_with_timestamp(red('[FAILED] ') + test_case.name) 337 print_log(map(yellow, test_case.log)) 338 print_with_timestamp('') 339 print_with_timestamp(DIVIDER) 340 fmt = green if test_result.status == TestStatus.SUCCESS else red 341 print_with_timestamp(
··· 45 FAILURE = auto() 46 TEST_CRASHED = auto() 47 NO_TESTS = auto() 48 + FAILURE_TO_PARSE_TESTS = auto() 49 50 kunit_start_re = re.compile(r'TAP version [0-9]+$') 51 kunit_end_re = re.compile('(List of all partitions:|' 52 + 'Kernel panic - not syncing: VFS:)') 53 54 def isolate_kunit_output(kernel_output): 55 started = False ··· 109 110 OK_NOT_OK_SUBTEST = re.compile(r'^[\s]+(ok|not ok) [0-9]+ - (.*)$') 111 112 + OK_NOT_OK_MODULE = re.compile(r'^(ok|not ok) ([0-9]+) - (.*)$') 113 114 def parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool: 115 save_non_diagnositic(lines, test_case) ··· 197 else: 198 return TestStatus.SUCCESS 199 200 + def parse_ok_not_ok_test_suite(lines: List[str], 201 + test_suite: TestSuite, 202 + expected_suite_index: int) -> bool: 203 consume_non_diagnositic(lines) 204 if not lines: 205 test_suite.status = TestStatus.TEST_CRASHED ··· 210 test_suite.status = TestStatus.SUCCESS 211 else: 212 test_suite.status = TestStatus.FAILURE 213 + suite_index = int(match.group(2)) 214 + if suite_index != expected_suite_index: 215 + print_with_timestamp( 216 + red('[ERROR] ') + 'expected_suite_index ' + 217 + str(expected_suite_index) + ', but got ' + 218 + str(suite_index)) 219 return True 220 else: 221 return False ··· 222 max_test_case_status = bubble_up_errors(lambda x: x.status, test_suite.cases) 223 return max_status(max_test_case_status, test_suite.status) 224 225 + def parse_test_suite(lines: List[str], expected_suite_index: int) -> TestSuite: 226 if not lines: 227 return None 228 consume_non_diagnositic(lines) ··· 241 break 242 test_suite.cases.append(test_case) 243 expected_test_case_num -= 1 244 + if parse_ok_not_ok_test_suite(lines, test_suite, expected_suite_index): 245 test_suite.status = bubble_up_test_case_errors(test_suite) 246 return test_suite 247 elif not lines: ··· 261 else: 262 return False 263 264 + TEST_PLAN = re.compile(r'[0-9]+\.\.([0-9]+)') 265 + 266 + def parse_test_plan(lines: List[str]) -> int: 267 + consume_non_diagnositic(lines) 268 + match = TEST_PLAN.match(lines[0]) 269 + if match: 270 + lines.pop(0) 271 + return int(match.group(1)) 272 + else: 273 + return None 274 + 275 def bubble_up_suite_errors(test_suite_list: List[TestSuite]) -> TestStatus: 276 return bubble_up_errors(lambda x: x.status, test_suite_list) 277 ··· 268 consume_non_diagnositic(lines) 269 if not lines or not parse_tap_header(lines): 270 return TestResult(TestStatus.NO_TESTS, [], lines) 271 + expected_test_suite_num = parse_test_plan(lines) 272 + if not expected_test_suite_num: 273 + return TestResult(TestStatus.FAILURE_TO_PARSE_TESTS, [], lines) 274 test_suites = [] 275 + for i in range(1, expected_test_suite_num + 1): 276 + test_suite = parse_test_suite(lines, i) 277 + if test_suite: 278 + test_suites.append(test_suite) 279 + else: 280 + print_with_timestamp( 281 + red('[ERROR] ') + ' expected ' + 282 + str(expected_test_suite_num) + 283 + ' test suites, but got ' + str(i - 2)) 284 + break 285 + test_suite = parse_test_suite(lines, -1) 286 + if test_suite: 287 + print_with_timestamp(red('[ERROR] ') + 288 + 'got unexpected test suite: ' + test_suite.name) 289 + if test_suites: 290 + return TestResult(bubble_up_suite_errors(test_suites), test_suites, lines) 291 + else: 292 + return TestResult(TestStatus.NO_TESTS, [], lines) 293 294 + def print_and_count_results(test_result: TestResult) -> None: 295 total_tests = 0 296 failed_tests = 0 297 crashed_tests = 0 298 for test_suite in test_result.suites: 299 if test_suite.status == TestStatus.SUCCESS: 300 print_suite_divider(green('[PASSED] ') + test_suite.name) ··· 303 print_with_timestamp(red('[FAILED] ') + test_case.name) 304 print_log(map(yellow, test_case.log)) 305 print_with_timestamp('') 306 + return total_tests, failed_tests, crashed_tests 307 + 308 + def parse_run_tests(kernel_output) -> TestResult: 309 + total_tests = 0 310 + failed_tests = 0 311 + crashed_tests = 0 312 + test_result = parse_test_result(list(isolate_kunit_output(kernel_output))) 313 + if test_result.status == TestStatus.NO_TESTS: 314 + print(red('[ERROR] ') + yellow('no tests run!')) 315 + elif test_result.status == TestStatus.FAILURE_TO_PARSE_TESTS: 316 + print(red('[ERROR] ') + yellow('could not parse test results!')) 317 + else: 318 + (total_tests, 319 + failed_tests, 320 + crashed_tests) = print_and_count_results(test_result) 321 print_with_timestamp(DIVIDER) 322 fmt = green if test_result.status == TestStatus.SUCCESS else red 323 print_with_timestamp(
+1
tools/testing/kunit/test_data/test_is_test_passed-all_passed.log
··· 1 TAP version 14 2 # Subtest: sysctl_test 3 1..8 4 # sysctl_test_dointvec_null_tbl_data: sysctl_test_dointvec_null_tbl_data passed
··· 1 TAP version 14 2 + 1..2 3 # Subtest: sysctl_test 4 1..8 5 # sysctl_test_dointvec_null_tbl_data: sysctl_test_dointvec_null_tbl_data passed
+1
tools/testing/kunit/test_data/test_is_test_passed-crash.log
··· 1 printk: console [tty0] enabled 2 printk: console [mc-1] enabled 3 TAP version 14 4 # Subtest: sysctl_test 5 1..8 6 # sysctl_test_dointvec_null_tbl_data: sysctl_test_dointvec_null_tbl_data passed
··· 1 printk: console [tty0] enabled 2 printk: console [mc-1] enabled 3 TAP version 14 4 + 1..2 5 # Subtest: sysctl_test 6 1..8 7 # sysctl_test_dointvec_null_tbl_data: sysctl_test_dointvec_null_tbl_data passed
+1
tools/testing/kunit/test_data/test_is_test_passed-failure.log
··· 1 TAP version 14 2 # Subtest: sysctl_test 3 1..8 4 # sysctl_test_dointvec_null_tbl_data: sysctl_test_dointvec_null_tbl_data passed
··· 1 TAP version 14 2 + 1..2 3 # Subtest: sysctl_test 4 1..8 5 # sysctl_test_dointvec_null_tbl_data: sysctl_test_dointvec_null_tbl_data passed