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