History

Merge tag 'linux-kselftest-kunit-5.17-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest

Pull KUnit updates from Shuah Khan: "This consists of several fixes and enhancements. A few highlights: - Option --kconfig_add option allows easily tweaking kunitconfigs - make build subcommand can reconfigure if needed - doesn't error on tests without test plans - doesn't crash if no parameters are generated - defaults --jobs to # of cups - reports test parameter results as (K)TAP subtests" * tag 'linux-kselftest-kunit-5.17-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest: kunit: tool: Default --jobs to number of CPUs kunit: tool: fix newly introduced typechecker errors kunit: tool: make `build` subcommand also reconfigure if needed kunit: tool: delete kunit_parser.TestResult type kunit: tool: use dataclass instead of collections.namedtuple kunit: tool: suggest using decode_stacktrace.sh on kernel crash kunit: tool: reconfigure when the used kunitconfig changes kunit: tool: revamp message for invalid kunitconfig kunit: tool: add --kconfig_add to allow easily tweaking kunitconfigs kunit: tool: move Kconfig read_from_file/parse_from_string to package-level kunit: tool: print parsed test results fully incrementally kunit: Report test parameter results as (K)TAP subtests kunit: Don't crash if no parameters are generated kunit: tool: Report an error if any test has no subtests kunit: tool: Do not error on tests without test plans kunit: add run_checks.py script to validate kunit changes Documentation: kunit: remove claims that kunit is a mocking framework kunit: tool: fix --json output for skipped tests

Linus Torvalds committed 4 years ago

Commit bf4eebf2 parents 4369b3c ad659cc13 files · +480 −204Browse files

Unified · Split

13 files
  1. Documentation/dev-tools/kunit/api/index.rst +1 −2
  2. Documentation/dev-tools/kunit/api/test.rst +1 −2
  3. Documentation/dev-tools/kunit/index.rst +1 −1
  4. Documentation/dev-tools/kunit/start.rst +4 −4
  5. lib/kunit/test.c +14 −11
  6. tools/testing/kunit/kunit.py +105 −77
  7. tools/testing/kunit/kunit_config.py +31 −30
  8. tools/testing/kunit/kunit_json.py +5 −3
  9. tools/testing/kunit/kunit_kernel.py +49 −27
  10. tools/testing/kunit/kunit_parser.py +34 −23
  11. tools/testing/kunit/kunit_tool_test.py +147 −24
  12. tools/testing/kunit/run_checks.py +81 −0
  13. tools/testing/kunit/test_data/test_is_test_passed-no_tests_no_plan.log +7 −0
Documentation/dev-tools/kunit/api/index.rst+1−2
@@ -12,5 +12,4 @@ following sections: · 11 unmodified lines
12 12
13Documentation/dev-tools/kunit/api/test.rst13Documentation/dev-tools/kunit/api/test.rst
14 14
15 - documents all of the standard testing API excluding mocking15 - documents all of the standard testing API
16 or mocking related features.
Documentation/dev-tools/kunit/api/test.rst+1−2
@@ -4,8 +4,7 @@ · 3 unmodified lines
4Test API4Test API
5========5========
6 6
7This file documents all of the standard testing API excluding mocking or mocking7This file documents all of the standard testing API.
8related features.
9 8
10.. kernel-doc:: include/kunit/test.h9.. kernel-doc:: include/kunit/test.h
11 :internal:10 :internal:
Documentation/dev-tools/kunit/index.rst+1−1
@@ -19,7 +19,7 @@ KUnit - Unit Testing for the Linux Kernel · 18 unmodified lines
19What is KUnit?19What is KUnit?
20==============20==============
21 21
22KUnit is a lightweight unit testing and mocking framework for the Linux kernel.22KUnit is a lightweight unit testing framework for the Linux kernel.
23 23
24KUnit is heavily inspired by JUnit, Python's unittest.mock, and24KUnit is heavily inspired by JUnit, Python's unittest.mock, and
25Googletest/Googlemock for C++. KUnit provides facilities for defining unit test25Googletest/Googlemock for C++. KUnit provides facilities for defining unit test
Documentation/dev-tools/kunit/start.rst+4−4
@@ -50,10 +50,10 @@ It'll warn you if you haven't included the dependencies of the options you're · 49 unmodified lines
50using.50using.
51 51
52.. note::52.. note::
53 Note that removing something from the ``.kunitconfig`` will not trigger a53 If you change the ``.kunitconfig``, kunit.py will trigger a rebuild of the
54 rebuild of the ``.config`` file: the configuration is only updated if the54 ``.config`` file. But you can edit the ``.config`` file directly or with
55 ``.kunitconfig`` is not a subset of ``.config``. This means that you can use55 tools like ``make menuconfig O=.kunit``. As long as its a superset of
56 other tools (such as make menuconfig) to adjust other config options.56 ``.kunitconfig``, kunit.py won't overwrite your changes.
57 57
58 58
59Running the tests (KUnit Wrapper)59Running the tests (KUnit Wrapper)
lib/kunit/test.c+14−11
@@ -504,25 +504,28 @@ int kunit_run_tests(struct kunit_suite *suite) · 503 unmodified lines
504 struct kunit_result_stats param_stats = { 0 };504 struct kunit_result_stats param_stats = { 0 };
505 test_case->status = KUNIT_SKIPPED;505 test_case->status = KUNIT_SKIPPED;
506 506
507 if (test_case->generate_params) {507 if (!test_case->generate_params) {
508 /* Non-parameterised test. */
509 kunit_run_case_catch_errors(suite, test_case, &test);
510 kunit_update_stats(&param_stats, test.status);
511 } else {
508 /* Get initial param. */512 /* Get initial param. */
509 param_desc[0] = '\0';513 param_desc[0] = '\0';
510 test.param_value = test_case->generate_params(NULL, param_desc);514 test.param_value = test_case->generate_params(NULL, param_desc);
511 }515 kunit_log(KERN_INFO, &test, KUNIT_SUBTEST_INDENT KUNIT_SUBTEST_INDENT
516 "# Subtest: %s", test_case->name);
512 517
513 do {518 while (test.param_value) {
514 kunit_run_case_catch_errors(suite, test_case, &test);519 kunit_run_case_catch_errors(suite, test_case, &test);
515 520
516 if (test_case->generate_params) {
517 if (param_desc[0] == '\0') {521 if (param_desc[0] == '\0') {
518 snprintf(param_desc, sizeof(param_desc),522 snprintf(param_desc, sizeof(param_desc),
519 "param-%d", test.param_index);523 "param-%d", test.param_index);
520 }524 }
521 525
522 kunit_log(KERN_INFO, &test,526 kunit_log(KERN_INFO, &test,
523 KUNIT_SUBTEST_INDENT527 KUNIT_SUBTEST_INDENT KUNIT_SUBTEST_INDENT
524 "# %s: %s %d - %s",528 "%s %d - %s",
525 test_case->name,
526 kunit_status_to_ok_not_ok(test.status),529 kunit_status_to_ok_not_ok(test.status),
527 test.param_index + 1, param_desc);530 test.param_index + 1, param_desc);
528 531
@@ -530,11 +533,11 @@ int kunit_run_tests(struct kunit_suite *suite) · 1 unmodified line
530 param_desc[0] = '\0';533 param_desc[0] = '\0';
531 test.param_value = test_case->generate_params(test.param_value, param_desc);534 test.param_value = test_case->generate_params(test.param_value, param_desc);
532 test.param_index++;535 test.param_index++;
533 }
534 536
535 kunit_update_stats(&param_stats, test.status);537 kunit_update_stats(&param_stats, test.status);
538 }
539 }
536 540
537 } while (test.param_value);
538 541
539 kunit_print_test_stats(&test, param_stats);542 kunit_print_test_stats(&test, param_stats);
540 543
tools/testing/kunit/kunit.py+105−77
@@ -15,38 +15,57 @@ import time · 14 unmodified lines
15 15
16assert sys.version_info >= (3, 7), "Python version is too old"16assert sys.version_info >= (3, 7), "Python version is too old"
17 17
18from collections import namedtuple18from dataclasses import dataclass
19from enum import Enum, auto19from enum import Enum, auto
20from typing import Iterable, Sequence, List20from typing import Any, Iterable, Sequence, List, Optional
21 21
22import kunit_json22import kunit_json
23import kunit_kernel23import kunit_kernel
24import kunit_parser24import kunit_parser
25 25
26KunitResult = namedtuple('KunitResult', ['status','result','elapsed_time'])
27
28KunitConfigRequest = namedtuple('KunitConfigRequest',
29 ['build_dir', 'make_options'])
30KunitBuildRequest = namedtuple('KunitBuildRequest',
31 ['jobs', 'build_dir', 'alltests',
32 'make_options'])
33KunitExecRequest = namedtuple('KunitExecRequest',
34 ['timeout', 'build_dir', 'alltests',
35 'filter_glob', 'kernel_args', 'run_isolated'])
36KunitParseRequest = namedtuple('KunitParseRequest',
37 ['raw_output', 'build_dir', 'json'])
38KunitRequest = namedtuple('KunitRequest', ['raw_output','timeout', 'jobs',
39 'build_dir', 'alltests', 'filter_glob',
40 'kernel_args', 'run_isolated', 'json', 'make_options'])
41
42KernelDirectoryPath = sys.argv[0].split('tools/testing/kunit/')[0]
43
44class KunitStatus(Enum):26class KunitStatus(Enum):
45 SUCCESS = auto()27 SUCCESS = auto()
46 CONFIG_FAILURE = auto()28 CONFIG_FAILURE = auto()
47 BUILD_FAILURE = auto()29 BUILD_FAILURE = auto()
48 TEST_FAILURE = auto()30 TEST_FAILURE = auto()
49 31
32@dataclass
33class KunitResult:
34 status: KunitStatus
35 result: Any
36 elapsed_time: float
37
38@dataclass
39class KunitConfigRequest:
40 build_dir: str
41 make_options: Optional[List[str]]
42
43@dataclass
44class KunitBuildRequest(KunitConfigRequest):
45 jobs: int
46 alltests: bool
47
48@dataclass
49class KunitParseRequest:
50 raw_output: Optional[str]
51 build_dir: str
52 json: Optional[str]
53
54@dataclass
55class KunitExecRequest(KunitParseRequest):
56 timeout: int
57 alltests: bool
58 filter_glob: str
59 kernel_args: Optional[List[str]]
60 run_isolated: Optional[str]
61
62@dataclass
63class KunitRequest(KunitExecRequest, KunitBuildRequest):
64 pass
65
66
67KernelDirectoryPath = sys.argv[0].split('tools/testing/kunit/')[0]
68
50def get_kernel_root_path() -> str:69def get_kernel_root_path() -> str:
51 path = sys.argv[0] if not __file__ else __file__70 path = sys.argv[0] if not __file__ else __file__
52 parts = os.path.realpath(path).split('tools/testing/kunit')71 parts = os.path.realpath(path).split('tools/testing/kunit')
@@ -91,6 +110,14 @@ def build_tests(linux: kunit_kernel.LinuxSourceTree, · 38 unmodified lines
91 'built kernel successfully',110 'built kernel successfully',
92 build_end - build_start)111 build_end - build_start)
93 112
113def config_and_build_tests(linux: kunit_kernel.LinuxSourceTree,
114 request: KunitBuildRequest) -> KunitResult:
115 config_result = config_tests(linux, request)
116 if config_result.status != KunitStatus.SUCCESS:
117 return config_result
118
119 return build_tests(linux, request)
120
94def _list_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> List[str]:121def _list_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> List[str]:
95 args = ['kunit.action=list']122 args = ['kunit.action=list']
96 if request.kernel_args:123 if request.kernel_args:
@@ -121,8 +148,7 @@ def _suites_from_test_list(tests: List[str]) -> List[str]: · 24 unmodified lines
121 148
122 149
123 150
124def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest,151def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> KunitResult:
125 parse_request: KunitParseRequest) -> KunitResult:
126 filter_globs = [request.filter_glob]152 filter_globs = [request.filter_glob]
127 if request.run_isolated:153 if request.run_isolated:
128 tests = _list_tests(linux, request)154 tests = _list_tests(linux, request)
@@ -147,17 +173,23 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest, · 18 unmodified lines
147 filter_glob=filter_glob,173 filter_glob=filter_glob,
148 build_dir=request.build_dir)174 build_dir=request.build_dir)
149 175
150 result = parse_tests(parse_request, run_result)176 result = parse_tests(request, run_result)
151 # run_kernel() doesn't block on the kernel exiting.177 # run_kernel() doesn't block on the kernel exiting.
152 # That only happens after we get the last line of output from `run_result`.178 # That only happens after we get the last line of output from `run_result`.
153 # So exec_time here actually contains parsing + execution time, which is fine.179 # So exec_time here actually contains parsing + execution time, which is fine.
154 test_end = time.time()180 test_end = time.time()
155 exec_time += test_end - test_start181 exec_time += test_end - test_start
156 182
157 test_counts.add_subtest_counts(result.result.test.counts)183 test_counts.add_subtest_counts(result.result.counts)
184
185 if len(filter_globs) == 1 and test_counts.crashed > 0:
186 bd = request.build_dir
187 print('The kernel seems to have crashed; you can decode the stack traces with:')
188 print('$ scripts/decode_stacktrace.sh {}/vmlinux {} < {} | tee {}/decoded.log | {} parse'.format(
189 bd, bd, kunit_kernel.get_outfile_path(bd), bd, sys.argv[0]))
158 190
159 kunit_status = _map_to_overall_status(test_counts.get_status())191 kunit_status = _map_to_overall_status(test_counts.get_status())
160 return KunitResult(status=kunit_status, result=result.result, elapsed_time=exec_time)192 return KunitResult(status=kunit_status, result=result, elapsed_time=exec_time)
161 193
162def _map_to_overall_status(test_status: kunit_parser.TestStatus) -> KunitStatus:194def _map_to_overall_status(test_status: kunit_parser.TestStatus) -> KunitStatus:
163 if test_status in (kunit_parser.TestStatus.SUCCESS, kunit_parser.TestStatus.SKIPPED):195 if test_status in (kunit_parser.TestStatus.SUCCESS, kunit_parser.TestStatus.SKIPPED):
@@ -168,14 +200,12 @@ def _map_to_overall_status(test_status: kunit_parser.TestStatus) -> KunitStatus: · 4 unmodified lines
168def parse_tests(request: KunitParseRequest, input_data: Iterable[str]) -> KunitResult:200def parse_tests(request: KunitParseRequest, input_data: Iterable[str]) -> KunitResult:
169 parse_start = time.time()201 parse_start = time.time()
170 202
171 test_result = kunit_parser.TestResult(kunit_parser.TestStatus.SUCCESS,203 test_result = kunit_parser.Test()
172 kunit_parser.Test(),
173 'Tests not Parsed.')
174 204
175 if request.raw_output:205 if request.raw_output:
176 # Treat unparsed results as one passing test.206 # Treat unparsed results as one passing test.
177 test_result.test.status = kunit_parser.TestStatus.SUCCESS207 test_result.status = kunit_parser.TestStatus.SUCCESS
178 test_result.test.counts.passed = 1208 test_result.counts.passed = 1
179 209
180 output: Iterable[str] = input_data210 output: Iterable[str] = input_data
181 if request.raw_output == 'all':211 if request.raw_output == 'all':
@@ -193,7 +223,7 @@ def parse_tests(request: KunitParseRequest, input_data: Iterable[str]) -> KunitR · 11 unmodified lines
193 223
194 if request.json:224 if request.json:
195 json_obj = kunit_json.get_json_result(225 json_obj = kunit_json.get_json_result(
196 test_result=test_result,226 test=test_result,
197 def_config='kunit_defconfig',227 def_config='kunit_defconfig',
198 build_dir=request.build_dir,228 build_dir=request.build_dir,
199 json_path=request.json)229 json_path=request.json)
@@ -211,27 +241,15 @@ def run_tests(linux: kunit_kernel.LinuxSourceTree, · 11 unmodified lines
211 request: KunitRequest) -> KunitResult:241 request: KunitRequest) -> KunitResult:
212 run_start = time.time()242 run_start = time.time()
213 243
214 config_request = KunitConfigRequest(request.build_dir,244 config_result = config_tests(linux, request)
215 request.make_options)
216 config_result = config_tests(linux, config_request)
217 if config_result.status != KunitStatus.SUCCESS:245 if config_result.status != KunitStatus.SUCCESS:
218 return config_result246 return config_result
219 247
220 build_request = KunitBuildRequest(request.jobs, request.build_dir,248 build_result = build_tests(linux, request)
221 request.alltests,
222 request.make_options)
223 build_result = build_tests(linux, build_request)
224 if build_result.status != KunitStatus.SUCCESS:249 if build_result.status != KunitStatus.SUCCESS:
225 return build_result250 return build_result
226 251
227 exec_request = KunitExecRequest(request.timeout, request.build_dir,252 exec_result = exec_tests(linux, request)
228 request.alltests, request.filter_glob,
229 request.kernel_args, request.run_isolated)
230 parse_request = KunitParseRequest(request.raw_output,
231 request.build_dir,
232 request.json)
233
234 exec_result = exec_tests(linux, exec_request, parse_request)
235 253
236 run_end = time.time()254 run_end = time.time()
237 255
@@ -264,6 +282,9 @@ def massage_argv(argv: Sequence[str]) -> Sequence[str]: · 26 unmodified lines
264 return f'{arg}={pseudo_bool_flag_defaults[arg]}'282 return f'{arg}={pseudo_bool_flag_defaults[arg]}'
265 return list(map(massage_arg, argv))283 return list(map(massage_arg, argv))
266 284
285def get_default_jobs() -> int:
286 return len(os.sched_getaffinity(0))
287
267def add_common_opts(parser) -> None:288def add_common_opts(parser) -> None:
268 parser.add_argument('--build_dir',289 parser.add_argument('--build_dir',
269 help='As in the make command, it specifies the build '290 help='As in the make command, it specifies the build '
@@ -280,6 +301,10 @@ def add_common_opts(parser) -> None: · 10 unmodified lines
280 ' If given a directory, (e.g. lib/kunit), "/.kunitconfig" '301 ' If given a directory, (e.g. lib/kunit), "/.kunitconfig" '
281 'will get automatically appended.',302 'will get automatically appended.',
282 metavar='kunitconfig')303 metavar='kunitconfig')
304 parser.add_argument('--kconfig_add',
305 help='Additional Kconfig options to append to the '
306 '.kunitconfig, e.g. CONFIG_KASAN=y. Can be repeated.',
307 action='append')
283 308
284 parser.add_argument('--arch',309 parser.add_argument('--arch',
285 help=('Specifies the architecture to run tests under. '310 help=('Specifies the architecture to run tests under. '
@@ -310,7 +335,7 @@ def add_build_opts(parser) -> None: · 24 unmodified lines
310 parser.add_argument('--jobs',335 parser.add_argument('--jobs',
311 help='As in the make command, "Specifies the number of '336 help='As in the make command, "Specifies the number of '
312 'jobs (commands) to run simultaneously."',337 'jobs (commands) to run simultaneously."',
313 type=int, default=8, metavar='jobs')338 type=int, default=get_default_jobs(), metavar='jobs')
314 339
315def add_exec_opts(parser) -> None:340def add_exec_opts(parser) -> None:
316 parser.add_argument('--timeout',341 parser.add_argument('--timeout',
@@ -398,20 +423,21 @@ def main(argv, linux=None): · 81 unmodified lines
398 if not linux:423 if not linux:
399 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,424 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
400 kunitconfig_path=cli_args.kunitconfig,425 kunitconfig_path=cli_args.kunitconfig,
426 kconfig_add=cli_args.kconfig_add,
401 arch=cli_args.arch,427 arch=cli_args.arch,
402 cross_compile=cli_args.cross_compile,428 cross_compile=cli_args.cross_compile,
403 qemu_config_path=cli_args.qemu_config)429 qemu_config_path=cli_args.qemu_config)
404 430
405 request = KunitRequest(cli_args.raw_output,431 request = KunitRequest(build_dir=cli_args.build_dir,
406 cli_args.timeout,432 make_options=cli_args.make_options,
407 cli_args.jobs,433 jobs=cli_args.jobs,
408 cli_args.build_dir,434 alltests=cli_args.alltests,
409 cli_args.alltests,435 raw_output=cli_args.raw_output,
410 cli_args.filter_glob,436 json=cli_args.json,
411 cli_args.kernel_args,437 timeout=cli_args.timeout,
412 cli_args.run_isolated,438 filter_glob=cli_args.filter_glob,
413 cli_args.json,439 kernel_args=cli_args.kernel_args,
414 cli_args.make_options)440 run_isolated=cli_args.run_isolated)
415 result = run_tests(linux, request)441 result = run_tests(linux, request)
416 if result.status != KunitStatus.SUCCESS:442 if result.status != KunitStatus.SUCCESS:
417 sys.exit(1)443 sys.exit(1)
@@ -423,12 +449,13 @@ def main(argv, linux=None): · 5 unmodified lines
423 if not linux:449 if not linux:
424 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,450 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
425 kunitconfig_path=cli_args.kunitconfig,451 kunitconfig_path=cli_args.kunitconfig,
452 kconfig_add=cli_args.kconfig_add,
426 arch=cli_args.arch,453 arch=cli_args.arch,
427 cross_compile=cli_args.cross_compile,454 cross_compile=cli_args.cross_compile,
428 qemu_config_path=cli_args.qemu_config)455 qemu_config_path=cli_args.qemu_config)
429 456
430 request = KunitConfigRequest(cli_args.build_dir,457 request = KunitConfigRequest(build_dir=cli_args.build_dir,
431 cli_args.make_options)458 make_options=cli_args.make_options)
432 result = config_tests(linux, request)459 result = config_tests(linux, request)
433 kunit_parser.print_with_timestamp((460 kunit_parser.print_with_timestamp((
434 'Elapsed time: %.3fs\n') % (461 'Elapsed time: %.3fs\n') % (
@@ -439,15 +466,16 @@ def main(argv, linux=None): · 4 unmodified lines
439 if not linux:466 if not linux:
440 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,467 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
441 kunitconfig_path=cli_args.kunitconfig,468 kunitconfig_path=cli_args.kunitconfig,
469 kconfig_add=cli_args.kconfig_add,
442 arch=cli_args.arch,470 arch=cli_args.arch,
443 cross_compile=cli_args.cross_compile,471 cross_compile=cli_args.cross_compile,
444 qemu_config_path=cli_args.qemu_config)472 qemu_config_path=cli_args.qemu_config)
445 473
446 request = KunitBuildRequest(cli_args.jobs,474 request = KunitBuildRequest(build_dir=cli_args.build_dir,
447 cli_args.build_dir,475 make_options=cli_args.make_options,
448 cli_args.alltests,476 jobs=cli_args.jobs,
449 cli_args.make_options)477 alltests=cli_args.alltests)
450 result = build_tests(linux, request)478 result = config_and_build_tests(linux, request)
451 kunit_parser.print_with_timestamp((479 kunit_parser.print_with_timestamp((
452 'Elapsed time: %.3fs\n') % (480 'Elapsed time: %.3fs\n') % (
453 result.elapsed_time))481 result.elapsed_time))
@@ -457,20 +485,20 @@ def main(argv, linux=None): · 3 unmodified lines
457 if not linux:485 if not linux:
458 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,486 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
459 kunitconfig_path=cli_args.kunitconfig,487 kunitconfig_path=cli_args.kunitconfig,
488 kconfig_add=cli_args.kconfig_add,
460 arch=cli_args.arch,489 arch=cli_args.arch,
461 cross_compile=cli_args.cross_compile,490 cross_compile=cli_args.cross_compile,
462 qemu_config_path=cli_args.qemu_config)491 qemu_config_path=cli_args.qemu_config)
463 492
464 exec_request = KunitExecRequest(cli_args.timeout,493 exec_request = KunitExecRequest(raw_output=cli_args.raw_output,
465 cli_args.build_dir,494 build_dir=cli_args.build_dir,
466 cli_args.alltests,495 json=cli_args.json,
467 cli_args.filter_glob,496 timeout=cli_args.timeout,
468 cli_args.kernel_args,497 alltests=cli_args.alltests,
469 cli_args.run_isolated)498 filter_glob=cli_args.filter_glob,
470 parse_request = KunitParseRequest(cli_args.raw_output,499 kernel_args=cli_args.kernel_args,
471 cli_args.build_dir,500 run_isolated=cli_args.run_isolated)
472 cli_args.json)501 result = exec_tests(linux, exec_request)
473 result = exec_tests(linux, exec_request, parse_request)
474 kunit_parser.print_with_timestamp((502 kunit_parser.print_with_timestamp((
475 'Elapsed time: %.3fs\n') % (result.elapsed_time))503 'Elapsed time: %.3fs\n') % (result.elapsed_time))
476 if result.status != KunitStatus.SUCCESS:504 if result.status != KunitStatus.SUCCESS:
@@ -482,9 +510,9 @@ def main(argv, linux=None): · 5 unmodified lines
482 else:510 else:
483 with open(cli_args.file, 'r', errors='backslashreplace') as f:511 with open(cli_args.file, 'r', errors='backslashreplace') as f:
484 kunit_output = f.read().splitlines()512 kunit_output = f.read().splitlines()
485 request = KunitParseRequest(cli_args.raw_output,513 request = KunitParseRequest(raw_output=cli_args.raw_output,
486 None,514 build_dir='',
487 cli_args.json)515 json=cli_args.json)
488 result = parse_tests(request, kunit_output)516 result = parse_tests(request, kunit_output)
489 if result.status != KunitStatus.SUCCESS:517 if result.status != KunitStatus.SUCCESS:
490 sys.exit(1)518 sys.exit(1)
tools/testing/kunit/kunit_config.py+31−30
@@ -62,33 +62,34 @@ class Kconfig(object): · 61 unmodified lines
62 for entry in self.entries():62 for entry in self.entries():
63 f.write(str(entry) + '\n')63 f.write(str(entry) + '\n')
64 64
65 def parse_from_string(self, blob: str) -> None:65def parse_file(path: str) -> Kconfig:
66 """Parses a string containing KconfigEntrys and populates this Kconfig."""66 with open(path, 'r') as f:
67 self._entries = []67 return parse_from_string(f.read())
68 is_not_set_matcher = re.compile(CONFIG_IS_NOT_SET_PATTERN)68
69 config_matcher = re.compile(CONFIG_PATTERN)69def parse_from_string(blob: str) -> Kconfig:
70 for line in blob.split('\n'):70 """Parses a string containing Kconfig entries."""
71 line = line.strip()71 kconfig = Kconfig()
72 if not line:72 is_not_set_matcher = re.compile(CONFIG_IS_NOT_SET_PATTERN)
73 continue73 config_matcher = re.compile(CONFIG_PATTERN)
74 74 for line in blob.split('\n'):
75 match = config_matcher.match(line)75 line = line.strip()
76 if match:76 if not line:
77 entry = KconfigEntry(match.group(1), match.group(2))77 continue
78 self.add_entry(entry)78
79 continue79 match = config_matcher.match(line)
80 80 if match:
81 empty_match = is_not_set_matcher.match(line)81 entry = KconfigEntry(match.group(1), match.group(2))
82 if empty_match:82 kconfig.add_entry(entry)
83 entry = KconfigEntry(empty_match.group(1), 'n')83 continue
84 self.add_entry(entry)84
85 continue85 empty_match = is_not_set_matcher.match(line)
86 86 if empty_match:
87 if line[0] == '#':87 entry = KconfigEntry(empty_match.group(1), 'n')
88 continue88 kconfig.add_entry(entry)
89 else:89 continue
90 raise KconfigParseError('Failed to parse: ' + line)90
91 91 if line[0] == '#':
92 def read_from_file(self, path: str) -> None:92 continue
93 with open(path, 'r') as f:93 else:
94 self.parse_from_string(f.read())94 raise KconfigParseError('Failed to parse: ' + line)
95 return kconfig
tools/testing/kunit/kunit_json.py+5−3
@@ -11,7 +11,7 @@ import os · 10 unmodified lines
11 11
12import kunit_parser12import kunit_parser
13 13
14from kunit_parser import Test, TestResult, TestStatus14from kunit_parser import Test, TestStatus
15from typing import Any, Dict, Optional15from typing import Any, Dict, Optional
16 16
17JsonObj = Dict[str, Any]17JsonObj = Dict[str, Any]
@@ -30,6 +30,8 @@ def _get_group_json(test: Test, def_config: str, · 12 unmodified lines
30 test_case = {"name": subtest.name, "status": "FAIL"}30 test_case = {"name": subtest.name, "status": "FAIL"}
31 if subtest.status == TestStatus.SUCCESS:31 if subtest.status == TestStatus.SUCCESS:
32 test_case["status"] = "PASS"32 test_case["status"] = "PASS"
33 elif subtest.status == TestStatus.SKIPPED:
34 test_case["status"] = "SKIP"
33 elif subtest.status == TestStatus.TEST_CRASHED:35 elif subtest.status == TestStatus.TEST_CRASHED:
34 test_case["status"] = "ERROR"36 test_case["status"] = "ERROR"
35 test_cases.append(test_case)37 test_cases.append(test_case)
@@ -48,9 +50,9 @@ def _get_group_json(test: Test, def_config: str, · 12 unmodified lines
48 }50 }
49 return test_group51 return test_group
50 52
51def get_json_result(test_result: TestResult, def_config: str,53def get_json_result(test: Test, def_config: str,
52 build_dir: Optional[str], json_path: str) -> str:54 build_dir: Optional[str], json_path: str) -> str:
53 test_group = _get_group_json(test_result.test, def_config, build_dir)55 test_group = _get_group_json(test, def_config, build_dir)
54 test_group["name"] = "KUnit Test Group"56 test_group["name"] = "KUnit Test Group"
55 json_obj = json.dumps(test_group, indent=4)57 json_obj = json.dumps(test_group, indent=4)
56 if json_path != 'stdout':58 if json_path != 'stdout':
tools/testing/kunit/kunit_kernel.py+49−27
@@ -21,6 +21,7 @@ import qemu_config · 20 unmodified lines
21 21
22KCONFIG_PATH = '.config'22KCONFIG_PATH = '.config'
23KUNITCONFIG_PATH = '.kunitconfig'23KUNITCONFIG_PATH = '.kunitconfig'
24OLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
24DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'25DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
25BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'26BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
26OUTFILE_PATH = 'test.log'27OUTFILE_PATH = 'test.log'
@@ -116,8 +117,7 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): · 89 unmodified lines
116 self._extra_qemu_params = qemu_arch_params.extra_qemu_params117 self._extra_qemu_params = qemu_arch_params.extra_qemu_params
117 118
118 def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:119 def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
119 kconfig = kunit_config.Kconfig()120 kconfig = kunit_config.parse_from_string(self._kconfig)
120 kconfig.parse_from_string(self._kconfig)
121 base_kunitconfig.merge_in_entries(kconfig)121 base_kunitconfig.merge_in_entries(kconfig)
122 122
123 def start(self, params: List[str], build_dir: str) -> subprocess.Popen:123 def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
@@ -180,6 +180,9 @@ def get_kconfig_path(build_dir) -> str: · 56 unmodified lines
180def get_kunitconfig_path(build_dir) -> str:180def get_kunitconfig_path(build_dir) -> str:
181 return get_file_path(build_dir, KUNITCONFIG_PATH)181 return get_file_path(build_dir, KUNITCONFIG_PATH)
182 182
183def get_old_kunitconfig_path(build_dir) -> str:
184 return get_file_path(build_dir, OLD_KUNITCONFIG_PATH)
185
183def get_outfile_path(build_dir) -> str:186def get_outfile_path(build_dir) -> str:
184 return get_file_path(build_dir, OUTFILE_PATH)187 return get_file_path(build_dir, OUTFILE_PATH)
185 188
@@ -206,6 +209,7 @@ def get_source_tree_ops_from_qemu_config(config_path: str, · 20 unmodified lines
206 # exists as a file.209 # exists as a file.
207 module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))210 module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
208 spec = importlib.util.spec_from_file_location(module_path, config_path)211 spec = importlib.util.spec_from_file_location(module_path, config_path)
212 assert spec is not None
209 config = importlib.util.module_from_spec(spec)213 config = importlib.util.module_from_spec(spec)
210 # See https://github.com/python/typeshed/pull/2626 for context.214 # See https://github.com/python/typeshed/pull/2626 for context.
211 assert isinstance(spec.loader, importlib.abc.Loader)215 assert isinstance(spec.loader, importlib.abc.Loader)
@@ -225,6 +229,7 @@ class LinuxSourceTree(object): · 13 unmodified lines
225 build_dir: str,229 build_dir: str,
226 load_config=True,230 load_config=True,
227 kunitconfig_path='',231 kunitconfig_path='',
232 kconfig_add: Optional[List[str]]=None,
228 arch=None,233 arch=None,
229 cross_compile=None,234 cross_compile=None,
230 qemu_config_path=None) -> None:235 qemu_config_path=None) -> None:
@@ -249,8 +254,11 @@ class LinuxSourceTree(object): · 18 unmodified lines
249 if not os.path.exists(kunitconfig_path):254 if not os.path.exists(kunitconfig_path):
250 shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)255 shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)
251 256
252 self._kconfig = kunit_config.Kconfig()257 self._kconfig = kunit_config.parse_file(kunitconfig_path)
253 self._kconfig.read_from_file(kunitconfig_path)258 if kconfig_add:
259 kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
260 self._kconfig.merge_in_entries(kconfig)
261
254 262
255 def clean(self) -> bool:263 def clean(self) -> bool:
256 try:264 try:
@@ -262,17 +270,18 @@ class LinuxSourceTree(object): · 5 unmodified lines
262 270
263 def validate_config(self, build_dir) -> bool:271 def validate_config(self, build_dir) -> bool:
264 kconfig_path = get_kconfig_path(build_dir)272 kconfig_path = get_kconfig_path(build_dir)
265 validated_kconfig = kunit_config.Kconfig()273 validated_kconfig = kunit_config.parse_file(kconfig_path)
266 validated_kconfig.read_from_file(kconfig_path)274 if self._kconfig.is_subset_of(validated_kconfig):
267 if not self._kconfig.is_subset_of(validated_kconfig):275 return True
268 invalid = self._kconfig.entries() - validated_kconfig.entries()276 invalid = self._kconfig.entries() - validated_kconfig.entries()
269 message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \277 message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
270 'but not in .config: %s' % (278 'This is probably due to unsatisfied dependencies.\n' \
271 ', '.join([str(e) for e in invalid])279 'Missing: ' + ', '.join([str(e) for e in invalid])
272 )280 if self._arch == 'um':
273 logging.error(message)281 message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \
274 return False282 'on a different architecture with something like "--arch=x86_64".'
275 return True283 logging.error(message)
284 return False
276 285
277 def build_config(self, build_dir, make_options) -> bool:286 def build_config(self, build_dir, make_options) -> bool:
278 kconfig_path = get_kconfig_path(build_dir)287 kconfig_path = get_kconfig_path(build_dir)
@@ -285,25 +294,38 @@ class LinuxSourceTree(object): · 6 unmodified lines
285 except ConfigError as e:294 except ConfigError as e:
286 logging.error(e)295 logging.error(e)
287 return False296 return False
288 return self.validate_config(build_dir)297 if not self.validate_config(build_dir):
298 return False
299
300 old_path = get_old_kunitconfig_path(build_dir)
301 if os.path.exists(old_path):
302 os.remove(old_path) # write_to_file appends to the file
303 self._kconfig.write_to_file(old_path)
304 return True
305
306 def _kunitconfig_changed(self, build_dir: str) -> bool:
307 old_path = get_old_kunitconfig_path(build_dir)
308 if not os.path.exists(old_path):
309 return True
310
311 old_kconfig = kunit_config.parse_file(old_path)
312 return old_kconfig.entries() != self._kconfig.entries()
289 313
290 def build_reconfig(self, build_dir, make_options) -> bool:314 def build_reconfig(self, build_dir, make_options) -> bool:
291 """Creates a new .config if it is not a subset of the .kunitconfig."""315 """Creates a new .config if it is not a subset of the .kunitconfig."""
292 kconfig_path = get_kconfig_path(build_dir)316 kconfig_path = get_kconfig_path(build_dir)
293 if os.path.exists(kconfig_path):317 if not os.path.exists(kconfig_path):
294 existing_kconfig = kunit_config.Kconfig()
295 existing_kconfig.read_from_file(kconfig_path)
296 self._ops.make_arch_qemuconfig(self._kconfig)
297 if not self._kconfig.is_subset_of(existing_kconfig):
298 print('Regenerating .config ...')
299 os.remove(kconfig_path)
300 return self.build_config(build_dir, make_options)
301 else:
302 return True
303 else:
304 print('Generating .config ...')318 print('Generating .config ...')
305 return self.build_config(build_dir, make_options)319 return self.build_config(build_dir, make_options)
306 320
321 existing_kconfig = kunit_config.parse_file(kconfig_path)
322 self._ops.make_arch_qemuconfig(self._kconfig)
323 if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir):
324 return True
325 print('Regenerating .config ...')
326 os.remove(kconfig_path)
327 return self.build_config(build_dir, make_options)
328
307 def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool:329 def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
308 try:330 try:
309 if alltests:331 if alltests:
tools/testing/kunit/kunit_parser.py+34−23
@@ -12,14 +12,11 @@ · 11 unmodified lines
12from __future__ import annotations12from __future__ import annotations
13import re13import re
14 14
15from collections import namedtuple15import datetime
16from datetime import datetime
17from enum import Enum, auto16from enum import Enum, auto
18from functools import reduce17from functools import reduce
19from typing import Iterable, Iterator, List, Optional, Tuple18from typing import Iterable, Iterator, List, Optional, Tuple
20 19
21TestResult = namedtuple('TestResult', ['status','test','log'])
22
23class Test(object):20class Test(object):
24 """21 """
25 A class to represent a test parsed from KTAP results. All KTAP22 A class to represent a test parsed from KTAP results. All KTAP
@@ -168,42 +165,51 @@ class TestCounts: · 142 unmodified lines
168class LineStream:165class LineStream:
169 """166 """
170 A class to represent the lines of kernel output.167 A class to represent the lines of kernel output.
171 Provides a peek()/pop() interface over an iterator of168 Provides a lazy peek()/pop() interface over an iterator of
172 (line#, text).169 (line#, text).
173 """170 """
174 _lines: Iterator[Tuple[int, str]]171 _lines: Iterator[Tuple[int, str]]
175 _next: Tuple[int, str]172 _next: Tuple[int, str]
173 _need_next: bool
176 _done: bool174 _done: bool
177 175
178 def __init__(self, lines: Iterator[Tuple[int, str]]):176 def __init__(self, lines: Iterator[Tuple[int, str]]):
179 """Creates a new LineStream that wraps the given iterator."""177 """Creates a new LineStream that wraps the given iterator."""
180 self._lines = lines178 self._lines = lines
181 self._done = False179 self._done = False
180 self._need_next = True
182 self._next = (0, '')181 self._next = (0, '')
183 self._get_next()
184 182
185 def _get_next(self) -> None:183 def _get_next(self) -> None:
186 """Advances the LineSteam to the next line."""184 """Advances the LineSteam to the next line, if necessary."""
185 if not self._need_next:
186 return
187 try:187 try:
188 self._next = next(self._lines)188 self._next = next(self._lines)
189 except StopIteration:189 except StopIteration:
190 self._done = True190 self._done = True
191 finally:
192 self._need_next = False
191 193
192 def peek(self) -> str:194 def peek(self) -> str:
193 """Returns the current line, without advancing the LineStream.195 """Returns the current line, without advancing the LineStream.
194 """196 """
197 self._get_next()
195 return self._next[1]198 return self._next[1]
196 199
197 def pop(self) -> str:200 def pop(self) -> str:
198 """Returns the current line and advances the LineStream to201 """Returns the current line and advances the LineStream to
199 the next line.202 the next line.
200 """203 """
201 n = self._next204 s = self.peek()
202 self._get_next()205 if self._done:
203 return n[1]206 raise ValueError(f'LineStream: going past EOF, last line was {s}')
207 self._need_next = True
208 return s
204 209
205 def __bool__(self) -> bool:210 def __bool__(self) -> bool:
206 """Returns True if stream has more lines."""211 """Returns True if stream has more lines."""
212 self._get_next()
207 return not self._done213 return not self._done
208 214
209 # Only used by kunit_tool_test.py.215 # Only used by kunit_tool_test.py.
@@ -216,6 +222,7 @@ class LineStream: · 6 unmodified lines
216 222
217 def line_number(self) -> int:223 def line_number(self) -> int:
218 """Returns the line number of the current line."""224 """Returns the line number of the current line."""
225 self._get_next()
219 return self._next[0]226 return self._next[0]
220 227
221# Parsing helper methods:228# Parsing helper methods:
@@ -340,8 +347,8 @@ def parse_test_plan(lines: LineStream, test: Test) -> bool: · 118 unmodified lines
340 """347 """
341 Parses test plan line and stores the expected number of subtests in348 Parses test plan line and stores the expected number of subtests in
342 test object. Reports an error if expected count is 0.349 test object. Reports an error if expected count is 0.
343 Returns False and reports missing test plan error if fails to parse350 Returns False and sets expected_count to None if there is no valid test
344 test plan.351 plan.
345 352
346 Accepted format:353 Accepted format:
347 - '1..[number of subtests]'354 - '1..[number of subtests]'
@@ -356,14 +363,10 @@ def parse_test_plan(lines: LineStream, test: Test) -> bool: · 8 unmodified lines
356 match = TEST_PLAN.match(lines.peek())363 match = TEST_PLAN.match(lines.peek())
357 if not match:364 if not match:
358 test.expected_count = None365 test.expected_count = None
359 test.add_error('missing plan line!')
360 return False366 return False
361 test.log.append(lines.pop())367 test.log.append(lines.pop())
362 expected_count = int(match.group(1))368 expected_count = int(match.group(1))
363 test.expected_count = expected_count369 test.expected_count = expected_count
364 if expected_count == 0:
365 test.status = TestStatus.NO_TESTS
366 test.add_error('0 tests run!')
367 return True370 return True
368 371
369TEST_RESULT = re.compile(r'^(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')372TEST_RESULT = re.compile(r'^(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
@@ -514,7 +517,7 @@ ANSI_LEN = len(red('')) · 144 unmodified lines
514 517
515def print_with_timestamp(message: str) -> None:518def print_with_timestamp(message: str) -> None:
516 """Prints message with timestamp at beginning."""519 """Prints message with timestamp at beginning."""
517 print('[%s] %s' % (datetime.now().strftime('%H:%M:%S'), message))520 print('[%s] %s' % (datetime.datetime.now().strftime('%H:%M:%S'), message))
518 521
519def format_test_divider(message: str, len_message: int) -> str:522def format_test_divider(message: str, len_message: int) -> str:
520 """523 """
@@ -590,6 +593,8 @@ def format_test_result(test: Test) -> str: · 69 unmodified lines
590 return (green('[PASSED] ') + test.name)593 return (green('[PASSED] ') + test.name)
591 elif test.status == TestStatus.SKIPPED:594 elif test.status == TestStatus.SKIPPED:
592 return (yellow('[SKIPPED] ') + test.name)595 return (yellow('[SKIPPED] ') + test.name)
596 elif test.status == TestStatus.NO_TESTS:
597 return (yellow('[NO TESTS RUN] ') + test.name)
593 elif test.status == TestStatus.TEST_CRASHED:598 elif test.status == TestStatus.TEST_CRASHED:
594 print_log(test.log)599 print_log(test.log)
595 return (red('[CRASHED] ') + test.name)600 return (red('[CRASHED] ') + test.name)
@@ -732,6 +737,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: · 136 unmodified lines
732 # test plan737 # test plan
733 test.name = "main"738 test.name = "main"
734 parse_test_plan(lines, test)739 parse_test_plan(lines, test)
740 parent_test = True
735 else:741 else:
736 # If KTAP/TAP header is not found, test must be subtest742 # If KTAP/TAP header is not found, test must be subtest
737 # header or test result line so parse attempt to parser743 # header or test result line so parse attempt to parser
@@ -745,7 +751,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: · 7 unmodified lines
745 expected_count = test.expected_count751 expected_count = test.expected_count
746 subtests = []752 subtests = []
747 test_num = 1753 test_num = 1
748 while expected_count is None or test_num <= expected_count:754 while parent_test and (expected_count is None or test_num <= expected_count):
749 # Loop to parse any subtests.755 # Loop to parse any subtests.
750 # Break after parsing expected number of tests or756 # Break after parsing expected number of tests or
751 # if expected number of tests is unknown break when test757 # if expected number of tests is unknown break when test
@@ -780,9 +786,15 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: · 28 unmodified lines
780 parse_test_result(lines, test, expected_num)786 parse_test_result(lines, test, expected_num)
781 else:787 else:
782 test.add_error('missing subtest result line!')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
783 # Add statuses to TestCounts attribute in Test object795 # Add statuses to TestCounts attribute in Test object
784 bubble_up_test_results(test)796 bubble_up_test_results(test)
785 if parent_test:797 if parent_test and not main:
786 # If test has subtests and is not the main test object, print798 # If test has subtests and is not the main test object, print
787 # footer.799 # footer.
788 print_test_footer(test)800 print_test_footer(test)
@@ -790,7 +802,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: · 1 unmodified line
790 print_test_result(test)802 print_test_result(test)
791 return test803 return test
792 804
793def parse_run_tests(kernel_output: Iterable[str]) -> TestResult:805def parse_run_tests(kernel_output: Iterable[str]) -> Test:
794 """806 """
795 Using kernel output, extract KTAP lines, parse the lines for test807 Using kernel output, extract KTAP lines, parse the lines for test
796 results and print condensed test results and summary line .808 results and print condensed test results and summary line .
@@ -799,8 +811,7 @@ def parse_run_tests(kernel_output: Iterable[str]) -> TestResult: · 2 unmodified lines
799 kernel_output - Iterable object contains lines of kernel output811 kernel_output - Iterable object contains lines of kernel output
800 812
801 Return:813 Return:
802 TestResult - Tuple containg status of main test object, main test814 Test - the main test object with all subtests.
803 object with all subtests, and log of all KTAP lines.
804 """815 """
805 print_with_timestamp(DIVIDER)816 print_with_timestamp(DIVIDER)
806 lines = extract_tap_lines(kernel_output)817 lines = extract_tap_lines(kernel_output)
@@ -814,4 +825,4 @@ def parse_run_tests(kernel_output: Iterable[str]) -> TestResult: · 7 unmodified lines
814 test.status = test.counts.get_status()825 test.status = test.counts.get_status()
815 print_with_timestamp(DIVIDER)826 print_with_timestamp(DIVIDER)
816 print_summary_line(test)827 print_summary_line(test)
817 return TestResult(test.status, test, lines)828 return test
tools/testing/kunit/kunit_tool_test.py+147−24
@@ -13,9 +13,10 @@ import tempfile, shutil # Handling test_tmpdir · 12 unmodified lines
13 13
14import itertools14import itertools
15import json15import json
16import os
16import signal17import signal
17import subprocess18import subprocess
18import os19from typing import Iterable
19 20
20import kunit_config21import kunit_config
21import kunit_parser22import kunit_parser
@@ -50,10 +51,9 @@ class KconfigTest(unittest.TestCase): · 28 unmodified lines
50 self.assertFalse(kconfig1.is_subset_of(kconfig0))51 self.assertFalse(kconfig1.is_subset_of(kconfig0))
51 52
52 def test_read_from_file(self):53 def test_read_from_file(self):
53 kconfig = kunit_config.Kconfig()
54 kconfig_path = test_data_path('test_read_from_file.kconfig')54 kconfig_path = test_data_path('test_read_from_file.kconfig')
55 55
56 kconfig.read_from_file(kconfig_path)56 kconfig = kunit_config.parse_file(kconfig_path)
57 57
58 expected_kconfig = kunit_config.Kconfig()58 expected_kconfig = kunit_config.Kconfig()
59 expected_kconfig.add_entry(59 expected_kconfig.add_entry(
@@ -86,8 +86,7 @@ class KconfigTest(unittest.TestCase): · 26 unmodified lines
86 86
87 expected_kconfig.write_to_file(kconfig_path)87 expected_kconfig.write_to_file(kconfig_path)
88 88
89 actual_kconfig = kunit_config.Kconfig()89 actual_kconfig = kunit_config.parse_file(kconfig_path)
90 actual_kconfig.read_from_file(kconfig_path)
91 90
92 self.assertEqual(actual_kconfig.entries(),91 self.assertEqual(actual_kconfig.entries(),
93 expected_kconfig.entries())92 expected_kconfig.entries())
@@ -179,7 +178,7 @@ class KUnitParserTest(unittest.TestCase): · 85 unmodified lines
179 with open(empty_log) as file:178 with open(empty_log) as file:
180 result = kunit_parser.parse_run_tests(179 result = kunit_parser.parse_run_tests(
181 kunit_parser.extract_tap_lines(file.readlines()))180 kunit_parser.extract_tap_lines(file.readlines()))
182 self.assertEqual(0, len(result.test.subtests))181 self.assertEqual(0, len(result.subtests))
183 self.assertEqual(182 self.assertEqual(
184 kunit_parser.TestStatus.FAILURE_TO_PARSE_TESTS,183 kunit_parser.TestStatus.FAILURE_TO_PARSE_TESTS,
185 result.status)184 result.status)
@@ -191,7 +190,10 @@ class KUnitParserTest(unittest.TestCase): · 5 unmodified lines
191 result = kunit_parser.parse_run_tests(190 result = kunit_parser.parse_run_tests(
192 kunit_parser.extract_tap_lines(191 kunit_parser.extract_tap_lines(
193 file.readlines()))192 file.readlines()))
194 self.assertEqual(2, result.test.counts.errors)193 # A missing test plan is not an error.
194 self.assertEqual(0, result.counts.errors)
195 # All tests should be accounted for.
196 self.assertEqual(10, result.counts.total())
195 self.assertEqual(197 self.assertEqual(
196 kunit_parser.TestStatus.SUCCESS,198 kunit_parser.TestStatus.SUCCESS,
197 result.status)199 result.status)
@@ -201,11 +203,23 @@ class KUnitParserTest(unittest.TestCase): · 3 unmodified lines
201 with open(header_log) as file:203 with open(header_log) as file:
202 result = kunit_parser.parse_run_tests(204 result = kunit_parser.parse_run_tests(
203 kunit_parser.extract_tap_lines(file.readlines()))205 kunit_parser.extract_tap_lines(file.readlines()))
204 self.assertEqual(0, len(result.test.subtests))206 self.assertEqual(0, len(result.subtests))
205 self.assertEqual(207 self.assertEqual(
206 kunit_parser.TestStatus.NO_TESTS,208 kunit_parser.TestStatus.NO_TESTS,
207 result.status)209 result.status)
208 210
211 def test_no_tests_no_plan(self):
212 no_plan_log = test_data_path('test_is_test_passed-no_tests_no_plan.log')
213 with open(no_plan_log) as file:
214 result = kunit_parser.parse_run_tests(
215 kunit_parser.extract_tap_lines(file.readlines()))
216 self.assertEqual(0, len(result.subtests[0].subtests[0].subtests))
217 self.assertEqual(
218 kunit_parser.TestStatus.NO_TESTS,
219 result.subtests[0].subtests[0].status)
220 self.assertEqual(1, result.counts.errors)
221
222
209 def test_no_kunit_output(self):223 def test_no_kunit_output(self):
210 crash_log = test_data_path('test_insufficient_memory.log')224 crash_log = test_data_path('test_insufficient_memory.log')
211 print_mock = mock.patch('builtins.print').start()225 print_mock = mock.patch('builtins.print').start()
@@ -214,7 +228,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
214 kunit_parser.extract_tap_lines(file.readlines()))228 kunit_parser.extract_tap_lines(file.readlines()))
215 print_mock.assert_any_call(StrContains('invalid KTAP input!'))229 print_mock.assert_any_call(StrContains('invalid KTAP input!'))
216 print_mock.stop()230 print_mock.stop()
217 self.assertEqual(0, len(result.test.subtests))231 self.assertEqual(0, len(result.subtests))
218 232
219 def test_crashed_test(self):233 def test_crashed_test(self):
220 crashed_log = test_data_path('test_is_test_passed-crash.log')234 crashed_log = test_data_path('test_is_test_passed-crash.log')
@@ -255,10 +269,10 @@ class KUnitParserTest(unittest.TestCase): · 34 unmodified lines
255 result.status)269 result.status)
256 self.assertEqual(270 self.assertEqual(
257 "sysctl_test",271 "sysctl_test",
258 result.test.subtests[0].name)272 result.subtests[0].name)
259 self.assertEqual(273 self.assertEqual(
260 "example",274 "example",
261 result.test.subtests[1].name)275 result.subtests[1].name)
262 file.close()276 file.close()
263 277
264 278
@@ -269,7 +283,7 @@ class KUnitParserTest(unittest.TestCase): · 4 unmodified lines
269 self.assertEqual(283 self.assertEqual(
270 kunit_parser.TestStatus.SUCCESS,284 kunit_parser.TestStatus.SUCCESS,
271 result.status)285 result.status)
272 self.assertEqual('kunit-resource-test', result.test.subtests[0].name)286 self.assertEqual('kunit-resource-test', result.subtests[0].name)
273 287
274 def test_ignores_multiple_prefixes(self):288 def test_ignores_multiple_prefixes(self):
275 prefix_log = test_data_path('test_multiple_prefixes.log')289 prefix_log = test_data_path('test_multiple_prefixes.log')
@@ -278,7 +292,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
278 self.assertEqual(292 self.assertEqual(
279 kunit_parser.TestStatus.SUCCESS,293 kunit_parser.TestStatus.SUCCESS,
280 result.status)294 result.status)
281 self.assertEqual('kunit-resource-test', result.test.subtests[0].name)295 self.assertEqual('kunit-resource-test', result.subtests[0].name)
282 296
283 def test_prefix_mixed_kernel_output(self):297 def test_prefix_mixed_kernel_output(self):
284 mixed_prefix_log = test_data_path('test_interrupted_tap_output.log')298 mixed_prefix_log = test_data_path('test_interrupted_tap_output.log')
@@ -287,7 +301,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
287 self.assertEqual(301 self.assertEqual(
288 kunit_parser.TestStatus.SUCCESS,302 kunit_parser.TestStatus.SUCCESS,
289 result.status)303 result.status)
290 self.assertEqual('kunit-resource-test', result.test.subtests[0].name)304 self.assertEqual('kunit-resource-test', result.subtests[0].name)
291 305
292 def test_prefix_poundsign(self):306 def test_prefix_poundsign(self):
293 pound_log = test_data_path('test_pound_sign.log')307 pound_log = test_data_path('test_pound_sign.log')
@@ -296,7 +310,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
296 self.assertEqual(310 self.assertEqual(
297 kunit_parser.TestStatus.SUCCESS,311 kunit_parser.TestStatus.SUCCESS,
298 result.status)312 result.status)
299 self.assertEqual('kunit-resource-test', result.test.subtests[0].name)313 self.assertEqual('kunit-resource-test', result.subtests[0].name)
300 314
301 def test_kernel_panic_end(self):315 def test_kernel_panic_end(self):
302 panic_log = test_data_path('test_kernel_panic_interrupt.log')316 panic_log = test_data_path('test_kernel_panic_interrupt.log')
@@ -305,7 +319,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
305 self.assertEqual(319 self.assertEqual(
306 kunit_parser.TestStatus.TEST_CRASHED,320 kunit_parser.TestStatus.TEST_CRASHED,
307 result.status)321 result.status)
308 self.assertEqual('kunit-resource-test', result.test.subtests[0].name)322 self.assertEqual('kunit-resource-test', result.subtests[0].name)
309 323
310 def test_pound_no_prefix(self):324 def test_pound_no_prefix(self):
311 pound_log = test_data_path('test_pound_no_prefix.log')325 pound_log = test_data_path('test_pound_no_prefix.log')
@@ -314,7 +328,46 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
314 self.assertEqual(328 self.assertEqual(
315 kunit_parser.TestStatus.SUCCESS,329 kunit_parser.TestStatus.SUCCESS,
316 result.status)330 result.status)
317 self.assertEqual('kunit-resource-test', result.test.subtests[0].name)331 self.assertEqual('kunit-resource-test', result.subtests[0].name)
332
333def line_stream_from_strs(strs: Iterable[str]) -> kunit_parser.LineStream:
334 return kunit_parser.LineStream(enumerate(strs, start=1))
335
336class LineStreamTest(unittest.TestCase):
337
338 def test_basic(self):
339 stream = line_stream_from_strs(['hello', 'world'])
340
341 self.assertTrue(stream, msg='Should be more input')
342 self.assertEqual(stream.line_number(), 1)
343 self.assertEqual(stream.peek(), 'hello')
344 self.assertEqual(stream.pop(), 'hello')
345
346 self.assertTrue(stream, msg='Should be more input')
347 self.assertEqual(stream.line_number(), 2)
348 self.assertEqual(stream.peek(), 'world')
349 self.assertEqual(stream.pop(), 'world')
350
351 self.assertFalse(stream, msg='Should be no more input')
352 with self.assertRaisesRegex(ValueError, 'LineStream: going past EOF'):
353 stream.pop()
354
355 def test_is_lazy(self):
356 called_times = 0
357 def generator():
358 nonlocal called_times
359 for i in range(1,5):
360 called_times += 1
361 yield called_times, str(called_times)
362
363 stream = kunit_parser.LineStream(generator())
364 self.assertEqual(called_times, 0)
365
366 self.assertEqual(stream.pop(), '1')
367 self.assertEqual(called_times, 1)
368
369 self.assertEqual(stream.pop(), '2')
370 self.assertEqual(called_times, 2)
318 371
319class LinuxSourceTreeTest(unittest.TestCase):372class LinuxSourceTreeTest(unittest.TestCase):
320 373
@@ -336,6 +389,10 @@ class LinuxSourceTreeTest(unittest.TestCase): · 15 unmodified lines
336 pass389 pass
337 kunit_kernel.LinuxSourceTree('', kunitconfig_path=dir)390 kunit_kernel.LinuxSourceTree('', kunitconfig_path=dir)
338 391
392 def test_kconfig_add(self):
393 tree = kunit_kernel.LinuxSourceTree('', kconfig_add=['CONFIG_NOT_REAL=y'])
394 self.assertIn(kunit_config.KconfigEntry('NOT_REAL', 'y'), tree._kconfig.entries())
395
339 def test_invalid_arch(self):396 def test_invalid_arch(self):
340 with self.assertRaisesRegex(kunit_kernel.ConfigError, 'not a valid arch, options are.*x86_64'):397 with self.assertRaisesRegex(kunit_kernel.ConfigError, 'not a valid arch, options are.*x86_64'):
341 kunit_kernel.LinuxSourceTree('', arch='invalid')398 kunit_kernel.LinuxSourceTree('', arch='invalid')
@@ -356,6 +413,51 @@ class LinuxSourceTreeTest(unittest.TestCase): · 14 unmodified lines
356 with open(kunit_kernel.get_outfile_path(build_dir), 'rt') as outfile:413 with open(kunit_kernel.get_outfile_path(build_dir), 'rt') as outfile:
357 self.assertEqual(outfile.read(), 'hi\nbye\n', msg='Missing some output')414 self.assertEqual(outfile.read(), 'hi\nbye\n', msg='Missing some output')
358 415
416 def test_build_reconfig_no_config(self):
417 with tempfile.TemporaryDirectory('') as build_dir:
418 with open(kunit_kernel.get_kunitconfig_path(build_dir), 'w') as f:
419 f.write('CONFIG_KUNIT=y')
420
421 tree = kunit_kernel.LinuxSourceTree(build_dir)
422 mock_build_config = mock.patch.object(tree, 'build_config').start()
423
424 # Should generate the .config
425 self.assertTrue(tree.build_reconfig(build_dir, make_options=[]))
426 mock_build_config.assert_called_once_with(build_dir, [])
427
428 def test_build_reconfig_existing_config(self):
429 with tempfile.TemporaryDirectory('') as build_dir:
430 # Existing .config is a superset, should not touch it
431 with open(kunit_kernel.get_kunitconfig_path(build_dir), 'w') as f:
432 f.write('CONFIG_KUNIT=y')
433 with open(kunit_kernel.get_old_kunitconfig_path(build_dir), 'w') as f:
434 f.write('CONFIG_KUNIT=y')
435 with open(kunit_kernel.get_kconfig_path(build_dir), 'w') as f:
436 f.write('CONFIG_KUNIT=y\nCONFIG_KUNIT_TEST=y')
437
438 tree = kunit_kernel.LinuxSourceTree(build_dir)
439 mock_build_config = mock.patch.object(tree, 'build_config').start()
440
441 self.assertTrue(tree.build_reconfig(build_dir, make_options=[]))
442 self.assertEqual(mock_build_config.call_count, 0)
443
444 def test_build_reconfig_remove_option(self):
445 with tempfile.TemporaryDirectory('') as build_dir:
446 # We removed CONFIG_KUNIT_TEST=y from our .kunitconfig...
447 with open(kunit_kernel.get_kunitconfig_path(build_dir), 'w') as f:
448 f.write('CONFIG_KUNIT=y')
449 with open(kunit_kernel.get_old_kunitconfig_path(build_dir), 'w') as f:
450 f.write('CONFIG_KUNIT=y\nCONFIG_KUNIT_TEST=y')
451 with open(kunit_kernel.get_kconfig_path(build_dir), 'w') as f:
452 f.write('CONFIG_KUNIT=y\nCONFIG_KUNIT_TEST=y')
453
454 tree = kunit_kernel.LinuxSourceTree(build_dir)
455 mock_build_config = mock.patch.object(tree, 'build_config').start()
456
457 # ... so we should trigger a call to build_config()
458 self.assertTrue(tree.build_reconfig(build_dir, make_options=[]))
459 mock_build_config.assert_called_once_with(build_dir, [])
460
359 # TODO: add more test cases.461 # TODO: add more test cases.
360 462
361 463
@@ -365,7 +467,7 @@ class KUnitJsonTest(unittest.TestCase): · 3 unmodified lines
365 with open(test_data_path(log_file)) as file:467 with open(test_data_path(log_file)) as file:
366 test_result = kunit_parser.parse_run_tests(file)468 test_result = kunit_parser.parse_run_tests(file)
367 json_obj = kunit_json.get_json_result(469 json_obj = kunit_json.get_json_result(
368 test_result=test_result,470 test=test_result,
369 def_config='kunit_defconfig',471 def_config='kunit_defconfig',
370 build_dir=None,472 build_dir=None,
371 json_path='stdout')473 json_path='stdout')
@@ -383,6 +485,12 @@ class KUnitJsonTest(unittest.TestCase): · 11 unmodified lines
383 {'name': 'example_simple_test', 'status': 'ERROR'},485 {'name': 'example_simple_test', 'status': 'ERROR'},
384 result["sub_groups"][1]["test_cases"][0])486 result["sub_groups"][1]["test_cases"][0])
385 487
488 def test_skipped_test_json(self):
489 result = self._json_for('test_skip_tests.log')
490 self.assertEqual(
491 {'name': 'example_skip_test', 'status': 'SKIP'},
492 result["sub_groups"][1]["test_cases"][1])
493
386 def test_no_tests_json(self):494 def test_no_tests_json(self):
387 result = self._json_for('test_is_test_passed-no_tests_run_with_header.log')495 result = self._json_for('test_is_test_passed-no_tests_run_with_header.log')
388 self.assertEqual(0, len(result['sub_groups']))496 self.assertEqual(0, len(result['sub_groups']))
@@ -418,8 +526,8 @@ class KUnitMainTest(unittest.TestCase): · 29 unmodified lines
418 526
419 def test_build_passes_args_pass(self):527 def test_build_passes_args_pass(self):
420 kunit.main(['build'], self.linux_source_mock)528 kunit.main(['build'], self.linux_source_mock)
421 self.assertEqual(self.linux_source_mock.build_reconfig.call_count, 0)529 self.assertEqual(self.linux_source_mock.build_reconfig.call_count, 1)
422 self.linux_source_mock.build_kernel.assert_called_once_with(False, 8, '.kunit', None)530 self.linux_source_mock.build_kernel.assert_called_once_with(False, kunit.get_default_jobs(), '.kunit', None)
423 self.assertEqual(self.linux_source_mock.run_kernel.call_count, 0)531 self.assertEqual(self.linux_source_mock.run_kernel.call_count, 0)
424 532
425 def test_exec_passes_args_pass(self):533 def test_exec_passes_args_pass(self):
@@ -525,8 +633,9 @@ class KUnitMainTest(unittest.TestCase): · 99 unmodified lines
525 633
526 def test_build_builddir(self):634 def test_build_builddir(self):
527 build_dir = '.kunit'635 build_dir = '.kunit'
636 jobs = kunit.get_default_jobs()
528 kunit.main(['build', '--build_dir', build_dir], self.linux_source_mock)637 kunit.main(['build', '--build_dir', build_dir], self.linux_source_mock)
529 self.linux_source_mock.build_kernel.assert_called_once_with(False, 8, build_dir, None)638 self.linux_source_mock.build_kernel.assert_called_once_with(False, jobs, build_dir, None)
530 639
531 def test_exec_builddir(self):640 def test_exec_builddir(self):
532 build_dir = '.kunit'641 build_dir = '.kunit'
@@ -542,6 +651,7 @@ class KUnitMainTest(unittest.TestCase): · 9 unmodified lines
542 # Just verify that we parsed and initialized it correctly here.651 # Just verify that we parsed and initialized it correctly here.
543 mock_linux_init.assert_called_once_with('.kunit',652 mock_linux_init.assert_called_once_with('.kunit',
544 kunitconfig_path='mykunitconfig',653 kunitconfig_path='mykunitconfig',
654 kconfig_add=None,
545 arch='um',655 arch='um',
546 cross_compile=None,656 cross_compile=None,
547 qemu_config_path=None)657 qemu_config_path=None)
@@ -553,6 +663,19 @@ class KUnitMainTest(unittest.TestCase): · 5 unmodified lines
553 # Just verify that we parsed and initialized it correctly here.663 # Just verify that we parsed and initialized it correctly here.
554 mock_linux_init.assert_called_once_with('.kunit',664 mock_linux_init.assert_called_once_with('.kunit',
555 kunitconfig_path='mykunitconfig',665 kunitconfig_path='mykunitconfig',
666 kconfig_add=None,
667 arch='um',
668 cross_compile=None,
669 qemu_config_path=None)
670
671 @mock.patch.object(kunit_kernel, 'LinuxSourceTree')
672 def test_run_kconfig_add(self, mock_linux_init):
673 mock_linux_init.return_value = self.linux_source_mock
674 kunit.main(['run', '--kconfig_add=CONFIG_KASAN=y', '--kconfig_add=CONFIG_KCSAN=y'])
675 # Just verify that we parsed and initialized it correctly here.
676 mock_linux_init.assert_called_once_with('.kunit',
677 kunitconfig_path=None,
678 kconfig_add=['CONFIG_KASAN=y', 'CONFIG_KCSAN=y'],
556 arch='um',679 arch='um',
557 cross_compile=None,680 cross_compile=None,
558 qemu_config_path=None)681 qemu_config_path=None)
@@ -569,7 +692,7 @@ class KUnitMainTest(unittest.TestCase): · 10 unmodified lines
569 self.linux_source_mock.run_kernel.return_value = ['TAP version 14', 'init: random output'] + want692 self.linux_source_mock.run_kernel.return_value = ['TAP version 14', 'init: random output'] + want
570 693
571 got = kunit._list_tests(self.linux_source_mock,694 got = kunit._list_tests(self.linux_source_mock,
572 kunit.KunitExecRequest(300, '.kunit', False, 'suite*', None, 'suite'))695 kunit.KunitExecRequest(None, '.kunit', None, 300, False, 'suite*', None, 'suite'))
573 696
574 self.assertEqual(got, want)697 self.assertEqual(got, want)
575 # Should respect the user's filter glob when listing tests.698 # Should respect the user's filter glob when listing tests.
@@ -584,7 +707,7 @@ class KUnitMainTest(unittest.TestCase): · 8 unmodified lines
584 707
585 # Should respect the user's filter glob when listing tests.708 # Should respect the user's filter glob when listing tests.
586 mock_tests.assert_called_once_with(mock.ANY,709 mock_tests.assert_called_once_with(mock.ANY,
587 kunit.KunitExecRequest(300, '.kunit', False, 'suite*.test*', None, 'suite'))710 kunit.KunitExecRequest(None, '.kunit', None, 300, False, 'suite*.test*', None, 'suite'))
588 self.linux_source_mock.run_kernel.assert_has_calls([711 self.linux_source_mock.run_kernel.assert_has_calls([
589 mock.call(args=None, build_dir='.kunit', filter_glob='suite.test*', timeout=300),712 mock.call(args=None, build_dir='.kunit', filter_glob='suite.test*', timeout=300),
590 mock.call(args=None, build_dir='.kunit', filter_glob='suite2.test*', timeout=300),713 mock.call(args=None, build_dir='.kunit', filter_glob='suite2.test*', timeout=300),
@@ -597,7 +720,7 @@ class KUnitMainTest(unittest.TestCase): · 6 unmodified lines
597 720
598 # Should respect the user's filter glob when listing tests.721 # Should respect the user's filter glob when listing tests.
599 mock_tests.assert_called_once_with(mock.ANY,722 mock_tests.assert_called_once_with(mock.ANY,
600 kunit.KunitExecRequest(300, '.kunit', False, 'suite*', None, 'test'))723 kunit.KunitExecRequest(None, '.kunit', None, 300, False, 'suite*', None, 'test'))
601 self.linux_source_mock.run_kernel.assert_has_calls([724 self.linux_source_mock.run_kernel.assert_has_calls([
602 mock.call(args=None, build_dir='.kunit', filter_glob='suite.test1', timeout=300),725 mock.call(args=None, build_dir='.kunit', filter_glob='suite.test1', timeout=300),
603 mock.call(args=None, build_dir='.kunit', filter_glob='suite.test2', timeout=300),726 mock.call(args=None, build_dir='.kunit', filter_glob='suite.test2', timeout=300),
tools/testing/kunit/run_checks.py+81−0
@@ -0,0 +1,81 @@
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3#
4# This file runs some basic checks to verify kunit works.
5# It is only of interest if you're making changes to KUnit itself.
6#
7# Copyright (C) 2021, Google LLC.
8# Author: Daniel Latypov <dlatypov@google.com.com>
9
10from concurrent import futures
11import datetime
12import os
13import shutil
14import subprocess
15import sys
16import textwrap
17from typing import Dict, List, Sequence, Tuple
18
19ABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
20TIMEOUT = datetime.timedelta(minutes=5).total_seconds()
21
22commands: Dict[str, Sequence[str]] = {
23 'kunit_tool_test.py': ['./kunit_tool_test.py'],
24 'kunit smoke test': ['./kunit.py', 'run', '--kunitconfig=lib/kunit', '--build_dir=kunit_run_checks'],
25 'pytype': ['/bin/sh', '-c', 'pytype *.py'],
26 'mypy': ['/bin/sh', '-c', 'mypy *.py'],
27}
28
29# The user might not have mypy or pytype installed, skip them if so.
30# Note: you can install both via `$ pip install mypy pytype`
31necessary_deps : Dict[str, str] = {
32 'pytype': 'pytype',
33 'mypy': 'mypy',
34}
35
36def main(argv: Sequence[str]) -> None:
37 if argv:
38 raise RuntimeError('This script takes no arguments')
39
40 future_to_name: Dict[futures.Future, str] = {}
41 executor = futures.ThreadPoolExecutor(max_workers=len(commands))
42 for name, argv in commands.items():
43 if name in necessary_deps and shutil.which(necessary_deps[name]) is None:
44 print(f'{name}: SKIPPED, {necessary_deps[name]} not in $PATH')
45 continue
46 f = executor.submit(run_cmd, argv)
47 future_to_name[f] = name
48
49 has_failures = False
50 print(f'Waiting on {len(future_to_name)} checks ({", ".join(future_to_name.values())})...')
51 for f in futures.as_completed(future_to_name.keys()):
52 name = future_to_name[f]
53 ex = f.exception()
54 if not ex:
55 print(f'{name}: PASSED')
56 continue
57
58 has_failures = True
59 if isinstance(ex, subprocess.TimeoutExpired):
60 print(f'{name}: TIMED OUT')
61 elif isinstance(ex, subprocess.CalledProcessError):
62 print(f'{name}: FAILED')
63 else:
64 print('{name}: unexpected exception: {ex}')
65 continue
66
67 output = ex.output
68 if output:
69 print(textwrap.indent(output.decode(), '> '))
70 executor.shutdown()
71
72 if has_failures:
73 sys.exit(1)
74
75
76def run_cmd(argv: Sequence[str]):
77 subprocess.check_output(argv, stderr=subprocess.STDOUT, cwd=ABS_TOOL_PATH, timeout=TIMEOUT)
78
79
80if __name__ == '__main__':
81 main(sys.argv[1:])
tools/testing/kunit/test_data/test_is_test_passed-no_tests_no_plan.log+7−0
@@ -0,0 +1,7 @@
1TAP version 14
21..1
3 # Subtest: suite
4 1..1
5 # Subtest: case
6 ok 1 - case # SKIP
7ok 1 - suite