lkml.org 
[lkml]   [2020]   [Jul]   [21]   [last100]   RSS Feed
Views: [wrap][no wrap]   [headers]  [forward] 
 
Messages in this thread
Patch in this message
/
From
Subject[RFC v3] kunit: Allows to install and load modules in root filesystem
Date
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 <vitor@massaru.org>
Reviewed-by: Brendan Higgins <brendanhiggins@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);
v3:
* rebased with last kunit branch
* Please apply this commit from kunit-fixes:
3f37d14b8a3152441f36b6bc74000996679f0998
---
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 d23385e3e159..3704f8de07a5 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 e20e2056cb38..0f21f7e879ed 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, stderr=subprocess.STDOUT)
+ output = subprocess.check_output(command, stderr=subprocess.STDOUT)
+ 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: d43c7fb05765152d4d4a39a8ef957c4ea14d8847
--
2.26.2

\
 
 \ /
  Last update: 2020-07-21 19:30    [W:0.059 / U:0.828 seconds]
©2003-2020 Jasper Spaans|hosted at Digital Ocean and TransIP|Read the blog|Advertise on this site