xref: /linux/tools/testing/kunit/kunit.py (revision 1ee2ba89bea86d6389509e426583b49ac19b86f2)
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
13ff9e09a3SDaniel Latypovimport sys
146ebf5866SFelix Guoimport time
156ebf5866SFelix Guo
16df4b0807SSeongJae Parkassert sys.version_info >= (3, 7), "Python version is too old"
17df4b0807SSeongJae Park
18db167981SDaniel Latypovfrom dataclasses import dataclass
196ebf5866SFelix Guofrom enum import Enum, auto
20db167981SDaniel Latypovfrom typing import Any, Iterable, Sequence, List, Optional
216ebf5866SFelix Guo
2221a6d178SHeidi Fahimimport kunit_json
236ebf5866SFelix Guoimport kunit_kernel
246ebf5866SFelix Guoimport kunit_parser
256ebf5866SFelix Guo
266ebf5866SFelix Guoclass KunitStatus(Enum):
276ebf5866SFelix Guo	SUCCESS = auto()
286ebf5866SFelix Guo	CONFIG_FAILURE = auto()
296ebf5866SFelix Guo	BUILD_FAILURE = auto()
306ebf5866SFelix Guo	TEST_FAILURE = auto()
316ebf5866SFelix Guo
32db167981SDaniel Latypov@dataclass
33db167981SDaniel Latypovclass KunitResult:
34db167981SDaniel Latypov	status: KunitStatus
35db167981SDaniel Latypov	result: Any
36db167981SDaniel Latypov	elapsed_time: float
37db167981SDaniel Latypov
38db167981SDaniel Latypov@dataclass
39db167981SDaniel Latypovclass KunitConfigRequest:
40db167981SDaniel Latypov	build_dir: str
41db167981SDaniel Latypov	make_options: Optional[List[str]]
42db167981SDaniel Latypov
43db167981SDaniel Latypov@dataclass
44db167981SDaniel Latypovclass KunitBuildRequest(KunitConfigRequest):
45db167981SDaniel Latypov	jobs: int
46db167981SDaniel Latypov	alltests: bool
47db167981SDaniel Latypov
48db167981SDaniel Latypov@dataclass
49db167981SDaniel Latypovclass KunitParseRequest:
50db167981SDaniel Latypov	raw_output: Optional[str]
51db167981SDaniel Latypov	build_dir: str
52db167981SDaniel Latypov	json: Optional[str]
53db167981SDaniel Latypov
54db167981SDaniel Latypov@dataclass
55db167981SDaniel Latypovclass KunitExecRequest(KunitParseRequest):
56db167981SDaniel Latypov	timeout: int
57db167981SDaniel Latypov	alltests: bool
58db167981SDaniel Latypov	filter_glob: str
59db167981SDaniel Latypov	kernel_args: Optional[List[str]]
60db167981SDaniel Latypov	run_isolated: Optional[str]
61db167981SDaniel Latypov
62db167981SDaniel Latypov@dataclass
63db167981SDaniel Latypovclass KunitRequest(KunitExecRequest, KunitBuildRequest):
64db167981SDaniel Latypov	pass
65db167981SDaniel Latypov
66db167981SDaniel Latypov
67db167981SDaniel LatypovKernelDirectoryPath = sys.argv[0].split('tools/testing/kunit/')[0]
68db167981SDaniel Latypov
6909641f7cSDaniel Latypovdef get_kernel_root_path() -> str:
7009641f7cSDaniel Latypov	path = sys.argv[0] if not __file__ else __file__
7109641f7cSDaniel Latypov	parts = os.path.realpath(path).split('tools/testing/kunit')
72be886ba9SHeidi Fahim	if len(parts) != 2:
73be886ba9SHeidi Fahim		sys.exit(1)
74be886ba9SHeidi Fahim	return parts[0]
75be886ba9SHeidi Fahim
7645ba7a89SDavid Gowdef config_tests(linux: kunit_kernel.LinuxSourceTree,
7745ba7a89SDavid Gow		 request: KunitConfigRequest) -> KunitResult:
7845ba7a89SDavid Gow	kunit_parser.print_with_timestamp('Configuring KUnit Kernel ...')
7945ba7a89SDavid Gow
806ebf5866SFelix Guo	config_start = time.time()
810476e69fSGreg Thelen	success = linux.build_reconfig(request.build_dir, request.make_options)
826ebf5866SFelix Guo	config_end = time.time()
836ebf5866SFelix Guo	if not success:
8445ba7a89SDavid Gow		return KunitResult(KunitStatus.CONFIG_FAILURE,
8545ba7a89SDavid Gow				   'could not configure kernel',
8645ba7a89SDavid Gow				   config_end - config_start)
8745ba7a89SDavid Gow	return KunitResult(KunitStatus.SUCCESS,
8845ba7a89SDavid Gow			   'configured kernel successfully',
8945ba7a89SDavid Gow			   config_end - config_start)
906ebf5866SFelix Guo
9145ba7a89SDavid Gowdef build_tests(linux: kunit_kernel.LinuxSourceTree,
9245ba7a89SDavid Gow		request: KunitBuildRequest) -> KunitResult:
936ebf5866SFelix Guo	kunit_parser.print_with_timestamp('Building KUnit Kernel ...')
946ebf5866SFelix Guo
956ebf5866SFelix Guo	build_start = time.time()
9687c9c163SBrendan Higgins	success = linux.build_kernel(request.alltests,
97021ed9f5SHeidi Fahim				     request.jobs,
980476e69fSGreg Thelen				     request.build_dir,
990476e69fSGreg Thelen				     request.make_options)
1006ebf5866SFelix Guo	build_end = time.time()
1016ebf5866SFelix Guo	if not success:
102ee61492aSDavid Gow		return KunitResult(KunitStatus.BUILD_FAILURE,
103ee61492aSDavid Gow				   'could not build kernel',
104ee61492aSDavid Gow				   build_end - build_start)
10545ba7a89SDavid Gow	if not success:
10645ba7a89SDavid Gow		return KunitResult(KunitStatus.BUILD_FAILURE,
10745ba7a89SDavid Gow				   'could not build kernel',
10845ba7a89SDavid Gow				   build_end - build_start)
10945ba7a89SDavid Gow	return KunitResult(KunitStatus.SUCCESS,
11045ba7a89SDavid Gow			   'built kernel successfully',
11145ba7a89SDavid Gow			   build_end - build_start)
1126ebf5866SFelix Guo
113*1ee2ba89SDaniel Latypovdef config_and_build_tests(linux: kunit_kernel.LinuxSourceTree,
114*1ee2ba89SDaniel Latypov			   request: KunitBuildRequest) -> KunitResult:
115*1ee2ba89SDaniel Latypov	config_result = config_tests(linux, request)
116*1ee2ba89SDaniel Latypov	if config_result.status != KunitStatus.SUCCESS:
117*1ee2ba89SDaniel Latypov		return config_result
118*1ee2ba89SDaniel Latypov
119*1ee2ba89SDaniel Latypov	return build_tests(linux, request)
120*1ee2ba89SDaniel Latypov
121ff9e09a3SDaniel Latypovdef _list_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> List[str]:
122ff9e09a3SDaniel Latypov	args = ['kunit.action=list']
123ff9e09a3SDaniel Latypov	if request.kernel_args:
124ff9e09a3SDaniel Latypov		args.extend(request.kernel_args)
125ff9e09a3SDaniel Latypov
126ff9e09a3SDaniel Latypov	output = linux.run_kernel(args=args,
127ff9e09a3SDaniel Latypov			   timeout=None if request.alltests else request.timeout,
128ff9e09a3SDaniel Latypov			   filter_glob=request.filter_glob,
129ff9e09a3SDaniel Latypov			   build_dir=request.build_dir)
130ff9e09a3SDaniel Latypov	lines = kunit_parser.extract_tap_lines(output)
131ff9e09a3SDaniel Latypov	# Hack! Drop the dummy TAP version header that the executor prints out.
132ff9e09a3SDaniel Latypov	lines.pop()
133ff9e09a3SDaniel Latypov
134ff9e09a3SDaniel Latypov	# Filter out any extraneous non-test output that might have gotten mixed in.
135ff9e09a3SDaniel Latypov	return [l for l in lines if re.match('^[^\s.]+\.[^\s.]+$', l)]
136ff9e09a3SDaniel Latypov
137ff9e09a3SDaniel Latypovdef _suites_from_test_list(tests: List[str]) -> List[str]:
138ff9e09a3SDaniel Latypov	"""Extracts all the suites from an ordered list of tests."""
139ff9e09a3SDaniel Latypov	suites = []  # type: List[str]
140ff9e09a3SDaniel Latypov	for t in tests:
141ff9e09a3SDaniel Latypov		parts = t.split('.', maxsplit=2)
142ff9e09a3SDaniel Latypov		if len(parts) != 2:
143ff9e09a3SDaniel Latypov			raise ValueError(f'internal KUnit error, test name should be of the form "<suite>.<test>", got "{t}"')
144ff9e09a3SDaniel Latypov		suite, case = parts
145ff9e09a3SDaniel Latypov		if not suites or suites[-1] != suite:
146ff9e09a3SDaniel Latypov			suites.append(suite)
147ff9e09a3SDaniel Latypov	return suites
148ff9e09a3SDaniel Latypov
149ff9e09a3SDaniel Latypov
150ff9e09a3SDaniel Latypov
151db167981SDaniel Latypovdef exec_tests(linux: kunit_kernel.LinuxSourceTree, request: KunitExecRequest) -> KunitResult:
152ff9e09a3SDaniel Latypov	filter_globs = [request.filter_glob]
153ff9e09a3SDaniel Latypov	if request.run_isolated:
154ff9e09a3SDaniel Latypov		tests = _list_tests(linux, request)
155ff9e09a3SDaniel Latypov		if request.run_isolated == 'test':
156ff9e09a3SDaniel Latypov			filter_globs = tests
157ff9e09a3SDaniel Latypov		if request.run_isolated == 'suite':
158ff9e09a3SDaniel Latypov			filter_globs = _suites_from_test_list(tests)
159ff9e09a3SDaniel Latypov			# Apply the test-part of the user's glob, if present.
160ff9e09a3SDaniel Latypov			if '.' in request.filter_glob:
161ff9e09a3SDaniel Latypov				test_glob = request.filter_glob.split('.', maxsplit=2)[1]
162ff9e09a3SDaniel Latypov				filter_globs = [g + '.'+ test_glob for g in filter_globs]
163ff9e09a3SDaniel Latypov
164d65d07cbSRae Moar	test_counts = kunit_parser.TestCounts()
165ff9e09a3SDaniel Latypov	exec_time = 0.0
166ff9e09a3SDaniel Latypov	for i, filter_glob in enumerate(filter_globs):
167ff9e09a3SDaniel Latypov		kunit_parser.print_with_timestamp('Starting KUnit Kernel ({}/{})...'.format(i+1, len(filter_globs)))
168ff9e09a3SDaniel Latypov
1696ebf5866SFelix Guo		test_start = time.time()
1707ef925eaSDaniel Latypov		run_result = linux.run_kernel(
1716cb51a18SDaniel Latypov			args=request.kernel_args,
172021ed9f5SHeidi Fahim			timeout=None if request.alltests else request.timeout,
173ff9e09a3SDaniel Latypov			filter_glob=filter_glob,
1746ec1b81dSSeongJae Park			build_dir=request.build_dir)
17545ba7a89SDavid Gow
176db167981SDaniel Latypov		result = parse_tests(request, run_result)
1775f6aa6d8SDaniel Latypov		# run_kernel() doesn't block on the kernel exiting.
1785f6aa6d8SDaniel Latypov		# That only happens after we get the last line of output from `run_result`.
1795f6aa6d8SDaniel Latypov		# So exec_time here actually contains parsing + execution time, which is fine.
1806ebf5866SFelix Guo		test_end = time.time()
181ff9e09a3SDaniel Latypov		exec_time += test_end - test_start
182ff9e09a3SDaniel Latypov
183e0cc8c05SDaniel Latypov		test_counts.add_subtest_counts(result.result.counts)
1846ebf5866SFelix Guo
1857fa7ffcfSDaniel Latypov	if len(filter_globs) == 1 and test_counts.crashed > 0:
1867fa7ffcfSDaniel Latypov		bd = request.build_dir
1877fa7ffcfSDaniel Latypov		print('The kernel seems to have crashed; you can decode the stack traces with:')
1887fa7ffcfSDaniel Latypov		print('$ scripts/decode_stacktrace.sh {}/vmlinux {} < {} | tee {}/decoded.log | {} parse'.format(
1897fa7ffcfSDaniel Latypov				bd, bd, kunit_kernel.get_outfile_path(bd), bd, sys.argv[0]))
1907fa7ffcfSDaniel Latypov
191d65d07cbSRae Moar	kunit_status = _map_to_overall_status(test_counts.get_status())
192e0cc8c05SDaniel Latypov	return KunitResult(status=kunit_status, result=result, elapsed_time=exec_time)
193d65d07cbSRae Moar
194d65d07cbSRae Moardef _map_to_overall_status(test_status: kunit_parser.TestStatus) -> KunitStatus:
195d65d07cbSRae Moar	if test_status in (kunit_parser.TestStatus.SUCCESS, kunit_parser.TestStatus.SKIPPED):
196d65d07cbSRae Moar		return KunitStatus.SUCCESS
197d65d07cbSRae Moar	else:
198d65d07cbSRae Moar		return KunitStatus.TEST_FAILURE
1997ef925eaSDaniel Latypov
2007ef925eaSDaniel Latypovdef parse_tests(request: KunitParseRequest, input_data: Iterable[str]) -> KunitResult:
20145ba7a89SDavid Gow	parse_start = time.time()
20245ba7a89SDavid Gow
203e0cc8c05SDaniel Latypov	test_result = kunit_parser.Test()
20421a6d178SHeidi Fahim
20545ba7a89SDavid Gow	if request.raw_output:
206d65d07cbSRae Moar		# Treat unparsed results as one passing test.
207e0cc8c05SDaniel Latypov		test_result.status = kunit_parser.TestStatus.SUCCESS
208e0cc8c05SDaniel Latypov		test_result.counts.passed = 1
209d65d07cbSRae Moar
2107ef925eaSDaniel Latypov		output: Iterable[str] = input_data
2116a499c9cSDaniel Latypov		if request.raw_output == 'all':
2126a499c9cSDaniel Latypov			pass
2136a499c9cSDaniel Latypov		elif request.raw_output == 'kunit':
2146a499c9cSDaniel Latypov			output = kunit_parser.extract_tap_lines(output)
2156a499c9cSDaniel Latypov		else:
2166a499c9cSDaniel Latypov			print(f'Unknown --raw_output option "{request.raw_output}"', file=sys.stderr)
2176a499c9cSDaniel Latypov		for line in output:
2186a499c9cSDaniel Latypov			print(line.rstrip())
2196a499c9cSDaniel Latypov
22045ba7a89SDavid Gow	else:
2217ef925eaSDaniel Latypov		test_result = kunit_parser.parse_run_tests(input_data)
22245ba7a89SDavid Gow	parse_end = time.time()
22345ba7a89SDavid Gow
22421a6d178SHeidi Fahim	if request.json:
22521a6d178SHeidi Fahim		json_obj = kunit_json.get_json_result(
226e0cc8c05SDaniel Latypov					test=test_result,
22721a6d178SHeidi Fahim					def_config='kunit_defconfig',
22821a6d178SHeidi Fahim					build_dir=request.build_dir,
22921a6d178SHeidi Fahim					json_path=request.json)
23021a6d178SHeidi Fahim		if request.json == 'stdout':
23121a6d178SHeidi Fahim			print(json_obj)
23221a6d178SHeidi Fahim
23345ba7a89SDavid Gow	if test_result.status != kunit_parser.TestStatus.SUCCESS:
23445ba7a89SDavid Gow		return KunitResult(KunitStatus.TEST_FAILURE, test_result,
23545ba7a89SDavid Gow				   parse_end - parse_start)
23645ba7a89SDavid Gow
23745ba7a89SDavid Gow	return KunitResult(KunitStatus.SUCCESS, test_result,
23845ba7a89SDavid Gow				parse_end - parse_start)
23945ba7a89SDavid Gow
24045ba7a89SDavid Gowdef run_tests(linux: kunit_kernel.LinuxSourceTree,
24145ba7a89SDavid Gow	      request: KunitRequest) -> KunitResult:
24245ba7a89SDavid Gow	run_start = time.time()
24345ba7a89SDavid Gow
244db167981SDaniel Latypov	config_result = config_tests(linux, request)
24545ba7a89SDavid Gow	if config_result.status != KunitStatus.SUCCESS:
24645ba7a89SDavid Gow		return config_result
24745ba7a89SDavid Gow
248db167981SDaniel Latypov	build_result = build_tests(linux, request)
24945ba7a89SDavid Gow	if build_result.status != KunitStatus.SUCCESS:
25045ba7a89SDavid Gow		return build_result
25145ba7a89SDavid Gow
252db167981SDaniel Latypov	exec_result = exec_tests(linux, request)
25345ba7a89SDavid Gow
25445ba7a89SDavid Gow	run_end = time.time()
25545ba7a89SDavid Gow
2566ebf5866SFelix Guo	kunit_parser.print_with_timestamp((
2576ebf5866SFelix Guo		'Elapsed time: %.3fs total, %.3fs configuring, %.3fs ' +
2586ebf5866SFelix Guo		'building, %.3fs running\n') % (
25945ba7a89SDavid Gow				run_end - run_start,
26045ba7a89SDavid Gow				config_result.elapsed_time,
26145ba7a89SDavid Gow				build_result.elapsed_time,
26245ba7a89SDavid Gow				exec_result.elapsed_time))
2637ef925eaSDaniel Latypov	return exec_result
2646ebf5866SFelix Guo
265d8c23eadSDaniel Latypov# Problem:
266d8c23eadSDaniel Latypov# $ kunit.py run --json
267d8c23eadSDaniel Latypov# works as one would expect and prints the parsed test results as JSON.
268d8c23eadSDaniel Latypov# $ kunit.py run --json suite_name
269d8c23eadSDaniel Latypov# would *not* pass suite_name as the filter_glob and print as json.
270d8c23eadSDaniel Latypov# argparse will consider it to be another way of writing
271d8c23eadSDaniel Latypov# $ kunit.py run --json=suite_name
272d8c23eadSDaniel Latypov# i.e. it would run all tests, and dump the json to a `suite_name` file.
273d8c23eadSDaniel Latypov# So we hackily automatically rewrite --json => --json=stdout
274d8c23eadSDaniel Latypovpseudo_bool_flag_defaults = {
275d8c23eadSDaniel Latypov		'--json': 'stdout',
276d8c23eadSDaniel Latypov		'--raw_output': 'kunit',
277d8c23eadSDaniel Latypov}
278d8c23eadSDaniel Latypovdef massage_argv(argv: Sequence[str]) -> Sequence[str]:
279d8c23eadSDaniel Latypov	def massage_arg(arg: str) -> str:
280d8c23eadSDaniel Latypov		if arg not in pseudo_bool_flag_defaults:
281d8c23eadSDaniel Latypov			return arg
282d8c23eadSDaniel Latypov		return  f'{arg}={pseudo_bool_flag_defaults[arg]}'
283d8c23eadSDaniel Latypov	return list(map(massage_arg, argv))
284d8c23eadSDaniel Latypov
28509641f7cSDaniel Latypovdef add_common_opts(parser) -> None:
28645ba7a89SDavid Gow	parser.add_argument('--build_dir',
28745ba7a89SDavid Gow			    help='As in the make command, it specifies the build '
28845ba7a89SDavid Gow			    'directory.',
289ddbd60c7SVitor Massaru Iha			    type=str, default='.kunit', metavar='build_dir')
29045ba7a89SDavid Gow	parser.add_argument('--make_options',
29145ba7a89SDavid Gow			    help='X=Y make option, can be repeated.',
29245ba7a89SDavid Gow			    action='append')
29345ba7a89SDavid Gow	parser.add_argument('--alltests',
29445ba7a89SDavid Gow			    help='Run all KUnit tests through allyesconfig',
2956ebf5866SFelix Guo			    action='store_true')
296243180f5SDaniel Latypov	parser.add_argument('--kunitconfig',
2979854781dSDaniel Latypov			     help='Path to Kconfig fragment that enables KUnit tests.'
2989854781dSDaniel Latypov			     ' If given a directory, (e.g. lib/kunit), "/.kunitconfig" '
2999854781dSDaniel Latypov			     'will get  automatically appended.',
300243180f5SDaniel Latypov			     metavar='kunitconfig')
3019f57cc76SDaniel Latypov	parser.add_argument('--kconfig_add',
3029f57cc76SDaniel Latypov			     help='Additional Kconfig options to append to the '
3039f57cc76SDaniel Latypov			     '.kunitconfig, e.g. CONFIG_KASAN=y. Can be repeated.',
3049f57cc76SDaniel Latypov			    action='append')
3056ebf5866SFelix Guo
30687c9c163SBrendan Higgins	parser.add_argument('--arch',
30787c9c163SBrendan Higgins			    help=('Specifies the architecture to run tests under. '
30887c9c163SBrendan Higgins				  'The architecture specified here must match the '
30987c9c163SBrendan Higgins				  'string passed to the ARCH make param, '
31087c9c163SBrendan Higgins				  'e.g. i386, x86_64, arm, um, etc. Non-UML '
31187c9c163SBrendan Higgins				  'architectures run on QEMU.'),
31287c9c163SBrendan Higgins			    type=str, default='um', metavar='arch')
31387c9c163SBrendan Higgins
31487c9c163SBrendan Higgins	parser.add_argument('--cross_compile',
31587c9c163SBrendan Higgins			    help=('Sets make\'s CROSS_COMPILE variable; it should '
31687c9c163SBrendan Higgins				  'be set to a toolchain path prefix (the prefix '
31787c9c163SBrendan Higgins				  'of gcc and other tools in your toolchain, for '
31887c9c163SBrendan Higgins				  'example `sparc64-linux-gnu-` if you have the '
31987c9c163SBrendan Higgins				  'sparc toolchain installed on your system, or '
32087c9c163SBrendan Higgins				  '`$HOME/toolchains/microblaze/gcc-9.2.0-nolibc/microblaze-linux/bin/microblaze-linux-` '
32187c9c163SBrendan Higgins				  'if you have downloaded the microblaze toolchain '
32287c9c163SBrendan Higgins				  'from the 0-day website to a directory in your '
32387c9c163SBrendan Higgins				  'home directory called `toolchains`).'),
32487c9c163SBrendan Higgins			    metavar='cross_compile')
32587c9c163SBrendan Higgins
32687c9c163SBrendan Higgins	parser.add_argument('--qemu_config',
32787c9c163SBrendan Higgins			    help=('Takes a path to a path to a file containing '
32887c9c163SBrendan Higgins				  'a QemuArchParams object.'),
32987c9c163SBrendan Higgins			    type=str, metavar='qemu_config')
33087c9c163SBrendan Higgins
33109641f7cSDaniel Latypovdef add_build_opts(parser) -> None:
33245ba7a89SDavid Gow	parser.add_argument('--jobs',
33345ba7a89SDavid Gow			    help='As in the make command, "Specifies  the number of '
33445ba7a89SDavid Gow			    'jobs (commands) to run simultaneously."',
33545ba7a89SDavid Gow			    type=int, default=8, metavar='jobs')
33645ba7a89SDavid Gow
33709641f7cSDaniel Latypovdef add_exec_opts(parser) -> None:
33845ba7a89SDavid Gow	parser.add_argument('--timeout',
3396ebf5866SFelix Guo			    help='maximum number of seconds to allow for all tests '
3406ebf5866SFelix Guo			    'to run. This does not include time taken to build the '
3416ebf5866SFelix Guo			    'tests.',
3426ebf5866SFelix Guo			    type=int,
3436ebf5866SFelix Guo			    default=300,
3446ebf5866SFelix Guo			    metavar='timeout')
345d992880bSDaniel Latypov	parser.add_argument('filter_glob',
346a127b154SDaniel Latypov			    help='Filter which KUnit test suites/tests run at '
347a127b154SDaniel Latypov			    'boot-time, e.g. list* or list*.*del_test',
348d992880bSDaniel Latypov			    type=str,
349d992880bSDaniel Latypov			    nargs='?',
350d992880bSDaniel Latypov			    default='',
351d992880bSDaniel Latypov			    metavar='filter_glob')
3526cb51a18SDaniel Latypov	parser.add_argument('--kernel_args',
3536cb51a18SDaniel Latypov			    help='Kernel command-line parameters. Maybe be repeated',
3546cb51a18SDaniel Latypov			     action='append')
355ff9e09a3SDaniel Latypov	parser.add_argument('--run_isolated', help='If set, boot the kernel for each '
356ff9e09a3SDaniel Latypov			    'individual suite/test. This is can be useful for debugging '
357ff9e09a3SDaniel Latypov			    'a non-hermetic test, one that might pass/fail based on '
358ff9e09a3SDaniel Latypov			    'what ran before it.',
359ff9e09a3SDaniel Latypov			    type=str,
360ff9e09a3SDaniel Latypov			    choices=['suite', 'test']),
3616ebf5866SFelix Guo
36209641f7cSDaniel Latypovdef add_parse_opts(parser) -> None:
3636a499c9cSDaniel Latypov	parser.add_argument('--raw_output', help='If set don\'t format output from kernel. '
3646a499c9cSDaniel Latypov			    'If set to --raw_output=kunit, filters to just KUnit output.',
3656a499c9cSDaniel Latypov			    type=str, nargs='?', const='all', default=None)
36621a6d178SHeidi Fahim	parser.add_argument('--json',
36721a6d178SHeidi Fahim			    nargs='?',
36821a6d178SHeidi Fahim			    help='Stores test results in a JSON, and either '
36921a6d178SHeidi Fahim			    'prints to stdout or saves to file if a '
37021a6d178SHeidi Fahim			    'filename is specified',
37121a6d178SHeidi Fahim			    type=str, const='stdout', default=None)
372021ed9f5SHeidi Fahim
37345ba7a89SDavid Gowdef main(argv, linux=None):
37445ba7a89SDavid Gow	parser = argparse.ArgumentParser(
37545ba7a89SDavid Gow			description='Helps writing and running KUnit tests.')
37645ba7a89SDavid Gow	subparser = parser.add_subparsers(dest='subcommand')
37745ba7a89SDavid Gow
37845ba7a89SDavid Gow	# The 'run' command will config, build, exec, and parse in one go.
37945ba7a89SDavid Gow	run_parser = subparser.add_parser('run', help='Runs KUnit tests.')
38045ba7a89SDavid Gow	add_common_opts(run_parser)
38145ba7a89SDavid Gow	add_build_opts(run_parser)
38245ba7a89SDavid Gow	add_exec_opts(run_parser)
38345ba7a89SDavid Gow	add_parse_opts(run_parser)
38445ba7a89SDavid Gow
38545ba7a89SDavid Gow	config_parser = subparser.add_parser('config',
38645ba7a89SDavid Gow						help='Ensures that .config contains all of '
38745ba7a89SDavid Gow						'the options in .kunitconfig')
38845ba7a89SDavid Gow	add_common_opts(config_parser)
38945ba7a89SDavid Gow
39045ba7a89SDavid Gow	build_parser = subparser.add_parser('build', help='Builds a kernel with KUnit tests')
39145ba7a89SDavid Gow	add_common_opts(build_parser)
39245ba7a89SDavid Gow	add_build_opts(build_parser)
39345ba7a89SDavid Gow
39445ba7a89SDavid Gow	exec_parser = subparser.add_parser('exec', help='Run a kernel with KUnit tests')
39545ba7a89SDavid Gow	add_common_opts(exec_parser)
39645ba7a89SDavid Gow	add_exec_opts(exec_parser)
39745ba7a89SDavid Gow	add_parse_opts(exec_parser)
39845ba7a89SDavid Gow
39945ba7a89SDavid Gow	# The 'parse' option is special, as it doesn't need the kernel source
40045ba7a89SDavid Gow	# (therefore there is no need for a build_dir, hence no add_common_opts)
40145ba7a89SDavid Gow	# and the '--file' argument is not relevant to 'run', so isn't in
40245ba7a89SDavid Gow	# add_parse_opts()
40345ba7a89SDavid Gow	parse_parser = subparser.add_parser('parse',
40445ba7a89SDavid Gow					    help='Parses KUnit results from a file, '
40545ba7a89SDavid Gow					    'and parses formatted results.')
40645ba7a89SDavid Gow	add_parse_opts(parse_parser)
40745ba7a89SDavid Gow	parse_parser.add_argument('file',
40845ba7a89SDavid Gow				  help='Specifies the file to read results from.',
40945ba7a89SDavid Gow				  type=str, nargs='?', metavar='input_file')
4100476e69fSGreg Thelen
411d8c23eadSDaniel Latypov	cli_args = parser.parse_args(massage_argv(argv))
4126ebf5866SFelix Guo
4135578d008SBrendan Higgins	if get_kernel_root_path():
4145578d008SBrendan Higgins		os.chdir(get_kernel_root_path())
4155578d008SBrendan Higgins
4166ebf5866SFelix Guo	if cli_args.subcommand == 'run':
417e3212513SSeongJae Park		if not os.path.exists(cli_args.build_dir):
418e3212513SSeongJae Park			os.mkdir(cli_args.build_dir)
41982206a0cSBrendan Higgins
420ff7b437fSBrendan Higgins		if not linux:
42187c9c163SBrendan Higgins			linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
42287c9c163SBrendan Higgins					kunitconfig_path=cli_args.kunitconfig,
4239f57cc76SDaniel Latypov					kconfig_add=cli_args.kconfig_add,
42487c9c163SBrendan Higgins					arch=cli_args.arch,
42587c9c163SBrendan Higgins					cross_compile=cli_args.cross_compile,
42687c9c163SBrendan Higgins					qemu_config_path=cli_args.qemu_config)
427fcdb0bc0SAndy Shevchenko
428db167981SDaniel Latypov		request = KunitRequest(build_dir=cli_args.build_dir,
429db167981SDaniel Latypov				       make_options=cli_args.make_options,
430db167981SDaniel Latypov				       jobs=cli_args.jobs,
431db167981SDaniel Latypov				       alltests=cli_args.alltests,
432db167981SDaniel Latypov				       raw_output=cli_args.raw_output,
433db167981SDaniel Latypov				       json=cli_args.json,
434db167981SDaniel Latypov				       timeout=cli_args.timeout,
435db167981SDaniel Latypov				       filter_glob=cli_args.filter_glob,
436db167981SDaniel Latypov				       kernel_args=cli_args.kernel_args,
437db167981SDaniel Latypov				       run_isolated=cli_args.run_isolated)
4386ebf5866SFelix Guo		result = run_tests(linux, request)
4396ebf5866SFelix Guo		if result.status != KunitStatus.SUCCESS:
4406ebf5866SFelix Guo			sys.exit(1)
44145ba7a89SDavid Gow	elif cli_args.subcommand == 'config':
44282206a0cSBrendan Higgins		if cli_args.build_dir and (
44382206a0cSBrendan Higgins				not os.path.exists(cli_args.build_dir)):
44445ba7a89SDavid Gow			os.mkdir(cli_args.build_dir)
44582206a0cSBrendan Higgins
44645ba7a89SDavid Gow		if not linux:
44787c9c163SBrendan Higgins			linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
44887c9c163SBrendan Higgins					kunitconfig_path=cli_args.kunitconfig,
4499f57cc76SDaniel Latypov					kconfig_add=cli_args.kconfig_add,
45087c9c163SBrendan Higgins					arch=cli_args.arch,
45187c9c163SBrendan Higgins					cross_compile=cli_args.cross_compile,
45287c9c163SBrendan Higgins					qemu_config_path=cli_args.qemu_config)
453fcdb0bc0SAndy Shevchenko
454db167981SDaniel Latypov		request = KunitConfigRequest(build_dir=cli_args.build_dir,
455db167981SDaniel Latypov					     make_options=cli_args.make_options)
45645ba7a89SDavid Gow		result = config_tests(linux, request)
45745ba7a89SDavid Gow		kunit_parser.print_with_timestamp((
45845ba7a89SDavid Gow			'Elapsed time: %.3fs\n') % (
45945ba7a89SDavid Gow				result.elapsed_time))
46045ba7a89SDavid Gow		if result.status != KunitStatus.SUCCESS:
46145ba7a89SDavid Gow			sys.exit(1)
46245ba7a89SDavid Gow	elif cli_args.subcommand == 'build':
46345ba7a89SDavid Gow		if not linux:
46487c9c163SBrendan Higgins			linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
46587c9c163SBrendan Higgins					kunitconfig_path=cli_args.kunitconfig,
4669f57cc76SDaniel Latypov					kconfig_add=cli_args.kconfig_add,
46787c9c163SBrendan Higgins					arch=cli_args.arch,
46887c9c163SBrendan Higgins					cross_compile=cli_args.cross_compile,
46987c9c163SBrendan Higgins					qemu_config_path=cli_args.qemu_config)
470fcdb0bc0SAndy Shevchenko
471db167981SDaniel Latypov		request = KunitBuildRequest(build_dir=cli_args.build_dir,
472db167981SDaniel Latypov					    make_options=cli_args.make_options,
473db167981SDaniel Latypov					    jobs=cli_args.jobs,
474db167981SDaniel Latypov					    alltests=cli_args.alltests)
475*1ee2ba89SDaniel Latypov		result = config_and_build_tests(linux, request)
47645ba7a89SDavid Gow		kunit_parser.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':
48245ba7a89SDavid Gow		if not linux:
48387c9c163SBrendan Higgins			linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
48487c9c163SBrendan Higgins					kunitconfig_path=cli_args.kunitconfig,
4859f57cc76SDaniel Latypov					kconfig_add=cli_args.kconfig_add,
48687c9c163SBrendan Higgins					arch=cli_args.arch,
48787c9c163SBrendan Higgins					cross_compile=cli_args.cross_compile,
48887c9c163SBrendan Higgins					qemu_config_path=cli_args.qemu_config)
489fcdb0bc0SAndy Shevchenko
490db167981SDaniel Latypov		exec_request = KunitExecRequest(raw_output=cli_args.raw_output,
491db167981SDaniel Latypov						build_dir=cli_args.build_dir,
492db167981SDaniel Latypov						json=cli_args.json,
493db167981SDaniel Latypov						timeout=cli_args.timeout,
494db167981SDaniel Latypov						alltests=cli_args.alltests,
495db167981SDaniel Latypov						filter_glob=cli_args.filter_glob,
496db167981SDaniel Latypov						kernel_args=cli_args.kernel_args,
497db167981SDaniel Latypov						run_isolated=cli_args.run_isolated)
498db167981SDaniel Latypov		result = exec_tests(linux, exec_request)
49945ba7a89SDavid Gow		kunit_parser.print_with_timestamp((
5007ef925eaSDaniel Latypov			'Elapsed time: %.3fs\n') % (result.elapsed_time))
50145ba7a89SDavid Gow		if result.status != KunitStatus.SUCCESS:
50245ba7a89SDavid Gow			sys.exit(1)
50345ba7a89SDavid Gow	elif cli_args.subcommand == 'parse':
50445ba7a89SDavid Gow		if cli_args.file == None:
5052ab5d5e6SDaniel Latypov			sys.stdin.reconfigure(errors='backslashreplace')  # pytype: disable=attribute-error
50645ba7a89SDavid Gow			kunit_output = sys.stdin
50745ba7a89SDavid Gow		else:
5082ab5d5e6SDaniel Latypov			with open(cli_args.file, 'r', errors='backslashreplace') as f:
50945ba7a89SDavid Gow				kunit_output = f.read().splitlines()
510db167981SDaniel Latypov		request = KunitParseRequest(raw_output=cli_args.raw_output,
511db167981SDaniel Latypov					    build_dir='',
512db167981SDaniel Latypov					    json=cli_args.json)
5137ef925eaSDaniel Latypov		result = parse_tests(request, kunit_output)
51445ba7a89SDavid Gow		if result.status != KunitStatus.SUCCESS:
51545ba7a89SDavid Gow			sys.exit(1)
5166ebf5866SFelix Guo	else:
5176ebf5866SFelix Guo		parser.print_help()
5186ebf5866SFelix Guo
5196ebf5866SFelix Guoif __name__ == '__main__':
520ff7b437fSBrendan Higgins	main(sys.argv[1:])
521