xref: /linux/tools/testing/kunit/kunit.py (revision 980ac3ad051215150b637e26f3f46873687909a9)
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 <felixguoxiuping@gmail.com>
86ebf5866SFelix Guo# Author: Brendan Higgins <brendanhiggins@google.com>
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
26e756dbebSDaniel Latypovfrom kunit_printer import stdout
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]
52db167981SDaniel Latypov
53db167981SDaniel Latypov@dataclass
54db167981SDaniel Latypovclass KunitExecRequest(KunitParseRequest):
55ee96d25fSDaniel Latypov	build_dir: str
56db167981SDaniel Latypov	timeout: int
57db167981SDaniel Latypov	filter_glob: str
58db167981SDaniel Latypov	kernel_args: Optional[List[str]]
59db167981SDaniel Latypov	run_isolated: Optional[str]
60db167981SDaniel Latypov
61db167981SDaniel Latypov@dataclass
62db167981SDaniel Latypovclass KunitRequest(KunitExecRequest, KunitBuildRequest):
63db167981SDaniel Latypov	pass
64db167981SDaniel Latypov
65db167981SDaniel Latypov
6609641f7cSDaniel Latypovdef get_kernel_root_path() -> str:
6709641f7cSDaniel Latypov	path = sys.argv[0] if not __file__ else __file__
6809641f7cSDaniel Latypov	parts = os.path.realpath(path).split('tools/testing/kunit')
69be886ba9SHeidi Fahim	if len(parts) != 2:
70be886ba9SHeidi Fahim		sys.exit(1)
71be886ba9SHeidi Fahim	return parts[0]
72be886ba9SHeidi Fahim
7345ba7a89SDavid Gowdef config_tests(linux: kunit_kernel.LinuxSourceTree,
7445ba7a89SDavid Gow		 request: KunitConfigRequest) -> KunitResult:
75e756dbebSDaniel Latypov	stdout.print_with_timestamp('Configuring KUnit Kernel ...')
7645ba7a89SDavid Gow
776ebf5866SFelix Guo	config_start = time.time()
780476e69fSGreg Thelen	success = linux.build_reconfig(request.build_dir, request.make_options)
796ebf5866SFelix Guo	config_end = time.time()
806ebf5866SFelix Guo	if not success:
8145ba7a89SDavid Gow		return KunitResult(KunitStatus.CONFIG_FAILURE,
8245ba7a89SDavid Gow				   config_end - config_start)
8345ba7a89SDavid Gow	return KunitResult(KunitStatus.SUCCESS,
8445ba7a89SDavid Gow			   config_end - config_start)
856ebf5866SFelix Guo
8645ba7a89SDavid Gowdef build_tests(linux: kunit_kernel.LinuxSourceTree,
8745ba7a89SDavid Gow		request: KunitBuildRequest) -> KunitResult:
88e756dbebSDaniel Latypov	stdout.print_with_timestamp('Building KUnit Kernel ...')
896ebf5866SFelix Guo
906ebf5866SFelix Guo	build_start = time.time()
91*980ac3adSDaniel Latypov	success = linux.build_kernel(request.jobs,
920476e69fSGreg Thelen				     request.build_dir,
930476e69fSGreg Thelen				     request.make_options)
946ebf5866SFelix Guo	build_end = time.time()
956ebf5866SFelix Guo	if not success:
96ee61492aSDavid Gow		return KunitResult(KunitStatus.BUILD_FAILURE,
97ee61492aSDavid Gow				   build_end - build_start)
9845ba7a89SDavid Gow	if not success:
9945ba7a89SDavid Gow		return KunitResult(KunitStatus.BUILD_FAILURE,
10045ba7a89SDavid Gow				   build_end - build_start)
10145ba7a89SDavid Gow	return KunitResult(KunitStatus.SUCCESS,
10245ba7a89SDavid Gow			   build_end - build_start)
1036ebf5866SFelix Guo
1041ee2ba89SDaniel Latypovdef config_and_build_tests(linux: kunit_kernel.LinuxSourceTree,
1051ee2ba89SDaniel Latypov			   request: KunitBuildRequest) -> KunitResult:
1061ee2ba89SDaniel Latypov	config_result = config_tests(linux, request)
1071ee2ba89SDaniel Latypov	if config_result.status != KunitStatus.SUCCESS:
1081ee2ba89SDaniel Latypov		return config_result
1091ee2ba89SDaniel Latypov
1101ee2ba89SDaniel Latypov	return build_tests(linux, request)
1111ee2ba89SDaniel Latypov
112ff9e09a3SDaniel Latypovdef _list_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> List[str]:
113ff9e09a3SDaniel Latypov	args = ['kunit.action=list']
114ff9e09a3SDaniel Latypov	if request.kernel_args:
115ff9e09a3SDaniel Latypov		args.extend(request.kernel_args)
116ff9e09a3SDaniel Latypov
117ff9e09a3SDaniel Latypov	output = linux.run_kernel(args=args,
118*980ac3adSDaniel Latypov			   timeout=request.timeout,
119ff9e09a3SDaniel Latypov			   filter_glob=request.filter_glob,
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.
1260453f984SDaniel Latypov	return [l for l in lines if re.match(r'^[^\s.]+\.[^\s.]+$', l)]
127ff9e09a3SDaniel Latypov
128ff9e09a3SDaniel Latypovdef _suites_from_test_list(tests: List[str]) -> List[str]:
129ff9e09a3SDaniel Latypov	"""Extracts all the suites from an ordered list of tests."""
130ff9e09a3SDaniel Latypov	suites = []  # type: List[str]
131ff9e09a3SDaniel Latypov	for t in tests:
132ff9e09a3SDaniel Latypov		parts = t.split('.', maxsplit=2)
133ff9e09a3SDaniel Latypov		if len(parts) != 2:
134ff9e09a3SDaniel Latypov			raise ValueError(f'internal KUnit error, test name should be of the form "<suite>.<test>", got "{t}"')
135ff9e09a3SDaniel Latypov		suite, case = parts
136ff9e09a3SDaniel Latypov		if not suites or suites[-1] != suite:
137ff9e09a3SDaniel Latypov			suites.append(suite)
138ff9e09a3SDaniel Latypov	return suites
139ff9e09a3SDaniel Latypov
140ff9e09a3SDaniel Latypov
141ff9e09a3SDaniel Latypov
142db167981SDaniel Latypovdef exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> KunitResult:
143ff9e09a3SDaniel Latypov	filter_globs = [request.filter_glob]
144ff9e09a3SDaniel Latypov	if request.run_isolated:
145ff9e09a3SDaniel Latypov		tests = _list_tests(linux, request)
146ff9e09a3SDaniel Latypov		if request.run_isolated == 'test':
147ff9e09a3SDaniel Latypov			filter_globs = tests
148ff9e09a3SDaniel Latypov		if request.run_isolated == 'suite':
149ff9e09a3SDaniel Latypov			filter_globs = _suites_from_test_list(tests)
150ff9e09a3SDaniel Latypov			# Apply the test-part of the user's glob, if present.
151ff9e09a3SDaniel Latypov			if '.' in request.filter_glob:
152ff9e09a3SDaniel Latypov				test_glob = request.filter_glob.split('.', maxsplit=2)[1]
153ff9e09a3SDaniel Latypov				filter_globs = [g + '.'+ test_glob for g in filter_globs]
154ff9e09a3SDaniel Latypov
155885210d3SDaniel Latypov	metadata = kunit_json.Metadata(arch=linux.arch(), build_dir=request.build_dir, def_config='kunit_defconfig')
156ee96d25fSDaniel Latypov
157d65d07cbSRae Moar	test_counts = kunit_parser.TestCounts()
158ff9e09a3SDaniel Latypov	exec_time = 0.0
159ff9e09a3SDaniel Latypov	for i, filter_glob in enumerate(filter_globs):
160e756dbebSDaniel Latypov		stdout.print_with_timestamp('Starting KUnit Kernel ({}/{})...'.format(i+1, len(filter_globs)))
161ff9e09a3SDaniel Latypov
1626ebf5866SFelix Guo		test_start = time.time()
1637ef925eaSDaniel Latypov		run_result = linux.run_kernel(
1646cb51a18SDaniel Latypov			args=request.kernel_args,
165*980ac3adSDaniel Latypov			timeout=request.timeout,
166ff9e09a3SDaniel Latypov			filter_glob=filter_glob,
1676ec1b81dSSeongJae Park			build_dir=request.build_dir)
16845ba7a89SDavid Gow
169ee96d25fSDaniel Latypov		_, test_result = parse_tests(request, metadata, run_result)
1705f6aa6d8SDaniel Latypov		# run_kernel() doesn't block on the kernel exiting.
1715f6aa6d8SDaniel Latypov		# That only happens after we get the last line of output from `run_result`.
1725f6aa6d8SDaniel Latypov		# So exec_time here actually contains parsing + execution time, which is fine.
1736ebf5866SFelix Guo		test_end = time.time()
174ff9e09a3SDaniel Latypov		exec_time += test_end - test_start
175ff9e09a3SDaniel Latypov
17695dcbc55SDaniel Latypov		test_counts.add_subtest_counts(test_result.counts)
1776ebf5866SFelix Guo
1787fa7ffcfSDaniel Latypov	if len(filter_globs) == 1 and test_counts.crashed > 0:
1797fa7ffcfSDaniel Latypov		bd = request.build_dir
1807fa7ffcfSDaniel Latypov		print('The kernel seems to have crashed; you can decode the stack traces with:')
1817fa7ffcfSDaniel Latypov		print('$ scripts/decode_stacktrace.sh {}/vmlinux {} < {} | tee {}/decoded.log | {} parse'.format(
1827fa7ffcfSDaniel Latypov				bd, bd, kunit_kernel.get_outfile_path(bd), bd, sys.argv[0]))
1837fa7ffcfSDaniel Latypov
184d65d07cbSRae Moar	kunit_status = _map_to_overall_status(test_counts.get_status())
18595dcbc55SDaniel Latypov	return KunitResult(status=kunit_status, elapsed_time=exec_time)
186d65d07cbSRae Moar
187d65d07cbSRae Moardef _map_to_overall_status(test_status: kunit_parser.TestStatus) -> KunitStatus:
188d65d07cbSRae Moar	if test_status in (kunit_parser.TestStatus.SUCCESS, kunit_parser.TestStatus.SKIPPED):
189d65d07cbSRae Moar		return KunitStatus.SUCCESS
190d65d07cbSRae Moar	return KunitStatus.TEST_FAILURE
1917ef925eaSDaniel Latypov
192ee96d25fSDaniel Latypovdef parse_tests(request: KunitParseRequest, metadata: kunit_json.Metadata, input_data: Iterable[str]) -> Tuple[KunitResult, kunit_parser.Test]:
19345ba7a89SDavid Gow	parse_start = time.time()
19445ba7a89SDavid Gow
195e0cc8c05SDaniel Latypov	test_result = kunit_parser.Test()
19621a6d178SHeidi Fahim
19745ba7a89SDavid Gow	if request.raw_output:
198d65d07cbSRae Moar		# Treat unparsed results as one passing test.
199e0cc8c05SDaniel Latypov		test_result.status = kunit_parser.TestStatus.SUCCESS
200e0cc8c05SDaniel Latypov		test_result.counts.passed = 1
201d65d07cbSRae Moar
2027ef925eaSDaniel Latypov		output: Iterable[str] = input_data
2036a499c9cSDaniel Latypov		if request.raw_output == 'all':
2046a499c9cSDaniel Latypov			pass
2056a499c9cSDaniel Latypov		elif request.raw_output == 'kunit':
206a15cfa39SDaniel Latypov			output = kunit_parser.extract_tap_lines(output, lstrip=False)
2076a499c9cSDaniel Latypov		for line in output:
2086a499c9cSDaniel Latypov			print(line.rstrip())
2096a499c9cSDaniel Latypov
21045ba7a89SDavid Gow	else:
2117ef925eaSDaniel Latypov		test_result = kunit_parser.parse_run_tests(input_data)
21245ba7a89SDavid Gow	parse_end = time.time()
21345ba7a89SDavid Gow
21421a6d178SHeidi Fahim	if request.json:
21500f75043SDaniel Latypov		json_str = kunit_json.get_json_result(
216e0cc8c05SDaniel Latypov					test=test_result,
217ee96d25fSDaniel Latypov					metadata=metadata)
21821a6d178SHeidi Fahim		if request.json == 'stdout':
21900f75043SDaniel Latypov			print(json_str)
22000f75043SDaniel Latypov		else:
22100f75043SDaniel Latypov			with open(request.json, 'w') as f:
22200f75043SDaniel Latypov				f.write(json_str)
223e756dbebSDaniel Latypov			stdout.print_with_timestamp("Test results stored in %s" %
22400f75043SDaniel Latypov				os.path.abspath(request.json))
22521a6d178SHeidi Fahim
22645ba7a89SDavid Gow	if test_result.status != kunit_parser.TestStatus.SUCCESS:
22795dcbc55SDaniel Latypov		return KunitResult(KunitStatus.TEST_FAILURE, parse_end - parse_start), test_result
22845ba7a89SDavid Gow
22995dcbc55SDaniel Latypov	return KunitResult(KunitStatus.SUCCESS, parse_end - parse_start), test_result
23045ba7a89SDavid Gow
23145ba7a89SDavid Gowdef run_tests(linux: kunit_kernel.LinuxSourceTree,
23245ba7a89SDavid Gow	      request: KunitRequest) -> KunitResult:
23345ba7a89SDavid Gow	run_start = time.time()
23445ba7a89SDavid Gow
235db167981SDaniel Latypov	config_result = config_tests(linux, request)
23645ba7a89SDavid Gow	if config_result.status != KunitStatus.SUCCESS:
23745ba7a89SDavid Gow		return config_result
23845ba7a89SDavid Gow
239db167981SDaniel Latypov	build_result = build_tests(linux, request)
24045ba7a89SDavid Gow	if build_result.status != KunitStatus.SUCCESS:
24145ba7a89SDavid Gow		return build_result
24245ba7a89SDavid Gow
243db167981SDaniel Latypov	exec_result = exec_tests(linux, request)
24445ba7a89SDavid Gow
24545ba7a89SDavid Gow	run_end = time.time()
24645ba7a89SDavid Gow
247e756dbebSDaniel Latypov	stdout.print_with_timestamp((
2486ebf5866SFelix Guo		'Elapsed time: %.3fs total, %.3fs configuring, %.3fs ' +
2496ebf5866SFelix Guo		'building, %.3fs running\n') % (
25045ba7a89SDavid Gow				run_end - run_start,
25145ba7a89SDavid Gow				config_result.elapsed_time,
25245ba7a89SDavid Gow				build_result.elapsed_time,
25345ba7a89SDavid Gow				exec_result.elapsed_time))
2547ef925eaSDaniel Latypov	return exec_result
2556ebf5866SFelix Guo
256d8c23eadSDaniel Latypov# Problem:
257d8c23eadSDaniel Latypov# $ kunit.py run --json
258d8c23eadSDaniel Latypov# works as one would expect and prints the parsed test results as JSON.
259d8c23eadSDaniel Latypov# $ kunit.py run --json suite_name
260d8c23eadSDaniel Latypov# would *not* pass suite_name as the filter_glob and print as json.
261d8c23eadSDaniel Latypov# argparse will consider it to be another way of writing
262d8c23eadSDaniel Latypov# $ kunit.py run --json=suite_name
263d8c23eadSDaniel Latypov# i.e. it would run all tests, and dump the json to a `suite_name` file.
264d8c23eadSDaniel Latypov# So we hackily automatically rewrite --json => --json=stdout
265d8c23eadSDaniel Latypovpseudo_bool_flag_defaults = {
266d8c23eadSDaniel Latypov		'--json': 'stdout',
267d8c23eadSDaniel Latypov		'--raw_output': 'kunit',
268d8c23eadSDaniel Latypov}
269d8c23eadSDaniel Latypovdef massage_argv(argv: Sequence[str]) -> Sequence[str]:
270d8c23eadSDaniel Latypov	def massage_arg(arg: str) -> str:
271d8c23eadSDaniel Latypov		if arg not in pseudo_bool_flag_defaults:
272d8c23eadSDaniel Latypov			return arg
273d8c23eadSDaniel Latypov		return  f'{arg}={pseudo_bool_flag_defaults[arg]}'
274d8c23eadSDaniel Latypov	return list(map(massage_arg, argv))
275d8c23eadSDaniel Latypov
276ad659ccbSDavid Gowdef get_default_jobs() -> int:
277ad659ccbSDavid Gow	return len(os.sched_getaffinity(0))
278ad659ccbSDavid Gow
27909641f7cSDaniel Latypovdef add_common_opts(parser) -> None:
28045ba7a89SDavid Gow	parser.add_argument('--build_dir',
28145ba7a89SDavid Gow			    help='As in the make command, it specifies the build '
28245ba7a89SDavid Gow			    'directory.',
283baa33315SDaniel Latypov			    type=str, default='.kunit', metavar='DIR')
28445ba7a89SDavid Gow	parser.add_argument('--make_options',
28545ba7a89SDavid Gow			    help='X=Y make option, can be repeated.',
286baa33315SDaniel Latypov			    action='append', metavar='X=Y')
28745ba7a89SDavid Gow	parser.add_argument('--alltests',
288*980ac3adSDaniel Latypov			    help='Run all KUnit tests via tools/testing/kunit/configs/all_tests.config',
2896ebf5866SFelix Guo			    action='store_true')
290243180f5SDaniel Latypov	parser.add_argument('--kunitconfig',
2919854781dSDaniel Latypov			     help='Path to Kconfig fragment that enables KUnit tests.'
2929854781dSDaniel Latypov			     ' If given a directory, (e.g. lib/kunit), "/.kunitconfig" '
29353b46621SDaniel Latypov			     'will get  automatically appended. If repeated, the files '
29453b46621SDaniel Latypov			     'blindly concatenated, which might not work in all cases.',
29553b46621SDaniel Latypov			     action='append', metavar='PATHS')
2969f57cc76SDaniel Latypov	parser.add_argument('--kconfig_add',
2979f57cc76SDaniel Latypov			     help='Additional Kconfig options to append to the '
2989f57cc76SDaniel Latypov			     '.kunitconfig, e.g. CONFIG_KASAN=y. Can be repeated.',
299baa33315SDaniel Latypov			    action='append', metavar='CONFIG_X=Y')
3006ebf5866SFelix Guo
30187c9c163SBrendan Higgins	parser.add_argument('--arch',
30287c9c163SBrendan Higgins			    help=('Specifies the architecture to run tests under. '
30387c9c163SBrendan Higgins				  'The architecture specified here must match the '
30487c9c163SBrendan Higgins				  'string passed to the ARCH make param, '
30587c9c163SBrendan Higgins				  'e.g. i386, x86_64, arm, um, etc. Non-UML '
30687c9c163SBrendan Higgins				  'architectures run on QEMU.'),
307baa33315SDaniel Latypov			    type=str, default='um', metavar='ARCH')
30887c9c163SBrendan Higgins
30987c9c163SBrendan Higgins	parser.add_argument('--cross_compile',
31087c9c163SBrendan Higgins			    help=('Sets make\'s CROSS_COMPILE variable; it should '
31187c9c163SBrendan Higgins				  'be set to a toolchain path prefix (the prefix '
31287c9c163SBrendan Higgins				  'of gcc and other tools in your toolchain, for '
31387c9c163SBrendan Higgins				  'example `sparc64-linux-gnu-` if you have the '
31487c9c163SBrendan Higgins				  'sparc toolchain installed on your system, or '
31587c9c163SBrendan Higgins				  '`$HOME/toolchains/microblaze/gcc-9.2.0-nolibc/microblaze-linux/bin/microblaze-linux-` '
31687c9c163SBrendan Higgins				  'if you have downloaded the microblaze toolchain '
31787c9c163SBrendan Higgins				  'from the 0-day website to a directory in your '
31887c9c163SBrendan Higgins				  'home directory called `toolchains`).'),
319baa33315SDaniel Latypov			    metavar='PREFIX')
32087c9c163SBrendan Higgins
32187c9c163SBrendan Higgins	parser.add_argument('--qemu_config',
32287c9c163SBrendan Higgins			    help=('Takes a path to a path to a file containing '
32387c9c163SBrendan Higgins				  'a QemuArchParams object.'),
324baa33315SDaniel Latypov			    type=str, metavar='FILE')
32587c9c163SBrendan Higgins
326a9333bd3SDaniel Latypov	parser.add_argument('--qemu_args',
327a9333bd3SDaniel Latypov			    help='Additional QEMU arguments, e.g. "-smp 8"',
328a9333bd3SDaniel Latypov			    action='append', metavar='')
329a9333bd3SDaniel Latypov
33009641f7cSDaniel Latypovdef add_build_opts(parser) -> None:
33145ba7a89SDavid Gow	parser.add_argument('--jobs',
33245ba7a89SDavid Gow			    help='As in the make command, "Specifies  the number of '
33345ba7a89SDavid Gow			    'jobs (commands) to run simultaneously."',
334baa33315SDaniel Latypov			    type=int, default=get_default_jobs(), metavar='N')
33545ba7a89SDavid Gow
33609641f7cSDaniel Latypovdef add_exec_opts(parser) -> None:
33745ba7a89SDavid Gow	parser.add_argument('--timeout',
3386ebf5866SFelix Guo			    help='maximum number of seconds to allow for all tests '
3396ebf5866SFelix Guo			    'to run. This does not include time taken to build the '
3406ebf5866SFelix Guo			    'tests.',
3416ebf5866SFelix Guo			    type=int,
3426ebf5866SFelix Guo			    default=300,
343baa33315SDaniel Latypov			    metavar='SECONDS')
344d992880bSDaniel Latypov	parser.add_argument('filter_glob',
345a127b154SDaniel Latypov			    help='Filter which KUnit test suites/tests run at '
346a127b154SDaniel Latypov			    'boot-time, e.g. list* or list*.*del_test',
347d992880bSDaniel Latypov			    type=str,
348d992880bSDaniel Latypov			    nargs='?',
349d992880bSDaniel Latypov			    default='',
350d992880bSDaniel Latypov			    metavar='filter_glob')
3516cb51a18SDaniel Latypov	parser.add_argument('--kernel_args',
3526cb51a18SDaniel Latypov			    help='Kernel command-line parameters. Maybe be repeated',
353baa33315SDaniel Latypov			     action='append', metavar='')
354ff9e09a3SDaniel Latypov	parser.add_argument('--run_isolated', help='If set, boot the kernel for each '
355ff9e09a3SDaniel Latypov			    'individual suite/test. This is can be useful for debugging '
356ff9e09a3SDaniel Latypov			    'a non-hermetic test, one that might pass/fail based on '
357ff9e09a3SDaniel Latypov			    'what ran before it.',
358ff9e09a3SDaniel Latypov			    type=str,
3590453f984SDaniel Latypov			    choices=['suite', 'test'])
3606ebf5866SFelix Guo
36109641f7cSDaniel Latypovdef add_parse_opts(parser) -> None:
3626a499c9cSDaniel Latypov	parser.add_argument('--raw_output', help='If set don\'t format output from kernel. '
3636a499c9cSDaniel Latypov			    'If set to --raw_output=kunit, filters to just KUnit output.',
364baa33315SDaniel Latypov			     type=str, nargs='?', const='all', default=None, choices=['all', 'kunit'])
36521a6d178SHeidi Fahim	parser.add_argument('--json',
36621a6d178SHeidi Fahim			    nargs='?',
36721a6d178SHeidi Fahim			    help='Stores test results in a JSON, and either '
36821a6d178SHeidi Fahim			    'prints to stdout or saves to file if a '
36921a6d178SHeidi Fahim			    'filename is specified',
370baa33315SDaniel Latypov			    type=str, const='stdout', default=None, metavar='FILE')
371021ed9f5SHeidi Fahim
3728a04930fSDaniel Latypov
3738a04930fSDaniel Latypovdef tree_from_args(cli_args: argparse.Namespace) -> kunit_kernel.LinuxSourceTree:
3748a04930fSDaniel Latypov	"""Returns a LinuxSourceTree based on the user's arguments."""
375a9333bd3SDaniel Latypov	# Allow users to specify multiple arguments in one string, e.g. '-smp 8'
376a9333bd3SDaniel Latypov	qemu_args: List[str] = []
377a9333bd3SDaniel Latypov	if cli_args.qemu_args:
378a9333bd3SDaniel Latypov		for arg in cli_args.qemu_args:
379a9333bd3SDaniel Latypov			qemu_args.extend(shlex.split(arg))
380a9333bd3SDaniel Latypov
381*980ac3adSDaniel Latypov	kunitconfigs = cli_args.kunitconfig if cli_args.kunitconfig else []
382*980ac3adSDaniel Latypov	if cli_args.alltests:
383*980ac3adSDaniel Latypov		# Prepend so user-specified options take prio if we ever allow
384*980ac3adSDaniel Latypov		# --kunitconfig options to have differing options.
385*980ac3adSDaniel Latypov		kunitconfigs = [kunit_kernel.ALL_TESTS_CONFIG_PATH] + kunitconfigs
386*980ac3adSDaniel Latypov
3878a04930fSDaniel Latypov	return kunit_kernel.LinuxSourceTree(cli_args.build_dir,
388*980ac3adSDaniel Latypov			kunitconfig_paths=kunitconfigs,
3898a04930fSDaniel Latypov			kconfig_add=cli_args.kconfig_add,
3908a04930fSDaniel Latypov			arch=cli_args.arch,
3918a04930fSDaniel Latypov			cross_compile=cli_args.cross_compile,
392a9333bd3SDaniel Latypov			qemu_config_path=cli_args.qemu_config,
393a9333bd3SDaniel Latypov			extra_qemu_args=qemu_args)
3948a04930fSDaniel Latypov
3958a04930fSDaniel Latypov
3968a04930fSDaniel Latypovdef main(argv):
39745ba7a89SDavid Gow	parser = argparse.ArgumentParser(
39845ba7a89SDavid Gow			description='Helps writing and running KUnit tests.')
39945ba7a89SDavid Gow	subparser = parser.add_subparsers(dest='subcommand')
40045ba7a89SDavid Gow
40145ba7a89SDavid Gow	# The 'run' command will config, build, exec, and parse in one go.
40245ba7a89SDavid Gow	run_parser = subparser.add_parser('run', help='Runs KUnit tests.')
40345ba7a89SDavid Gow	add_common_opts(run_parser)
40445ba7a89SDavid Gow	add_build_opts(run_parser)
40545ba7a89SDavid Gow	add_exec_opts(run_parser)
40645ba7a89SDavid Gow	add_parse_opts(run_parser)
40745ba7a89SDavid Gow
40845ba7a89SDavid Gow	config_parser = subparser.add_parser('config',
40945ba7a89SDavid Gow						help='Ensures that .config contains all of '
41045ba7a89SDavid Gow						'the options in .kunitconfig')
41145ba7a89SDavid Gow	add_common_opts(config_parser)
41245ba7a89SDavid Gow
41345ba7a89SDavid Gow	build_parser = subparser.add_parser('build', help='Builds a kernel with KUnit tests')
41445ba7a89SDavid Gow	add_common_opts(build_parser)
41545ba7a89SDavid Gow	add_build_opts(build_parser)
41645ba7a89SDavid Gow
41745ba7a89SDavid Gow	exec_parser = subparser.add_parser('exec', help='Run a kernel with KUnit tests')
41845ba7a89SDavid Gow	add_common_opts(exec_parser)
41945ba7a89SDavid Gow	add_exec_opts(exec_parser)
42045ba7a89SDavid Gow	add_parse_opts(exec_parser)
42145ba7a89SDavid Gow
42245ba7a89SDavid Gow	# The 'parse' option is special, as it doesn't need the kernel source
42345ba7a89SDavid Gow	# (therefore there is no need for a build_dir, hence no add_common_opts)
42445ba7a89SDavid Gow	# and the '--file' argument is not relevant to 'run', so isn't in
42545ba7a89SDavid Gow	# add_parse_opts()
42645ba7a89SDavid Gow	parse_parser = subparser.add_parser('parse',
42745ba7a89SDavid Gow					    help='Parses KUnit results from a file, '
42845ba7a89SDavid Gow					    'and parses formatted results.')
42945ba7a89SDavid Gow	add_parse_opts(parse_parser)
43045ba7a89SDavid Gow	parse_parser.add_argument('file',
43145ba7a89SDavid Gow				  help='Specifies the file to read results from.',
43245ba7a89SDavid Gow				  type=str, nargs='?', metavar='input_file')
4330476e69fSGreg Thelen
434d8c23eadSDaniel Latypov	cli_args = parser.parse_args(massage_argv(argv))
4356ebf5866SFelix Guo
4365578d008SBrendan Higgins	if get_kernel_root_path():
4375578d008SBrendan Higgins		os.chdir(get_kernel_root_path())
4385578d008SBrendan Higgins
4396ebf5866SFelix Guo	if cli_args.subcommand == 'run':
440e3212513SSeongJae Park		if not os.path.exists(cli_args.build_dir):
441e3212513SSeongJae Park			os.mkdir(cli_args.build_dir)
44282206a0cSBrendan Higgins
4438a04930fSDaniel Latypov		linux = tree_from_args(cli_args)
444db167981SDaniel Latypov		request = KunitRequest(build_dir=cli_args.build_dir,
445db167981SDaniel Latypov				       make_options=cli_args.make_options,
446db167981SDaniel Latypov				       jobs=cli_args.jobs,
447db167981SDaniel Latypov				       raw_output=cli_args.raw_output,
448db167981SDaniel Latypov				       json=cli_args.json,
449db167981SDaniel Latypov				       timeout=cli_args.timeout,
450db167981SDaniel Latypov				       filter_glob=cli_args.filter_glob,
451db167981SDaniel Latypov				       kernel_args=cli_args.kernel_args,
452db167981SDaniel Latypov				       run_isolated=cli_args.run_isolated)
4536ebf5866SFelix Guo		result = run_tests(linux, request)
4546ebf5866SFelix Guo		if result.status != KunitStatus.SUCCESS:
4556ebf5866SFelix Guo			sys.exit(1)
45645ba7a89SDavid Gow	elif cli_args.subcommand == 'config':
45782206a0cSBrendan Higgins		if cli_args.build_dir and (
45882206a0cSBrendan Higgins				not os.path.exists(cli_args.build_dir)):
45945ba7a89SDavid Gow			os.mkdir(cli_args.build_dir)
46082206a0cSBrendan Higgins
4618a04930fSDaniel Latypov		linux = tree_from_args(cli_args)
462db167981SDaniel Latypov		request = KunitConfigRequest(build_dir=cli_args.build_dir,
463db167981SDaniel Latypov					     make_options=cli_args.make_options)
46445ba7a89SDavid Gow		result = config_tests(linux, request)
465e756dbebSDaniel Latypov		stdout.print_with_timestamp((
46645ba7a89SDavid Gow			'Elapsed time: %.3fs\n') % (
46745ba7a89SDavid Gow				result.elapsed_time))
46845ba7a89SDavid Gow		if result.status != KunitStatus.SUCCESS:
46945ba7a89SDavid Gow			sys.exit(1)
47045ba7a89SDavid Gow	elif cli_args.subcommand == 'build':
4718a04930fSDaniel Latypov		linux = tree_from_args(cli_args)
472db167981SDaniel Latypov		request = KunitBuildRequest(build_dir=cli_args.build_dir,
473db167981SDaniel Latypov					    make_options=cli_args.make_options,
474*980ac3adSDaniel Latypov					    jobs=cli_args.jobs)
4751ee2ba89SDaniel Latypov		result = config_and_build_tests(linux, request)
476e756dbebSDaniel Latypov		stdout.print_with_timestamp((
47745ba7a89SDavid Gow			'Elapsed time: %.3fs\n') % (
47845ba7a89SDavid Gow				result.elapsed_time))
47945ba7a89SDavid Gow		if result.status != KunitStatus.SUCCESS:
48045ba7a89SDavid Gow			sys.exit(1)
48145ba7a89SDavid Gow	elif cli_args.subcommand == 'exec':
4828a04930fSDaniel Latypov		linux = tree_from_args(cli_args)
483db167981SDaniel Latypov		exec_request = KunitExecRequest(raw_output=cli_args.raw_output,
484db167981SDaniel Latypov						build_dir=cli_args.build_dir,
485db167981SDaniel Latypov						json=cli_args.json,
486db167981SDaniel Latypov						timeout=cli_args.timeout,
487db167981SDaniel Latypov						filter_glob=cli_args.filter_glob,
488db167981SDaniel Latypov						kernel_args=cli_args.kernel_args,
489db167981SDaniel Latypov						run_isolated=cli_args.run_isolated)
490db167981SDaniel Latypov		result = exec_tests(linux, exec_request)
491e756dbebSDaniel Latypov		stdout.print_with_timestamp((
4927ef925eaSDaniel Latypov			'Elapsed time: %.3fs\n') % (result.elapsed_time))
49345ba7a89SDavid Gow		if result.status != KunitStatus.SUCCESS:
49445ba7a89SDavid Gow			sys.exit(1)
49545ba7a89SDavid Gow	elif cli_args.subcommand == 'parse':
4960453f984SDaniel Latypov		if cli_args.file is None:
4972ab5d5e6SDaniel Latypov			sys.stdin.reconfigure(errors='backslashreplace')  # pytype: disable=attribute-error
49845ba7a89SDavid Gow			kunit_output = sys.stdin
49945ba7a89SDavid Gow		else:
5002ab5d5e6SDaniel Latypov			with open(cli_args.file, 'r', errors='backslashreplace') as f:
50145ba7a89SDavid Gow				kunit_output = f.read().splitlines()
502ee96d25fSDaniel Latypov		# We know nothing about how the result was created!
503ee96d25fSDaniel Latypov		metadata = kunit_json.Metadata()
504db167981SDaniel Latypov		request = KunitParseRequest(raw_output=cli_args.raw_output,
505db167981SDaniel Latypov					    json=cli_args.json)
506ee96d25fSDaniel Latypov		result, _ = parse_tests(request, metadata, kunit_output)
50745ba7a89SDavid Gow		if result.status != KunitStatus.SUCCESS:
50845ba7a89SDavid Gow			sys.exit(1)
5096ebf5866SFelix Guo	else:
5106ebf5866SFelix Guo		parser.print_help()
5116ebf5866SFelix Guo
5126ebf5866SFelix Guoif __name__ == '__main__':
513ff7b437fSBrendan Higgins	main(sys.argv[1:])
514