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
1212
1313 Documentation/dev-tools/kunit/api/test.rst
1414
15− - documents all of the standard testing API excluding mocking
16− or mocking related features.
15+ - documents all of the standard testing API
Documentation/dev-tools/kunit/api/test.rst+1−2
@@ -4,8 +4,7 @@ · 3 unmodified lines
44 Test API
55 ========
66
7−This file documents all of the standard testing API excluding mocking or mocking
8−related features.
7+This file documents all of the standard testing API.
98
109 .. kernel-doc:: include/kunit/test.h
1110 :internal:
Documentation/dev-tools/kunit/index.rst+1−1
@@ -19,7 +19,7 @@ KUnit - Unit Testing for the Linux Kernel · 18 unmodified lines
1919 What is KUnit?
2020 ==============
2121
22−KUnit is a lightweight unit testing and mocking framework for the Linux kernel.
22+KUnit is a lightweight unit testing framework for the Linux kernel.
2323
2424 KUnit is heavily inspired by JUnit, Python's unittest.mock, and
2525 Googletest/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
5050 using.
5151
5252 .. note::
53− Note that removing something from the ``.kunitconfig`` will not trigger a
54− rebuild of the ``.config`` file: the configuration is only updated if the
55− ``.kunitconfig`` is not a subset of ``.config``. This means that you can use
56− other tools (such as make menuconfig) to adjust other config options.
53+ If you change the ``.kunitconfig``, kunit.py will trigger a rebuild of the
54+ ``.config`` file. But you can edit the ``.config`` file directly or with
55+ tools like ``make menuconfig O=.kunit``. As long as its a superset of
56+ ``.kunitconfig``, kunit.py won't overwrite your changes.
5757
5858
5959 Running 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
504504 struct kunit_result_stats param_stats = { 0 };
505505 test_case->status = KUNIT_SKIPPED;
506506
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 {
508512 /* Get initial param. */
509513 param_desc[0] = '\0';
510514 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);
512517
513− do {
514− kunit_run_case_catch_errors(suite, test_case, &test);
518+ while (test.param_value) {
519+ kunit_run_case_catch_errors(suite, test_case, &test);
515520
516− if (test_case->generate_params) {
517521 if (param_desc[0] == '\0') {
518522 snprintf(param_desc, sizeof(param_desc),
519523 "param-%d", test.param_index);
520524 }
521525
522526 kunit_log(KERN_INFO, &test,
523− KUNIT_SUBTEST_INDENT
524− "# %s: %s %d - %s",
525− test_case->name,
527+ KUNIT_SUBTEST_INDENT KUNIT_SUBTEST_INDENT
528+ "%s %d - %s",
526529 kunit_status_to_ok_not_ok(test.status),
527530 test.param_index + 1, param_desc);
528531
@@ -530,11 +533,11 @@ int kunit_run_tests(struct kunit_suite *suite) · 1 unmodified line
530533 param_desc[0] = '\0';
531534 test.param_value = test_case->generate_params(test.param_value, param_desc);
532535 test.param_index++;
533− }
534536
535− kunit_update_stats(&param_stats, test.status);
537+ kunit_update_stats(&param_stats, test.status);
538+ }
539+ }
536540
537− } while (test.param_value);
538541
539542 kunit_print_test_stats(&test, param_stats);
540543
tools/testing/kunit/kunit.py+105−77
@@ -15,38 +15,57 @@ import time · 14 unmodified lines
1515
1616 assert sys.version_info >= (3, 7), "Python version is too old"
1717
18−from collections import namedtuple
18+from dataclasses import dataclass
1919 from enum import Enum, auto
20−from typing import Iterable, Sequence, List
20+from typing import Any, Iterable, Sequence, List, Optional
2121
2222 import kunit_json
2323 import kunit_kernel
2424 import kunit_parser
2525
26−KunitResult = namedtuple('KunitResult', ['status','result','elapsed_time'])
27−
28−KunitConfigRequest = namedtuple('KunitConfigRequest',
29− ['build_dir', 'make_options'])
30−KunitBuildRequest = namedtuple('KunitBuildRequest',
31− ['jobs', 'build_dir', 'alltests',
32− 'make_options'])
33−KunitExecRequest = namedtuple('KunitExecRequest',
34− ['timeout', 'build_dir', 'alltests',
35− 'filter_glob', 'kernel_args', 'run_isolated'])
36−KunitParseRequest = namedtuple('KunitParseRequest',
37− ['raw_output', 'build_dir', 'json'])
38−KunitRequest = namedtuple('KunitRequest', ['raw_output','timeout', 'jobs',
39− 'build_dir', 'alltests', 'filter_glob',
40− 'kernel_args', 'run_isolated', 'json', 'make_options'])
41−
42−KernelDirectoryPath = sys.argv[0].split('tools/testing/kunit/')[0]
43−
4426 class KunitStatus(Enum):
4527 SUCCESS = auto()
4628 CONFIG_FAILURE = auto()
4729 BUILD_FAILURE = auto()
4830 TEST_FAILURE = auto()
4931
32+@dataclass
33+class KunitResult:
34+ status: KunitStatus
35+ result: Any
36+ elapsed_time: float
37+
38+@dataclass
39+class KunitConfigRequest:
40+ build_dir: str
41+ make_options: Optional[List[str]]
42+
43+@dataclass
44+class KunitBuildRequest(KunitConfigRequest):
45+ jobs: int
46+ alltests: bool
47+
48+@dataclass
49+class KunitParseRequest:
50+ raw_output: Optional[str]
51+ build_dir: str
52+ json: Optional[str]
53+
54+@dataclass
55+class 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
63+class KunitRequest(KunitExecRequest, KunitBuildRequest):
64+ pass
65+
66+
67+KernelDirectoryPath = sys.argv[0].split('tools/testing/kunit/')[0]
68+
5069 def get_kernel_root_path() -> str:
5170 path = sys.argv[0] if not __file__ else __file__
5271 parts = os.path.realpath(path).split('tools/testing/kunit')
@@ -91,6 +110,14 @@ def build_tests(linux: kunit_kernel.LinuxSourceTree, · 38 unmodified lines
91110 'built kernel successfully',
92111 build_end - build_start)
93112
113+def 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+
94121 def _list_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> List[str]:
95122 args = ['kunit.action=list']
96123 if request.kernel_args:
@@ -121,8 +148,7 @@ def _suites_from_test_list(tests: List[str]) -> List[str]: · 24 unmodified lines
121148
122149
123150
124−def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest,
125− parse_request: KunitParseRequest) -> KunitResult:
151+def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> KunitResult:
126152 filter_globs = [request.filter_glob]
127153 if request.run_isolated:
128154 tests = _list_tests(linux, request)
@@ -147,17 +173,23 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest, · 18 unmodified lines
147173 filter_glob=filter_glob,
148174 build_dir=request.build_dir)
149175
150− result = parse_tests(parse_request, run_result)
176+ result = parse_tests(request, run_result)
151177 # run_kernel() doesn't block on the kernel exiting.
152178 # That only happens after we get the last line of output from `run_result`.
153179 # So exec_time here actually contains parsing + execution time, which is fine.
154180 test_end = time.time()
155181 exec_time += test_end - test_start
156182
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]))
158190
159191 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)
161193
162194 def _map_to_overall_status(test_status: kunit_parser.TestStatus) -> KunitStatus:
163195 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
168200 def parse_tests(request: KunitParseRequest, input_data: Iterable[str]) -> KunitResult:
169201 parse_start = time.time()
170202
171− test_result = kunit_parser.TestResult(kunit_parser.TestStatus.SUCCESS,
172− kunit_parser.Test(),
173− 'Tests not Parsed.')
203+ test_result = kunit_parser.Test()
174204
175205 if request.raw_output:
176206 # Treat unparsed results as one passing test.
177− test_result.test.status = kunit_parser.TestStatus.SUCCESS
178− test_result.test.counts.passed = 1
207+ test_result.status = kunit_parser.TestStatus.SUCCESS
208+ test_result.counts.passed = 1
179209
180210 output: Iterable[str] = input_data
181211 if request.raw_output == 'all':
@@ -193,7 +223,7 @@ def parse_tests(request: KunitParseRequest, input_data: Iterable[str]) -> KunitR · 11 unmodified lines
193223
194224 if request.json:
195225 json_obj = kunit_json.get_json_result(
196− test_result=test_result,
226+ test=test_result,
197227 def_config='kunit_defconfig',
198228 build_dir=request.build_dir,
199229 json_path=request.json)
@@ -211,27 +241,15 @@ def run_tests(linux: kunit_kernel.LinuxSourceTree, · 11 unmodified lines
211241 request: KunitRequest) -> KunitResult:
212242 run_start = time.time()
213243
214− config_request = KunitConfigRequest(request.build_dir,
215− request.make_options)
216− config_result = config_tests(linux, config_request)
244+ config_result = config_tests(linux, request)
217245 if config_result.status != KunitStatus.SUCCESS:
218246 return config_result
219247
220− build_request = KunitBuildRequest(request.jobs, request.build_dir,
221− request.alltests,
222− request.make_options)
223− build_result = build_tests(linux, build_request)
248+ build_result = build_tests(linux, request)
224249 if build_result.status != KunitStatus.SUCCESS:
225250 return build_result
226251
227− exec_request = KunitExecRequest(request.timeout, request.build_dir,
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)
252+ exec_result = exec_tests(linux, request)
235253
236254 run_end = time.time()
237255
@@ -264,6 +282,9 @@ def massage_argv(argv: Sequence[str]) -> Sequence[str]: · 26 unmodified lines
264282 return f'{arg}={pseudo_bool_flag_defaults[arg]}'
265283 return list(map(massage_arg, argv))
266284
285+def get_default_jobs() -> int:
286+ return len(os.sched_getaffinity(0))
287+
267288 def add_common_opts(parser) -> None:
268289 parser.add_argument('--build_dir',
269290 help='As in the make command, it specifies the build '
@@ -280,6 +301,10 @@ def add_common_opts(parser) -> None: · 10 unmodified lines
280301 ' If given a directory, (e.g. lib/kunit), "/.kunitconfig" '
281302 'will get automatically appended.',
282303 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')
283308
284309 parser.add_argument('--arch',
285310 help=('Specifies the architecture to run tests under. '
@@ -310,7 +335,7 @@ def add_build_opts(parser) -> None: · 24 unmodified lines
310335 parser.add_argument('--jobs',
311336 help='As in the make command, "Specifies the number of '
312337 'jobs (commands) to run simultaneously."',
313− type=int, default=8, metavar='jobs')
338+ type=int, default=get_default_jobs(), metavar='jobs')
314339
315340 def add_exec_opts(parser) -> None:
316341 parser.add_argument('--timeout',
@@ -398,20 +423,21 @@ def main(argv, linux=None): · 81 unmodified lines
398423 if not linux:
399424 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
400425 kunitconfig_path=cli_args.kunitconfig,
426+ kconfig_add=cli_args.kconfig_add,
401427 arch=cli_args.arch,
402428 cross_compile=cli_args.cross_compile,
403429 qemu_config_path=cli_args.qemu_config)
404430
405− request = KunitRequest(cli_args.raw_output,
406− cli_args.timeout,
407− cli_args.jobs,
408− cli_args.build_dir,
409− cli_args.alltests,
410− cli_args.filter_glob,
411− cli_args.kernel_args,
412− cli_args.run_isolated,
413− cli_args.json,
414− cli_args.make_options)
431+ request = KunitRequest(build_dir=cli_args.build_dir,
432+ make_options=cli_args.make_options,
433+ jobs=cli_args.jobs,
434+ alltests=cli_args.alltests,
435+ raw_output=cli_args.raw_output,
436+ json=cli_args.json,
437+ timeout=cli_args.timeout,
438+ filter_glob=cli_args.filter_glob,
439+ kernel_args=cli_args.kernel_args,
440+ run_isolated=cli_args.run_isolated)
415441 result = run_tests(linux, request)
416442 if result.status != KunitStatus.SUCCESS:
417443 sys.exit(1)
@@ -423,12 +449,13 @@ def main(argv, linux=None): · 5 unmodified lines
423449 if not linux:
424450 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
425451 kunitconfig_path=cli_args.kunitconfig,
452+ kconfig_add=cli_args.kconfig_add,
426453 arch=cli_args.arch,
427454 cross_compile=cli_args.cross_compile,
428455 qemu_config_path=cli_args.qemu_config)
429456
430− request = KunitConfigRequest(cli_args.build_dir,
431− cli_args.make_options)
457+ request = KunitConfigRequest(build_dir=cli_args.build_dir,
458+ make_options=cli_args.make_options)
432459 result = config_tests(linux, request)
433460 kunit_parser.print_with_timestamp((
434461 'Elapsed time: %.3fs\n') % (
@@ -439,15 +466,16 @@ def main(argv, linux=None): · 4 unmodified lines
439466 if not linux:
440467 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
441468 kunitconfig_path=cli_args.kunitconfig,
469+ kconfig_add=cli_args.kconfig_add,
442470 arch=cli_args.arch,
443471 cross_compile=cli_args.cross_compile,
444472 qemu_config_path=cli_args.qemu_config)
445473
446− request = KunitBuildRequest(cli_args.jobs,
447− cli_args.build_dir,
448− cli_args.alltests,
449− cli_args.make_options)
450− result = build_tests(linux, request)
474+ request = KunitBuildRequest(build_dir=cli_args.build_dir,
475+ make_options=cli_args.make_options,
476+ jobs=cli_args.jobs,
477+ alltests=cli_args.alltests)
478+ result = config_and_build_tests(linux, request)
451479 kunit_parser.print_with_timestamp((
452480 'Elapsed time: %.3fs\n') % (
453481 result.elapsed_time))
@@ -457,20 +485,20 @@ def main(argv, linux=None): · 3 unmodified lines
457485 if not linux:
458486 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
459487 kunitconfig_path=cli_args.kunitconfig,
488+ kconfig_add=cli_args.kconfig_add,
460489 arch=cli_args.arch,
461490 cross_compile=cli_args.cross_compile,
462491 qemu_config_path=cli_args.qemu_config)
463492
464− exec_request = KunitExecRequest(cli_args.timeout,
465− cli_args.build_dir,
466− cli_args.alltests,
467− cli_args.filter_glob,
468− cli_args.kernel_args,
469− cli_args.run_isolated)
470− parse_request = KunitParseRequest(cli_args.raw_output,
471− cli_args.build_dir,
472− cli_args.json)
473− result = exec_tests(linux, exec_request, parse_request)
493+ exec_request = KunitExecRequest(raw_output=cli_args.raw_output,
494+ build_dir=cli_args.build_dir,
495+ json=cli_args.json,
496+ timeout=cli_args.timeout,
497+ alltests=cli_args.alltests,
498+ filter_glob=cli_args.filter_glob,
499+ kernel_args=cli_args.kernel_args,
500+ run_isolated=cli_args.run_isolated)
501+ result = exec_tests(linux, exec_request)
474502 kunit_parser.print_with_timestamp((
475503 'Elapsed time: %.3fs\n') % (result.elapsed_time))
476504 if result.status != KunitStatus.SUCCESS:
@@ -482,9 +510,9 @@ def main(argv, linux=None): · 5 unmodified lines
482510 else:
483511 with open(cli_args.file, 'r', errors='backslashreplace') as f:
484512 kunit_output = f.read().splitlines()
485− request = KunitParseRequest(cli_args.raw_output,
486− None,
487− cli_args.json)
513+ request = KunitParseRequest(raw_output=cli_args.raw_output,
514+ build_dir='',
515+ json=cli_args.json)
488516 result = parse_tests(request, kunit_output)
489517 if result.status != KunitStatus.SUCCESS:
490518 sys.exit(1)
tools/testing/kunit/kunit_config.py+31−30
@@ -62,33 +62,34 @@ class Kconfig(object): · 61 unmodified lines
6262 for entry in self.entries():
6363 f.write(str(entry) + '\n')
6464
65− def parse_from_string(self, blob: str) -> None:
66− """Parses a string containing KconfigEntrys and populates this Kconfig."""
67− self._entries = []
68− is_not_set_matcher = re.compile(CONFIG_IS_NOT_SET_PATTERN)
69− config_matcher = re.compile(CONFIG_PATTERN)
70− for line in blob.split('\n'):
71− line = line.strip()
72− if not line:
73− continue
74−
75− match = config_matcher.match(line)
76− if match:
77− entry = KconfigEntry(match.group(1), match.group(2))
78− self.add_entry(entry)
79− continue
80−
81− empty_match = is_not_set_matcher.match(line)
82− if empty_match:
83− entry = KconfigEntry(empty_match.group(1), 'n')
84− self.add_entry(entry)
85− continue
86−
87− if line[0] == '#':
88− continue
89− else:
90− raise KconfigParseError('Failed to parse: ' + line)
91−
92− def read_from_file(self, path: str) -> None:
93− with open(path, 'r') as f:
94− self.parse_from_string(f.read())
65+def parse_file(path: str) -> Kconfig:
66+ with open(path, 'r') as f:
67+ return parse_from_string(f.read())
68+
69+def parse_from_string(blob: str) -> Kconfig:
70+ """Parses a string containing Kconfig entries."""
71+ kconfig = Kconfig()
72+ is_not_set_matcher = re.compile(CONFIG_IS_NOT_SET_PATTERN)
73+ config_matcher = re.compile(CONFIG_PATTERN)
74+ for line in blob.split('\n'):
75+ line = line.strip()
76+ if not line:
77+ continue
78+
79+ match = config_matcher.match(line)
80+ if match:
81+ entry = KconfigEntry(match.group(1), match.group(2))
82+ kconfig.add_entry(entry)
83+ continue
84+
85+ empty_match = is_not_set_matcher.match(line)
86+ if empty_match:
87+ entry = KconfigEntry(empty_match.group(1), 'n')
88+ kconfig.add_entry(entry)
89+ continue
90+
91+ if line[0] == '#':
92+ continue
93+ else:
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
1111
1212 import kunit_parser
1313
14−from kunit_parser import Test, TestResult, TestStatus
14+from kunit_parser import Test, TestStatus
1515 from typing import Any, Dict, Optional
1616
1717 JsonObj = Dict[str, Any]
@@ -30,6 +30,8 @@ def _get_group_json(test: Test, def_config: str, · 12 unmodified lines
3030 test_case = {"name": subtest.name, "status": "FAIL"}
3131 if subtest.status == TestStatus.SUCCESS:
3232 test_case["status"] = "PASS"
33+ elif subtest.status == TestStatus.SKIPPED:
34+ test_case["status"] = "SKIP"
3335 elif subtest.status == TestStatus.TEST_CRASHED:
3436 test_case["status"] = "ERROR"
3537 test_cases.append(test_case)
@@ -48,9 +50,9 @@ def _get_group_json(test: Test, def_config: str, · 12 unmodified lines
4850 }
4951 return test_group
5052
51−def get_json_result(test_result: TestResult, def_config: str,
53+def get_json_result(test: Test, def_config: str,
5254 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)
5456 test_group["name"] = "KUnit Test Group"
5557 json_obj = json.dumps(test_group, indent=4)
5658 if json_path != 'stdout':
tools/testing/kunit/kunit_kernel.py+49−27
@@ -21,6 +21,7 @@ import qemu_config · 20 unmodified lines
2121
2222 KCONFIG_PATH = '.config'
2323 KUNITCONFIG_PATH = '.kunitconfig'
24+OLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
2425 DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
2526 BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
2627 OUTFILE_PATH = 'test.log'
@@ -116,8 +117,7 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations): · 89 unmodified lines
116117 self._extra_qemu_params = qemu_arch_params.extra_qemu_params
117118
118119 def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
119− kconfig = kunit_config.Kconfig()
120− kconfig.parse_from_string(self._kconfig)
120+ kconfig = kunit_config.parse_from_string(self._kconfig)
121121 base_kunitconfig.merge_in_entries(kconfig)
122122
123123 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
180180 def get_kunitconfig_path(build_dir) -> str:
181181 return get_file_path(build_dir, KUNITCONFIG_PATH)
182182
183+def get_old_kunitconfig_path(build_dir) -> str:
184+ return get_file_path(build_dir, OLD_KUNITCONFIG_PATH)
185+
183186 def get_outfile_path(build_dir) -> str:
184187 return get_file_path(build_dir, OUTFILE_PATH)
185188
@@ -206,6 +209,7 @@ def get_source_tree_ops_from_qemu_config(config_path: str, · 20 unmodified lines
206209 # exists as a file.
207210 module_path = '.' + os.path.join(os.path.basename(QEMU_CONFIGS_DIR), os.path.basename(config_path))
208211 spec = importlib.util.spec_from_file_location(module_path, config_path)
212+ assert spec is not None
209213 config = importlib.util.module_from_spec(spec)
210214 # See https://github.com/python/typeshed/pull/2626 for context.
211215 assert isinstance(spec.loader, importlib.abc.Loader)
@@ -225,6 +229,7 @@ class LinuxSourceTree(object): · 13 unmodified lines
225229 build_dir: str,
226230 load_config=True,
227231 kunitconfig_path='',
232+ kconfig_add: Optional[List[str]]=None,
228233 arch=None,
229234 cross_compile=None,
230235 qemu_config_path=None) -> None:
@@ -249,8 +254,11 @@ class LinuxSourceTree(object): · 18 unmodified lines
249254 if not os.path.exists(kunitconfig_path):
250255 shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)
251256
252− self._kconfig = kunit_config.Kconfig()
253− self._kconfig.read_from_file(kunitconfig_path)
257+ self._kconfig = kunit_config.parse_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+
254262
255263 def clean(self) -> bool:
256264 try:
@@ -262,17 +270,18 @@ class LinuxSourceTree(object): · 5 unmodified lines
262270
263271 def validate_config(self, build_dir) -> bool:
264272 kconfig_path = get_kconfig_path(build_dir)
265− validated_kconfig = kunit_config.Kconfig()
266− validated_kconfig.read_from_file(kconfig_path)
267− if not self._kconfig.is_subset_of(validated_kconfig):
268− invalid = self._kconfig.entries() - validated_kconfig.entries()
269− message = 'Provided Kconfig is not contained in validated .config. Following fields found in kunitconfig, ' \
270− 'but not in .config: %s' % (
271− ', '.join([str(e) for e in invalid])
272− )
273− logging.error(message)
274− return False
275− return True
273+ validated_kconfig = kunit_config.parse_file(kconfig_path)
274+ if self._kconfig.is_subset_of(validated_kconfig):
275+ return True
276+ invalid = self._kconfig.entries() - validated_kconfig.entries()
277+ message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
278+ 'This is probably due to unsatisfied dependencies.\n' \
279+ 'Missing: ' + ', '.join([str(e) for e in invalid])
280+ if self._arch == 'um':
281+ message += '\nNote: many Kconfig options aren\'t available on UML. You can try running ' \
282+ 'on a different architecture with something like "--arch=x86_64".'
283+ logging.error(message)
284+ return False
276285
277286 def build_config(self, build_dir, make_options) -> bool:
278287 kconfig_path = get_kconfig_path(build_dir)
@@ -285,25 +294,38 @@ class LinuxSourceTree(object): · 6 unmodified lines
285294 except ConfigError as e:
286295 logging.error(e)
287296 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()
289313
290314 def build_reconfig(self, build_dir, make_options) -> bool:
291315 """Creates a new .config if it is not a subset of the .kunitconfig."""
292316 kconfig_path = get_kconfig_path(build_dir)
293− if 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:
317+ if not os.path.exists(kconfig_path):
304318 print('Generating .config ...')
305319 return self.build_config(build_dir, make_options)
306320
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+
307329 def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
308330 try:
309331 if alltests:
tools/testing/kunit/kunit_parser.py+34−23
@@ -12,14 +12,11 @@ · 11 unmodified lines
1212 from __future__ import annotations
1313 import re
1414
15−from collections import namedtuple
16−from datetime import datetime
15+import datetime
1716 from enum import Enum, auto
1817 from functools import reduce
1918 from typing import Iterable, Iterator, List, Optional, Tuple
2019
21−TestResult = namedtuple('TestResult', ['status','test','log'])
22−
2320 class Test(object):
2421 """
2522 A class to represent a test parsed from KTAP results. All KTAP
@@ -168,42 +165,51 @@ class TestCounts: · 142 unmodified lines
168165 class LineStream:
169166 """
170167 A class to represent the lines of kernel output.
171− Provides a peek()/pop() interface over an iterator of
168+ Provides a lazy peek()/pop() interface over an iterator of
172169 (line#, text).
173170 """
174171 _lines: Iterator[Tuple[int, str]]
175172 _next: Tuple[int, str]
173+ _need_next: bool
176174 _done: bool
177175
178176 def __init__(self, lines: Iterator[Tuple[int, str]]):
179177 """Creates a new LineStream that wraps the given iterator."""
180178 self._lines = lines
181179 self._done = False
180+ self._need_next = True
182181 self._next = (0, '')
183− self._get_next()
184182
185183 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
187187 try:
188188 self._next = next(self._lines)
189189 except StopIteration:
190190 self._done = True
191+ finally:
192+ self._need_next = False
191193
192194 def peek(self) -> str:
193195 """Returns the current line, without advancing the LineStream.
194196 """
197+ self._get_next()
195198 return self._next[1]
196199
197200 def pop(self) -> str:
198201 """Returns the current line and advances the LineStream to
199202 the next line.
200203 """
201− n = self._next
202− self._get_next()
203− return n[1]
204+ s = self.peek()
205+ if self._done:
206+ raise ValueError(f'LineStream: going past EOF, last line was {s}')
207+ self._need_next = True
208+ return s
204209
205210 def __bool__(self) -> bool:
206211 """Returns True if stream has more lines."""
212+ self._get_next()
207213 return not self._done
208214
209215 # Only used by kunit_tool_test.py.
@@ -216,6 +222,7 @@ class LineStream: · 6 unmodified lines
216222
217223 def line_number(self) -> int:
218224 """Returns the line number of the current line."""
225+ self._get_next()
219226 return self._next[0]
220227
221228 # Parsing helper methods:
@@ -340,8 +347,8 @@ def parse_test_plan(lines: LineStream, test: Test) -> bool: · 118 unmodified lines
340347 """
341348 Parses test plan line and stores the expected number of subtests in
342349 test object. Reports an error if expected count is 0.
343− Returns False and reports missing test plan error if fails to parse
344− test plan.
350+ Returns False and sets expected_count to None if there is no valid test
351+ plan.
345352
346353 Accepted format:
347354 - '1..[number of subtests]'
@@ -356,14 +363,10 @@ def parse_test_plan(lines: LineStream, test: Test) -> bool: · 8 unmodified lines
356363 match = TEST_PLAN.match(lines.peek())
357364 if not match:
358365 test.expected_count = None
359− test.add_error('missing plan line!')
360366 return False
361367 test.log.append(lines.pop())
362368 expected_count = int(match.group(1))
363369 test.expected_count = expected_count
364− if expected_count == 0:
365− test.status = TestStatus.NO_TESTS
366− test.add_error('0 tests run!')
367370 return True
368371
369372 TEST_RESULT = re.compile(r'^(ok|not ok) ([0-9]+) (- )?([^#]*)( # .*)?$')
@@ -514,7 +517,7 @@ ANSI_LEN = len(red('')) · 144 unmodified lines
514517
515518 def print_with_timestamp(message: str) -> None:
516519 """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))
518521
519522 def format_test_divider(message: str, len_message: int) -> str:
520523 """
@@ -590,6 +593,8 @@ def format_test_result(test: Test) -> str: · 69 unmodified lines
590593 return (green('[PASSED] ') + test.name)
591594 elif test.status == TestStatus.SKIPPED:
592595 return (yellow('[SKIPPED] ') + test.name)
596+ elif test.status == TestStatus.NO_TESTS:
597+ return (yellow('[NO TESTS RUN] ') + test.name)
593598 elif test.status == TestStatus.TEST_CRASHED:
594599 print_log(test.log)
595600 return (red('[CRASHED] ') + test.name)
@@ -732,6 +737,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: · 136 unmodified lines
732737 # test plan
733738 test.name = "main"
734739 parse_test_plan(lines, test)
740+ parent_test = True
735741 else:
736742 # If KTAP/TAP header is not found, test must be subtest
737743 # 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
745751 expected_count = test.expected_count
746752 subtests = []
747753 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):
749755 # Loop to parse any subtests.
750756 # Break after parsing expected number of tests or
751757 # 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
780786 parse_test_result(lines, test, expected_num)
781787 else:
782788 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+
783795 # Add statuses to TestCounts attribute in Test object
784796 bubble_up_test_results(test)
785− if parent_test:
797+ if parent_test and not main:
786798 # If test has subtests and is not the main test object, print
787799 # footer.
788800 print_test_footer(test)
@@ -790,7 +802,7 @@ def parse_test(lines: LineStream, expected_num: int, log: List[str]) -> Test: · 1 unmodified line
790802 print_test_result(test)
791803 return test
792804
793−def parse_run_tests(kernel_output: Iterable[str]) -> TestResult:
805+def parse_run_tests(kernel_output: Iterable[str]) -> Test:
794806 """
795807 Using kernel output, extract KTAP lines, parse the lines for test
796808 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
799811 kernel_output - Iterable object contains lines of kernel output
800812
801813 Return:
802− TestResult - Tuple containg status of main test object, main test
803− object with all subtests, and log of all KTAP lines.
814+ Test - the main test object with all subtests.
804815 """
805816 print_with_timestamp(DIVIDER)
806817 lines = extract_tap_lines(kernel_output)
@@ -814,4 +825,4 @@ def parse_run_tests(kernel_output: Iterable[str]) -> TestResult: · 7 unmodified lines
814825 test.status = test.counts.get_status()
815826 print_with_timestamp(DIVIDER)
816827 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
1313
1414 import itertools
1515 import json
16+import os
1617 import signal
1718 import subprocess
18−import os
19+from typing import Iterable
1920
2021 import kunit_config
2122 import kunit_parser
@@ -50,10 +51,9 @@ class KconfigTest(unittest.TestCase): · 28 unmodified lines
5051 self.assertFalse(kconfig1.is_subset_of(kconfig0))
5152
5253 def test_read_from_file(self):
53− kconfig = kunit_config.Kconfig()
5454 kconfig_path = test_data_path('test_read_from_file.kconfig')
5555
56− kconfig.read_from_file(kconfig_path)
56+ kconfig = kunit_config.parse_file(kconfig_path)
5757
5858 expected_kconfig = kunit_config.Kconfig()
5959 expected_kconfig.add_entry(
@@ -86,8 +86,7 @@ class KconfigTest(unittest.TestCase): · 26 unmodified lines
8686
8787 expected_kconfig.write_to_file(kconfig_path)
8888
89− actual_kconfig = kunit_config.Kconfig()
90− actual_kconfig.read_from_file(kconfig_path)
89+ actual_kconfig = kunit_config.parse_file(kconfig_path)
9190
9291 self.assertEqual(actual_kconfig.entries(),
9392 expected_kconfig.entries())
@@ -179,7 +178,7 @@ class KUnitParserTest(unittest.TestCase): · 85 unmodified lines
179178 with open(empty_log) as file:
180179 result = kunit_parser.parse_run_tests(
181180 kunit_parser.extract_tap_lines(file.readlines()))
182− self.assertEqual(0, len(result.test.subtests))
181+ self.assertEqual(0, len(result.subtests))
183182 self.assertEqual(
184183 kunit_parser.TestStatus.FAILURE_TO_PARSE_TESTS,
185184 result.status)
@@ -191,7 +190,10 @@ class KUnitParserTest(unittest.TestCase): · 5 unmodified lines
191190 result = kunit_parser.parse_run_tests(
192191 kunit_parser.extract_tap_lines(
193192 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())
195197 self.assertEqual(
196198 kunit_parser.TestStatus.SUCCESS,
197199 result.status)
@@ -201,11 +203,23 @@ class KUnitParserTest(unittest.TestCase): · 3 unmodified lines
201203 with open(header_log) as file:
202204 result = kunit_parser.parse_run_tests(
203205 kunit_parser.extract_tap_lines(file.readlines()))
204− self.assertEqual(0, len(result.test.subtests))
206+ self.assertEqual(0, len(result.subtests))
205207 self.assertEqual(
206208 kunit_parser.TestStatus.NO_TESTS,
207209 result.status)
208210
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+
209223 def test_no_kunit_output(self):
210224 crash_log = test_data_path('test_insufficient_memory.log')
211225 print_mock = mock.patch('builtins.print').start()
@@ -214,7 +228,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
214228 kunit_parser.extract_tap_lines(file.readlines()))
215229 print_mock.assert_any_call(StrContains('invalid KTAP input!'))
216230 print_mock.stop()
217− self.assertEqual(0, len(result.test.subtests))
231+ self.assertEqual(0, len(result.subtests))
218232
219233 def test_crashed_test(self):
220234 crashed_log = test_data_path('test_is_test_passed-crash.log')
@@ -255,10 +269,10 @@ class KUnitParserTest(unittest.TestCase): · 34 unmodified lines
255269 result.status)
256270 self.assertEqual(
257271 "sysctl_test",
258− result.test.subtests[0].name)
272+ result.subtests[0].name)
259273 self.assertEqual(
260274 "example",
261− result.test.subtests[1].name)
275+ result.subtests[1].name)
262276 file.close()
263277
264278
@@ -269,7 +283,7 @@ class KUnitParserTest(unittest.TestCase): · 4 unmodified lines
269283 self.assertEqual(
270284 kunit_parser.TestStatus.SUCCESS,
271285 result.status)
272− self.assertEqual('kunit-resource-test', result.test.subtests[0].name)
286+ self.assertEqual('kunit-resource-test', result.subtests[0].name)
273287
274288 def test_ignores_multiple_prefixes(self):
275289 prefix_log = test_data_path('test_multiple_prefixes.log')
@@ -278,7 +292,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
278292 self.assertEqual(
279293 kunit_parser.TestStatus.SUCCESS,
280294 result.status)
281− self.assertEqual('kunit-resource-test', result.test.subtests[0].name)
295+ self.assertEqual('kunit-resource-test', result.subtests[0].name)
282296
283297 def test_prefix_mixed_kernel_output(self):
284298 mixed_prefix_log = test_data_path('test_interrupted_tap_output.log')
@@ -287,7 +301,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
287301 self.assertEqual(
288302 kunit_parser.TestStatus.SUCCESS,
289303 result.status)
290− self.assertEqual('kunit-resource-test', result.test.subtests[0].name)
304+ self.assertEqual('kunit-resource-test', result.subtests[0].name)
291305
292306 def test_prefix_poundsign(self):
293307 pound_log = test_data_path('test_pound_sign.log')
@@ -296,7 +310,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
296310 self.assertEqual(
297311 kunit_parser.TestStatus.SUCCESS,
298312 result.status)
299− self.assertEqual('kunit-resource-test', result.test.subtests[0].name)
313+ self.assertEqual('kunit-resource-test', result.subtests[0].name)
300314
301315 def test_kernel_panic_end(self):
302316 panic_log = test_data_path('test_kernel_panic_interrupt.log')
@@ -305,7 +319,7 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
305319 self.assertEqual(
306320 kunit_parser.TestStatus.TEST_CRASHED,
307321 result.status)
308− self.assertEqual('kunit-resource-test', result.test.subtests[0].name)
322+ self.assertEqual('kunit-resource-test', result.subtests[0].name)
309323
310324 def test_pound_no_prefix(self):
311325 pound_log = test_data_path('test_pound_no_prefix.log')
@@ -314,7 +328,46 @@ class KUnitParserTest(unittest.TestCase): · 2 unmodified lines
314328 self.assertEqual(
315329 kunit_parser.TestStatus.SUCCESS,
316330 result.status)
317− self.assertEqual('kunit-resource-test', result.test.subtests[0].name)
331+ self.assertEqual('kunit-resource-test', result.subtests[0].name)
332+
333+def line_stream_from_strs(strs: Iterable[str]) -> kunit_parser.LineStream:
334+ return kunit_parser.LineStream(enumerate(strs, start=1))
335+
336+class 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)
318371
319372 class LinuxSourceTreeTest(unittest.TestCase):
320373
@@ -336,6 +389,10 @@ class LinuxSourceTreeTest(unittest.TestCase): · 15 unmodified lines
336389 pass
337390 kunit_kernel.LinuxSourceTree('', kunitconfig_path=dir)
338391
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+
339396 def test_invalid_arch(self):
340397 with self.assertRaisesRegex(kunit_kernel.ConfigError, 'not a valid arch, options are.*x86_64'):
341398 kunit_kernel.LinuxSourceTree('', arch='invalid')
@@ -356,6 +413,51 @@ class LinuxSourceTreeTest(unittest.TestCase): · 14 unmodified lines
356413 with open(kunit_kernel.get_outfile_path(build_dir), 'rt') as outfile:
357414 self.assertEqual(outfile.read(), 'hi\nbye\n', msg='Missing some output')
358415
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+
359461 # TODO: add more test cases.
360462
361463
@@ -365,7 +467,7 @@ class KUnitJsonTest(unittest.TestCase): · 3 unmodified lines
365467 with open(test_data_path(log_file)) as file:
366468 test_result = kunit_parser.parse_run_tests(file)
367469 json_obj = kunit_json.get_json_result(
368− test_result=test_result,
470+ test=test_result,
369471 def_config='kunit_defconfig',
370472 build_dir=None,
371473 json_path='stdout')
@@ -383,6 +485,12 @@ class KUnitJsonTest(unittest.TestCase): · 11 unmodified lines
383485 {'name': 'example_simple_test', 'status': 'ERROR'},
384486 result["sub_groups"][1]["test_cases"][0])
385487
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+
386494 def test_no_tests_json(self):
387495 result = self._json_for('test_is_test_passed-no_tests_run_with_header.log')
388496 self.assertEqual(0, len(result['sub_groups']))
@@ -418,8 +526,8 @@ class KUnitMainTest(unittest.TestCase): · 29 unmodified lines
418526
419527 def test_build_passes_args_pass(self):
420528 kunit.main(['build'], self.linux_source_mock)
421− self.assertEqual(self.linux_source_mock.build_reconfig.call_count, 0)
422− self.linux_source_mock.build_kernel.assert_called_once_with(False, 8, '.kunit', None)
529+ self.assertEqual(self.linux_source_mock.build_reconfig.call_count, 1)
530+ self.linux_source_mock.build_kernel.assert_called_once_with(False, kunit.get_default_jobs(), '.kunit', None)
423531 self.assertEqual(self.linux_source_mock.run_kernel.call_count, 0)
424532
425533 def test_exec_passes_args_pass(self):
@@ -525,8 +633,9 @@ class KUnitMainTest(unittest.TestCase): · 99 unmodified lines
525633
526634 def test_build_builddir(self):
527635 build_dir = '.kunit'
636+ jobs = kunit.get_default_jobs()
528637 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)
530639
531640 def test_exec_builddir(self):
532641 build_dir = '.kunit'
@@ -542,6 +651,7 @@ class KUnitMainTest(unittest.TestCase): · 9 unmodified lines
542651 # Just verify that we parsed and initialized it correctly here.
543652 mock_linux_init.assert_called_once_with('.kunit',
544653 kunitconfig_path='mykunitconfig',
654+ kconfig_add=None,
545655 arch='um',
546656 cross_compile=None,
547657 qemu_config_path=None)
@@ -553,6 +663,19 @@ class KUnitMainTest(unittest.TestCase): · 5 unmodified lines
553663 # Just verify that we parsed and initialized it correctly here.
554664 mock_linux_init.assert_called_once_with('.kunit',
555665 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'],
556679 arch='um',
557680 cross_compile=None,
558681 qemu_config_path=None)
@@ -569,7 +692,7 @@ class KUnitMainTest(unittest.TestCase): · 10 unmodified lines
569692 self.linux_source_mock.run_kernel.return_value = ['TAP version 14', 'init: random output'] + want
570693
571694 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'))
573696
574697 self.assertEqual(got, want)
575698 # Should respect the user's filter glob when listing tests.
@@ -584,7 +707,7 @@ class KUnitMainTest(unittest.TestCase): · 8 unmodified lines
584707
585708 # Should respect the user's filter glob when listing tests.
586709 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'))
588711 self.linux_source_mock.run_kernel.assert_has_calls([
589712 mock.call(args=None, build_dir='.kunit', filter_glob='suite.test*', timeout=300),
590713 mock.call(args=None, build_dir='.kunit', filter_glob='suite2.test*', timeout=300),
@@ -597,7 +720,7 @@ class KUnitMainTest(unittest.TestCase): · 6 unmodified lines
597720
598721 # Should respect the user's filter glob when listing tests.
599722 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'))
601724 self.linux_source_mock.run_kernel.assert_has_calls([
602725 mock.call(args=None, build_dir='.kunit', filter_glob='suite.test1', timeout=300),
603726 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+
10+from concurrent import futures
11+import datetime
12+import os
13+import shutil
14+import subprocess
15+import sys
16+import textwrap
17+from typing import Dict, List, Sequence, Tuple
18+
19+ABS_TOOL_PATH = os.path.abspath(os.path.dirname(__file__))
20+TIMEOUT = datetime.timedelta(minutes=5).total_seconds()
21+
22+commands: 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`
31+necessary_deps : Dict[str, str] = {
32+ 'pytype': 'pytype',
33+ 'mypy': 'mypy',
34+}
35+
36+def 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+
76+def run_cmd(argv: Sequence[str]):
77+ subprocess.check_output(argv, stderr=subprocess.STDOUT, cwd=ABS_TOOL_PATH, timeout=TIMEOUT)
78+
79+
80+if __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 @@
1+TAP version 14
2+1..1
3+ # Subtest: suite
4+ 1..1
5+ # Subtest: case
6+ ok 1 - case # SKIP
7+ok 1 - suite