Re: [PATCH v2] kunit: tool: reconfigure when the used kunitconfig changes

From: David Gow
Date: Fri Nov 19 2021 - 19:40:47 EST


On Sat, Nov 20, 2021 at 7:23 AM Daniel Latypov <dlatypov@xxxxxxxxxx> wrote:
>
> Problem: currently, if you remove something from your kunitconfig,
> kunit.py will not regenerate the .config file.
> The same thing happens if you did --kunitconfig_add=CONFIG_KASAN=y [1]
> and then ran again without it. Your new run will still have KASAN.
>
> The reason is that kunit.py won't regenerate the .config file if it's a
> superset of the kunitconfig. This speeds it up a bit for iterating.
>
> This patch adds an additional check that forces kunit.py to regenerate
> the .config file if the current kunitconfig doesn't match the previous
> one.
>
> What this means:
> * deleting entries from .kunitconfig works as one would expect
> * dropping a --kunitconfig_add also triggers a rebuild
> * you can still edit .config directly to turn on new options
>
> We implement this by creating a `last_used_kunitconfig` file in the
> build directory (so .kunit, by default) after we generate the .config.
> When comparing the kconfigs, we compare python sets, so duplicates and
> permutations don't trip us up.
>
> The majority of this patch is adding unit tests for the existing logic
> and for the new case where `last_used_kunitconfig` differs.
>
> [1] https://lore.kernel.org/linux-kselftest/20211106013058.2621799-2-dlatypov@xxxxxxxxxx/
>
> Signed-off-by: Daniel Latypov <dlatypov@xxxxxxxxxx>
> ---

Thanks! No problems with this version, looks good to go!

Reviewed-by: David Gow <davidgow@xxxxxxxxxx>

Cheers,
-- David

> Note: this patch is based on
> https://lore.kernel.org/linux-kselftest/20211106013058.2621799-1-dlatypov@xxxxxxxxxx/
> This patch will work without it, but there'll be a false merge conflict.
>
> v1 -> v2:
> * always regenerate if last_used_kunitconfig doesn't exist
> * don't call os.remove() if last_used_kunitconfig doesn't exist
> * add in get_old_kunitconfig_path() to match get_kunitconfig_path()
> * s/_kconfig_changed/_kunitconfig_changed
> ---
> Documentation/dev-tools/kunit/start.rst | 8 ++---
> tools/testing/kunit/kunit_kernel.py | 40 ++++++++++++++++------
> tools/testing/kunit/kunit_tool_test.py | 45 +++++++++++++++++++++++++
> 3 files changed, 78 insertions(+), 15 deletions(-)
>
> diff --git a/Documentation/dev-tools/kunit/start.rst b/Documentation/dev-tools/kunit/start.rst
> index 1e00f9226f74..0a5e65540974 100644
> --- a/Documentation/dev-tools/kunit/start.rst
> +++ b/Documentation/dev-tools/kunit/start.rst
> @@ -50,10 +50,10 @@ It'll warn you if you haven't included the dependencies of the options you're
> using.
>
> .. note::
> - Note that removing something from the ``.kunitconfig`` will not trigger a
> - rebuild of the ``.config`` file: the configuration is only updated if the
> - ``.kunitconfig`` is not a subset of ``.config``. This means that you can use
> - other tools (such as make menuconfig) to adjust other config options.
> + If you change the ``.kunitconfig``, kunit.py will trigger a rebuild of the
> + ``.config`` file. But you can edit the ``.config`` file directly or with
> + tools like ``make menuconfig O=.kunit``. As long as its a superset of
> + ``.kunitconfig``, kunit.py won't overwrite your changes.
>
>
> Running the tests (KUnit Wrapper)
> diff --git a/tools/testing/kunit/kunit_kernel.py b/tools/testing/kunit/kunit_kernel.py
> index 350883672be0..12085e04a80c 100644
> --- a/tools/testing/kunit/kunit_kernel.py
> +++ b/tools/testing/kunit/kunit_kernel.py
> @@ -21,6 +21,7 @@ import qemu_config
>
> KCONFIG_PATH = '.config'
> KUNITCONFIG_PATH = '.kunitconfig'
> +OLD_KUNITCONFIG_PATH = 'last_used_kunitconfig'
> DEFAULT_KUNITCONFIG_PATH = 'tools/testing/kunit/configs/default.config'
> BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
> OUTFILE_PATH = 'test.log'
> @@ -179,6 +180,9 @@ def get_kconfig_path(build_dir) -> str:
> def get_kunitconfig_path(build_dir) -> str:
> return get_file_path(build_dir, KUNITCONFIG_PATH)
>
> +def get_old_kunitconfig_path(build_dir) -> str:
> + return get_file_path(build_dir, OLD_KUNITCONFIG_PATH)
> +
> def get_outfile_path(build_dir) -> str:
> return get_file_path(build_dir, OUTFILE_PATH)
>
> @@ -289,24 +293,38 @@ class LinuxSourceTree(object):
> except ConfigError as e:
> logging.error(e)
> return False
> - return self.validate_config(build_dir)
> + if not self.validate_config(build_dir):
> + return False
> +
> + old_path = get_old_kunitconfig_path(build_dir)
> + if os.path.exists(old_path):
> + os.remove(old_path) # write_to_file appends to the file
> + self._kconfig.write_to_file(old_path)
> + return True
> +
> + def _kunitconfig_changed(self, build_dir: str) -> bool:
> + old_path = get_old_kunitconfig_path(build_dir)
> + if not os.path.exists(old_path):
> + return True
> +
> + old_kconfig = kunit_config.parse_file(old_path)
> + return old_kconfig.entries() != self._kconfig.entries()
>
> def build_reconfig(self, build_dir, make_options) -> bool:
> """Creates a new .config if it is not a subset of the .kunitconfig."""
> kconfig_path = get_kconfig_path(build_dir)
> - if os.path.exists(kconfig_path):
> - existing_kconfig = kunit_config.parse_file(kconfig_path)
> - self._ops.make_arch_qemuconfig(self._kconfig)
> - if not self._kconfig.is_subset_of(existing_kconfig):
> - print('Regenerating .config ...')
> - os.remove(kconfig_path)
> - return self.build_config(build_dir, make_options)
> - else:
> - return True
> - else:
> + if not os.path.exists(kconfig_path):
> print('Generating .config ...')
> return self.build_config(build_dir, make_options)
>
> + existing_kconfig = kunit_config.parse_file(kconfig_path)
> + 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 ...')
> + os.remove(kconfig_path)
> + return self.build_config(build_dir, make_options)
> +
> def build_kernel(self, alltests, jobs, build_dir, make_options) -> bool:
> try:
> if alltests:
> diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
> index 7e42a7c27987..572f133511aa 100755
> --- a/tools/testing/kunit/kunit_tool_test.py
> +++ b/tools/testing/kunit/kunit_tool_test.py
> @@ -358,6 +358,51 @@ class LinuxSourceTreeTest(unittest.TestCase):
> with open(kunit_kernel.get_outfile_path(build_dir), 'rt') as outfile:
> self.assertEqual(outfile.read(), 'hi\nbye\n', msg='Missing some output')
>
> + def test_build_reconfig_no_config(self):
> + with tempfile.TemporaryDirectory('') as build_dir:
> + with open(kunit_kernel.get_kunitconfig_path(build_dir), 'w') as f:
> + f.write('CONFIG_KUNIT=y')
> +
> + tree = kunit_kernel.LinuxSourceTree(build_dir)
> + mock_build_config = mock.patch.object(tree, 'build_config').start()
> +
> + # Should generate the .config
> + self.assertTrue(tree.build_reconfig(build_dir, make_options=[]))
> + mock_build_config.assert_called_once_with(build_dir, [])
> +
> + def test_build_reconfig_existing_config(self):
> + with tempfile.TemporaryDirectory('') as build_dir:
> + # Existing .config is a superset, should not touch it
> + with open(kunit_kernel.get_kunitconfig_path(build_dir), 'w') as f:
> + f.write('CONFIG_KUNIT=y')
> + with open(kunit_kernel.get_old_kunitconfig_path(build_dir), 'w') as f:
> + f.write('CONFIG_KUNIT=y')
> + with open(kunit_kernel.get_kconfig_path(build_dir), 'w') as f:
> + f.write('CONFIG_KUNIT=y\nCONFIG_KUNIT_TEST=y')
> +
> + tree = kunit_kernel.LinuxSourceTree(build_dir)
> + mock_build_config = mock.patch.object(tree, 'build_config').start()
> +
> + self.assertTrue(tree.build_reconfig(build_dir, make_options=[]))
> + self.assertEqual(mock_build_config.call_count, 0)
> +
> + def test_build_reconfig_remove_option(self):
> + with tempfile.TemporaryDirectory('') as build_dir:
> + # We removed CONFIG_KUNIT_TEST=y from our .kunitconfig...
> + with open(kunit_kernel.get_kunitconfig_path(build_dir), 'w') as f:
> + f.write('CONFIG_KUNIT=y')
> + with open(kunit_kernel.get_old_kunitconfig_path(build_dir), 'w') as f:
> + f.write('CONFIG_KUNIT=y\nCONFIG_KUNIT_TEST=y')
> + with open(kunit_kernel.get_kconfig_path(build_dir), 'w') as f:
> + f.write('CONFIG_KUNIT=y\nCONFIG_KUNIT_TEST=y')
> +
> + tree = kunit_kernel.LinuxSourceTree(build_dir)
> + mock_build_config = mock.patch.object(tree, 'build_config').start()
> +
> + # ... so we should trigger a call to build_config()
> + self.assertTrue(tree.build_reconfig(build_dir, make_options=[]))
> + mock_build_config.assert_called_once_with(build_dir, [])
> +
> # TODO: add more test cases.
>
>
> --
> 2.34.0.rc2.393.gf8c9666880-goog
>