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

kunit: tool: make --kunitconfig repeatable, blindly concat

It's come up a few times that it would be useful to have --kunitconfig
be repeatable [1][2].

This could be done before with a bit of shell-fu, e.g.
$ find fs/ -name '.kunitconfig' -exec cat {} + | \
./tools/testing/kunit/kunit.py run --kunitconfig=/dev/stdin
or equivalently:
$ cat fs/ext4/.kunitconfig fs/fat/.kunitconfig | \
./tools/testing/kunit/kunit.py run --kunitconfig=/dev/stdin

But this can be fairly clunky to use in practice.

And having explicit support in kunit.py opens the door to having more
config fragments of interest, e.g. options for PCI on UML [1], UML
coverage [2], variants of tests [3].
There's another argument to be made that users can just use multiple
--kconfig_add's, but this gets very clunky very fast (e.g. [2]).

Note: there's a big caveat here that some kconfig options might be
incompatible. We try to give a clearish error message in the simple case
where the same option appears multiple times with conflicting values,
but more subtle ones (e.g. mutually exclusive options) will be
potentially very confusing for the user. I don't know we can do better.

Note 2: if you want to combine a --kunitconfig with the default, you
either have to do to specify the current build_dir
> --kunitconfig=.kunit --kunitconfig=additional.config
or
> --kunitconfig=tools/testing/kunit/configs/default.config --kunitconifg=additional.config
each of which have their downsides (former depends on --build_dir,
doesn't work if you don't have a .kunitconfig yet), etc.

Example with conflicting values:
> $ ./tools/testing/kunit/kunit.py config --kunitconfig=lib/kunit --kunitconfig=/dev/stdin <<EOF
> CONFIG_KUNIT_TEST=n
> CONFIG_KUNIT=m
> EOF
> ...
> kunit_kernel.ConfigError: Multiple values specified for 2 options in kunitconfig:
> CONFIG_KUNIT=y
> vs from /dev/stdin
> CONFIG_KUNIT=m
>
> CONFIG_KUNIT_TEST=y
> vs from /dev/stdin
> # CONFIG_KUNIT_TEST is not set

[1] https://lists.freedesktop.org/archives/dri-devel/2022-June/357616.html
[2] https://lore.kernel.org/linux-kselftest/CAFd5g45f3X3xF2vz2BkTHRqOC4uW6GZxtUUMaP5mwwbK8uNVtA@mail.gmail.com/
[3] https://lore.kernel.org/linux-kselftest/CANpmjNOdSy6DuO6CYZ4UxhGxqhjzx4tn0sJMbRqo2xRFv9kX6Q@mail.gmail.com/

Signed-off-by: Daniel Latypov <dlatypov@google.com>
Reviewed-by: Brendan Higgins <brendanhiggins@google.com>
Signed-off-by: Shuah Khan <skhan@linuxfoundation.org>

authored by

Daniel Latypov and committed by
Shuah Khan
53b46621 1d202d14

+89 -23
+4 -3
tools/testing/kunit/kunit.py
··· 293 293 parser.add_argument('--kunitconfig', 294 294 help='Path to Kconfig fragment that enables KUnit tests.' 295 295 ' If given a directory, (e.g. lib/kunit), "/.kunitconfig" ' 296 - 'will get automatically appended.', 297 - metavar='PATH') 296 + 'will get automatically appended. If repeated, the files ' 297 + 'blindly concatenated, which might not work in all cases.', 298 + action='append', metavar='PATHS') 298 299 parser.add_argument('--kconfig_add', 299 300 help='Additional Kconfig options to append to the ' 300 301 '.kunitconfig, e.g. CONFIG_KASAN=y. Can be repeated.', ··· 382 381 qemu_args.extend(shlex.split(arg)) 383 382 384 383 return kunit_kernel.LinuxSourceTree(cli_args.build_dir, 385 - kunitconfig_path=cli_args.kunitconfig, 384 + kunitconfig_paths=cli_args.kunitconfig, 386 385 kconfig_add=cli_args.kconfig_add, 387 386 arch=cli_args.arch, 388 387 cross_compile=cli_args.cross_compile,
+10 -1
tools/testing/kunit/kunit_config.py
··· 8 8 9 9 from dataclasses import dataclass 10 10 import re 11 - from typing import Dict, Iterable, Set 11 + from typing import Dict, Iterable, List, Set, Tuple 12 12 13 13 CONFIG_IS_NOT_SET_PATTERN = r'^# CONFIG_(\w+) is not set$' 14 14 CONFIG_PATTERN = r'^CONFIG_(\w+)=(\S+|".*")$' ··· 59 59 if value != b: 60 60 return False 61 61 return True 62 + 63 + def conflicting_options(self, other: 'Kconfig') -> List[Tuple[KconfigEntry, KconfigEntry]]: 64 + diff = [] # type: List[Tuple[KconfigEntry, KconfigEntry]] 65 + for name, value in self._entries.items(): 66 + b = other._entries.get(name) 67 + if b and value != b: 68 + pair = (KconfigEntry(name, value), KconfigEntry(name, b)) 69 + diff.append(pair) 70 + return diff 62 71 63 72 def merge_in_entries(self, other: 'Kconfig') -> None: 64 73 for name, value in other._entries.items():
+26 -12
tools/testing/kunit/kunit_kernel.py
··· 177 177 def get_old_kunitconfig_path(build_dir: str) -> str: 178 178 return os.path.join(build_dir, OLD_KUNITCONFIG_PATH) 179 179 180 + def get_parsed_kunitconfig(build_dir: str, 181 + kunitconfig_paths: Optional[List[str]]=None) -> kunit_config.Kconfig: 182 + if not kunitconfig_paths: 183 + path = get_kunitconfig_path(build_dir) 184 + if not os.path.exists(path): 185 + shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, path) 186 + return kunit_config.parse_file(path) 187 + 188 + merged = kunit_config.Kconfig() 189 + 190 + for path in kunitconfig_paths: 191 + if os.path.isdir(path): 192 + path = os.path.join(path, KUNITCONFIG_PATH) 193 + if not os.path.exists(path): 194 + raise ConfigError(f'Specified kunitconfig ({path}) does not exist') 195 + 196 + partial = kunit_config.parse_file(path) 197 + diff = merged.conflicting_options(partial) 198 + if diff: 199 + diff_str = '\n\n'.join(f'{a}\n vs from {path}\n{b}' for a, b in diff) 200 + raise ConfigError(f'Multiple values specified for {len(diff)} options in kunitconfig:\n{diff_str}') 201 + merged.merge_in_entries(partial) 202 + return merged 203 + 180 204 def get_outfile_path(build_dir: str) -> str: 181 205 return os.path.join(build_dir, OUTFILE_PATH) 182 206 ··· 245 221 def __init__( 246 222 self, 247 223 build_dir: str, 248 - kunitconfig_path='', 224 + kunitconfig_paths: Optional[List[str]]=None, 249 225 kconfig_add: Optional[List[str]]=None, 250 226 arch=None, 251 227 cross_compile=None, ··· 262 238 qemu_config_path = _default_qemu_config_path(self._arch) 263 239 _, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile) 264 240 265 - if kunitconfig_path: 266 - if os.path.isdir(kunitconfig_path): 267 - kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH) 268 - if not os.path.exists(kunitconfig_path): 269 - raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist') 270 - else: 271 - kunitconfig_path = get_kunitconfig_path(build_dir) 272 - if not os.path.exists(kunitconfig_path): 273 - shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path) 274 - 275 - self._kconfig = kunit_config.parse_file(kunitconfig_path) 241 + self._kconfig = get_parsed_kunitconfig(build_dir, kunitconfig_paths) 276 242 if kconfig_add: 277 243 kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add)) 278 244 self._kconfig.merge_in_entries(kconfig)
+49 -7
tools/testing/kunit/kunit_tool_test.py
··· 356 356 357 357 def test_invalid_kunitconfig(self): 358 358 with self.assertRaisesRegex(kunit_kernel.ConfigError, 'nonexistent.* does not exist'): 359 - kunit_kernel.LinuxSourceTree('', kunitconfig_path='/nonexistent_file') 359 + kunit_kernel.LinuxSourceTree('', kunitconfig_paths=['/nonexistent_file']) 360 360 361 361 def test_valid_kunitconfig(self): 362 362 with tempfile.NamedTemporaryFile('wt') as kunitconfig: 363 - kunit_kernel.LinuxSourceTree('', kunitconfig_path=kunitconfig.name) 363 + kunit_kernel.LinuxSourceTree('', kunitconfig_paths=[kunitconfig.name]) 364 364 365 365 def test_dir_kunitconfig(self): 366 366 with tempfile.TemporaryDirectory('') as dir: 367 367 with open(os.path.join(dir, '.kunitconfig'), 'w'): 368 368 pass 369 - kunit_kernel.LinuxSourceTree('', kunitconfig_path=dir) 369 + kunit_kernel.LinuxSourceTree('', kunitconfig_paths=[dir]) 370 + 371 + def test_multiple_kunitconfig(self): 372 + want_kconfig = kunit_config.Kconfig() 373 + want_kconfig.add_entry('KUNIT', 'y') 374 + want_kconfig.add_entry('KUNIT_TEST', 'm') 375 + 376 + with tempfile.TemporaryDirectory('') as dir: 377 + other = os.path.join(dir, 'otherkunitconfig') 378 + with open(os.path.join(dir, '.kunitconfig'), 'w') as f: 379 + f.write('CONFIG_KUNIT=y') 380 + with open(other, 'w') as f: 381 + f.write('CONFIG_KUNIT_TEST=m') 382 + pass 383 + 384 + tree = kunit_kernel.LinuxSourceTree('', kunitconfig_paths=[dir, other]) 385 + self.assertTrue(want_kconfig.is_subset_of(tree._kconfig), msg=tree._kconfig) 386 + 387 + 388 + def test_multiple_kunitconfig_invalid(self): 389 + with tempfile.TemporaryDirectory('') as dir: 390 + other = os.path.join(dir, 'otherkunitconfig') 391 + with open(os.path.join(dir, '.kunitconfig'), 'w') as f: 392 + f.write('CONFIG_KUNIT=y') 393 + with open(other, 'w') as f: 394 + f.write('CONFIG_KUNIT=m') 395 + 396 + with self.assertRaisesRegex(kunit_kernel.ConfigError, '(?s)Multiple values.*CONFIG_KUNIT'): 397 + kunit_kernel.LinuxSourceTree('', kunitconfig_paths=[dir, other]) 398 + 370 399 371 400 def test_kconfig_add(self): 372 401 want_kconfig = kunit_config.Kconfig() ··· 665 636 kunit.main(['run', '--kunitconfig=mykunitconfig']) 666 637 # Just verify that we parsed and initialized it correctly here. 667 638 self.mock_linux_init.assert_called_once_with('.kunit', 668 - kunitconfig_path='mykunitconfig', 639 + kunitconfig_paths=['mykunitconfig'], 669 640 kconfig_add=None, 670 641 arch='um', 671 642 cross_compile=None, ··· 676 647 kunit.main(['config', '--kunitconfig=mykunitconfig']) 677 648 # Just verify that we parsed and initialized it correctly here. 678 649 self.mock_linux_init.assert_called_once_with('.kunit', 679 - kunitconfig_path='mykunitconfig', 650 + kunitconfig_paths=['mykunitconfig'], 680 651 kconfig_add=None, 681 652 arch='um', 682 653 cross_compile=None, 683 654 qemu_config_path=None, 684 655 extra_qemu_args=[]) 685 656 657 + @mock.patch.object(kunit_kernel, 'LinuxSourceTree') 658 + def test_run_multiple_kunitconfig(self, mock_linux_init): 659 + mock_linux_init.return_value = self.linux_source_mock 660 + kunit.main(['run', '--kunitconfig=mykunitconfig', '--kunitconfig=other']) 661 + # Just verify that we parsed and initialized it correctly here. 662 + mock_linux_init.assert_called_once_with('.kunit', 663 + kunitconfig_paths=['mykunitconfig', 'other'], 664 + kconfig_add=None, 665 + arch='um', 666 + cross_compile=None, 667 + qemu_config_path=None, 668 + extra_qemu_args=[]) 669 + 686 670 def test_run_kconfig_add(self): 687 671 kunit.main(['run', '--kconfig_add=CONFIG_KASAN=y', '--kconfig_add=CONFIG_KCSAN=y']) 688 672 # Just verify that we parsed and initialized it correctly here. 689 673 self.mock_linux_init.assert_called_once_with('.kunit', 690 - kunitconfig_path=None, 674 + kunitconfig_paths=None, 691 675 kconfig_add=['CONFIG_KASAN=y', 'CONFIG_KCSAN=y'], 692 676 arch='um', 693 677 cross_compile=None, ··· 711 669 kunit.main(['run', '--arch=x86_64', '--qemu_args', '-m 2048']) 712 670 # Just verify that we parsed and initialized it correctly here. 713 671 self.mock_linux_init.assert_called_once_with('.kunit', 714 - kunitconfig_path=None, 672 + kunitconfig_paths=None, 715 673 kconfig_add=None, 716 674 arch='x86_64', 717 675 cross_compile=None,