Re: [PATCH] kunit: tool: refactor internal kconfig handling, allow overriding

From: David Gow
Date: Fri May 20 2022 - 02:13:11 EST


On Fri, May 20, 2022 at 12:45 AM Daniel Latypov <dlatypov@xxxxxxxxxx> wrote:
>
> Currently, you cannot ovewrwrite what's in your kunitconfig via
> --kconfig_add.
> Nor can you override something in a qemu_config via either means.
>
> This patch makes it so we have this level of priority
> * --kconfig_add
> * kunitconfig file (the default or the one from --kunitconfig)
> * qemu_config
>
> The rationale for this order is that the more "dynamic" sources of
> kconfig options should take priority.
>
> --kconfig_add is obviously the most dynamic.
> And for kunitconfig, users probably tweak the file manually or specify
> --kunitconfig more often than they delve into qemu_config python files.
>
> And internally, we convert the kconfigs from a python list into a set or
> dict fairly often. We should just use a dict internally.
> We exposed the set transform in the past since we didn't define __eq__,
> so also take the chance to shore up the kunit_kconfig.Kconfig interface.
>
> Example
> =======
>
> Let's consider the unrealistic example where someone would want to
> disable CONFIG_KUNIT.
> I.e. they run
> $ ./tools/testing/kunit/kunit.py config --kconfig_add=CONFIG_KUNIT=n
>
> Before
> ------
> We'd write the following
> > # CONFIG_KUNIT is not set
> > CONFIG_KUNIT_ALL_TESTS=y
> > CONFIG_KUNIT_TEST=y
> > CONFIG_KUNIT=y
> > CONFIG_KUNIT_EXAMPLE_TEST=y
>
> And we'd error out with
> > ERROR:root:Not all Kconfig options selected in kunitconfig were in the generated .config.
> > This is probably due to unsatisfied dependencies.
> > Missing: # CONFIG_KUNIT is not set
>
> After
> -----
> We'd write the following
> > # CONFIG_KUNIT is not set
> > CONFIG_KUNIT_TEST=y
> > CONFIG_KUNIT_ALL_TESTS=y
> > CONFIG_KUNIT_EXAMPLE_TEST=y
>
> And we'd error out with
> > ERROR:root:Not all Kconfig options selected in kunitconfig were in the generated .config.
> > This is probably due to unsatisfied dependencies.
> > Missing: CONFIG_KUNIT_EXAMPLE_TEST=y, CONFIG_KUNIT_TEST=y, CONFIG_KUNIT_ALL_TESTS=y
>
> Signed-off-by: Daniel Latypov <dlatypov@xxxxxxxxxx>
> ---

I like this, but do think there are a few gaps this doesn't handle
properly. (Though exactly how we'd deal with them, I'm not yet sure.)

In particular, it's not possible to disable a pair of options where
one depends on the other: disabling the parent option will result in
the child one not being present in the generated config. This will
conflict both with "=y" and "=n/not set": we'd need a way to _remove_
a kconfig option for that to work.

The ideal thing would be for us to work out what the default value is,
and remove the option automatically if it matches, but that seems like
it'd be quite difficult. Otherwise, I guess a
--kconfig_{remove,delete,etc} option would work.

Otherwise, this seems okay at first glance, but I haven't had the time
to fully review it in detail yet...

Cheers,
-- David

> tools/testing/kunit/kunit_config.py | 49 +++++++++++++++-----------
> tools/testing/kunit/kunit_kernel.py | 21 ++++++-----
> tools/testing/kunit/kunit_tool_test.py | 45 ++++++++++-------------
> 3 files changed, 59 insertions(+), 56 deletions(-)
>
> diff --git a/tools/testing/kunit/kunit_config.py b/tools/testing/kunit/kunit_config.py
> index 75a8dc1683d4..89443400b17e 100644
> --- a/tools/testing/kunit/kunit_config.py
> +++ b/tools/testing/kunit/kunit_config.py
> @@ -8,7 +8,7 @@
>
> from dataclasses import dataclass
> import re
> -from typing import List, Set
> +from typing import Dict, Iterable, Set
>
> CONFIG_IS_NOT_SET_PATTERN = r'^# CONFIG_(\w+) is not set$'
> CONFIG_PATTERN = r'^CONFIG_(\w+)=(\S+|".*")$'
> @@ -32,35 +32,46 @@ class Kconfig:
> """Represents defconfig or .config specified using the Kconfig language."""
>
> def __init__(self) -> None:
> - self._entries = [] # type: List[KconfigEntry]
> + self._entries = {} # type: Dict[str, str]
>
> - def entries(self) -> Set[KconfigEntry]:
> - return set(self._entries)
> + def __eq__(self, other) -> bool:
> + if not isinstance(other, self.__class__):
> + return False
> + return self._entries == other._entries
>
> - def add_entry(self, entry: KconfigEntry) -> None:
> - self._entries.append(entry)
> + def __repr__(self) -> str:
> + return ','.join(str(e) for e in self._as_entries())
> +
> +
> + def _as_entries(self) -> Iterable[KconfigEntry]:
> + for name, value in self._entries.items():
> + yield KconfigEntry(name, value)
> +
> + def add_entry(self, name: str, value: str) -> None:
> + self._entries[name] = value
>
> def is_subset_of(self, other: 'Kconfig') -> bool:
> - other_dict = {e.name: e.value for e in other.entries()}
> - for a in self.entries():
> - b = other_dict.get(a.name)
> + for name, value in self._entries.items():
> + b = other._entries.get(name)
> if b is None:
> - if a.value == 'n':
> + if value == 'n':
> continue
> return False
> - if a.value != b:
> + if value != b:
> return False
> return True
>
> + def set_diff(self, other: 'Kconfig') -> Set[KconfigEntry]:
> + return set(self._as_entries()) - set(other._as_entries())
> +
> def merge_in_entries(self, other: 'Kconfig') -> None:
> - if other.is_subset_of(self):
> - return
> - self._entries = list(self.entries().union(other.entries()))
> + for name, value in other._entries.items():
> + self._entries[name] = value
>
> def write_to_file(self, path: str) -> None:
> with open(path, 'a+') as f:
> - for entry in self.entries():
> - f.write(str(entry) + '\n')
> + for e in self._as_entries():
> + f.write(str(e) + '\n')
>
> def parse_file(path: str) -> Kconfig:
> with open(path, 'r') as f:
> @@ -78,14 +89,12 @@ def parse_from_string(blob: str) -> Kconfig:
>
> match = config_matcher.match(line)
> if match:
> - entry = KconfigEntry(match.group(1), match.group(2))
> - kconfig.add_entry(entry)
> + kconfig.add_entry(match.group(1), match.group(2))
> continue
>
> empty_match = is_not_set_matcher.match(line)
> if empty_match:
> - entry = KconfigEntry(empty_match.group(1), 'n')
> - kconfig.add_entry(entry)
> + kconfig.add_entry(empty_match.group(1), 'n')
> continue
>
> if line[0] == '#':
> diff --git a/tools/testing/kunit/kunit_kernel.py b/tools/testing/kunit/kunit_kernel.py
> index 3539efaf99ba..ebd2d91af710 100644
> --- a/tools/testing/kunit/kunit_kernel.py
> +++ b/tools/testing/kunit/kunit_kernel.py
> @@ -53,8 +53,8 @@ class LinuxSourceTreeOperations:
> except subprocess.CalledProcessError as e:
> raise ConfigError(e.output.decode())
>
> - def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
> - pass
> + def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
> + return base_kunitconfig
>
> def make_allyesconfig(self, build_dir: str, make_options) -> None:
> raise ConfigError('Only the "um" arch is supported for alltests')
> @@ -109,9 +109,10 @@ class LinuxSourceTreeOperationsQemu(LinuxSourceTreeOperations):
> self._kernel_command_line = qemu_arch_params.kernel_command_line + ' kunit_shutdown=reboot'
> self._extra_qemu_params = qemu_arch_params.extra_qemu_params
>
> - def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> None:
> + def make_arch_qemuconfig(self, base_kunitconfig: kunit_config.Kconfig) -> kunit_config.Kconfig:
> kconfig = kunit_config.parse_from_string(self._kconfig)
> - base_kunitconfig.merge_in_entries(kconfig)
> + kconfig.merge_in_entries(base_kunitconfig)
> + return kconfig
>
> def start(self, params: List[str], build_dir: str) -> subprocess.Popen:
> kernel_path = os.path.join(build_dir, self._kernel_path)
> @@ -265,9 +266,10 @@ class LinuxSourceTree:
> def validate_config(self, build_dir: str) -> bool:
> kconfig_path = get_kconfig_path(build_dir)
> validated_kconfig = kunit_config.parse_file(kconfig_path)
> - if self._kconfig.is_subset_of(validated_kconfig):
> + invalid = self._kconfig.set_diff(validated_kconfig)
> + if not invalid:
> return True
> - invalid = self._kconfig.entries() - validated_kconfig.entries()
> +
> message = 'Not all Kconfig options selected in kunitconfig were in the generated .config.\n' \
> 'This is probably due to unsatisfied dependencies.\n' \
> 'Missing: ' + ', '.join([str(e) for e in invalid])
> @@ -282,7 +284,7 @@ class LinuxSourceTree:
> if build_dir and not os.path.exists(build_dir):
> os.mkdir(build_dir)
> try:
> - self._ops.make_arch_qemuconfig(self._kconfig)
> + self._kconfig = self._ops.make_arch_qemuconfig(self._kconfig)
> self._kconfig.write_to_file(kconfig_path)
> self._ops.make_olddefconfig(build_dir, make_options)
> except ConfigError as e:
> @@ -303,7 +305,7 @@ class LinuxSourceTree:
> return True
>
> old_kconfig = kunit_config.parse_file(old_path)
> - return old_kconfig.entries() != self._kconfig.entries()
> + return old_kconfig != self._kconfig
>
> def build_reconfig(self, build_dir: str, make_options) -> bool:
> """Creates a new .config if it is not a subset of the .kunitconfig."""
> @@ -313,7 +315,8 @@ class LinuxSourceTree:
> return self.build_config(build_dir, make_options)
>
> existing_kconfig = kunit_config.parse_file(kconfig_path)
> - self._ops.make_arch_qemuconfig(self._kconfig)
> + self._kconfig = self._ops.make_arch_qemuconfig(self._kconfig)
> +
> if self._kconfig.is_subset_of(existing_kconfig) and not self._kunitconfig_changed(build_dir):
> return True
> print('Regenerating .config ...')
> diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
> index 25a2eb3bf114..3a8f638ff092 100755
> --- a/tools/testing/kunit/kunit_tool_test.py
> +++ b/tools/testing/kunit/kunit_tool_test.py
> @@ -45,7 +45,7 @@ class KconfigTest(unittest.TestCase):
> self.assertTrue(kconfig0.is_subset_of(kconfig0))
>
> kconfig1 = kunit_config.Kconfig()
> - kconfig1.add_entry(kunit_config.KconfigEntry('TEST', 'y'))
> + kconfig1.add_entry('TEST', 'y')
> self.assertTrue(kconfig1.is_subset_of(kconfig1))
> self.assertTrue(kconfig0.is_subset_of(kconfig1))
> self.assertFalse(kconfig1.is_subset_of(kconfig0))
> @@ -56,40 +56,28 @@ class KconfigTest(unittest.TestCase):
> kconfig = kunit_config.parse_file(kconfig_path)
>
> expected_kconfig = kunit_config.Kconfig()
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('UML', 'y'))
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('MMU', 'y'))
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('TEST', 'y'))
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('EXAMPLE_TEST', 'y'))
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('MK8', 'n'))
> -
> - self.assertEqual(kconfig.entries(), expected_kconfig.entries())
> + expected_kconfig.add_entry('UML', 'y')
> + expected_kconfig.add_entry('MMU', 'y')
> + expected_kconfig.add_entry('TEST', 'y')
> + expected_kconfig.add_entry('EXAMPLE_TEST', 'y')
> + expected_kconfig.add_entry('MK8', 'n')
> +
> + self.assertEqual(kconfig, expected_kconfig)
>
> def test_write_to_file(self):
> kconfig_path = os.path.join(test_tmpdir, '.config')
>
> expected_kconfig = kunit_config.Kconfig()
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('UML', 'y'))
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('MMU', 'y'))
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('TEST', 'y'))
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('EXAMPLE_TEST', 'y'))
> - expected_kconfig.add_entry(
> - kunit_config.KconfigEntry('MK8', 'n'))
> + expected_kconfig.add_entry('UML', 'y')
> + expected_kconfig.add_entry('MMU', 'y')
> + expected_kconfig.add_entry('TEST', 'y')
> + expected_kconfig.add_entry('EXAMPLE_TEST', 'y')
> + expected_kconfig.add_entry('MK8', 'n')
>
> expected_kconfig.write_to_file(kconfig_path)
>
> actual_kconfig = kunit_config.parse_file(kconfig_path)
> -
> - self.assertEqual(actual_kconfig.entries(),
> - expected_kconfig.entries())
> + self.assertEqual(actual_kconfig, expected_kconfig)
>
> class KUnitParserTest(unittest.TestCase):
>
> @@ -381,8 +369,11 @@ class LinuxSourceTreeTest(unittest.TestCase):
> kunit_kernel.LinuxSourceTree('', kunitconfig_path=dir)
>
> def test_kconfig_add(self):
> + want_kconfig = kunit_config.Kconfig()
> + want_kconfig.add_entry('NOT_REAL', 'y')
> +
> tree = kunit_kernel.LinuxSourceTree('', kconfig_add=['CONFIG_NOT_REAL=y'])
> - self.assertIn(kunit_config.KconfigEntry('NOT_REAL', 'y'), tree._kconfig.entries())
> + self.assertFalse(want_kconfig.set_diff(tree._kconfig))
>
> def test_invalid_arch(self):
> with self.assertRaisesRegex(kunit_kernel.ConfigError, 'not a valid arch, options are.*x86_64'):
>
> base-commit: 1b11063d32d7e11366e48be64215ff517ce32217
> --
> 2.36.1.124.g0e6072fb45-goog
>

Attachment: smime.p7s
Description: S/MIME Cryptographic Signature