From 945b722f13808a6dfd3c00cb1ef78c3c41011687 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Fri, 5 Apr 2024 17:19:59 +0100 Subject: [PATCH] test: Add mkosi-based integration test runner The first two tests are included to ensure parallel test execution is demonstrable. --- meson.build | 12 ++ meson_options.txt | 2 + mkosi.conf | 3 +- mkosi.images/system/mkosi.conf | 3 + .../user@.service.d/99-SYSTEMD_UNIT_PATH.conf | 2 + test/README.testsuite | 16 +++ test/TEST-02-UNITTESTS/meson.build | 13 ++ test/integration_test_wrapper.py | 134 ++++++++++++++++++ test/meson.build | 41 ++++++ 9 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 mkosi.images/system/mkosi.extra/usr/lib/systemd/system/user@.service.d/99-SYSTEMD_UNIT_PATH.conf create mode 100644 test/TEST-02-UNITTESTS/meson.build create mode 100755 test/integration_test_wrapper.py diff --git a/meson.build b/meson.build index 1a160960cc6..e2de1480952 100644 --- a/meson.build +++ b/meson.build @@ -2573,6 +2573,18 @@ endif ##################################################################### +if get_option('integration-tests') != false + system_mkosi = custom_target('system_mkosi', + build_always_stale : true, + output : 'system', + console : true, + command : ['mkosi', '-C', meson.project_source_root(), '--image=system', '--format=disk', '--output-dir', meson.project_build_root() / '@OUTPUT@', '--without-tests', '-fi', 'build'], + depends : [executables_by_name['bootctl'], executables_by_name['systemd-measure'], executables_by_name['systemd-repart'], ukify], + ) +endif + +############################################################ + subdir('rules.d') subdir('test') diff --git a/meson_options.txt b/meson_options.txt index 41a524b0dcb..d52ca4e4b56 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -498,6 +498,8 @@ option('install-tests', type : 'boolean', value : false, description : 'install test executables') option('log-message-verification', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, description : 'do fake printf() calls to verify format strings') +option('integration-tests', type : 'boolean', value : false, + description : 'run the integration tests') option('ok-color', type : 'combo', choices : ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', diff --git a/mkosi.conf b/mkosi.conf index 1cc7a51d961..b9c928b027b 100644 --- a/mkosi.conf +++ b/mkosi.conf @@ -1,7 +1,7 @@ # SPDX-License-Identifier: LGPL-2.1-or-later [Config] -Images=system +@Images=system MinimumVersion=23~devel [Output] @@ -21,6 +21,7 @@ BuildSourcesEphemeral=yes @Incremental=yes @RuntimeSize=8G @RuntimeBuildSources=yes +@QemuSmp=2 ToolsTreePackages=virtiofsd KernelCommandLineExtra=systemd.crash_shell systemd.log_level=debug,console:info diff --git a/mkosi.images/system/mkosi.conf b/mkosi.images/system/mkosi.conf index ed09d841b84..90f302e44d0 100644 --- a/mkosi.images/system/mkosi.conf +++ b/mkosi.images/system/mkosi.conf @@ -5,6 +5,9 @@ [Content] Autologin=yes +ExtraTrees= + %D/mkosi.crt:/usr/lib/verity.d/mkosi.crt # sysext verification key + Packages= acl bash-completion diff --git a/mkosi.images/system/mkosi.extra/usr/lib/systemd/system/user@.service.d/99-SYSTEMD_UNIT_PATH.conf b/mkosi.images/system/mkosi.extra/usr/lib/systemd/system/user@.service.d/99-SYSTEMD_UNIT_PATH.conf new file mode 100644 index 00000000000..fa634935cb6 --- /dev/null +++ b/mkosi.images/system/mkosi.extra/usr/lib/systemd/system/user@.service.d/99-SYSTEMD_UNIT_PATH.conf @@ -0,0 +1,2 @@ +[Service] +PassEnvironment=SYSTEMD_UNIT_PATH diff --git a/test/README.testsuite b/test/README.testsuite index 0c04e2d4a6a..44e59ea951d 100644 --- a/test/README.testsuite +++ b/test/README.testsuite @@ -28,6 +28,22 @@ To run just one of the cases: $ sudo make -C test/TEST-01-BASIC clean setup run +To run the meson-based integration test config +enable integration tests and options for required commands with the following: + +$ meson configure build -Dintegration-tests=true -Dremote=enabled -Dopenssl=enabled -Dblkid=enabled -Dtpm2=enabled + +Once enabled the integration tests can be run with: + +$ sudo meson test -C build/ --suite integration-tests --num-processes "$((nproc / 2))" + +As usual, specific tests can be run in meson by appending the name of the test +which is usually the name of the directory e.g. + +$ sudo meson test -C build/ --suite integration-tests --num-processes "$((nproc / 2))" TEST-01-BASIC + +See `meson introspect build --tests` for a list of tests. + Specifying the build directory ============================== diff --git a/test/TEST-02-UNITTESTS/meson.build b/test/TEST-02-UNITTESTS/meson.build new file mode 100644 index 00000000000..6bc04838fb9 --- /dev/null +++ b/test/TEST-02-UNITTESTS/meson.build @@ -0,0 +1,13 @@ +test_params += { + 'mkosi_args': test_params['mkosi_args'] + [ + '--kernel-command-line-extra=' + ' '.join([ + ''' +frobnicate! + +systemd.setenv=TEST_CMDLINE_NEWLINE=foo +systemd.setenv=TEST_CMDLINE_NEWLINE=bar + +''', + ]), + ], +} diff --git a/test/integration_test_wrapper.py b/test/integration_test_wrapper.py new file mode 100755 index 00000000000..138b6afc244 --- /dev/null +++ b/test/integration_test_wrapper.py @@ -0,0 +1,134 @@ +#!/usr/bin/python3 +# SPDX-License-Identifier: LGPL-2.1-or-later + +'''Test wrapper command for driving integration tests. + +Note: This is deliberately rough and only intended to drive existing tests +with the expectation that as part of formally defining the API it will be tidy. + +''' + +import argparse +import logging +import os +from pathlib import Path +import shlex +import subprocess + + +TEST_EXIT_DROPIN = """\ +[Unit] +SuccessAction=exit +FailureAction=exit +""" + + +EMERGENCY_EXIT_DROPIN = """\ +[Unit] +Wants=emergency-exit.service +""" + + +EMERGENCY_EXIT_SERVICE = """\ +[Unit] +DefaultDependencies=no +Conflicts=shutdown.target +Conflicts=rescue.service +Before=shutdown.target +Before=rescue.service +FailureAction=exit + +[Service] +ExecStart=false +""" + + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--test-name', required=True) +parser.add_argument('--mkosi-image-name', required=True) +parser.add_argument('--mkosi-output-path', required=True, type=Path) +parser.add_argument('--test-number', required=True) +parser.add_argument('--no-emergency-exit', + dest='emergency_exit', default=True, action='store_false', + help="Disable emergency exit drop-ins for interactive debugging") +parser.add_argument('mkosi_args', nargs="*") + +def main(): + logging.basicConfig(level=logging.DEBUG) + args = parser.parse_args() + + test_unit_name = f"testsuite-{args.test_number}.service" + # Machine names shouldn't have / since it's used as a file name + # and it must be a valid hostname so 64 chars max + machine_name = args.test_name.replace('/', '_')[:64] + + logging.debug(f"test name: {args.test_name}\n" + f"test number: {args.test_number}\n" + f"image: {args.mkosi_image_name}\n" + f"mkosi output path: {args.mkosi_output_path}\n" + f"mkosi args: {args.mkosi_args}\n" + f"emergency exit: {args.emergency_exit}") + + journal_file = Path(f"{machine_name}.journal").absolute() + logging.info(f"Capturing journal to {journal_file}") + + mkosi_args = [ + 'mkosi', + '--directory', Path('..').resolve(), + '--output-dir', args.mkosi_output_path.absolute(), + '--machine', machine_name, + '--image', args.mkosi_image_name, + '--format=disk', + '--runtime-build-sources=no', + '--ephemeral', + '--forward-journal', journal_file, + *( + [ + '--credential', + f"systemd.extra-unit.emergency-exit.service={shlex.quote(EMERGENCY_EXIT_SERVICE)} " + f"systemd.unit-dropin.emergency.target={shlex.quote(EMERGENCY_EXIT_DROPIN)}", + ] + if args.emergency_exit + else [] + ), + f"--credential=systemd.unit-dropin.{test_unit_name}={shlex.quote(TEST_EXIT_DROPIN)}", + '--append', + '--kernel-command-line-extra', + ' '.join([ + 'systemd.hostname=H', + f"SYSTEMD_UNIT_PATH=/usr/lib/systemd/tests/testdata/testsuite-{args.test_number}.units:/usr/lib/systemd/tests/testdata/units:", + 'systemd.unit=testsuite.target', + f"systemd.wants={test_unit_name}", + ]), + *args.mkosi_args, + ] + + mkosi_args += ['qemu'] + + logging.debug(f"Running {shlex.join(os.fspath(a) for a in mkosi_args)}") + + try: + subprocess.run(mkosi_args, check=True) + except subprocess.CalledProcessError as e: + if e.returncode not in (0, 77): + suggested_command = [ + 'journalctl', + '--all', + '--no-hostname', + '-o', 'short-monotonic', + '--file', journal_file, + f"_SYSTEMD_UNIT={test_unit_name}", + '+', f"SYSLOG_IDENTIFIER=testsuite-{args.test_number}.sh", + '+', 'PRIORITY=4', + '+', 'PRIORITY=3', + '+', 'PRIORITY=2', + '+', 'PRIORITY=1', + '+', 'PRIORITY=0', + ] + logging.info("Test failed, relevant logs can be viewed with: " + f"{shlex.join(os.fspath(a) for a in suggested_command)}") + exit(e.returncode) + + +if __name__ == '__main__': + main() diff --git a/test/meson.build b/test/meson.build index aadf37a139e..4009eeece2c 100644 --- a/test/meson.build +++ b/test/meson.build @@ -331,3 +331,44 @@ if want_tests != 'false' and conf.get('ENABLE_KERNEL_INSTALL') == 1 depends : deps, suite : 'kernel-install') endif + +############################################################ + +if get_option('integration-tests') != false + integration_test_wrapper = find_program('integration_test_wrapper.py') + integration_tests = { + '01': 'TEST-01-BASIC', + '02': 'TEST-02-UNITTESTS', + } + foreach test_number, dirname : integration_tests + test_unit_name = f'testsuite-@test_number@.service' + test_params = { + 'test_name' : dirname, + 'mkosi_image_name' : 'system', + 'mkosi_output_path' : system_mkosi, + 'test_number' : test_number, + 'mkosi_args' : [], + 'depends' : [system_mkosi], + 'timeout' : 600, + } + + # TODO: This fs.exists call isn't included in rebuild logic + # so if you add a new meson.build in a subdir + # you need to touch another build file to get it to reparse. + if fs.exists(dirname / 'meson.build') + subdir(dirname) + endif + args = ['--test-name', test_params['test_name'], + '--mkosi-image-name', test_params['mkosi_image_name'], + '--mkosi-output-path', test_params['mkosi_output_path'], + '--test-number', test_params['test_number']] + args += ['--'] + test_params['mkosi_args'] + test(test_params['test_name'], + integration_test_wrapper, + env: test_env, + args : args, + depends : test_params['depends'], + timeout : test_params['timeout'], + suite : 'integration-tests') + endforeach +endif -- 2.39.2