xref: /linux-6.15/tools/testing/kunit/kunit.py (revision 56530007)
1c25ce589SFinn Behrens#!/usr/bin/env python3
26ebf5866SFelix Guo# SPDX-License-Identifier: GPL-2.0
36ebf5866SFelix Guo#
46ebf5866SFelix Guo# A thin wrapper on top of the KUnit Kernel
56ebf5866SFelix Guo#
66ebf5866SFelix Guo# Copyright (C) 2019, Google LLC.
76ebf5866SFelix Guo# Author: Felix Guo <[email protected]>
86ebf5866SFelix Guo# Author: Brendan Higgins <[email protected]>
96ebf5866SFelix Guo
106ebf5866SFelix Guoimport argparse
116ebf5866SFelix Guoimport os
12ff9e09a3SDaniel Latypovimport re
13a9333bd3SDaniel Latypovimport shlex
14ff9e09a3SDaniel Latypovimport sys
156ebf5866SFelix Guoimport time
166ebf5866SFelix Guo
17df4b0807SSeongJae Parkassert sys.version_info >= (3, 7), "Python version is too old"
18df4b0807SSeongJae Park
19db167981SDaniel Latypovfrom dataclasses import dataclass
206ebf5866SFelix Guofrom enum import Enum, auto
2195dcbc55SDaniel Latypovfrom typing import Iterable, List, Optional, Sequence, Tuple
226ebf5866SFelix Guo
2321a6d178SHeidi Fahimimport kunit_json
246ebf5866SFelix Guoimport kunit_kernel
256ebf5866SFelix Guoimport kunit_parser
26062a9dd9SDavid Gowfrom kunit_printer import stdout, null_printer
276ebf5866SFelix Guo
286ebf5866SFelix Guoclass KunitStatus(Enum):
296ebf5866SFelix Guo	SUCCESS = auto()
306ebf5866SFelix Guo	CONFIG_FAILURE = auto()
316ebf5866SFelix Guo	BUILD_FAILURE = auto()
326ebf5866SFelix Guo	TEST_FAILURE = auto()
336ebf5866SFelix Guo
34db167981SDaniel Latypov@dataclass
35db167981SDaniel Latypovclass KunitResult:
36db167981SDaniel Latypov	status: KunitStatus
37db167981SDaniel Latypov	elapsed_time: float
38db167981SDaniel Latypov
39db167981SDaniel Latypov@dataclass
40db167981SDaniel Latypovclass KunitConfigRequest:
41db167981SDaniel Latypov	build_dir: str
42db167981SDaniel Latypov	make_options: Optional[List[str]]
43db167981SDaniel Latypov
44db167981SDaniel Latypov@dataclass
45db167981SDaniel Latypovclass KunitBuildRequest(KunitConfigRequest):
46db167981SDaniel Latypov	jobs: int
47db167981SDaniel Latypov
48db167981SDaniel Latypov@dataclass
49db167981SDaniel Latypovclass KunitParseRequest:
50db167981SDaniel Latypov	raw_output: Optional[str]
51db167981SDaniel Latypov	json: Optional[str]
52062a9dd9SDavid Gow	summary: bool
533c67a2c0SRae Moar	failed: bool
54db167981SDaniel Latypov
55db167981SDaniel Latypov@dataclass
56db167981SDaniel Latypovclass KunitExecRequest(KunitParseRequest):
57ee96d25fSDaniel Latypov	build_dir: str
58db167981SDaniel Latypov	timeout: int
59db167981SDaniel Latypov	filter_glob: str
60723c8258SRae Moar	filter: str
61723c8258SRae Moar	filter_action: Optional[str]
62db167981SDaniel Latypov	kernel_args: Optional[List[str]]
63db167981SDaniel Latypov	run_isolated: Optional[str]
64723c8258SRae Moar	list_tests: bool
65723c8258SRae Moar	list_tests_attr: bool
66db167981SDaniel Latypov
67db167981SDaniel Latypov@dataclass
68db167981SDaniel Latypovclass KunitRequest(KunitExecRequest, KunitBuildRequest):
69db167981SDaniel Latypov	pass
70db167981SDaniel Latypov
71db167981SDaniel Latypov
7209641f7cSDaniel Latypovdef get_kernel_root_path() -> str:
7309641f7cSDaniel Latypov	path = sys.argv[0] if not __file__ else __file__
7409641f7cSDaniel Latypov	parts = os.path.realpath(path).split('tools/testing/kunit')
75be886ba9SHeidi Fahim	if len(parts) != 2:
76be886ba9SHeidi Fahim		sys.exit(1)
77be886ba9SHeidi Fahim	return parts[0]
78be886ba9SHeidi Fahim
7945ba7a89SDavid Gowdef config_tests(linux: kunit_kernel.LinuxSourceTree,
8045ba7a89SDavid Gow		 request: KunitConfigRequest) -> KunitResult:
81e756dbebSDaniel Latypov	stdout.print_with_timestamp('Configuring KUnit Kernel ...')
8245ba7a89SDavid Gow
836ebf5866SFelix Guo	config_start = time.time()
840476e69fSGreg Thelen	success = linux.build_reconfig(request.build_dir, request.make_options)
856ebf5866SFelix Guo	config_end = time.time()
861fdc6f4fSAlexander Pantyukhin	status = KunitStatus.SUCCESS if success else KunitStatus.CONFIG_FAILURE
871fdc6f4fSAlexander Pantyukhin	return KunitResult(status, config_end - config_start)
886ebf5866SFelix Guo
8945ba7a89SDavid Gowdef build_tests(linux: kunit_kernel.LinuxSourceTree,
9045ba7a89SDavid Gow		request: KunitBuildRequest) -> KunitResult:
91e756dbebSDaniel Latypov	stdout.print_with_timestamp('Building KUnit Kernel ...')
926ebf5866SFelix Guo
936ebf5866SFelix Guo	build_start = time.time()
94980ac3adSDaniel Latypov	success = linux.build_kernel(request.jobs,
950476e69fSGreg Thelen				     request.build_dir,
960476e69fSGreg Thelen				     request.make_options)
976ebf5866SFelix Guo	build_end = time.time()
981fdc6f4fSAlexander Pantyukhin	status = KunitStatus.SUCCESS if success else KunitStatus.BUILD_FAILURE
991fdc6f4fSAlexander Pantyukhin	return KunitResult(status, build_end - build_start)
1006ebf5866SFelix Guo
1011ee2ba89SDaniel Latypovdef config_and_build_tests(linux: kunit_kernel.LinuxSourceTree,
1021ee2ba89SDaniel Latypov			   request: KunitBuildRequest) -> KunitResult:
1031ee2ba89SDaniel Latypov	config_result = config_tests(linux, request)
1041ee2ba89SDaniel Latypov	if config_result.status != KunitStatus.SUCCESS:
1051ee2ba89SDaniel Latypov		return config_result
1061ee2ba89SDaniel Latypov
1071ee2ba89SDaniel Latypov	return build_tests(linux, request)
1081ee2ba89SDaniel Latypov
109ff9e09a3SDaniel Latypovdef _list_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> List[str]:
110ff9e09a3SDaniel Latypov	args = ['kunit.action=list']
111723c8258SRae Moar
112ff9e09a3SDaniel Latypov	if request.kernel_args:
113ff9e09a3SDaniel Latypov		args.extend(request.kernel_args)
114ff9e09a3SDaniel Latypov
115ff9e09a3SDaniel Latypov	output = linux.run_kernel(args=args,
116980ac3adSDaniel Latypov			   timeout=request.timeout,
117ff9e09a3SDaniel Latypov			   filter_glob=request.filter_glob,
118723c8258SRae Moar			   filter=request.filter,
119723c8258SRae Moar			   filter_action=request.filter_action,
120ff9e09a3SDaniel Latypov			   build_dir=request.build_dir)
121ff9e09a3SDaniel Latypov	lines = kunit_parser.extract_tap_lines(output)
122ff9e09a3SDaniel Latypov	# Hack! Drop the dummy TAP version header that the executor prints out.
123ff9e09a3SDaniel Latypov	lines.pop()
124ff9e09a3SDaniel Latypov
125ff9e09a3SDaniel Latypov	# Filter out any extraneous non-test output that might have gotten mixed in.
126723c8258SRae Moar	return [l for l in output if re.match(r'^[^\s.]+\.[^\s.]+$', l)]
127723c8258SRae Moar
128723c8258SRae Moardef _list_tests_attr(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> Iterable[str]:
129723c8258SRae Moar	args = ['kunit.action=list_attr']
130723c8258SRae Moar
131723c8258SRae Moar	if request.kernel_args:
132723c8258SRae Moar		args.extend(request.kernel_args)
133723c8258SRae Moar
134723c8258SRae Moar	output = linux.run_kernel(args=args,
135723c8258SRae Moar			   timeout=request.timeout,
136723c8258SRae Moar			   filter_glob=request.filter_glob,
137723c8258SRae Moar			   filter=request.filter,
138723c8258SRae Moar			   filter_action=request.filter_action,
139723c8258SRae Moar			   build_dir=request.build_dir)
140723c8258SRae Moar	lines = kunit_parser.extract_tap_lines(output)
141723c8258SRae Moar	# Hack! Drop the dummy TAP version header that the executor prints out.
142723c8258SRae Moar	lines.pop()
143723c8258SRae Moar
144723c8258SRae Moar	# Filter out any extraneous non-test output that might have gotten mixed in.
145723c8258SRae Moar	return lines
146ff9e09a3SDaniel Latypov
147ff9e09a3SDaniel Latypovdef _suites_from_test_list(tests: List[str]) -> List[str]:
148ff9e09a3SDaniel Latypov	"""Extracts all the suites from an ordered list of tests."""
149ff9e09a3SDaniel Latypov	suites = []  # type: List[str]
150ff9e09a3SDaniel Latypov	for t in tests:
151ff9e09a3SDaniel Latypov		parts = t.split('.', maxsplit=2)
152ff9e09a3SDaniel Latypov		if len(parts) != 2:
153ff9e09a3SDaniel Latypov			raise ValueError(f'internal KUnit error, test name should be of the form "<suite>.<test>", got "{t}"')
154126901baSDaniel Latypov		suite, _ = parts
155ff9e09a3SDaniel Latypov		if not suites or suites[-1] != suite:
156ff9e09a3SDaniel Latypov			suites.append(suite)
157ff9e09a3SDaniel Latypov	return suites
158ff9e09a3SDaniel Latypov
159db167981SDaniel Latypovdef exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> KunitResult:
160ff9e09a3SDaniel Latypov	filter_globs = [request.filter_glob]
161723c8258SRae Moar	if request.list_tests:
162723c8258SRae Moar		output = _list_tests(linux, request)
163723c8258SRae Moar		for line in output:
164723c8258SRae Moar			print(line.rstrip())
165723c8258SRae Moar		return KunitResult(status=KunitStatus.SUCCESS, elapsed_time=0.0)
166723c8258SRae Moar	if request.list_tests_attr:
167723c8258SRae Moar		attr_output = _list_tests_attr(linux, request)
168723c8258SRae Moar		for line in attr_output:
169723c8258SRae Moar			print(line.rstrip())
170723c8258SRae Moar		return KunitResult(status=KunitStatus.SUCCESS, elapsed_time=0.0)
171ff9e09a3SDaniel Latypov	if request.run_isolated:
172ff9e09a3SDaniel Latypov		tests = _list_tests(linux, request)
173ff9e09a3SDaniel Latypov		if request.run_isolated == 'test':
174ff9e09a3SDaniel Latypov			filter_globs = tests
1751fdc6f4fSAlexander Pantyukhin		elif request.run_isolated == 'suite':
176ff9e09a3SDaniel Latypov			filter_globs = _suites_from_test_list(tests)
177ff9e09a3SDaniel Latypov			# Apply the test-part of the user's glob, if present.
178ff9e09a3SDaniel Latypov			if '.' in request.filter_glob:
179ff9e09a3SDaniel Latypov				test_glob = request.filter_glob.split('.', maxsplit=2)[1]
180ff9e09a3SDaniel Latypov				filter_globs = [g + '.'+ test_glob for g in filter_globs]
181ff9e09a3SDaniel Latypov
182885210d3SDaniel Latypov	metadata = kunit_json.Metadata(arch=linux.arch(), build_dir=request.build_dir, def_config='kunit_defconfig')
183ee96d25fSDaniel Latypov
184d65d07cbSRae Moar	test_counts = kunit_parser.TestCounts()
185ff9e09a3SDaniel Latypov	exec_time = 0.0
186ff9e09a3SDaniel Latypov	for i, filter_glob in enumerate(filter_globs):
187e756dbebSDaniel Latypov		stdout.print_with_timestamp('Starting KUnit Kernel ({}/{})...'.format(i+1, len(filter_globs)))
188ff9e09a3SDaniel Latypov
1896ebf5866SFelix Guo		test_start = time.time()
1907ef925eaSDaniel Latypov		run_result = linux.run_kernel(
1916cb51a18SDaniel Latypov			args=request.kernel_args,
192980ac3adSDaniel Latypov			timeout=request.timeout,
193ff9e09a3SDaniel Latypov			filter_glob=filter_glob,
194723c8258SRae Moar			filter=request.filter,
195723c8258SRae Moar			filter_action=request.filter_action,
1966ec1b81dSSeongJae Park			build_dir=request.build_dir)
19745ba7a89SDavid Gow
198ee96d25fSDaniel Latypov		_, test_result = parse_tests(request, metadata, run_result)
1995f6aa6d8SDaniel Latypov		# run_kernel() doesn't block on the kernel exiting.
2005f6aa6d8SDaniel Latypov		# That only happens after we get the last line of output from `run_result`.
2015f6aa6d8SDaniel Latypov		# So exec_time here actually contains parsing + execution time, which is fine.
2026ebf5866SFelix Guo		test_end = time.time()
203ff9e09a3SDaniel Latypov		exec_time += test_end - test_start
204ff9e09a3SDaniel Latypov
20595dcbc55SDaniel Latypov		test_counts.add_subtest_counts(test_result.counts)
2066ebf5866SFelix Guo
2077fa7ffcfSDaniel Latypov	if len(filter_globs) == 1 and test_counts.crashed > 0:
2087fa7ffcfSDaniel Latypov		bd = request.build_dir
2097fa7ffcfSDaniel Latypov		print('The kernel seems to have crashed; you can decode the stack traces with:')
2107fa7ffcfSDaniel Latypov		print('$ scripts/decode_stacktrace.sh {}/vmlinux {} < {} | tee {}/decoded.log | {} parse'.format(
2117fa7ffcfSDaniel Latypov				bd, bd, kunit_kernel.get_outfile_path(bd), bd, sys.argv[0]))
2127fa7ffcfSDaniel Latypov
213d65d07cbSRae Moar	kunit_status = _map_to_overall_status(test_counts.get_status())
21495dcbc55SDaniel Latypov	return KunitResult(status=kunit_status, elapsed_time=exec_time)
215d65d07cbSRae Moar
216d65d07cbSRae Moardef _map_to_overall_status(test_status: kunit_parser.TestStatus) -> KunitStatus:
217d65d07cbSRae Moar	if test_status in (kunit_parser.TestStatus.SUCCESS, kunit_parser.TestStatus.SKIPPED):
218d65d07cbSRae Moar		return KunitStatus.SUCCESS
219d65d07cbSRae Moar	return KunitStatus.TEST_FAILURE
2207ef925eaSDaniel Latypov
221ee96d25fSDaniel Latypovdef parse_tests(request: KunitParseRequest, metadata: kunit_json.Metadata, input_data: Iterable[str]) -> Tuple[KunitResult, kunit_parser.Test]:
22245ba7a89SDavid Gow	parse_start = time.time()
22345ba7a89SDavid Gow
22445ba7a89SDavid Gow	if request.raw_output:
225d65d07cbSRae Moar		# Treat unparsed results as one passing test.
226309e22efSDaniel Latypov		fake_test = kunit_parser.Test()
227309e22efSDaniel Latypov		fake_test.status = kunit_parser.TestStatus.SUCCESS
228309e22efSDaniel Latypov		fake_test.counts.passed = 1
229d65d07cbSRae Moar
2307ef925eaSDaniel Latypov		output: Iterable[str] = input_data
2316a499c9cSDaniel Latypov		if request.raw_output == 'all':
2326a499c9cSDaniel Latypov			pass
2336a499c9cSDaniel Latypov		elif request.raw_output == 'kunit':
234c2bb92bcSDaniel Latypov			output = kunit_parser.extract_tap_lines(output)
2356a499c9cSDaniel Latypov		for line in output:
2366a499c9cSDaniel Latypov			print(line.rstrip())
237309e22efSDaniel Latypov		parse_time = time.time() - parse_start
238309e22efSDaniel Latypov		return KunitResult(KunitStatus.SUCCESS, parse_time), fake_test
2396a499c9cSDaniel Latypov
240062a9dd9SDavid Gow	default_printer = stdout
2413c67a2c0SRae Moar	if request.summary or request.failed:
242062a9dd9SDavid Gow		default_printer = null_printer
243309e22efSDaniel Latypov
244309e22efSDaniel Latypov	# Actually parse the test results.
245062a9dd9SDavid Gow	test = kunit_parser.parse_run_tests(input_data, default_printer)
246309e22efSDaniel Latypov	parse_time = time.time() - parse_start
24745ba7a89SDavid Gow
2483c67a2c0SRae Moar	if request.failed:
2493c67a2c0SRae Moar		kunit_parser.print_test(test, request.failed, stdout)
250062a9dd9SDavid Gow	kunit_parser.print_summary_line(test, stdout)
251062a9dd9SDavid Gow
25221a6d178SHeidi Fahim	if request.json:
25300f75043SDaniel Latypov		json_str = kunit_json.get_json_result(
254309e22efSDaniel Latypov					test=test,
255ee96d25fSDaniel Latypov					metadata=metadata)
25621a6d178SHeidi Fahim		if request.json == 'stdout':
25700f75043SDaniel Latypov			print(json_str)
25800f75043SDaniel Latypov		else:
25900f75043SDaniel Latypov			with open(request.json, 'w') as f:
26000f75043SDaniel Latypov				f.write(json_str)
261e756dbebSDaniel Latypov			stdout.print_with_timestamp("Test results stored in %s" %
26200f75043SDaniel Latypov				os.path.abspath(request.json))
26321a6d178SHeidi Fahim
264309e22efSDaniel Latypov	if test.status != kunit_parser.TestStatus.SUCCESS:
265309e22efSDaniel Latypov		return KunitResult(KunitStatus.TEST_FAILURE, parse_time), test
26645ba7a89SDavid Gow
267309e22efSDaniel Latypov	return KunitResult(KunitStatus.SUCCESS, parse_time), test
26845ba7a89SDavid Gow
26945ba7a89SDavid Gowdef run_tests(linux: kunit_kernel.LinuxSourceTree,
27045ba7a89SDavid Gow	      request: KunitRequest) -> KunitResult:
27145ba7a89SDavid Gow	run_start = time.time()
27245ba7a89SDavid Gow
273db167981SDaniel Latypov	config_result = config_tests(linux, request)
27445ba7a89SDavid Gow	if config_result.status != KunitStatus.SUCCESS:
27545ba7a89SDavid Gow		return config_result
27645ba7a89SDavid Gow
277db167981SDaniel Latypov	build_result = build_tests(linux, request)
27845ba7a89SDavid Gow	if build_result.status != KunitStatus.SUCCESS:
27945ba7a89SDavid Gow		return build_result
28045ba7a89SDavid Gow
281db167981SDaniel Latypov	exec_result = exec_tests(linux, request)
28245ba7a89SDavid Gow
28345ba7a89SDavid Gow	run_end = time.time()
28445ba7a89SDavid Gow
285e756dbebSDaniel Latypov	stdout.print_with_timestamp((
2866ebf5866SFelix Guo		'Elapsed time: %.3fs total, %.3fs configuring, %.3fs ' +
2876ebf5866SFelix Guo		'building, %.3fs running\n') % (
28845ba7a89SDavid Gow				run_end - run_start,
28945ba7a89SDavid Gow				config_result.elapsed_time,
29045ba7a89SDavid Gow				build_result.elapsed_time,
29145ba7a89SDavid Gow				exec_result.elapsed_time))
2927ef925eaSDaniel Latypov	return exec_result
2936ebf5866SFelix Guo
294d8c23eadSDaniel Latypov# Problem:
295d8c23eadSDaniel Latypov# $ kunit.py run --json
296d8c23eadSDaniel Latypov# works as one would expect and prints the parsed test results as JSON.
297d8c23eadSDaniel Latypov# $ kunit.py run --json suite_name
298d8c23eadSDaniel Latypov# would *not* pass suite_name as the filter_glob and print as json.
299d8c23eadSDaniel Latypov# argparse will consider it to be another way of writing
300d8c23eadSDaniel Latypov# $ kunit.py run --json=suite_name
301d8c23eadSDaniel Latypov# i.e. it would run all tests, and dump the json to a `suite_name` file.
302d8c23eadSDaniel Latypov# So we hackily automatically rewrite --json => --json=stdout
303d8c23eadSDaniel Latypovpseudo_bool_flag_defaults = {
304d8c23eadSDaniel Latypov		'--json': 'stdout',
305d8c23eadSDaniel Latypov		'--raw_output': 'kunit',
306d8c23eadSDaniel Latypov}
307d8c23eadSDaniel Latypovdef massage_argv(argv: Sequence[str]) -> Sequence[str]:
308d8c23eadSDaniel Latypov	def massage_arg(arg: str) -> str:
309d8c23eadSDaniel Latypov		if arg not in pseudo_bool_flag_defaults:
310d8c23eadSDaniel Latypov			return arg
311d8c23eadSDaniel Latypov		return  f'{arg}={pseudo_bool_flag_defaults[arg]}'
312d8c23eadSDaniel Latypov	return list(map(massage_arg, argv))
313d8c23eadSDaniel Latypov
314ad659ccbSDavid Gowdef get_default_jobs() -> int:
315*56530007STamir Duberstein	if sys.version_info >= (3, 13):
316*56530007STamir Duberstein		if (ncpu := os.process_cpu_count()) is not None:
317*56530007STamir Duberstein			return ncpu
318*56530007STamir Duberstein		raise RuntimeError("os.process_cpu_count() returned None")
319*56530007STamir Duberstein	 # See https://github.com/python/cpython/blob/b61fece/Lib/os.py#L1175-L1186.
320*56530007STamir Duberstein	if sys.platform != "darwin":
321ad659ccbSDavid Gow		return len(os.sched_getaffinity(0))
322*56530007STamir Duberstein	if (ncpu := os.cpu_count()) is not None:
323*56530007STamir Duberstein		return ncpu
324*56530007STamir Duberstein	raise RuntimeError("os.cpu_count() returned None")
325ad659ccbSDavid Gow
3261da2e622SDaniel Latypovdef add_common_opts(parser: argparse.ArgumentParser) -> None:
32745ba7a89SDavid Gow	parser.add_argument('--build_dir',
32845ba7a89SDavid Gow			    help='As in the make command, it specifies the build '
32945ba7a89SDavid Gow			    'directory.',
330baa33315SDaniel Latypov			    type=str, default='.kunit', metavar='DIR')
33145ba7a89SDavid Gow	parser.add_argument('--make_options',
33245ba7a89SDavid Gow			    help='X=Y make option, can be repeated.',
333baa33315SDaniel Latypov			    action='append', metavar='X=Y')
33445ba7a89SDavid Gow	parser.add_argument('--alltests',
335980ac3adSDaniel Latypov			    help='Run all KUnit tests via tools/testing/kunit/configs/all_tests.config',
3366ebf5866SFelix Guo			    action='store_true')
337243180f5SDaniel Latypov	parser.add_argument('--kunitconfig',
3389854781dSDaniel Latypov			     help='Path to Kconfig fragment that enables KUnit tests.'
3399854781dSDaniel Latypov			     ' If given a directory, (e.g. lib/kunit), "/.kunitconfig" '
34053b46621SDaniel Latypov			     'will get  automatically appended. If repeated, the files '
34153b46621SDaniel Latypov			     'blindly concatenated, which might not work in all cases.',
34253b46621SDaniel Latypov			     action='append', metavar='PATHS')
3439f57cc76SDaniel Latypov	parser.add_argument('--kconfig_add',
3449f57cc76SDaniel Latypov			     help='Additional Kconfig options to append to the '
3459f57cc76SDaniel Latypov			     '.kunitconfig, e.g. CONFIG_KASAN=y. Can be repeated.',
346baa33315SDaniel Latypov			    action='append', metavar='CONFIG_X=Y')
3476ebf5866SFelix Guo
34887c9c163SBrendan Higgins	parser.add_argument('--arch',
34987c9c163SBrendan Higgins			    help=('Specifies the architecture to run tests under. '
35087c9c163SBrendan Higgins				  'The architecture specified here must match the '
35187c9c163SBrendan Higgins				  'string passed to the ARCH make param, '
35287c9c163SBrendan Higgins				  'e.g. i386, x86_64, arm, um, etc. Non-UML '
35387c9c163SBrendan Higgins				  'architectures run on QEMU.'),
354baa33315SDaniel Latypov			    type=str, default='um', metavar='ARCH')
35587c9c163SBrendan Higgins
35687c9c163SBrendan Higgins	parser.add_argument('--cross_compile',
35787c9c163SBrendan Higgins			    help=('Sets make\'s CROSS_COMPILE variable; it should '
35887c9c163SBrendan Higgins				  'be set to a toolchain path prefix (the prefix '
35987c9c163SBrendan Higgins				  'of gcc and other tools in your toolchain, for '
36087c9c163SBrendan Higgins				  'example `sparc64-linux-gnu-` if you have the '
36187c9c163SBrendan Higgins				  'sparc toolchain installed on your system, or '
36287c9c163SBrendan Higgins				  '`$HOME/toolchains/microblaze/gcc-9.2.0-nolibc/microblaze-linux/bin/microblaze-linux-` '
36387c9c163SBrendan Higgins				  'if you have downloaded the microblaze toolchain '
36487c9c163SBrendan Higgins				  'from the 0-day website to a directory in your '
36587c9c163SBrendan Higgins				  'home directory called `toolchains`).'),
366baa33315SDaniel Latypov			    metavar='PREFIX')
36787c9c163SBrendan Higgins
36887c9c163SBrendan Higgins	parser.add_argument('--qemu_config',
36987c9c163SBrendan Higgins			    help=('Takes a path to a path to a file containing '
37087c9c163SBrendan Higgins				  'a QemuArchParams object.'),
371baa33315SDaniel Latypov			    type=str, metavar='FILE')
37287c9c163SBrendan Higgins
373a9333bd3SDaniel Latypov	parser.add_argument('--qemu_args',
374a9333bd3SDaniel Latypov			    help='Additional QEMU arguments, e.g. "-smp 8"',
375a9333bd3SDaniel Latypov			    action='append', metavar='')
376a9333bd3SDaniel Latypov
3771da2e622SDaniel Latypovdef add_build_opts(parser: argparse.ArgumentParser) -> None:
37845ba7a89SDavid Gow	parser.add_argument('--jobs',
37945ba7a89SDavid Gow			    help='As in the make command, "Specifies  the number of '
38045ba7a89SDavid Gow			    'jobs (commands) to run simultaneously."',
381baa33315SDaniel Latypov			    type=int, default=get_default_jobs(), metavar='N')
38245ba7a89SDavid Gow
3831da2e622SDaniel Latypovdef add_exec_opts(parser: argparse.ArgumentParser) -> None:
38445ba7a89SDavid Gow	parser.add_argument('--timeout',
3856ebf5866SFelix Guo			    help='maximum number of seconds to allow for all tests '
3866ebf5866SFelix Guo			    'to run. This does not include time taken to build the '
3876ebf5866SFelix Guo			    'tests.',
3886ebf5866SFelix Guo			    type=int,
3896ebf5866SFelix Guo			    default=300,
390baa33315SDaniel Latypov			    metavar='SECONDS')
391d992880bSDaniel Latypov	parser.add_argument('filter_glob',
392a127b154SDaniel Latypov			    help='Filter which KUnit test suites/tests run at '
393a127b154SDaniel Latypov			    'boot-time, e.g. list* or list*.*del_test',
394d992880bSDaniel Latypov			    type=str,
395d992880bSDaniel Latypov			    nargs='?',
396d992880bSDaniel Latypov			    default='',
397d992880bSDaniel Latypov			    metavar='filter_glob')
398723c8258SRae Moar	parser.add_argument('--filter',
399723c8258SRae Moar			    help='Filter KUnit tests with attributes, '
400723c8258SRae Moar			    'e.g. module=example or speed>slow',
401723c8258SRae Moar			    type=str,
402723c8258SRae Moar				default='')
403723c8258SRae Moar	parser.add_argument('--filter_action',
404723c8258SRae Moar			    help='If set to skip, filtered tests will be skipped, '
405723c8258SRae Moar				'e.g. --filter_action=skip. Otherwise they will not run.',
406723c8258SRae Moar			    type=str,
407723c8258SRae Moar				choices=['skip'])
4086cb51a18SDaniel Latypov	parser.add_argument('--kernel_args',
4096cb51a18SDaniel Latypov			    help='Kernel command-line parameters. Maybe be repeated',
410baa33315SDaniel Latypov			     action='append', metavar='')
411ff9e09a3SDaniel Latypov	parser.add_argument('--run_isolated', help='If set, boot the kernel for each '
412ff9e09a3SDaniel Latypov			    'individual suite/test. This is can be useful for debugging '
413ff9e09a3SDaniel Latypov			    'a non-hermetic test, one that might pass/fail based on '
414ff9e09a3SDaniel Latypov			    'what ran before it.',
415ff9e09a3SDaniel Latypov			    type=str,
4160453f984SDaniel Latypov			    choices=['suite', 'test'])
417723c8258SRae Moar	parser.add_argument('--list_tests', help='If set, list all tests that will be '
418723c8258SRae Moar			    'run.',
419723c8258SRae Moar			    action='store_true')
420723c8258SRae Moar	parser.add_argument('--list_tests_attr', help='If set, list all tests and test '
421723c8258SRae Moar			    'attributes.',
422723c8258SRae Moar			    action='store_true')
4236ebf5866SFelix Guo
4241da2e622SDaniel Latypovdef add_parse_opts(parser: argparse.ArgumentParser) -> None:
425309e22efSDaniel Latypov	parser.add_argument('--raw_output', help='If set don\'t parse output from kernel. '
426309e22efSDaniel Latypov			    'By default, filters to just KUnit output. Use '
427309e22efSDaniel Latypov			    '--raw_output=all to show everything',
428baa33315SDaniel Latypov			     type=str, nargs='?', const='all', default=None, choices=['all', 'kunit'])
42921a6d178SHeidi Fahim	parser.add_argument('--json',
43021a6d178SHeidi Fahim			    nargs='?',
431309e22efSDaniel Latypov			    help='Prints parsed test results as JSON to stdout or a file if '
432309e22efSDaniel Latypov			    'a filename is specified. Does nothing if --raw_output is set.',
433baa33315SDaniel Latypov			    type=str, const='stdout', default=None, metavar='FILE')
434062a9dd9SDavid Gow	parser.add_argument('--summary',
435062a9dd9SDavid Gow			    help='Prints only the summary line for parsed test results.'
436062a9dd9SDavid Gow				'Does nothing if --raw_output is set.',
437062a9dd9SDavid Gow			    action='store_true')
4383c67a2c0SRae Moar	parser.add_argument('--failed',
4393c67a2c0SRae Moar			    help='Prints only the failed parsed test results and summary line.'
4403c67a2c0SRae Moar				'Does nothing if --raw_output is set.',
4413c67a2c0SRae Moar			    action='store_true')
442021ed9f5SHeidi Fahim
4438a04930fSDaniel Latypov
4448a04930fSDaniel Latypovdef tree_from_args(cli_args: argparse.Namespace) -> kunit_kernel.LinuxSourceTree:
4458a04930fSDaniel Latypov	"""Returns a LinuxSourceTree based on the user's arguments."""
446a9333bd3SDaniel Latypov	# Allow users to specify multiple arguments in one string, e.g. '-smp 8'
447a9333bd3SDaniel Latypov	qemu_args: List[str] = []
448a9333bd3SDaniel Latypov	if cli_args.qemu_args:
449a9333bd3SDaniel Latypov		for arg in cli_args.qemu_args:
450a9333bd3SDaniel Latypov			qemu_args.extend(shlex.split(arg))
451a9333bd3SDaniel Latypov
452980ac3adSDaniel Latypov	kunitconfigs = cli_args.kunitconfig if cli_args.kunitconfig else []
453980ac3adSDaniel Latypov	if cli_args.alltests:
454980ac3adSDaniel Latypov		# Prepend so user-specified options take prio if we ever allow
455980ac3adSDaniel Latypov		# --kunitconfig options to have differing options.
456980ac3adSDaniel Latypov		kunitconfigs = [kunit_kernel.ALL_TESTS_CONFIG_PATH] + kunitconfigs
457980ac3adSDaniel Latypov
4588a04930fSDaniel Latypov	return kunit_kernel.LinuxSourceTree(cli_args.build_dir,
459980ac3adSDaniel Latypov			kunitconfig_paths=kunitconfigs,
4608a04930fSDaniel Latypov			kconfig_add=cli_args.kconfig_add,
4618a04930fSDaniel Latypov			arch=cli_args.arch,
4628a04930fSDaniel Latypov			cross_compile=cli_args.cross_compile,
463a9333bd3SDaniel Latypov			qemu_config_path=cli_args.qemu_config,
464a9333bd3SDaniel Latypov			extra_qemu_args=qemu_args)
4658a04930fSDaniel Latypov
4668a04930fSDaniel Latypov
4671da2e622SDaniel Latypovdef run_handler(cli_args: argparse.Namespace) -> None:
4682dc9d6caSAlexander Pantyukhin	if not os.path.exists(cli_args.build_dir):
4692dc9d6caSAlexander Pantyukhin		os.mkdir(cli_args.build_dir)
4702dc9d6caSAlexander Pantyukhin
4712dc9d6caSAlexander Pantyukhin	linux = tree_from_args(cli_args)
4722dc9d6caSAlexander Pantyukhin	request = KunitRequest(build_dir=cli_args.build_dir,
4732dc9d6caSAlexander Pantyukhin					make_options=cli_args.make_options,
4742dc9d6caSAlexander Pantyukhin					jobs=cli_args.jobs,
4752dc9d6caSAlexander Pantyukhin					raw_output=cli_args.raw_output,
4762dc9d6caSAlexander Pantyukhin					json=cli_args.json,
477062a9dd9SDavid Gow					summary=cli_args.summary,
4783c67a2c0SRae Moar					failed=cli_args.failed,
4792dc9d6caSAlexander Pantyukhin					timeout=cli_args.timeout,
4802dc9d6caSAlexander Pantyukhin					filter_glob=cli_args.filter_glob,
481723c8258SRae Moar					filter=cli_args.filter,
482723c8258SRae Moar					filter_action=cli_args.filter_action,
4832dc9d6caSAlexander Pantyukhin					kernel_args=cli_args.kernel_args,
484723c8258SRae Moar					run_isolated=cli_args.run_isolated,
485723c8258SRae Moar					list_tests=cli_args.list_tests,
486723c8258SRae Moar					list_tests_attr=cli_args.list_tests_attr)
4872dc9d6caSAlexander Pantyukhin	result = run_tests(linux, request)
4882dc9d6caSAlexander Pantyukhin	if result.status != KunitStatus.SUCCESS:
4892dc9d6caSAlexander Pantyukhin		sys.exit(1)
4902dc9d6caSAlexander Pantyukhin
4912dc9d6caSAlexander Pantyukhin
4921da2e622SDaniel Latypovdef config_handler(cli_args: argparse.Namespace) -> None:
4932dc9d6caSAlexander Pantyukhin	if cli_args.build_dir and (
4942dc9d6caSAlexander Pantyukhin			not os.path.exists(cli_args.build_dir)):
4952dc9d6caSAlexander Pantyukhin		os.mkdir(cli_args.build_dir)
4962dc9d6caSAlexander Pantyukhin
4972dc9d6caSAlexander Pantyukhin	linux = tree_from_args(cli_args)
4982dc9d6caSAlexander Pantyukhin	request = KunitConfigRequest(build_dir=cli_args.build_dir,
4992dc9d6caSAlexander Pantyukhin						make_options=cli_args.make_options)
5002dc9d6caSAlexander Pantyukhin	result = config_tests(linux, request)
5012dc9d6caSAlexander Pantyukhin	stdout.print_with_timestamp((
5022dc9d6caSAlexander Pantyukhin		'Elapsed time: %.3fs\n') % (
5032dc9d6caSAlexander Pantyukhin			result.elapsed_time))
5042dc9d6caSAlexander Pantyukhin	if result.status != KunitStatus.SUCCESS:
5052dc9d6caSAlexander Pantyukhin		sys.exit(1)
5062dc9d6caSAlexander Pantyukhin
5072dc9d6caSAlexander Pantyukhin
5081da2e622SDaniel Latypovdef build_handler(cli_args: argparse.Namespace) -> None:
5092dc9d6caSAlexander Pantyukhin	linux = tree_from_args(cli_args)
5102dc9d6caSAlexander Pantyukhin	request = KunitBuildRequest(build_dir=cli_args.build_dir,
5112dc9d6caSAlexander Pantyukhin					make_options=cli_args.make_options,
5122dc9d6caSAlexander Pantyukhin					jobs=cli_args.jobs)
5132dc9d6caSAlexander Pantyukhin	result = config_and_build_tests(linux, request)
5142dc9d6caSAlexander Pantyukhin	stdout.print_with_timestamp((
5152dc9d6caSAlexander Pantyukhin		'Elapsed time: %.3fs\n') % (
5162dc9d6caSAlexander Pantyukhin			result.elapsed_time))
5172dc9d6caSAlexander Pantyukhin	if result.status != KunitStatus.SUCCESS:
5182dc9d6caSAlexander Pantyukhin		sys.exit(1)
5192dc9d6caSAlexander Pantyukhin
5202dc9d6caSAlexander Pantyukhin
5211da2e622SDaniel Latypovdef exec_handler(cli_args: argparse.Namespace) -> None:
5222dc9d6caSAlexander Pantyukhin	linux = tree_from_args(cli_args)
5232dc9d6caSAlexander Pantyukhin	exec_request = KunitExecRequest(raw_output=cli_args.raw_output,
5242dc9d6caSAlexander Pantyukhin					build_dir=cli_args.build_dir,
5252dc9d6caSAlexander Pantyukhin					json=cli_args.json,
526062a9dd9SDavid Gow					summary=cli_args.summary,
5273c67a2c0SRae Moar					failed=cli_args.failed,
5282dc9d6caSAlexander Pantyukhin					timeout=cli_args.timeout,
5292dc9d6caSAlexander Pantyukhin					filter_glob=cli_args.filter_glob,
530723c8258SRae Moar					filter=cli_args.filter,
531723c8258SRae Moar					filter_action=cli_args.filter_action,
5322dc9d6caSAlexander Pantyukhin					kernel_args=cli_args.kernel_args,
533723c8258SRae Moar					run_isolated=cli_args.run_isolated,
534723c8258SRae Moar					list_tests=cli_args.list_tests,
535723c8258SRae Moar					list_tests_attr=cli_args.list_tests_attr)
5362dc9d6caSAlexander Pantyukhin	result = exec_tests(linux, exec_request)
5372dc9d6caSAlexander Pantyukhin	stdout.print_with_timestamp((
5382dc9d6caSAlexander Pantyukhin		'Elapsed time: %.3fs\n') % (result.elapsed_time))
5392dc9d6caSAlexander Pantyukhin	if result.status != KunitStatus.SUCCESS:
5402dc9d6caSAlexander Pantyukhin		sys.exit(1)
5412dc9d6caSAlexander Pantyukhin
5422dc9d6caSAlexander Pantyukhin
5431da2e622SDaniel Latypovdef parse_handler(cli_args: argparse.Namespace) -> None:
5442dc9d6caSAlexander Pantyukhin	if cli_args.file is None:
5451da2e622SDaniel Latypov		sys.stdin.reconfigure(errors='backslashreplace')  # type: ignore
5461da2e622SDaniel Latypov		kunit_output = sys.stdin  # type: Iterable[str]
5472dc9d6caSAlexander Pantyukhin	else:
5482dc9d6caSAlexander Pantyukhin		with open(cli_args.file, 'r', errors='backslashreplace') as f:
5492dc9d6caSAlexander Pantyukhin			kunit_output = f.read().splitlines()
5502dc9d6caSAlexander Pantyukhin	# We know nothing about how the result was created!
5512dc9d6caSAlexander Pantyukhin	metadata = kunit_json.Metadata()
5522dc9d6caSAlexander Pantyukhin	request = KunitParseRequest(raw_output=cli_args.raw_output,
5533c67a2c0SRae Moar					json=cli_args.json, summary=cli_args.summary,
5543c67a2c0SRae Moar					failed=cli_args.failed)
5552dc9d6caSAlexander Pantyukhin	result, _ = parse_tests(request, metadata, kunit_output)
5562dc9d6caSAlexander Pantyukhin	if result.status != KunitStatus.SUCCESS:
5572dc9d6caSAlexander Pantyukhin		sys.exit(1)
5582dc9d6caSAlexander Pantyukhin
5592dc9d6caSAlexander Pantyukhin
5602dc9d6caSAlexander Pantyukhinsubcommand_handlers_map = {
5612dc9d6caSAlexander Pantyukhin	'run': run_handler,
5622dc9d6caSAlexander Pantyukhin	'config': config_handler,
5632dc9d6caSAlexander Pantyukhin	'build': build_handler,
5642dc9d6caSAlexander Pantyukhin	'exec': exec_handler,
5652dc9d6caSAlexander Pantyukhin	'parse': parse_handler
5662dc9d6caSAlexander Pantyukhin}
5672dc9d6caSAlexander Pantyukhin
5682dc9d6caSAlexander Pantyukhin
5691da2e622SDaniel Latypovdef main(argv: Sequence[str]) -> None:
57045ba7a89SDavid Gow	parser = argparse.ArgumentParser(
57145ba7a89SDavid Gow			description='Helps writing and running KUnit tests.')
57245ba7a89SDavid Gow	subparser = parser.add_subparsers(dest='subcommand')
57345ba7a89SDavid Gow
57445ba7a89SDavid Gow	# The 'run' command will config, build, exec, and parse in one go.
57545ba7a89SDavid Gow	run_parser = subparser.add_parser('run', help='Runs KUnit tests.')
57645ba7a89SDavid Gow	add_common_opts(run_parser)
57745ba7a89SDavid Gow	add_build_opts(run_parser)
57845ba7a89SDavid Gow	add_exec_opts(run_parser)
57945ba7a89SDavid Gow	add_parse_opts(run_parser)
58045ba7a89SDavid Gow
58145ba7a89SDavid Gow	config_parser = subparser.add_parser('config',
58245ba7a89SDavid Gow						help='Ensures that .config contains all of '
58345ba7a89SDavid Gow						'the options in .kunitconfig')
58445ba7a89SDavid Gow	add_common_opts(config_parser)
58545ba7a89SDavid Gow
58645ba7a89SDavid Gow	build_parser = subparser.add_parser('build', help='Builds a kernel with KUnit tests')
58745ba7a89SDavid Gow	add_common_opts(build_parser)
58845ba7a89SDavid Gow	add_build_opts(build_parser)
58945ba7a89SDavid Gow
59045ba7a89SDavid Gow	exec_parser = subparser.add_parser('exec', help='Run a kernel with KUnit tests')
59145ba7a89SDavid Gow	add_common_opts(exec_parser)
59245ba7a89SDavid Gow	add_exec_opts(exec_parser)
59345ba7a89SDavid Gow	add_parse_opts(exec_parser)
59445ba7a89SDavid Gow
59545ba7a89SDavid Gow	# The 'parse' option is special, as it doesn't need the kernel source
59645ba7a89SDavid Gow	# (therefore there is no need for a build_dir, hence no add_common_opts)
59745ba7a89SDavid Gow	# and the '--file' argument is not relevant to 'run', so isn't in
59845ba7a89SDavid Gow	# add_parse_opts()
59945ba7a89SDavid Gow	parse_parser = subparser.add_parser('parse',
60045ba7a89SDavid Gow					    help='Parses KUnit results from a file, '
60145ba7a89SDavid Gow					    'and parses formatted results.')
60245ba7a89SDavid Gow	add_parse_opts(parse_parser)
60345ba7a89SDavid Gow	parse_parser.add_argument('file',
60445ba7a89SDavid Gow				  help='Specifies the file to read results from.',
60545ba7a89SDavid Gow				  type=str, nargs='?', metavar='input_file')
6060476e69fSGreg Thelen
607d8c23eadSDaniel Latypov	cli_args = parser.parse_args(massage_argv(argv))
6086ebf5866SFelix Guo
6095578d008SBrendan Higgins	if get_kernel_root_path():
6105578d008SBrendan Higgins		os.chdir(get_kernel_root_path())
6115578d008SBrendan Higgins
6122dc9d6caSAlexander Pantyukhin	subcomand_handler = subcommand_handlers_map.get(cli_args.subcommand, None)
61382206a0cSBrendan Higgins
6142dc9d6caSAlexander Pantyukhin	if subcomand_handler is None:
6156ebf5866SFelix Guo		parser.print_help()
6162dc9d6caSAlexander Pantyukhin		return
6172dc9d6caSAlexander Pantyukhin
6182dc9d6caSAlexander Pantyukhin	subcomand_handler(cli_args)
6192dc9d6caSAlexander Pantyukhin
6206ebf5866SFelix Guo
6216ebf5866SFelix Guoif __name__ == '__main__':
622ff7b437fSBrendan Higgins	main(sys.argv[1:])
623