2 # SPDX-License-Identifier: GPL-2.0
4 # A thin wrapper on top of the KUnit Kernel
6 # Copyright (C) 2019, Google LLC.
7 # Author: Felix Guo <felixguoxiuping@gmail.com>
8 # Author: Brendan Higgins <brendanhiggins@google.com>
15 assert sys.version_info >= (3, 7), "Python version is too old"
17 from collections import namedtuple
18 from enum import Enum, auto
19 from typing import Iterable
26 KunitResult = namedtuple('KunitResult', ['status','result','elapsed_time'])
28 KunitConfigRequest = namedtuple('KunitConfigRequest',
29 ['build_dir', 'make_options'])
30 KunitBuildRequest = namedtuple('KunitBuildRequest',
31 ['jobs', 'build_dir', 'alltests',
33 KunitExecRequest = namedtuple('KunitExecRequest',
34 ['timeout', 'build_dir', 'alltests',
35 'filter_glob', 'kernel_args'])
36 KunitParseRequest = namedtuple('KunitParseRequest',
37 ['raw_output', 'input_data', 'build_dir', 'json'])
38 KunitRequest = namedtuple('KunitRequest', ['raw_output','timeout', 'jobs',
39 'build_dir', 'alltests', 'filter_glob',
40 'kernel_args', 'json', 'make_options'])
42 KernelDirectoryPath = sys.argv[0].split('tools/testing/kunit/')[0]
44 class KunitStatus(Enum):
46 CONFIG_FAILURE = auto()
47 BUILD_FAILURE = auto()
50 def get_kernel_root_path() -> str:
51 path = sys.argv[0] if not __file__ else __file__
52 parts = os.path.realpath(path).split('tools/testing/kunit')
57 def config_tests(linux: kunit_kernel.LinuxSourceTree,
58 request: KunitConfigRequest) -> KunitResult:
59 kunit_parser.print_with_timestamp('Configuring KUnit Kernel ...')
61 config_start = time.time()
62 success = linux.build_reconfig(request.build_dir, request.make_options)
63 config_end = time.time()
65 return KunitResult(KunitStatus.CONFIG_FAILURE,
66 'could not configure kernel',
67 config_end - config_start)
68 return KunitResult(KunitStatus.SUCCESS,
69 'configured kernel successfully',
70 config_end - config_start)
72 def build_tests(linux: kunit_kernel.LinuxSourceTree,
73 request: KunitBuildRequest) -> KunitResult:
74 kunit_parser.print_with_timestamp('Building KUnit Kernel ...')
76 build_start = time.time()
77 success = linux.build_kernel(request.alltests,
81 build_end = time.time()
83 return KunitResult(KunitStatus.BUILD_FAILURE,
84 'could not build kernel',
85 build_end - build_start)
87 return KunitResult(KunitStatus.BUILD_FAILURE,
88 'could not build kernel',
89 build_end - build_start)
90 return KunitResult(KunitStatus.SUCCESS,
91 'built kernel successfully',
92 build_end - build_start)
94 def exec_tests(linux: kunit_kernel.LinuxSourceTree,
95 request: KunitExecRequest) -> KunitResult:
96 kunit_parser.print_with_timestamp('Starting KUnit Kernel ...')
97 test_start = time.time()
98 result = linux.run_kernel(
99 args=request.kernel_args,
100 timeout=None if request.alltests else request.timeout,
101 filter_glob=request.filter_glob,
102 build_dir=request.build_dir)
104 test_end = time.time()
106 return KunitResult(KunitStatus.SUCCESS,
108 test_end - test_start)
110 def parse_tests(request: KunitParseRequest) -> KunitResult:
111 parse_start = time.time()
113 test_result = kunit_parser.TestResult(kunit_parser.TestStatus.SUCCESS,
117 if request.raw_output:
118 output: Iterable[str] = request.input_data
119 if request.raw_output == 'all':
121 elif request.raw_output == 'kunit':
122 output = kunit_parser.extract_tap_lines(output)
124 print(f'Unknown --raw_output option "{request.raw_output}"', file=sys.stderr)
129 test_result = kunit_parser.parse_run_tests(request.input_data)
130 parse_end = time.time()
133 json_obj = kunit_json.get_json_result(
134 test_result=test_result,
135 def_config='kunit_defconfig',
136 build_dir=request.build_dir,
137 json_path=request.json)
138 if request.json == 'stdout':
141 if test_result.status != kunit_parser.TestStatus.SUCCESS:
142 return KunitResult(KunitStatus.TEST_FAILURE, test_result,
143 parse_end - parse_start)
145 return KunitResult(KunitStatus.SUCCESS, test_result,
146 parse_end - parse_start)
148 def run_tests(linux: kunit_kernel.LinuxSourceTree,
149 request: KunitRequest) -> KunitResult:
150 run_start = time.time()
152 config_request = KunitConfigRequest(request.build_dir,
153 request.make_options)
154 config_result = config_tests(linux, config_request)
155 if config_result.status != KunitStatus.SUCCESS:
158 build_request = KunitBuildRequest(request.jobs, request.build_dir,
160 request.make_options)
161 build_result = build_tests(linux, build_request)
162 if build_result.status != KunitStatus.SUCCESS:
165 exec_request = KunitExecRequest(request.timeout, request.build_dir,
166 request.alltests, request.filter_glob,
168 exec_result = exec_tests(linux, exec_request)
169 if exec_result.status != KunitStatus.SUCCESS:
172 parse_request = KunitParseRequest(request.raw_output,
176 parse_result = parse_tests(parse_request)
178 run_end = time.time()
180 kunit_parser.print_with_timestamp((
181 'Elapsed time: %.3fs total, %.3fs configuring, %.3fs ' +
182 'building, %.3fs running\n') % (
184 config_result.elapsed_time,
185 build_result.elapsed_time,
186 exec_result.elapsed_time))
189 def add_common_opts(parser) -> None:
190 parser.add_argument('--build_dir',
191 help='As in the make command, it specifies the build '
193 type=str, default='.kunit', metavar='build_dir')
194 parser.add_argument('--make_options',
195 help='X=Y make option, can be repeated.',
197 parser.add_argument('--alltests',
198 help='Run all KUnit tests through allyesconfig',
200 parser.add_argument('--kunitconfig',
201 help='Path to Kconfig fragment that enables KUnit tests.'
202 ' If given a directory, (e.g. lib/kunit), "/.kunitconfig" '
203 'will get automatically appended.',
204 metavar='kunitconfig')
206 parser.add_argument('--arch',
207 help=('Specifies the architecture to run tests under. '
208 'The architecture specified here must match the '
209 'string passed to the ARCH make param, '
210 'e.g. i386, x86_64, arm, um, etc. Non-UML '
211 'architectures run on QEMU.'),
212 type=str, default='um', metavar='arch')
214 parser.add_argument('--cross_compile',
215 help=('Sets make\'s CROSS_COMPILE variable; it should '
216 'be set to a toolchain path prefix (the prefix '
217 'of gcc and other tools in your toolchain, for '
218 'example `sparc64-linux-gnu-` if you have the '
219 'sparc toolchain installed on your system, or '
220 '`$HOME/toolchains/microblaze/gcc-9.2.0-nolibc/microblaze-linux/bin/microblaze-linux-` '
221 'if you have downloaded the microblaze toolchain '
222 'from the 0-day website to a directory in your '
223 'home directory called `toolchains`).'),
224 metavar='cross_compile')
226 parser.add_argument('--qemu_config',
227 help=('Takes a path to a path to a file containing '
228 'a QemuArchParams object.'),
229 type=str, metavar='qemu_config')
231 def add_build_opts(parser) -> None:
232 parser.add_argument('--jobs',
233 help='As in the make command, "Specifies the number of '
234 'jobs (commands) to run simultaneously."',
235 type=int, default=8, metavar='jobs')
237 def add_exec_opts(parser) -> None:
238 parser.add_argument('--timeout',
239 help='maximum number of seconds to allow for all tests '
240 'to run. This does not include time taken to build the '
245 parser.add_argument('filter_glob',
246 help='maximum number of seconds to allow for all tests '
247 'to run. This does not include time taken to build the '
252 metavar='filter_glob')
253 parser.add_argument('--kernel_args',
254 help='Kernel command-line parameters. Maybe be repeated',
257 def add_parse_opts(parser) -> None:
258 parser.add_argument('--raw_output', help='If set don\'t format output from kernel. '
259 'If set to --raw_output=kunit, filters to just KUnit output.',
260 type=str, nargs='?', const='all', default=None)
261 parser.add_argument('--json',
263 help='Stores test results in a JSON, and either '
264 'prints to stdout or saves to file if a '
265 'filename is specified',
266 type=str, const='stdout', default=None)
268 def main(argv, linux=None):
269 parser = argparse.ArgumentParser(
270 description='Helps writing and running KUnit tests.')
271 subparser = parser.add_subparsers(dest='subcommand')
273 # The 'run' command will config, build, exec, and parse in one go.
274 run_parser = subparser.add_parser('run', help='Runs KUnit tests.')
275 add_common_opts(run_parser)
276 add_build_opts(run_parser)
277 add_exec_opts(run_parser)
278 add_parse_opts(run_parser)
280 config_parser = subparser.add_parser('config',
281 help='Ensures that .config contains all of '
282 'the options in .kunitconfig')
283 add_common_opts(config_parser)
285 build_parser = subparser.add_parser('build', help='Builds a kernel with KUnit tests')
286 add_common_opts(build_parser)
287 add_build_opts(build_parser)
289 exec_parser = subparser.add_parser('exec', help='Run a kernel with KUnit tests')
290 add_common_opts(exec_parser)
291 add_exec_opts(exec_parser)
292 add_parse_opts(exec_parser)
294 # The 'parse' option is special, as it doesn't need the kernel source
295 # (therefore there is no need for a build_dir, hence no add_common_opts)
296 # and the '--file' argument is not relevant to 'run', so isn't in
298 parse_parser = subparser.add_parser('parse',
299 help='Parses KUnit results from a file, '
300 'and parses formatted results.')
301 add_parse_opts(parse_parser)
302 parse_parser.add_argument('file',
303 help='Specifies the file to read results from.',
304 type=str, nargs='?', metavar='input_file')
306 cli_args = parser.parse_args(argv)
308 if get_kernel_root_path():
309 os.chdir(get_kernel_root_path())
311 if cli_args.subcommand == 'run':
312 if not os.path.exists(cli_args.build_dir):
313 os.mkdir(cli_args.build_dir)
316 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
317 kunitconfig_path=cli_args.kunitconfig,
319 cross_compile=cli_args.cross_compile,
320 qemu_config_path=cli_args.qemu_config)
322 request = KunitRequest(cli_args.raw_output,
327 cli_args.filter_glob,
328 cli_args.kernel_args,
330 cli_args.make_options)
331 result = run_tests(linux, request)
332 if result.status != KunitStatus.SUCCESS:
334 elif cli_args.subcommand == 'config':
335 if cli_args.build_dir and (
336 not os.path.exists(cli_args.build_dir)):
337 os.mkdir(cli_args.build_dir)
340 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
341 kunitconfig_path=cli_args.kunitconfig,
343 cross_compile=cli_args.cross_compile,
344 qemu_config_path=cli_args.qemu_config)
346 request = KunitConfigRequest(cli_args.build_dir,
347 cli_args.make_options)
348 result = config_tests(linux, request)
349 kunit_parser.print_with_timestamp((
350 'Elapsed time: %.3fs\n') % (
351 result.elapsed_time))
352 if result.status != KunitStatus.SUCCESS:
354 elif cli_args.subcommand == 'build':
356 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
357 kunitconfig_path=cli_args.kunitconfig,
359 cross_compile=cli_args.cross_compile,
360 qemu_config_path=cli_args.qemu_config)
362 request = KunitBuildRequest(cli_args.jobs,
365 cli_args.make_options)
366 result = build_tests(linux, request)
367 kunit_parser.print_with_timestamp((
368 'Elapsed time: %.3fs\n') % (
369 result.elapsed_time))
370 if result.status != KunitStatus.SUCCESS:
372 elif cli_args.subcommand == 'exec':
374 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir,
375 kunitconfig_path=cli_args.kunitconfig,
377 cross_compile=cli_args.cross_compile,
378 qemu_config_path=cli_args.qemu_config)
380 exec_request = KunitExecRequest(cli_args.timeout,
383 cli_args.filter_glob,
384 cli_args.kernel_args)
385 exec_result = exec_tests(linux, exec_request)
386 parse_request = KunitParseRequest(cli_args.raw_output,
390 result = parse_tests(parse_request)
391 kunit_parser.print_with_timestamp((
392 'Elapsed time: %.3fs\n') % (
393 exec_result.elapsed_time))
394 if result.status != KunitStatus.SUCCESS:
396 elif cli_args.subcommand == 'parse':
397 if cli_args.file == None:
398 kunit_output = sys.stdin
400 with open(cli_args.file, 'r') as f:
401 kunit_output = f.read().splitlines()
402 request = KunitParseRequest(cli_args.raw_output,
406 result = parse_tests(request)
407 if result.status != KunitStatus.SUCCESS:
412 if __name__ == '__main__':