Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge tag 'linux-kselftest-kunit-fixes-5.11-rc5' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest

Pull kunit fixes from Shuah :
"Five fixes to the kunit tool and documentation from Daniel Latypov and
David Gow"

* tag 'linux-kselftest-kunit-fixes-5.11-rc5' of git://git.kernel.org/pub/scm/linux/kernel/git/shuah/linux-kselftest:
kunit: tool: move kunitconfig parsing into __init__, make it optional
kunit: tool: fix minor typing issue with None status
kunit: tool: surface and address more typing issues
Documentation: kunit: include example of a parameterized test
kunit: tool: Fix spelling of "diagnostic" in kunit_parser

+142 -95
+57
Documentation/dev-tools/kunit/usage.rst
··· 522 522 * E.g. if we wanted to also test ``sha256sum``, we could add a ``sha256`` 523 523 field and reuse ``cases``. 524 524 525 + * be converted to a "parameterized test", see below. 526 + 527 + Parameterized Testing 528 + ~~~~~~~~~~~~~~~~~~~~~ 529 + 530 + The table-driven testing pattern is common enough that KUnit has special 531 + support for it. 532 + 533 + Reusing the same ``cases`` array from above, we can write the test as a 534 + "parameterized test" with the following. 535 + 536 + .. code-block:: c 537 + 538 + // This is copy-pasted from above. 539 + struct sha1_test_case { 540 + const char *str; 541 + const char *sha1; 542 + }; 543 + struct sha1_test_case cases[] = { 544 + { 545 + .str = "hello world", 546 + .sha1 = "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed", 547 + }, 548 + { 549 + .str = "hello world!", 550 + .sha1 = "430ce34d020724ed75a196dfc2ad67c77772d169", 551 + }, 552 + }; 553 + 554 + // Need a helper function to generate a name for each test case. 555 + static void case_to_desc(const struct sha1_test_case *t, char *desc) 556 + { 557 + strcpy(desc, t->str); 558 + } 559 + // Creates `sha1_gen_params()` to iterate over `cases`. 560 + KUNIT_ARRAY_PARAM(sha1, cases, case_to_desc); 561 + 562 + // Looks no different from a normal test. 563 + static void sha1_test(struct kunit *test) 564 + { 565 + // This function can just contain the body of the for-loop. 566 + // The former `cases[i]` is accessible under test->param_value. 567 + char out[40]; 568 + struct sha1_test_case *test_param = (struct sha1_test_case *)(test->param_value); 569 + 570 + sha1sum(test_param->str, out); 571 + KUNIT_EXPECT_STREQ_MSG(test, (char *)out, test_param->sha1, 572 + "sha1sum(%s)", test_param->str); 573 + } 574 + 575 + // Instead of KUNIT_CASE, we use KUNIT_CASE_PARAM and pass in the 576 + // function declared by KUNIT_ARRAY_PARAM. 577 + static struct kunit_case sha1_test_cases[] = { 578 + KUNIT_CASE_PARAM(sha1_test, sha1_gen_params), 579 + {} 580 + }; 581 + 525 582 .. _kunit-on-non-uml: 526 583 527 584 KUnit on non-UML architectures
+11 -23
tools/testing/kunit/kunit.py
··· 43 43 BUILD_FAILURE = auto() 44 44 TEST_FAILURE = auto() 45 45 46 - def get_kernel_root_path(): 47 - parts = sys.argv[0] if not __file__ else __file__ 48 - parts = os.path.realpath(parts).split('tools/testing/kunit') 46 + def 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 49 if len(parts) != 2: 50 50 sys.exit(1) 51 51 return parts[0] ··· 171 171 exec_result.elapsed_time)) 172 172 return parse_result 173 173 174 - def add_common_opts(parser): 174 + def add_common_opts(parser) -> None: 175 175 parser.add_argument('--build_dir', 176 176 help='As in the make command, it specifies the build ' 177 177 'directory.', ··· 183 183 help='Run all KUnit tests through allyesconfig', 184 184 action='store_true') 185 185 186 - def add_build_opts(parser): 186 + def add_build_opts(parser) -> None: 187 187 parser.add_argument('--jobs', 188 188 help='As in the make command, "Specifies the number of ' 189 189 'jobs (commands) to run simultaneously."', 190 190 type=int, default=8, metavar='jobs') 191 191 192 - def add_exec_opts(parser): 192 + def add_exec_opts(parser) -> None: 193 193 parser.add_argument('--timeout', 194 194 help='maximum number of seconds to allow for all tests ' 195 195 'to run. This does not include time taken to build the ' ··· 198 198 default=300, 199 199 metavar='timeout') 200 200 201 - def add_parse_opts(parser): 201 + def add_parse_opts(parser) -> None: 202 202 parser.add_argument('--raw_output', help='don\'t format output from kernel', 203 203 action='store_true') 204 204 parser.add_argument('--json', ··· 256 256 os.mkdir(cli_args.build_dir) 257 257 258 258 if not linux: 259 - linux = kunit_kernel.LinuxSourceTree() 260 - 261 - linux.create_kunitconfig(cli_args.build_dir) 262 - linux.read_kunitconfig(cli_args.build_dir) 259 + linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir) 263 260 264 261 request = KunitRequest(cli_args.raw_output, 265 262 cli_args.timeout, ··· 274 277 os.mkdir(cli_args.build_dir) 275 278 276 279 if not linux: 277 - linux = kunit_kernel.LinuxSourceTree() 278 - 279 - linux.create_kunitconfig(cli_args.build_dir) 280 - linux.read_kunitconfig(cli_args.build_dir) 280 + linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir) 281 281 282 282 request = KunitConfigRequest(cli_args.build_dir, 283 283 cli_args.make_options) ··· 286 292 sys.exit(1) 287 293 elif cli_args.subcommand == 'build': 288 294 if not linux: 289 - linux = kunit_kernel.LinuxSourceTree() 290 - 291 - linux.create_kunitconfig(cli_args.build_dir) 292 - linux.read_kunitconfig(cli_args.build_dir) 295 + linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir) 293 296 294 297 request = KunitBuildRequest(cli_args.jobs, 295 298 cli_args.build_dir, ··· 300 309 sys.exit(1) 301 310 elif cli_args.subcommand == 'exec': 302 311 if not linux: 303 - linux = kunit_kernel.LinuxSourceTree() 304 - 305 - linux.create_kunitconfig(cli_args.build_dir) 306 - linux.read_kunitconfig(cli_args.build_dir) 312 + linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir) 307 313 308 314 exec_request = KunitExecRequest(cli_args.timeout, 309 315 cli_args.build_dir,
+4 -3
tools/testing/kunit/kunit_config.py
··· 8 8 9 9 import collections 10 10 import re 11 + from typing import List, Set 11 12 12 13 CONFIG_IS_NOT_SET_PATTERN = r'^# CONFIG_(\w+) is not set$' 13 14 CONFIG_PATTERN = r'^CONFIG_(\w+)=(\S+|".*")$' ··· 31 30 class Kconfig(object): 32 31 """Represents defconfig or .config specified using the Kconfig language.""" 33 32 34 - def __init__(self): 35 - self._entries = [] 33 + def __init__(self) -> None: 34 + self._entries = [] # type: List[KconfigEntry] 36 35 37 - def entries(self): 36 + def entries(self) -> Set[KconfigEntry]: 38 37 return set(self._entries) 39 38 40 39 def add_entry(self, entry: KconfigEntry) -> None:
+1 -1
tools/testing/kunit/kunit_json.py
··· 13 13 14 14 from kunit_parser import TestStatus 15 15 16 - def get_json_result(test_result, def_config, build_dir, json_path): 16 + def get_json_result(test_result, def_config, build_dir, json_path) -> str: 17 17 sub_groups = [] 18 18 19 19 # Each test suite is mapped to a KernelCI sub_group
+29 -27
tools/testing/kunit/kunit_kernel.py
··· 11 11 import os 12 12 import shutil 13 13 import signal 14 + from typing import Iterator 14 15 15 16 from contextlib import ExitStack 16 17 ··· 40 39 class LinuxSourceTreeOperations(object): 41 40 """An abstraction over command line operations performed on a source tree.""" 42 41 43 - def make_mrproper(self): 42 + def make_mrproper(self) -> None: 44 43 try: 45 44 subprocess.check_output(['make', 'mrproper'], stderr=subprocess.STDOUT) 46 45 except OSError as e: ··· 48 47 except subprocess.CalledProcessError as e: 49 48 raise ConfigError(e.output.decode()) 50 49 51 - def make_olddefconfig(self, build_dir, make_options): 50 + def make_olddefconfig(self, build_dir, make_options) -> None: 52 51 command = ['make', 'ARCH=um', 'olddefconfig'] 53 52 if make_options: 54 53 command.extend(make_options) ··· 61 60 except subprocess.CalledProcessError as e: 62 61 raise ConfigError(e.output.decode()) 63 62 64 - def make_allyesconfig(self, build_dir, make_options): 63 + def make_allyesconfig(self, build_dir, make_options) -> None: 65 64 kunit_parser.print_with_timestamp( 66 65 'Enabling all CONFIGs for UML...') 67 66 command = ['make', 'ARCH=um', 'allyesconfig'] ··· 83 82 kunit_parser.print_with_timestamp( 84 83 'Starting Kernel with all configs takes a few minutes...') 85 84 86 - def make(self, jobs, build_dir, make_options): 85 + def make(self, jobs, build_dir, make_options) -> None: 87 86 command = ['make', 'ARCH=um', '--jobs=' + str(jobs)] 88 87 if make_options: 89 88 command.extend(make_options) ··· 101 100 if stderr: # likely only due to build warnings 102 101 print(stderr.decode()) 103 102 104 - def linux_bin(self, params, timeout, build_dir): 103 + def linux_bin(self, params, timeout, build_dir) -> None: 105 104 """Runs the Linux UML binary. Must be named 'linux'.""" 106 105 linux_bin = get_file_path(build_dir, 'linux') 107 106 outfile = get_outfile_path(build_dir) ··· 111 110 stderr=subprocess.STDOUT) 112 111 process.wait(timeout) 113 112 114 - def get_kconfig_path(build_dir): 113 + def get_kconfig_path(build_dir) -> str: 115 114 return get_file_path(build_dir, KCONFIG_PATH) 116 115 117 - def get_kunitconfig_path(build_dir): 116 + def get_kunitconfig_path(build_dir) -> str: 118 117 return get_file_path(build_dir, KUNITCONFIG_PATH) 119 118 120 - def get_outfile_path(build_dir): 119 + def get_outfile_path(build_dir) -> str: 121 120 return get_file_path(build_dir, OUTFILE_PATH) 122 121 123 122 class LinuxSourceTree(object): 124 123 """Represents a Linux kernel source tree with KUnit tests.""" 125 124 126 - def __init__(self): 127 - self._ops = LinuxSourceTreeOperations() 125 + def __init__(self, build_dir: str, load_config=True, defconfig=DEFAULT_KUNITCONFIG_PATH) -> None: 128 126 signal.signal(signal.SIGINT, self.signal_handler) 129 127 130 - def clean(self): 128 + self._ops = LinuxSourceTreeOperations() 129 + 130 + if not load_config: 131 + return 132 + 133 + kunitconfig_path = get_kunitconfig_path(build_dir) 134 + if not os.path.exists(kunitconfig_path): 135 + shutil.copyfile(defconfig, kunitconfig_path) 136 + 137 + self._kconfig = kunit_config.Kconfig() 138 + self._kconfig.read_from_file(kunitconfig_path) 139 + 140 + def clean(self) -> bool: 131 141 try: 132 142 self._ops.make_mrproper() 133 143 except ConfigError as e: ··· 146 134 return False 147 135 return True 148 136 149 - def create_kunitconfig(self, build_dir, defconfig=DEFAULT_KUNITCONFIG_PATH): 150 - kunitconfig_path = get_kunitconfig_path(build_dir) 151 - if not os.path.exists(kunitconfig_path): 152 - shutil.copyfile(defconfig, kunitconfig_path) 153 - 154 - def read_kunitconfig(self, build_dir): 155 - kunitconfig_path = get_kunitconfig_path(build_dir) 156 - self._kconfig = kunit_config.Kconfig() 157 - self._kconfig.read_from_file(kunitconfig_path) 158 - 159 - def validate_config(self, build_dir): 137 + def validate_config(self, build_dir) -> bool: 160 138 kconfig_path = get_kconfig_path(build_dir) 161 139 validated_kconfig = kunit_config.Kconfig() 162 140 validated_kconfig.read_from_file(kconfig_path) ··· 160 158 return False 161 159 return True 162 160 163 - def build_config(self, build_dir, make_options): 161 + def build_config(self, build_dir, make_options) -> bool: 164 162 kconfig_path = get_kconfig_path(build_dir) 165 163 if build_dir and not os.path.exists(build_dir): 166 164 os.mkdir(build_dir) ··· 172 170 return False 173 171 return self.validate_config(build_dir) 174 172 175 - def build_reconfig(self, build_dir, make_options): 173 + def build_reconfig(self, build_dir, make_options) -> bool: 176 174 """Creates a new .config if it is not a subset of the .kunitconfig.""" 177 175 kconfig_path = get_kconfig_path(build_dir) 178 176 if os.path.exists(kconfig_path): ··· 188 186 print('Generating .config ...') 189 187 return self.build_config(build_dir, make_options) 190 188 191 - def build_um_kernel(self, alltests, jobs, build_dir, make_options): 189 + def build_um_kernel(self, alltests, jobs, build_dir, make_options) -> bool: 192 190 try: 193 191 if alltests: 194 192 self._ops.make_allyesconfig(build_dir, make_options) ··· 199 197 return False 200 198 return self.validate_config(build_dir) 201 199 202 - def run_kernel(self, args=[], build_dir='', timeout=None): 200 + def run_kernel(self, args=[], build_dir='', timeout=None) -> Iterator[str]: 203 201 args.extend(['mem=1G', 'console=tty']) 204 202 self._ops.linux_bin(args, timeout, build_dir) 205 203 outfile = get_outfile_path(build_dir) ··· 208 206 for line in file: 209 207 yield line 210 208 211 - def signal_handler(self, sig, frame): 209 + def signal_handler(self, sig, frame) -> None: 212 210 logging.error('Build interruption occurred. Cleaning console.') 213 211 subprocess.call(['stty', 'sane'])
+40 -41
tools/testing/kunit/kunit_parser.py
··· 12 12 from datetime import datetime 13 13 from enum import Enum, auto 14 14 from functools import reduce 15 - from typing import List, Optional, Tuple 15 + from typing import Iterable, Iterator, List, Optional, Tuple 16 16 17 17 TestResult = namedtuple('TestResult', ['status','suites','log']) 18 18 19 19 class TestSuite(object): 20 - def __init__(self): 21 - self.status = None 22 - self.name = None 23 - self.cases = [] 20 + def __init__(self) -> None: 21 + self.status = TestStatus.SUCCESS 22 + self.name = '' 23 + self.cases = [] # type: List[TestCase] 24 24 25 - def __str__(self): 26 - return 'TestSuite(' + self.status + ',' + self.name + ',' + str(self.cases) + ')' 25 + def __str__(self) -> str: 26 + return 'TestSuite(' + str(self.status) + ',' + self.name + ',' + str(self.cases) + ')' 27 27 28 - def __repr__(self): 28 + def __repr__(self) -> str: 29 29 return str(self) 30 30 31 31 class TestCase(object): 32 - def __init__(self): 33 - self.status = None 32 + def __init__(self) -> None: 33 + self.status = TestStatus.SUCCESS 34 34 self.name = '' 35 - self.log = [] 35 + self.log = [] # type: List[str] 36 36 37 - def __str__(self): 38 - return 'TestCase(' + self.status + ',' + self.name + ',' + str(self.log) + ')' 37 + def __str__(self) -> str: 38 + return 'TestCase(' + str(self.status) + ',' + self.name + ',' + str(self.log) + ')' 39 39 40 - def __repr__(self): 40 + def __repr__(self) -> str: 41 41 return str(self) 42 42 43 43 class TestStatus(Enum): ··· 51 51 kunit_end_re = re.compile('(List of all partitions:|' 52 52 'Kernel panic - not syncing: VFS:)') 53 53 54 - def isolate_kunit_output(kernel_output): 54 + def isolate_kunit_output(kernel_output) -> Iterator[str]: 55 55 started = False 56 56 for line in kernel_output: 57 57 line = line.rstrip() # line always has a trailing \n ··· 64 64 elif started: 65 65 yield line[prefix_len:] if prefix_len > 0 else line 66 66 67 - def raw_output(kernel_output): 67 + def raw_output(kernel_output) -> None: 68 68 for line in kernel_output: 69 69 print(line.rstrip()) 70 70 ··· 72 72 73 73 RESET = '\033[0;0m' 74 74 75 - def red(text): 75 + def red(text) -> str: 76 76 return '\033[1;31m' + text + RESET 77 77 78 - def yellow(text): 78 + def yellow(text) -> str: 79 79 return '\033[1;33m' + text + RESET 80 80 81 - def green(text): 81 + def green(text) -> str: 82 82 return '\033[1;32m' + text + RESET 83 83 84 - def print_with_timestamp(message): 84 + def print_with_timestamp(message) -> None: 85 85 print('[%s] %s' % (datetime.now().strftime('%H:%M:%S'), message)) 86 86 87 - def format_suite_divider(message): 87 + def format_suite_divider(message) -> str: 88 88 return '======== ' + message + ' ========' 89 89 90 - def print_suite_divider(message): 90 + def print_suite_divider(message) -> None: 91 91 print_with_timestamp(DIVIDER) 92 92 print_with_timestamp(format_suite_divider(message)) 93 93 94 - def print_log(log): 94 + def print_log(log) -> None: 95 95 for m in log: 96 96 print_with_timestamp(m) 97 97 98 98 TAP_ENTRIES = re.compile(r'^(TAP|[\s]*ok|[\s]*not ok|[\s]*[0-9]+\.\.[0-9]+|[\s]*#).*$') 99 99 100 - def consume_non_diagnositic(lines: List[str]) -> None: 100 + def consume_non_diagnostic(lines: List[str]) -> None: 101 101 while lines and not TAP_ENTRIES.match(lines[0]): 102 102 lines.pop(0) 103 103 104 - def save_non_diagnositic(lines: List[str], test_case: TestCase) -> None: 104 + def save_non_diagnostic(lines: List[str], test_case: TestCase) -> None: 105 105 while lines and not TAP_ENTRIES.match(lines[0]): 106 106 test_case.log.append(lines[0]) 107 107 lines.pop(0) ··· 113 113 OK_NOT_OK_MODULE = re.compile(r'^(ok|not ok) ([0-9]+) - (.*)$') 114 114 115 115 def parse_ok_not_ok_test_case(lines: List[str], test_case: TestCase) -> bool: 116 - save_non_diagnositic(lines, test_case) 116 + save_non_diagnostic(lines, test_case) 117 117 if not lines: 118 118 test_case.status = TestStatus.TEST_CRASHED 119 119 return True ··· 139 139 DIAGNOSTIC_CRASH_MESSAGE = re.compile(r'^[\s]+# .*?: kunit test case crashed!$') 140 140 141 141 def parse_diagnostic(lines: List[str], test_case: TestCase) -> bool: 142 - save_non_diagnositic(lines, test_case) 142 + save_non_diagnostic(lines, test_case) 143 143 if not lines: 144 144 return False 145 145 line = lines[0] ··· 155 155 156 156 def parse_test_case(lines: List[str]) -> Optional[TestCase]: 157 157 test_case = TestCase() 158 - save_non_diagnositic(lines, test_case) 158 + save_non_diagnostic(lines, test_case) 159 159 while parse_diagnostic(lines, test_case): 160 160 pass 161 161 if parse_ok_not_ok_test_case(lines, test_case): ··· 166 166 SUBTEST_HEADER = re.compile(r'^[\s]+# Subtest: (.*)$') 167 167 168 168 def parse_subtest_header(lines: List[str]) -> Optional[str]: 169 - consume_non_diagnositic(lines) 169 + consume_non_diagnostic(lines) 170 170 if not lines: 171 171 return None 172 172 match = SUBTEST_HEADER.match(lines[0]) ··· 179 179 SUBTEST_PLAN = re.compile(r'[\s]+[0-9]+\.\.([0-9]+)') 180 180 181 181 def parse_subtest_plan(lines: List[str]) -> Optional[int]: 182 - consume_non_diagnositic(lines) 182 + consume_non_diagnostic(lines) 183 183 match = SUBTEST_PLAN.match(lines[0]) 184 184 if match: 185 185 lines.pop(0) ··· 202 202 def parse_ok_not_ok_test_suite(lines: List[str], 203 203 test_suite: TestSuite, 204 204 expected_suite_index: int) -> bool: 205 - consume_non_diagnositic(lines) 205 + consume_non_diagnostic(lines) 206 206 if not lines: 207 207 test_suite.status = TestStatus.TEST_CRASHED 208 208 return False ··· 224 224 else: 225 225 return False 226 226 227 - def bubble_up_errors(to_status, status_container_list) -> TestStatus: 228 - status_list = map(to_status, status_container_list) 229 - return reduce(max_status, status_list, TestStatus.SUCCESS) 227 + def bubble_up_errors(statuses: Iterable[TestStatus]) -> TestStatus: 228 + return reduce(max_status, statuses, TestStatus.SUCCESS) 230 229 231 230 def bubble_up_test_case_errors(test_suite: TestSuite) -> TestStatus: 232 - max_test_case_status = bubble_up_errors(lambda x: x.status, test_suite.cases) 231 + max_test_case_status = bubble_up_errors(x.status for x in test_suite.cases) 233 232 return max_status(max_test_case_status, test_suite.status) 234 233 235 234 def parse_test_suite(lines: List[str], expected_suite_index: int) -> Optional[TestSuite]: 236 235 if not lines: 237 236 return None 238 - consume_non_diagnositic(lines) 237 + consume_non_diagnostic(lines) 239 238 test_suite = TestSuite() 240 239 test_suite.status = TestStatus.SUCCESS 241 240 name = parse_subtest_header(lines) ··· 263 264 TAP_HEADER = re.compile(r'^TAP version 14$') 264 265 265 266 def parse_tap_header(lines: List[str]) -> bool: 266 - consume_non_diagnositic(lines) 267 + consume_non_diagnostic(lines) 267 268 if TAP_HEADER.match(lines[0]): 268 269 lines.pop(0) 269 270 return True ··· 273 274 TEST_PLAN = re.compile(r'[0-9]+\.\.([0-9]+)') 274 275 275 276 def parse_test_plan(lines: List[str]) -> Optional[int]: 276 - consume_non_diagnositic(lines) 277 + consume_non_diagnostic(lines) 277 278 match = TEST_PLAN.match(lines[0]) 278 279 if match: 279 280 lines.pop(0) ··· 281 282 else: 282 283 return None 283 284 284 - def bubble_up_suite_errors(test_suite_list: List[TestSuite]) -> TestStatus: 285 - return bubble_up_errors(lambda x: x.status, test_suite_list) 285 + def bubble_up_suite_errors(test_suites: Iterable[TestSuite]) -> TestStatus: 286 + return bubble_up_errors(x.status for x in test_suites) 286 287 287 288 def parse_test_result(lines: List[str]) -> TestResult: 288 - consume_non_diagnositic(lines) 289 + consume_non_diagnostic(lines) 289 290 if not lines or not parse_tap_header(lines): 290 291 return TestResult(TestStatus.NO_TESTS, [], lines) 291 292 expected_test_suite_num = parse_test_plan(lines)