1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3# 4# A thin wrapper on top of the KUnit Kernel 5# 6# Copyright (C) 2019, Google LLC. 7# Author: Felix Guo <felixguoxiuping@gmail.com> 8# Author: Brendan Higgins <brendanhiggins@google.com> 9 10import argparse 11import sys 12import os 13import time 14 15from collections import namedtuple 16from enum import Enum, auto 17 18import kunit_config 19import kunit_json 20import kunit_kernel 21import kunit_parser 22 23KunitResult = namedtuple('KunitResult', ['status','result','elapsed_time']) 24 25KunitConfigRequest = namedtuple('KunitConfigRequest', 26 ['build_dir', 'make_options']) 27KunitBuildRequest = namedtuple('KunitBuildRequest', 28 ['jobs', 'build_dir', 'alltests', 29 'make_options']) 30KunitExecRequest = namedtuple('KunitExecRequest', 31 ['timeout', 'build_dir', 'alltests', 'filter_glob']) 32KunitParseRequest = namedtuple('KunitParseRequest', 33 ['raw_output', 'input_data', 'build_dir', 'json']) 34KunitRequest = namedtuple('KunitRequest', ['raw_output','timeout', 'jobs', 35 'build_dir', 'alltests', 'filter_glob', 36 'json', 'make_options']) 37 38KernelDirectoryPath = sys.argv[0].split('tools/testing/kunit/')[0] 39 40class KunitStatus(Enum): 41 SUCCESS = auto() 42 CONFIG_FAILURE = auto() 43 BUILD_FAILURE = auto() 44 TEST_FAILURE = auto() 45 46def get_kernel_root_path() -> str: 47 path = sys.argv[0] if not __file__ else __file__ 48 parts = os.path.realpath(path).split('tools/testing/kunit') 49 if len(parts) != 2: 50 sys.exit(1) 51 return parts[0] 52 53def config_tests(linux: kunit_kernel.LinuxSourceTree, 54 request: KunitConfigRequest) -> KunitResult: 55 kunit_parser.print_with_timestamp('Configuring KUnit Kernel ...') 56 57 config_start = time.time() 58 success = linux.build_reconfig(request.build_dir, request.make_options) 59 config_end = time.time() 60 if not success: 61 return KunitResult(KunitStatus.CONFIG_FAILURE, 62 'could not configure kernel', 63 config_end - config_start) 64 return KunitResult(KunitStatus.SUCCESS, 65 'configured kernel successfully', 66 config_end - config_start) 67 68def build_tests(linux: kunit_kernel.LinuxSourceTree, 69 request: KunitBuildRequest) -> KunitResult: 70 kunit_parser.print_with_timestamp('Building KUnit Kernel ...') 71 72 build_start = time.time() 73 success = linux.build_kernel(request.alltests, 74 request.jobs, 75 request.build_dir, 76 request.make_options) 77 build_end = time.time() 78 if not success: 79 return KunitResult(KunitStatus.BUILD_FAILURE, 80 'could not build kernel', 81 build_end - build_start) 82 if not success: 83 return KunitResult(KunitStatus.BUILD_FAILURE, 84 'could not build kernel', 85 build_end - build_start) 86 return KunitResult(KunitStatus.SUCCESS, 87 'built kernel successfully', 88 build_end - build_start) 89 90def exec_tests(linux: kunit_kernel.LinuxSourceTree, 91 request: KunitExecRequest) -> KunitResult: 92 kunit_parser.print_with_timestamp('Starting KUnit Kernel ...') 93 test_start = time.time() 94 result = linux.run_kernel( 95 timeout=None if request.alltests else request.timeout, 96 filter_glob=request.filter_glob, 97 build_dir=request.build_dir) 98 99 test_end = time.time() 100 101 return KunitResult(KunitStatus.SUCCESS, 102 result, 103 test_end - test_start) 104 105def parse_tests(request: KunitParseRequest) -> KunitResult: 106 parse_start = time.time() 107 108 test_result = kunit_parser.TestResult(kunit_parser.TestStatus.SUCCESS, 109 [], 110 'Tests not Parsed.') 111 112 if request.raw_output: 113 kunit_parser.raw_output(request.input_data) 114 else: 115 test_result = kunit_parser.parse_run_tests(request.input_data) 116 parse_end = time.time() 117 118 if request.json: 119 json_obj = kunit_json.get_json_result( 120 test_result=test_result, 121 def_config='kunit_defconfig', 122 build_dir=request.build_dir, 123 json_path=request.json) 124 if request.json == 'stdout': 125 print(json_obj) 126 127 if test_result.status != kunit_parser.TestStatus.SUCCESS: 128 return KunitResult(KunitStatus.TEST_FAILURE, test_result, 129 parse_end - parse_start) 130 131 return KunitResult(KunitStatus.SUCCESS, test_result, 132 parse_end - parse_start) 133 134 135def run_tests(linux: kunit_kernel.LinuxSourceTree, 136 request: KunitRequest) -> KunitResult: 137 run_start = time.time() 138 139 config_request = KunitConfigRequest(request.build_dir, 140 request.make_options) 141 config_result = config_tests(linux, config_request) 142 if config_result.status != KunitStatus.SUCCESS: 143 return config_result 144 145 build_request = KunitBuildRequest(request.jobs, request.build_dir, 146 request.alltests, 147 request.make_options) 148 build_result = build_tests(linux, build_request) 149 if build_result.status != KunitStatus.SUCCESS: 150 return build_result 151 152 exec_request = KunitExecRequest(request.timeout, request.build_dir, 153 request.alltests, request.filter_glob) 154 exec_result = exec_tests(linux, exec_request) 155 if exec_result.status != KunitStatus.SUCCESS: 156 return exec_result 157 158 parse_request = KunitParseRequest(request.raw_output, 159 exec_result.result, 160 request.build_dir, 161 request.json) 162 parse_result = parse_tests(parse_request) 163 164 run_end = time.time() 165 166 kunit_parser.print_with_timestamp(( 167 'Elapsed time: %.3fs total, %.3fs configuring, %.3fs ' + 168 'building, %.3fs running\n') % ( 169 run_end - run_start, 170 config_result.elapsed_time, 171 build_result.elapsed_time, 172 exec_result.elapsed_time)) 173 return parse_result 174 175def add_common_opts(parser) -> None: 176 parser.add_argument('--build_dir', 177 help='As in the make command, it specifies the build ' 178 'directory.', 179 type=str, default='.kunit', metavar='build_dir') 180 parser.add_argument('--make_options', 181 help='X=Y make option, can be repeated.', 182 action='append') 183 parser.add_argument('--alltests', 184 help='Run all KUnit tests through allyesconfig', 185 action='store_true') 186 parser.add_argument('--kunitconfig', 187 help='Path to Kconfig fragment that enables KUnit tests.' 188 ' If given a directory, (e.g. lib/kunit), "/.kunitconfig" ' 189 'will get automatically appended.', 190 metavar='kunitconfig') 191 192 parser.add_argument('--arch', 193 help=('Specifies the architecture to run tests under. ' 194 'The architecture specified here must match the ' 195 'string passed to the ARCH make param, ' 196 'e.g. i386, x86_64, arm, um, etc. Non-UML ' 197 'architectures run on QEMU.'), 198 type=str, default='um', metavar='arch') 199 200 parser.add_argument('--cross_compile', 201 help=('Sets make\'s CROSS_COMPILE variable; it should ' 202 'be set to a toolchain path prefix (the prefix ' 203 'of gcc and other tools in your toolchain, for ' 204 'example `sparc64-linux-gnu-` if you have the ' 205 'sparc toolchain installed on your system, or ' 206 '`$HOME/toolchains/microblaze/gcc-9.2.0-nolibc/microblaze-linux/bin/microblaze-linux-` ' 207 'if you have downloaded the microblaze toolchain ' 208 'from the 0-day website to a directory in your ' 209 'home directory called `toolchains`).'), 210 metavar='cross_compile') 211 212 parser.add_argument('--qemu_config', 213 help=('Takes a path to a path to a file containing ' 214 'a QemuArchParams object.'), 215 type=str, metavar='qemu_config') 216 217def add_build_opts(parser) -> None: 218 parser.add_argument('--jobs', 219 help='As in the make command, "Specifies the number of ' 220 'jobs (commands) to run simultaneously."', 221 type=int, default=8, metavar='jobs') 222 223def add_exec_opts(parser) -> None: 224 parser.add_argument('--timeout', 225 help='maximum number of seconds to allow for all tests ' 226 'to run. This does not include time taken to build the ' 227 'tests.', 228 type=int, 229 default=300, 230 metavar='timeout') 231 parser.add_argument('filter_glob', 232 help='maximum number of seconds to allow for all tests ' 233 'to run. This does not include time taken to build the ' 234 'tests.', 235 type=str, 236 nargs='?', 237 default='', 238 metavar='filter_glob') 239 240def add_parse_opts(parser) -> None: 241 parser.add_argument('--raw_output', help='don\'t format output from kernel', 242 action='store_true') 243 parser.add_argument('--json', 244 nargs='?', 245 help='Stores test results in a JSON, and either ' 246 'prints to stdout or saves to file if a ' 247 'filename is specified', 248 type=str, const='stdout', default=None) 249 250def main(argv, linux=None): 251 parser = argparse.ArgumentParser( 252 description='Helps writing and running KUnit tests.') 253 subparser = parser.add_subparsers(dest='subcommand') 254 255 # The 'run' command will config, build, exec, and parse in one go. 256 run_parser = subparser.add_parser('run', help='Runs KUnit tests.') 257 add_common_opts(run_parser) 258 add_build_opts(run_parser) 259 add_exec_opts(run_parser) 260 add_parse_opts(run_parser) 261 262 config_parser = subparser.add_parser('config', 263 help='Ensures that .config contains all of ' 264 'the options in .kunitconfig') 265 add_common_opts(config_parser) 266 267 build_parser = subparser.add_parser('build', help='Builds a kernel with KUnit tests') 268 add_common_opts(build_parser) 269 add_build_opts(build_parser) 270 271 exec_parser = subparser.add_parser('exec', help='Run a kernel with KUnit tests') 272 add_common_opts(exec_parser) 273 add_exec_opts(exec_parser) 274 add_parse_opts(exec_parser) 275 276 # The 'parse' option is special, as it doesn't need the kernel source 277 # (therefore there is no need for a build_dir, hence no add_common_opts) 278 # and the '--file' argument is not relevant to 'run', so isn't in 279 # add_parse_opts() 280 parse_parser = subparser.add_parser('parse', 281 help='Parses KUnit results from a file, ' 282 'and parses formatted results.') 283 add_parse_opts(parse_parser) 284 parse_parser.add_argument('file', 285 help='Specifies the file to read results from.', 286 type=str, nargs='?', metavar='input_file') 287 288 cli_args = parser.parse_args(argv) 289 290 if get_kernel_root_path(): 291 os.chdir(get_kernel_root_path()) 292 293 if cli_args.subcommand == 'run': 294 if not os.path.exists(cli_args.build_dir): 295 os.mkdir(cli_args.build_dir) 296 297 if not linux: 298 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, 299 kunitconfig_path=cli_args.kunitconfig, 300 arch=cli_args.arch, 301 cross_compile=cli_args.cross_compile, 302 qemu_config_path=cli_args.qemu_config) 303 304 request = KunitRequest(cli_args.raw_output, 305 cli_args.timeout, 306 cli_args.jobs, 307 cli_args.build_dir, 308 cli_args.alltests, 309 cli_args.filter_glob, 310 cli_args.json, 311 cli_args.make_options) 312 result = run_tests(linux, request) 313 if result.status != KunitStatus.SUCCESS: 314 sys.exit(1) 315 elif cli_args.subcommand == 'config': 316 if cli_args.build_dir and ( 317 not os.path.exists(cli_args.build_dir)): 318 os.mkdir(cli_args.build_dir) 319 320 if not linux: 321 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, 322 kunitconfig_path=cli_args.kunitconfig, 323 arch=cli_args.arch, 324 cross_compile=cli_args.cross_compile, 325 qemu_config_path=cli_args.qemu_config) 326 327 request = KunitConfigRequest(cli_args.build_dir, 328 cli_args.make_options) 329 result = config_tests(linux, request) 330 kunit_parser.print_with_timestamp(( 331 'Elapsed time: %.3fs\n') % ( 332 result.elapsed_time)) 333 if result.status != KunitStatus.SUCCESS: 334 sys.exit(1) 335 elif cli_args.subcommand == 'build': 336 if not linux: 337 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, 338 kunitconfig_path=cli_args.kunitconfig, 339 arch=cli_args.arch, 340 cross_compile=cli_args.cross_compile, 341 qemu_config_path=cli_args.qemu_config) 342 343 request = KunitBuildRequest(cli_args.jobs, 344 cli_args.build_dir, 345 cli_args.alltests, 346 cli_args.make_options) 347 result = build_tests(linux, request) 348 kunit_parser.print_with_timestamp(( 349 'Elapsed time: %.3fs\n') % ( 350 result.elapsed_time)) 351 if result.status != KunitStatus.SUCCESS: 352 sys.exit(1) 353 elif cli_args.subcommand == 'exec': 354 if not linux: 355 linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, 356 kunitconfig_path=cli_args.kunitconfig, 357 arch=cli_args.arch, 358 cross_compile=cli_args.cross_compile, 359 qemu_config_path=cli_args.qemu_config) 360 361 exec_request = KunitExecRequest(cli_args.timeout, 362 cli_args.build_dir, 363 cli_args.alltests, 364 cli_args.filter_glob) 365 exec_result = exec_tests(linux, exec_request) 366 parse_request = KunitParseRequest(cli_args.raw_output, 367 exec_result.result, 368 cli_args.build_dir, 369 cli_args.json) 370 result = parse_tests(parse_request) 371 kunit_parser.print_with_timestamp(( 372 'Elapsed time: %.3fs\n') % ( 373 exec_result.elapsed_time)) 374 if result.status != KunitStatus.SUCCESS: 375 sys.exit(1) 376 elif cli_args.subcommand == 'parse': 377 if cli_args.file == None: 378 kunit_output = sys.stdin 379 else: 380 with open(cli_args.file, 'r') as f: 381 kunit_output = f.read().splitlines() 382 request = KunitParseRequest(cli_args.raw_output, 383 kunit_output, 384 None, 385 cli_args.json) 386 result = parse_tests(request) 387 if result.status != KunitStatus.SUCCESS: 388 sys.exit(1) 389 else: 390 parser.print_help() 391 392if __name__ == '__main__': 393 main(sys.argv[1:]) 394