This makes possible to install and load KUnit tests
as modules in a root filesystem and boot UML with
this filesystem.

The filesystem was created using debootstrap:
   sudo debootstrap buster .uml_rootfs

And change the owner of the root filesystem files
for your user:
   sudo chown -R $USER:$USER .uml_rootfs

For the kunit-tool to correctly capture the test results,
uml_utilities must be installed on the host to halt uml.

Signed-off-by: Vitor Massaru Iha <vi...@massaru.org>
Reviewed-by: Brendan Higgins <brendanhigg...@google.com>
---
v2:
    * splitted patch in 3:
        - Allows to install and load modules in root filesystem;
        - Provides an userspace memory context when tests are compiled
          as module;
        - Convert test_user_copy to KUnit test;
    * added documentation;
    * removed the use of global variable;
    * capitalized make_cmd to look like a constant;
    * adjusted unit tests to the new parameter (--uml_rootfs_dir);
---
 Documentation/dev-tools/kunit/start.rst |  15 ++++
 tools/testing/kunit/kunit.py            |  40 +++++++--
 tools/testing/kunit/kunit_kernel.py     | 106 ++++++++++++++++++++----
 tools/testing/kunit/kunit_tool_test.py  |  16 ++--
 4 files changed, 146 insertions(+), 31 deletions(-)

diff --git a/Documentation/dev-tools/kunit/start.rst 
b/Documentation/dev-tools/kunit/start.rst
index bb112cf70624..af2b6b9f870f 100644
--- a/Documentation/dev-tools/kunit/start.rst
+++ b/Documentation/dev-tools/kunit/start.rst
@@ -9,6 +9,21 @@ Installing dependencies
 KUnit has the same dependencies as the Linux kernel. As long as you can build
 the kernel, you can run KUnit.
 
+Unless you need to run the tests with a root filesystem, in this case you will
+need to install debootsrap and uml_tools (uml-tools in Debian flavor distro).
+
+To install the root filesystem:
+
+.. code-block:: bash
+
+       sudo debootstrap buster .uml_rootfs
+
+And change the owner of the root filesystem for your user:
+
+.. code-block:: bash
+
+       sudo chown -R $USER:$USER .uml_rootfs
+
 Running tests with the KUnit Wrapper
 ====================================
 Included with KUnit is a simple Python wrapper which runs tests under User Mode
diff --git a/tools/testing/kunit/kunit.py b/tools/testing/kunit/kunit.py
index 2ece17e9eab5..8abe3a61f798 100755
--- a/tools/testing/kunit/kunit.py
+++ b/tools/testing/kunit/kunit.py
@@ -23,16 +23,16 @@ import kunit_parser
 KunitResult = namedtuple('KunitResult', ['status','result','elapsed_time'])
 
 KunitConfigRequest = namedtuple('KunitConfigRequest',
-                               ['build_dir', 'make_options'])
+                               ['build_dir', 'uml_rootfs_path', 
'make_options'])
 KunitBuildRequest = namedtuple('KunitBuildRequest',
-                              ['jobs', 'build_dir', 'alltests',
+                              ['jobs', 'build_dir', 'uml_rootfs_path', 
'alltests',
                                'make_options'])
 KunitExecRequest = namedtuple('KunitExecRequest',
-                             ['timeout', 'build_dir', 'alltests'])
+                             ['timeout', 'build_dir', 'uml_rootfs_path', 
'alltests'])
 KunitParseRequest = namedtuple('KunitParseRequest',
                               ['raw_output', 'input_data'])
 KunitRequest = namedtuple('KunitRequest', ['raw_output','timeout', 'jobs',
-                                          'build_dir', 'alltests',
+                                          'build_dir', 'uml_rootfs_path', 
'alltests',
                                           'make_options'])
 
 KernelDirectoryPath = sys.argv[0].split('tools/testing/kunit/')[0]
@@ -47,7 +47,6 @@ def create_default_kunitconfig():
        if not os.path.exists(kunit_kernel.kunitconfig_path):
                shutil.copyfile('arch/um/configs/kunit_defconfig',
                                kunit_kernel.kunitconfig_path)
-
 def get_kernel_root_path():
        parts = sys.argv[0] if not __file__ else __file__
        parts = os.path.realpath(parts).split('tools/testing/kunit')
@@ -61,7 +60,8 @@ def config_tests(linux: kunit_kernel.LinuxSourceTree,
 
        config_start = time.time()
        create_default_kunitconfig()
-       success = linux.build_reconfig(request.build_dir, request.make_options)
+
+       success = linux.build_reconfig(request.build_dir, 
request.uml_rootfs_path, request.make_options)
        config_end = time.time()
        if not success:
                return KunitResult(KunitStatus.CONFIG_FAILURE,
@@ -79,10 +79,9 @@ def build_tests(linux: kunit_kernel.LinuxSourceTree,
        success = linux.build_um_kernel(request.alltests,
                                        request.jobs,
                                        request.build_dir,
+                                       request.uml_rootfs_path,
                                        request.make_options)
        build_end = time.time()
-       if not success:
-               return KunitResult(KunitStatus.BUILD_FAILURE, 'could not build 
kernel')
        if not success:
                return KunitResult(KunitStatus.BUILD_FAILURE,
                                   'could not build kernel',
@@ -97,7 +96,8 @@ def exec_tests(linux: kunit_kernel.LinuxSourceTree,
        test_start = time.time()
        result = linux.run_kernel(
                timeout=None if request.alltests else request.timeout,
-               build_dir=request.build_dir)
+               build_dir=request.build_dir,
+               uml_rootfs_path=request.uml_rootfs_path)
 
        test_end = time.time()
 
@@ -130,12 +130,14 @@ def run_tests(linux: kunit_kernel.LinuxSourceTree,
        run_start = time.time()
 
        config_request = KunitConfigRequest(request.build_dir,
+                                           request.uml_rootfs_path,
                                            request.make_options)
        config_result = config_tests(linux, config_request)
        if config_result.status != KunitStatus.SUCCESS:
                return config_result
 
        build_request = KunitBuildRequest(request.jobs, request.build_dir,
+                                         request.uml_rootfs_path,
                                          request.alltests,
                                          request.make_options)
        build_result = build_tests(linux, build_request)
@@ -143,6 +145,7 @@ def run_tests(linux: kunit_kernel.LinuxSourceTree,
                return build_result
 
        exec_request = KunitExecRequest(request.timeout, request.build_dir,
+                                       request.uml_rootfs_path,
                                        request.alltests)
        exec_result = exec_tests(linux, exec_request)
        if exec_result.status != KunitStatus.SUCCESS:
@@ -168,6 +171,10 @@ def add_common_opts(parser):
                            help='As in the make command, it specifies the 
build '
                            'directory.',
                             type=str, default='.kunit', metavar='build_dir')
+       parser.add_argument('--uml_rootfs_dir',
+                           help='To load modules, a root filesystem is '
+                           'required to install and load these modules.',
+                            type=str, default=None, metavar='uml_rootfs_dir')
        parser.add_argument('--make_options',
                            help='X=Y make option, can be repeated.',
                            action='append')
@@ -196,6 +203,7 @@ def add_parse_opts(parser):
 
 
 def main(argv, linux=None):
+       uml_rootfs_path = None
        parser = argparse.ArgumentParser(
                        description='Helps writing and running KUnit tests.')
        subparser = parser.add_subparsers(dest='subcommand')
@@ -235,6 +243,16 @@ def main(argv, linux=None):
 
        cli_args = parser.parse_args(argv)
 
+       if cli_args.uml_rootfs_dir \
+                       and os.path.exists(cli_args.uml_rootfs_dir) \
+                       and os.path.abspath(cli_args.uml_rootfs_dir) != \
+                               os.path.abspath(os.getcwd()) \
+                       and os.path.abspath(cli_args.uml_rootfs_dir) != '/':
+               uml_rootfs_path = os.path.abspath(cli_args.uml_rootfs_dir)
+       elif cli_args.uml_rootfs_dir != None:
+               print("Invalid uml_rootfs_dir: 
{}".format(cli_args.uml_rootfs_dir))
+               sys.exit(1)
+
        if cli_args.subcommand == 'run':
                if not os.path.exists(cli_args.build_dir):
                        os.mkdir(cli_args.build_dir)
@@ -246,6 +264,7 @@ def main(argv, linux=None):
                                       cli_args.timeout,
                                       cli_args.jobs,
                                       cli_args.build_dir,
+                                      uml_rootfs_path,
                                       cli_args.alltests,
                                       cli_args.make_options)
                result = run_tests(linux, request)
@@ -260,6 +279,7 @@ def main(argv, linux=None):
                        linux = kunit_kernel.LinuxSourceTree()
 
                request = KunitConfigRequest(cli_args.build_dir,
+                                            uml_rootfs_path,
                                             cli_args.make_options)
                result = config_tests(linux, request)
                kunit_parser.print_with_timestamp((
@@ -277,6 +297,7 @@ def main(argv, linux=None):
 
                request = KunitBuildRequest(cli_args.jobs,
                                            cli_args.build_dir,
+                                           uml_rootfs_path,
                                            cli_args.alltests,
                                            cli_args.make_options)
                result = build_tests(linux, request)
@@ -295,6 +316,7 @@ def main(argv, linux=None):
 
                exec_request = KunitExecRequest(cli_args.timeout,
                                                cli_args.build_dir,
+                                               uml_rootfs_path,
                                                cli_args.alltests)
                exec_result = exec_tests(linux, exec_request)
                parse_request = KunitParseRequest(cli_args.raw_output,
diff --git a/tools/testing/kunit/kunit_kernel.py 
b/tools/testing/kunit/kunit_kernel.py
index 63dbda2d029f..aeffd024816c 100644
--- a/tools/testing/kunit/kunit_kernel.py
+++ b/tools/testing/kunit/kunit_kernel.py
@@ -11,6 +11,7 @@ import logging
 import subprocess
 import os
 import signal
+import time
 
 from contextlib import ExitStack
 
@@ -19,8 +20,58 @@ import kunit_parser
 
 KCONFIG_PATH = '.config'
 kunitconfig_path = '.kunitconfig'
+X86_64_DEFCONFIG_PATH = 'arch/um/configs/x86_64_defconfig'
 BROKEN_ALLCONFIG_PATH = 'tools/testing/kunit/configs/broken_on_uml.config'
 
+MAKE_CMD = {
+       'make': {
+               'command': ['make', 'ARCH=um'],
+               'msg_error': 'Could not call execute make: ',
+       },
+       'make_modules': {
+               'command': ['make', 'modules', 'ARCH=um'],
+               'msg_error': 'Could not call execute make modules: ',
+       },
+       'make_modules_install': {
+               'command': ['make', 'modules_install', 'ARCH=um'],
+               'msg_error': 'Could not call execute make modules_install: ',
+       }
+}
+
+def halt_uml():
+       try:
+               subprocess.call(['uml_mconsole', 'kunitid', 'halt'])
+       except OSError as e:
+               raise ConfigError('Could not call uml_mconsole ' + e)
+       except subprocess.CalledProcessError as e:
+                       raise ConfigError(e.output)
+
+def enable_uml_modules_on_boot(output_command, uml_rootfs_path):
+       uml_modules_path = None
+       found_kernel_version = False
+       modules = []
+       for i in output_command.decode('utf-8').split():
+               if found_kernel_version:
+                       kernel_version = i
+                       uml_modules_path = os.path.join(uml_rootfs_path,
+                             'lib/modules/', kernel_version, 'kernel/lib/')
+                       break
+               if 'DEPMOD' in i:
+                       found_kernel_version = True
+
+       try:
+               if os.path.exists(uml_modules_path):
+                       modules = subprocess.check_output(['ls',
+                                           
uml_modules_path]).decode('utf-8').split()
+       except OSError as e:
+               raise ConfigError('Could not list directory ' + e)
+       except subprocess.CalledProcessError as e:
+                       raise ConfigError(e.output)
+
+       with open(os.path.join(uml_rootfs_path, 'etc/modules'), 'w') as f:
+               for i in modules:
+                       f.write(i.replace('.ko', ''))
+
 class ConfigError(Exception):
        """Represents an error trying to configure the Linux kernel."""
 
@@ -70,20 +121,28 @@ class LinuxSourceTreeOperations(object):
                kunit_parser.print_with_timestamp(
                        'Starting Kernel with all configs takes a few 
minutes...')
 
-       def make(self, jobs, build_dir, make_options):
-               command = ['make', 'ARCH=um', '--jobs=' + str(jobs)]
+       def make(self, cmd, jobs, build_dir, uml_rootfs_path, make_options):
+               command = MAKE_CMD[cmd]['command'] + ['--jobs=' + str(jobs)]
+
                if make_options:
                        command.extend(make_options)
                if build_dir:
                        command += ['O=' + build_dir]
+
+               if cmd == 'make_modules_install':
+                       command += ['INSTALL_MOD_PATH=' + uml_rootfs_path]
+
                try:
-                       subprocess.check_output(command)
+                       output = subprocess.check_output(command)
+                       if cmd == 'make_modules_install':
+                                enable_uml_modules_on_boot(output,
+                                                           uml_rootfs_path)
                except OSError as e:
-                       raise BuildError('Could not call execute make: ' + e)
+                       raise BuildError(MAKE_CMD[cmd]['msg_error'] + e)
                except subprocess.CalledProcessError as e:
                        raise BuildError(e.output)
 
-       def linux_bin(self, params, timeout, build_dir, outfile):
+       def linux_bin(self, params, timeout, build_dir, uml_rootfs_path, 
outfile):
                """Runs the Linux UML binary. Must be named 'linux'."""
                linux_bin = './linux'
                if build_dir:
@@ -92,7 +151,11 @@ class LinuxSourceTreeOperations(object):
                        process = subprocess.Popen([linux_bin] + params,
                                                   stdout=output,
                                                   stderr=subprocess.STDOUT)
-                       process.wait(timeout)
+                       if uml_rootfs_path:
+                               time.sleep(timeout)
+                               halt_uml()
+                       else:
+                               process.wait(timeout)
 
 
 def get_kconfig_path(build_dir):
@@ -132,11 +195,16 @@ class LinuxSourceTree(object):
                        return False
                return True
 
-       def build_config(self, build_dir, make_options):
+       def build_config(self, build_dir, uml_rootfs_path, make_options):
                kconfig_path = get_kconfig_path(build_dir)
                if build_dir and not os.path.exists(build_dir):
                        os.mkdir(build_dir)
                self._kconfig.write_to_file(kconfig_path)
+
+               if uml_rootfs_path:
+                       with open(kconfig_path, 'a') as fw:
+                               with open(X86_64_DEFCONFIG_PATH, 'r') as fr:
+                                       fw.write(fr.read())
                try:
                        self._ops.make_olddefconfig(build_dir, make_options)
                except ConfigError as e:
@@ -144,7 +212,7 @@ class LinuxSourceTree(object):
                        return False
                return self.validate_config(build_dir)
 
-       def build_reconfig(self, build_dir, make_options):
+       def build_reconfig(self, build_dir, uml_rootfs_path, make_options):
                """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):
@@ -153,28 +221,38 @@ class LinuxSourceTree(object):
                        if not self._kconfig.is_subset_of(existing_kconfig):
                                print('Regenerating .config ...')
                                os.remove(kconfig_path)
-                               return self.build_config(build_dir, 
make_options)
+                               return self.build_config(build_dir, 
uml_rootfs_path,
+                                                        make_options)
                        else:
                                return True
                else:
                        print('Generating .config ...')
-                       return self.build_config(build_dir, make_options)
+                       return self.build_config(build_dir, uml_rootfs_path, 
make_options)
 
-       def build_um_kernel(self, alltests, jobs, build_dir, make_options):
+       def build_um_kernel(self, alltests, jobs, build_dir,
+                           uml_rootfs_path, make_options):
                if alltests:
                        self._ops.make_allyesconfig()
                try:
                        self._ops.make_olddefconfig(build_dir, make_options)
-                       self._ops.make(jobs, build_dir, make_options)
+                       self._ops.make('make', jobs, build_dir, 
uml_rootfs_path, make_options)
+                       if uml_rootfs_path:
+                               self._ops.make('make_modules', jobs, build_dir,
+                                               uml_rootfs_path, make_options)
+                               self._ops.make('make_modules_install', jobs,
+                                               build_dir, uml_rootfs_path, 
make_options)
                except (ConfigError, BuildError) as e:
                        logging.error(e)
                        return False
                return self.validate_config(build_dir)
 
-       def run_kernel(self, args=[], build_dir='', timeout=None):
+       def run_kernel(self, args=[], build_dir='', uml_rootfs_path=None, 
timeout=None):
                args.extend(['mem=1G'])
+               if uml_rootfs_path:
+                       args.extend(['root=/dev/root', 'rootfstype=hostfs',
+                                    'rootflags=' + uml_rootfs_path, 
'umid=kunitid'])
                outfile = 'test.log'
-               self._ops.linux_bin(args, timeout, build_dir, outfile)
+               self._ops.linux_bin(args, timeout, build_dir, uml_rootfs_path, 
outfile)
                subprocess.call(['stty', 'sane'])
                with open(outfile, 'r') as file:
                        for line in file:
diff --git a/tools/testing/kunit/kunit_tool_test.py 
b/tools/testing/kunit/kunit_tool_test.py
index ebedfc57a39b..2359c77c8efb 100755
--- a/tools/testing/kunit/kunit_tool_test.py
+++ b/tools/testing/kunit/kunit_tool_test.py
@@ -247,14 +247,14 @@ class KUnitMainTest(unittest.TestCase):
        def test_build_passes_args_pass(self):
                kunit.main(['build'], self.linux_source_mock)
                assert self.linux_source_mock.build_reconfig.call_count == 0
-               
self.linux_source_mock.build_um_kernel.assert_called_once_with(False, 8, 
'.kunit', None)
+               
self.linux_source_mock.build_um_kernel.assert_called_once_with(False, 8, 
'.kunit', None, None)
                assert self.linux_source_mock.run_kernel.call_count == 0
 
        def test_exec_passes_args_pass(self):
                kunit.main(['exec'], self.linux_source_mock)
                assert self.linux_source_mock.build_reconfig.call_count == 0
                assert self.linux_source_mock.run_kernel.call_count == 1
-               
self.linux_source_mock.run_kernel.assert_called_once_with(build_dir='.kunit', 
timeout=300)
+               
self.linux_source_mock.run_kernel.assert_called_once_with(build_dir='.kunit', 
uml_rootfs_path=None, timeout=300)
                self.print_mock.assert_any_call(StrContains('Testing 
complete.'))
 
        def test_run_passes_args_pass(self):
@@ -262,7 +262,7 @@ class KUnitMainTest(unittest.TestCase):
                assert self.linux_source_mock.build_reconfig.call_count == 1
                assert self.linux_source_mock.run_kernel.call_count == 1
                self.linux_source_mock.run_kernel.assert_called_once_with(
-                       build_dir='.kunit', timeout=300)
+                       build_dir='.kunit', uml_rootfs_path=None, timeout=300)
                self.print_mock.assert_any_call(StrContains('Testing 
complete.'))
 
        def test_exec_passes_args_fail(self):
@@ -302,7 +302,7 @@ class KUnitMainTest(unittest.TestCase):
        def test_exec_timeout(self):
                timeout = 3453
                kunit.main(['exec', '--timeout', str(timeout)], 
self.linux_source_mock)
-               
self.linux_source_mock.run_kernel.assert_called_once_with(build_dir='.kunit', 
timeout=timeout)
+               
self.linux_source_mock.run_kernel.assert_called_once_with(build_dir='.kunit', 
uml_rootfs_path=None, timeout=timeout)
                self.print_mock.assert_any_call(StrContains('Testing 
complete.'))
 
        def test_run_timeout(self):
@@ -310,7 +310,7 @@ class KUnitMainTest(unittest.TestCase):
                kunit.main(['run', '--timeout', str(timeout)], 
self.linux_source_mock)
                assert self.linux_source_mock.build_reconfig.call_count == 1
                self.linux_source_mock.run_kernel.assert_called_once_with(
-                       build_dir='.kunit', timeout=timeout)
+                       build_dir='.kunit', uml_rootfs_path=None, 
timeout=timeout)
                self.print_mock.assert_any_call(StrContains('Testing 
complete.'))
 
        def test_run_builddir(self):
@@ -318,7 +318,7 @@ class KUnitMainTest(unittest.TestCase):
                kunit.main(['run', '--build_dir=.kunit'], 
self.linux_source_mock)
                assert self.linux_source_mock.build_reconfig.call_count == 1
                self.linux_source_mock.run_kernel.assert_called_once_with(
-                       build_dir=build_dir, timeout=300)
+                       build_dir=build_dir, uml_rootfs_path=None, timeout=300)
                self.print_mock.assert_any_call(StrContains('Testing 
complete.'))
 
        def test_config_builddir(self):
@@ -329,12 +329,12 @@ class KUnitMainTest(unittest.TestCase):
        def test_build_builddir(self):
                build_dir = '.kunit'
                kunit.main(['build', '--build_dir', build_dir], 
self.linux_source_mock)
-               
self.linux_source_mock.build_um_kernel.assert_called_once_with(False, 8, 
build_dir, None)
+               
self.linux_source_mock.build_um_kernel.assert_called_once_with(False, 8, 
build_dir, None, None)
 
        def test_exec_builddir(self):
                build_dir = '.kunit'
                kunit.main(['exec', '--build_dir', build_dir], 
self.linux_source_mock)
-               
self.linux_source_mock.run_kernel.assert_called_once_with(build_dir=build_dir, 
timeout=300)
+               
self.linux_source_mock.run_kernel.assert_called_once_with(build_dir=build_dir, 
uml_rootfs_path=None, timeout=300)
                self.print_mock.assert_any_call(StrContains('Testing 
complete.'))
 
 if __name__ == '__main__':

base-commit: 725aca9585956676687c4cb803e88f770b0df2b2
prerequisite-patch-id: 5e5f9a8a05c5680fda1b04c9ab1b95ce91dc88b2
prerequisite-patch-id: 4d997940f4a9f303424af9bac412de1af861f9d9
prerequisite-patch-id: 582b6d9d28ce4b71628890ec832df6522ca68de0
-- 
2.26.2

Reply via email to