Skip to content

Commit

Permalink
kunit: tool: make --kunitconfig repeatable, blindly concat
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Daniel Latypov authored and Shuah Khan committed Jul 8, 2022
1 parent 1d202d1 commit 53b4662
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 23 deletions.
7 changes: 4 additions & 3 deletions tools/testing/kunit/kunit.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,9 @@ def add_common_opts(parser) -> None:
parser.add_argument('--kunitconfig',
help='Path to Kconfig fragment that enables KUnit tests.'
' If given a directory, (e.g. lib/kunit), "/.kunitconfig" '
'will get automatically appended.',
metavar='PATH')
'will get automatically appended. If repeated, the files '
'blindly concatenated, which might not work in all cases.',
action='append', metavar='PATHS')
parser.add_argument('--kconfig_add',
help='Additional Kconfig options to append to the '
'.kunitconfig, e.g. CONFIG_KASAN=y. Can be repeated.',
Expand Down Expand Up @@ -381,7 +382,7 @@ def tree_from_args(cli_args: argparse.Namespace) -> kunit_kernel.LinuxSourceTree
qemu_args.extend(shlex.split(arg))

return kunit_kernel.LinuxSourceTree(cli_args.build_dir,
kunitconfig_path=cli_args.kunitconfig,
kunitconfig_paths=cli_args.kunitconfig,
kconfig_add=cli_args.kconfig_add,
arch=cli_args.arch,
cross_compile=cli_args.cross_compile,
Expand Down
11 changes: 10 additions & 1 deletion tools/testing/kunit/kunit_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from dataclasses import dataclass
import re
from typing import Dict, Iterable, Set
from typing import Dict, Iterable, List, Set, Tuple

CONFIG_IS_NOT_SET_PATTERN = r'^# CONFIG_(\w+) is not set$'
CONFIG_PATTERN = r'^CONFIG_(\w+)=(\S+|".*")$'
Expand Down Expand Up @@ -60,6 +60,15 @@ def is_subset_of(self, other: 'Kconfig') -> bool:
return False
return True

def conflicting_options(self, other: 'Kconfig') -> List[Tuple[KconfigEntry, KconfigEntry]]:
diff = [] # type: List[Tuple[KconfigEntry, KconfigEntry]]
for name, value in self._entries.items():
b = other._entries.get(name)
if b and value != b:
pair = (KconfigEntry(name, value), KconfigEntry(name, b))
diff.append(pair)
return diff

def merge_in_entries(self, other: 'Kconfig') -> None:
for name, value in other._entries.items():
self._entries[name] = value
Expand Down
38 changes: 26 additions & 12 deletions tools/testing/kunit/kunit_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,30 @@ def get_kunitconfig_path(build_dir: str) -> str:
def get_old_kunitconfig_path(build_dir: str) -> str:
return os.path.join(build_dir, OLD_KUNITCONFIG_PATH)

def get_parsed_kunitconfig(build_dir: str,
kunitconfig_paths: Optional[List[str]]=None) -> kunit_config.Kconfig:
if not kunitconfig_paths:
path = get_kunitconfig_path(build_dir)
if not os.path.exists(path):
shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, path)
return kunit_config.parse_file(path)

merged = kunit_config.Kconfig()

for path in kunitconfig_paths:
if os.path.isdir(path):
path = os.path.join(path, KUNITCONFIG_PATH)
if not os.path.exists(path):
raise ConfigError(f'Specified kunitconfig ({path}) does not exist')

partial = kunit_config.parse_file(path)
diff = merged.conflicting_options(partial)
if diff:
diff_str = '\n\n'.join(f'{a}\n vs from {path}\n{b}' for a, b in diff)
raise ConfigError(f'Multiple values specified for {len(diff)} options in kunitconfig:\n{diff_str}')
merged.merge_in_entries(partial)
return merged

def get_outfile_path(build_dir: str) -> str:
return os.path.join(build_dir, OUTFILE_PATH)

Expand Down Expand Up @@ -221,7 +245,7 @@ class LinuxSourceTree:
def __init__(
self,
build_dir: str,
kunitconfig_path='',
kunitconfig_paths: Optional[List[str]]=None,
kconfig_add: Optional[List[str]]=None,
arch=None,
cross_compile=None,
Expand All @@ -238,17 +262,7 @@ def __init__(
qemu_config_path = _default_qemu_config_path(self._arch)
_, self._ops = _get_qemu_ops(qemu_config_path, extra_qemu_args, cross_compile)

if kunitconfig_path:
if os.path.isdir(kunitconfig_path):
kunitconfig_path = os.path.join(kunitconfig_path, KUNITCONFIG_PATH)
if not os.path.exists(kunitconfig_path):
raise ConfigError(f'Specified kunitconfig ({kunitconfig_path}) does not exist')
else:
kunitconfig_path = get_kunitconfig_path(build_dir)
if not os.path.exists(kunitconfig_path):
shutil.copyfile(DEFAULT_KUNITCONFIG_PATH, kunitconfig_path)

self._kconfig = kunit_config.parse_file(kunitconfig_path)
self._kconfig = get_parsed_kunitconfig(build_dir, kunitconfig_paths)
if kconfig_add:
kconfig = kunit_config.parse_from_string('\n'.join(kconfig_add))
self._kconfig.merge_in_entries(kconfig)
Expand Down
56 changes: 49 additions & 7 deletions tools/testing/kunit/kunit_tool_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,17 +356,46 @@ def setUp(self):

def test_invalid_kunitconfig(self):
with self.assertRaisesRegex(kunit_kernel.ConfigError, 'nonexistent.* does not exist'):
kunit_kernel.LinuxSourceTree('', kunitconfig_path='/nonexistent_file')
kunit_kernel.LinuxSourceTree('', kunitconfig_paths=['/nonexistent_file'])

def test_valid_kunitconfig(self):
with tempfile.NamedTemporaryFile('wt') as kunitconfig:
kunit_kernel.LinuxSourceTree('', kunitconfig_path=kunitconfig.name)
kunit_kernel.LinuxSourceTree('', kunitconfig_paths=[kunitconfig.name])

def test_dir_kunitconfig(self):
with tempfile.TemporaryDirectory('') as dir:
with open(os.path.join(dir, '.kunitconfig'), 'w'):
pass
kunit_kernel.LinuxSourceTree('', kunitconfig_path=dir)
kunit_kernel.LinuxSourceTree('', kunitconfig_paths=[dir])

def test_multiple_kunitconfig(self):
want_kconfig = kunit_config.Kconfig()
want_kconfig.add_entry('KUNIT', 'y')
want_kconfig.add_entry('KUNIT_TEST', 'm')

with tempfile.TemporaryDirectory('') as dir:
other = os.path.join(dir, 'otherkunitconfig')
with open(os.path.join(dir, '.kunitconfig'), 'w') as f:
f.write('CONFIG_KUNIT=y')
with open(other, 'w') as f:
f.write('CONFIG_KUNIT_TEST=m')
pass

tree = kunit_kernel.LinuxSourceTree('', kunitconfig_paths=[dir, other])
self.assertTrue(want_kconfig.is_subset_of(tree._kconfig), msg=tree._kconfig)


def test_multiple_kunitconfig_invalid(self):
with tempfile.TemporaryDirectory('') as dir:
other = os.path.join(dir, 'otherkunitconfig')
with open(os.path.join(dir, '.kunitconfig'), 'w') as f:
f.write('CONFIG_KUNIT=y')
with open(other, 'w') as f:
f.write('CONFIG_KUNIT=m')

with self.assertRaisesRegex(kunit_kernel.ConfigError, '(?s)Multiple values.*CONFIG_KUNIT'):
kunit_kernel.LinuxSourceTree('', kunitconfig_paths=[dir, other])


def test_kconfig_add(self):
want_kconfig = kunit_config.Kconfig()
Expand Down Expand Up @@ -636,7 +665,7 @@ def test_run_kunitconfig(self):
kunit.main(['run', '--kunitconfig=mykunitconfig'])
# Just verify that we parsed and initialized it correctly here.
self.mock_linux_init.assert_called_once_with('.kunit',
kunitconfig_path='mykunitconfig',
kunitconfig_paths=['mykunitconfig'],
kconfig_add=None,
arch='um',
cross_compile=None,
Expand All @@ -647,18 +676,31 @@ def test_config_kunitconfig(self):
kunit.main(['config', '--kunitconfig=mykunitconfig'])
# Just verify that we parsed and initialized it correctly here.
self.mock_linux_init.assert_called_once_with('.kunit',
kunitconfig_path='mykunitconfig',
kunitconfig_paths=['mykunitconfig'],
kconfig_add=None,
arch='um',
cross_compile=None,
qemu_config_path=None,
extra_qemu_args=[])

@mock.patch.object(kunit_kernel, 'LinuxSourceTree')
def test_run_multiple_kunitconfig(self, mock_linux_init):
mock_linux_init.return_value = self.linux_source_mock
kunit.main(['run', '--kunitconfig=mykunitconfig', '--kunitconfig=other'])
# Just verify that we parsed and initialized it correctly here.
mock_linux_init.assert_called_once_with('.kunit',
kunitconfig_paths=['mykunitconfig', 'other'],
kconfig_add=None,
arch='um',
cross_compile=None,
qemu_config_path=None,
extra_qemu_args=[])

def test_run_kconfig_add(self):
kunit.main(['run', '--kconfig_add=CONFIG_KASAN=y', '--kconfig_add=CONFIG_KCSAN=y'])
# Just verify that we parsed and initialized it correctly here.
self.mock_linux_init.assert_called_once_with('.kunit',
kunitconfig_path=None,
kunitconfig_paths=None,
kconfig_add=['CONFIG_KASAN=y', 'CONFIG_KCSAN=y'],
arch='um',
cross_compile=None,
Expand All @@ -669,7 +711,7 @@ def test_run_qemu_args(self):
kunit.main(['run', '--arch=x86_64', '--qemu_args', '-m 2048'])
# Just verify that we parsed and initialized it correctly here.
self.mock_linux_init.assert_called_once_with('.kunit',
kunitconfig_path=None,
kunitconfig_paths=None,
kconfig_add=None,
arch='x86_64',
cross_compile=None,
Expand Down

0 comments on commit 53b4662

Please sign in to comment.