diff mbox series

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

Message ID 20220520224200.3764027-1-dlatypov@google.com (mailing list archive)
State New
Delegated to: Brendan Higgins
Headers show
Series [v2] kunit: tool: refactor internal kconfig handling, allow overriding | expand

Commit Message

Daniel Latypov May 20, 2022, 10:42 p.m. UTC
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@google.com>
---
v1 -> v2: fix validate_config() func.
There was a bug found by David, see
https://lore.kernel.org/linux-kselftest/CAGS_qxpF338dvbB+6QW1n8_agddeS10+nkTQNmT+0UcvoE1gBw@mail.gmail.com/
---
 tools/testing/kunit/kunit_config.py    | 49 +++++++++++++++-----------
 tools/testing/kunit/kunit_kernel.py    | 18 +++++-----
 tools/testing/kunit/kunit_tool_test.py | 45 ++++++++++-------------
 3 files changed, 57 insertions(+), 55 deletions(-)


base-commit: 1b11063d32d7e11366e48be64215ff517ce32217

Comments

David Gow June 24, 2022, 7:55 a.m. UTC | #1
On Sat, May 21, 2022 at 6:42 AM Daniel Latypov <dlatypov@google.com> 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@google.com>
> ---
> v1 -> v2: fix validate_config() func.
> There was a bug found by David, see
> https://lore.kernel.org/linux-kselftest/CAGS_qxpF338dvbB+6QW1n8_agddeS10+nkTQNmT+0UcvoE1gBw@mail.gmail.com/
> ---

Sorry for the delay, finally getting around to reviewing this version.

It looks good to me, and works well enough in testing that I've got no
real complaints. A couple of minor comments below, but nothing
actually worth doing a new version for, IMO.

Reviewed-by: David Gow <davidgow@google.com>

Cheers,
-- David

>  tools/testing/kunit/kunit_config.py    | 49 +++++++++++++++-----------
>  tools/testing/kunit/kunit_kernel.py    | 18 +++++-----
>  tools/testing/kunit/kunit_tool_test.py | 45 ++++++++++-------------
>  3 files changed, 57 insertions(+), 55 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())
> +

It took me a couple of goes to realise that this was looking at the
difference between sets, not trying to set the difference. Maybe
different_entries() or something like that'd be clearer, but I can't
say it bothers me enough to be worth a new version.

Then again (as noted below), the direct set difference isn't exactly
what we want, it's more the equivalent of is_subset_of(). The
follow-up repeated-kunitconfig patch adds differing_options(), which
is closer to what we'd want, I think:
https://lore.kernel.org/linux-kselftest/20220624001247.3255978-1-dlatypov@google.com/

Probably easiest to accept this patch as-is, followed by those
follow-up ones, and adjust it then, if that's worth doing, though...

>         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..6d994bb24999 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)
> @@ -267,7 +268,7 @@ class LinuxSourceTree:
>                 validated_kconfig = kunit_config.parse_file(kconfig_path)
>                 if self._kconfig.is_subset_of(validated_kconfig):
>                         return True
> -               invalid = self._kconfig.entries() - validated_kconfig.entries()
> +               invalid = self._kconfig.set_diff(validated_kconfig)

The fact that the set 'invalid' is not actually equal to the set of
conflicting entries is_subset_of() finds bothers me slightly, though
due to the early return, I don't think it should ever be a problem in
pracitce.

Maybe having a 'subset_of' function, instead of set_diff, which does
the whole "not set" is equivalent to "n" check would be better?

Not worth changing this now, though: let's leave anything too heroic
for the next bout of kconfig-related stuff. (For example, there are
plans afoot to actually user CONFIG_x=n instead of is not set in the
files:
https://lore.kernel.org/lkml/CA+icZUXkd=dtbBX3UKLRzGiVSKC=TeW7ATiRKD9dnYtmm6RZqg@mail.gmail.com/T/


)

>                 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 +283,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 +304,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 +314,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
>
Daniel Latypov June 24, 2022, 8:24 p.m. UTC | #2
On Fri, Jun 24, 2022 at 12:55 AM David Gow <davidgow@google.com> wrote:
> >
> > +       def set_diff(self, other: 'Kconfig') -> Set[KconfigEntry]:
> > +               return set(self._as_entries()) - set(other._as_entries())
> > +
>
> It took me a couple of goes to realise that this was looking at the
> difference between sets, not trying to set the difference. Maybe
> different_entries() or something like that'd be clearer, but I can't
> say it bothers me enough to be worth a new version.

Wdyt about `set_difference()`?
Or maybe adding a verb: `get_set_diff()`, `compute_set_diff()`?

But we do want to make it clear it has set (asymmetric) difference
semantics, see below.

>
> Then again (as noted below), the direct set difference isn't exactly
> what we want, it's more the equivalent of is_subset_of(). The
> follow-up repeated-kunitconfig patch adds differing_options(), which
> is closer to what we'd want, I think:
> https://lore.kernel.org/linux-kselftest/20220624001247.3255978-1-dlatypov@google.com/

differing_options() does not have the right semantics.
For this, we do explicitly want a set difference.

Context: This is the func used to print out these warnings:
$ .../kunit.py run --kconfig_add=CONFIG_PCI=y
...
Missing: CONFIG_PCI=y

That comes from
  invalid = self._kconfig.set_diff(validated_kconfig)
Using differing_options() to get our version of the configs:
  invalid = (want for want, got in
self._kconfig.differing_options(validated_kconfig))
we instead get an empty list.

The problem is that differing_options() only shows config options that
are explicitly to different values.

There's probably a way I can name these functions better to make the
difference more clear.
Or perhaps we should move set_diff() out of this class and have
kunit_kernel.py itself due the computation.

E.g.
  // make as_entries() "public", s/invalid/missing
  missing = set(self._kconfig.as_entries()) - set(validated_config.as_entries())

Thoughts?

Daniel
David Gow June 25, 2022, 7:01 a.m. UTC | #3
On Sat, Jun 25, 2022 at 4:24 AM Daniel Latypov <dlatypov@google.com> wrote:
>
> On Fri, Jun 24, 2022 at 12:55 AM David Gow <davidgow@google.com> wrote:
> > >
> > > +       def set_diff(self, other: 'Kconfig') -> Set[KconfigEntry]:
> > > +               return set(self._as_entries()) - set(other._as_entries())
> > > +
> >
> > It took me a couple of goes to realise that this was looking at the
> > difference between sets, not trying to set the difference. Maybe
> > different_entries() or something like that'd be clearer, but I can't
> > say it bothers me enough to be worth a new version.
>
> Wdyt about `set_difference()`?
> Or maybe adding a verb: `get_set_diff()`, `compute_set_diff()`?
>
> But we do want to make it clear it has set (asymmetric) difference
> semantics, see below.
>

I like adding a verb, personally.

> >
> > Then again (as noted below), the direct set difference isn't exactly
> > what we want, it's more the equivalent of is_subset_of(). The
> > follow-up repeated-kunitconfig patch adds differing_options(), which
> > is closer to what we'd want, I think:
> > https://lore.kernel.org/linux-kselftest/20220624001247.3255978-1-dlatypov@google.com/
>
> differing_options() does not have the right semantics.
> For this, we do explicitly want a set difference.
>
> Context: This is the func used to print out these warnings:
> $ .../kunit.py run --kconfig_add=CONFIG_PCI=y
> ...
> Missing: CONFIG_PCI=y
>
> That comes from
>   invalid = self._kconfig.set_diff(validated_kconfig)
> Using differing_options() to get our version of the configs:
>   invalid = (want for want, got in
> self._kconfig.differing_options(validated_kconfig))
> we instead get an empty list.
>
> The problem is that differing_options() only shows config options that
> are explicitly to different values.

Ah, yep: my mistake. I was mixing up the (!b && value=n) check in
is_subset_of with the (b && value != b) in differing options. Whoops.

But, thinking about it, the special handling of "=n" in set A not
needing an equivalent in set B should fall out from the set difference
anyway. So no problem here at all.

> There's probably a way I can name these functions better to make the
> difference more clear.

How about "conflicting_options" versus "missing_options"?

> Or perhaps we should move set_diff() out of this class and have
> kunit_kernel.py itself due the computation.
>
> E.g.
>   // make as_entries() "public", s/invalid/missing
>   missing = set(self._kconfig.as_entries()) - set(validated_config.as_entries())
>
> Thoughts?

Fair enough point:

>
> Daniel
diff mbox series

Patch

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..6d994bb24999 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)
@@ -267,7 +268,7 @@  class LinuxSourceTree:
 		validated_kconfig = kunit_config.parse_file(kconfig_path)
 		if self._kconfig.is_subset_of(validated_kconfig):
 			return True
-		invalid = self._kconfig.entries() - validated_kconfig.entries()
+		invalid = self._kconfig.set_diff(validated_kconfig)
 		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 +283,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 +304,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 +314,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'):