Re: [PATCH] kunit: make kunit_tool accept optional path to .kunitconfig fragment

From: Daniel Latypov
Date: Fri Jan 29 2021 - 12:49:45 EST


On Thu, Jan 28, 2021 at 10:33 PM David Gow <davidgow@xxxxxxxxxx> wrote:
>
> On Sat, Jan 23, 2021 at 8:17 AM Daniel Latypov <dlatypov@xxxxxxxxxx> wrote:
> >
> > Currently running tests via KUnit tool means tweaking a .kunitconfig
> > file, which you'd keep around locally and never commit.
> > This changes makes it so users can pass in a path to a kunitconfig.
> >
> > One of the imagined use cases is having kunitconfig fragments in-tree
> > to formalize interesting sets of tests for features/subsystems, e.g.
> > $ ./tools/testing/kunit/kunit.py run fs/ext4/kunitconfig
> >
> > For now, this hypothetical fs/ext4/kunitconfig would contain
> > CONFIG_KUNIT=y
> > CONFIG_EXT4_FS=y
> > CONFIG_EXT4_KUNIT_TESTS=y
> >
> > At the moment, it's not hard to manually whip up this file, but as more
> > and more tests get added, this will get tedious.
> >
> > It also opens the door to documenting how to run all the tests relevant
> > to a specific subsystem or feature as a simple one-liner.
> >
> > This can be seen as an analogue to tools/testing/selftests/*/config
> > But in the case of KUnit, the tests live in the same directory as the
> > code-under-test, so it feels more natural to allow the kunitconfig
> > fragments to live anywhere. (Though, people could create a separate
> > directory if wanted; this patch imposes no restrictions on the path).
> >
> > Signed-off-by: Daniel Latypov <dlatypov@xxxxxxxxxx>
> > ---
>
> Really glad this is finally happening. I tried it out, and it seemed
> to work pretty well.
>
> I was wondering whether a positional argument like this was best, or
> whether it'd be better to have an explicitly named argument
> (--kunitconfig=path). Thinking about it though, I'm quite happy with
> having this as-is: the only real other contender for a coveted
> positional argument spot would've been the name of a test or test
> suite (e.g., kunit.py run ext4_inode_test), and that's not really
> possible with the kunit_tool architecture as-is.

Same, I was on the fence about this for a good while.

I had originally intended to make it a flag, but
* as you noted, that would require bigger changes to kunit_tool, but
also KUnit (the C code) itself to handle this.
* I felt that the kunitconfig fragment essentially takes the place of it.
* E.g. If I want to run a specific test, I can manually create a
fragment for just that.
* It's sadly more work than just specifying a single test for
bisection, but it would work.

That said, adding in support for running specific tests/suites by name
seems like it would be a somewhat smaller change than I thought.
So I might lean more towards making this a flag now.

Will wait to get Brendan's opinion and then send out a v2.

>
> One other comment below (should this work for kunit.py config?),
> otherwise it looks good.
>
> -- David
>
> > tools/testing/kunit/kunit.py | 9 ++++++---
> > tools/testing/kunit/kunit_kernel.py | 12 ++++++++----
> > tools/testing/kunit/kunit_tool_test.py | 25 +++++++++++++++++++++++++
> > 3 files changed, 39 insertions(+), 7 deletions(-)
> >
> > diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
> > index e808a47c839b..3204a23bd16e 100755
> > --- a/tools/testing/kunit/kunit.py
> > +++ b/tools/testing/kunit/kunit.py
> > @@ -188,6 +188,9 @@ def add_build_opts(parser) -> None:
> > help='As in the make command, "Specifies the number of '
> > 'jobs (commands) to run simultaneously."',
> > type=int, default=8, metavar='jobs')
> > + parser.add_argument('kunitconfig',
> > + help='Path to Kconfig fragment that enables KUnit tests',
> > + type=str, nargs='?', metavar='kunitconfig')
> >
>
> Should this maybe be in add_common_opts()? I'd assume that we want
> kunit.py config to accept this custom kunitconfig path as well.

Good point.
Moved it over into add_common_opts() and including this minimal test
case as well since `kunit.py config` is probably a very key command
that should work with custom fragments.

diff --git a/tools/testing/kunit/kunit_tool_test.py
b/tools/testing/kunit/kunit_tool_test.py
index 533fe41b5123..a74877ee2c90 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -424,5 +424,12 @@ class KUnitMainTest(unittest.TestCase):
# Just verify that we parsed and initialized it correctly here.
mock_linux_init.assert_called_once_with('.kunit',
kunitconfig_path='mykunitconfig')

+ @mock.patch.object(kunit_kernel, 'LinuxSourceTree')
+ def test_config_kunitconfig(self, mock_linux_init):
+ mock_linux_init.return_value = self.linux_source_mock
+ kunit.main(['config', 'mykunitconfig'])
+ # Just verify that we parsed and initialized it correctly here.
+ mock_linux_init.assert_called_once_with('.kunit',
kunitconfig_path='mykunitconfig')
+
if __name__ == '__main__':
unittest.main()

>
> > def add_exec_opts(parser) -> None:
> > parser.add_argument('--timeout',
> > @@ -256,7 +259,7 @@ def main(argv, linux=None):
> > os.mkdir(cli_args.build_dir)
> >
> > if not linux:
> > - linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
> > + linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, kunitconfig_path=cli_args.kunitconfig)
> >
> > request = KunitRequest(cli_args.raw_output,
> > cli_args.timeout,
> > @@ -274,7 +277,7 @@ def main(argv, linux=None):
> > os.mkdir(cli_args.build_dir)
> >
> > if not linux:
> > - linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
> > + linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, kunitconfig_path=cli_args.kunitconfig)
> >
> > request = KunitConfigRequest(cli_args.build_dir,
> > cli_args.make_options)
> > @@ -286,7 +289,7 @@ def main(argv, linux=None):
> > sys.exit(1)
> > elif cli_args.subcommand == 'build':
> > if not linux:
> > - linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir)
> > + linux = kunit_kernel.LinuxSourceTree(cli_args.build_dir, kunitconfig_path=cli_args.kunitconfig)
> >
> > request = KunitBuildRequest(cli_args.jobs,
> > cli_args.build_dir,
> > diff --git a/tools/testing/kunit/kunit_kernel.py b/tools/testing/kunit/kunit_kernel.py
> > index 2076a5a2d060..0b461663e7d9 100644
> > --- a/tools/testing/kunit/kunit_kernel.py
> > +++ b/tools/testing/kunit/kunit_kernel.py
> > @@ -123,7 +123,7 @@ def get_outfile_path(build_dir) -> str:
> > class LinuxSourceTree(object):
> > """Represents a Linux kernel source tree with KUnit tests."""
> >
> > - def __init__(self, build_dir: str, load_config=True, defconfig=DEFAULT_KUNITCONFIG_PATH) -> None:
> > + def __init__(self, build_dir: str, load_config=True, kunitconfig_path='') -> None:
> > signal.signal(signal.SIGINT, self.signal_handler)
> >
> > self._ops = LinuxSourceTreeOperations()
> > @@ -131,9 +131,13 @@ class LinuxSourceTree(object):
> > if not load_config:
> > return
> >
> > - kunitconfig_path = get_kunitconfig_path(build_dir)
> > - if not os.path.exists(kunitconfig_path):
> > - shutil.copyfile(defconfig, kunitconfig_path)
> > + if 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.Kconfig()
> > self._kconfig.read_from_file(kunitconfig_path)
> > diff --git a/tools/testing/kunit/kunit_tool_test.py b/tools/testing/kunit/kunit_tool_test.py
> > index b593f4448e83..533fe41b5123 100755
> > --- a/tools/testing/kunit/kunit_tool_test.py
> > +++ b/tools/testing/kunit/kunit_tool_test.py
> > @@ -12,6 +12,7 @@ from unittest import mock
> > import tempfile, shutil # Handling test_tmpdir
> >
> > import json
> > +import signal
> > import os
> >
> > import kunit_config
> > @@ -250,6 +251,23 @@ class KUnitParserTest(unittest.TestCase):
> > result.status)
> > self.assertEqual('kunit-resource-test', result.suites[0].name)
> >
> > +class LinuxSourceTreeTest(unittest.TestCase):
> > +
> > + def setUp(self):
> > + mock.patch.object(signal, 'signal').start()
> > + self.addCleanup(mock.patch.stopall)
> > +
> > + def test_invalid_kunitconfig(self):
> > + with self.assertRaisesRegex(kunit_kernel.ConfigError, 'nonexistent.* does not exist'):
> > + kunit_kernel.LinuxSourceTree('', kunitconfig_path='/nonexistent_file')
> > +
> > + def test_valid_kunitconfig(self):
> > + with tempfile.NamedTemporaryFile('wt') as kunitconfig:
> > + tree = kunit_kernel.LinuxSourceTree('', kunitconfig_path=kunitconfig.name)
> > +
> > + # TODO: add more test cases.
> > +
> > +
> > class KUnitJsonTest(unittest.TestCase):
> >
> > def _json_for(self, log_file):
> > @@ -399,5 +417,12 @@ class KUnitMainTest(unittest.TestCase):
> > self.linux_source_mock.run_kernel.assert_called_once_with(build_dir=build_dir, timeout=300)
> > self.print_mock.assert_any_call(StrContains('Testing complete.'))
> >
> > + @mock.patch.object(kunit_kernel, 'LinuxSourceTree')
> > + def test_run_kunitconfig(self, mock_linux_init):
> > + mock_linux_init.return_value = self.linux_source_mock
> > + kunit.main(['run', 'mykunitconfig'])
> > + # Just verify that we parsed and initialized it correctly here.
> > + mock_linux_init.assert_called_once_with('.kunit', kunitconfig_path='mykunitconfig')
> > +
> > if __name__ == '__main__':
> > unittest.main()
> >
> > base-commit: 2b8fdbbf1c616300312f71fe5b21fe8f03129950
> > --
> > 2.30.0.280.ga3ce27912f-goog
> >