test: Add mkosi-based integration test runner

The first two tests are included to ensure parallel test execution is
demonstrable.
This commit is contained in:
Richard Maw 2024-04-05 17:19:59 +01:00
parent 20c7c570b9
commit 945b722f13
9 changed files with 225 additions and 1 deletions

View file

@ -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')

View file

@ -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',

View file

@ -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

View file

@ -5,6 +5,9 @@
[Content]
Autologin=yes
ExtraTrees=
%D/mkosi.crt:/usr/lib/verity.d/mkosi.crt # sysext verification key
Packages=
acl
bash-completion

View file

@ -0,0 +1,2 @@
[Service]
PassEnvironment=SYSTEMD_UNIT_PATH

View file

@ -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
==============================

View file

@ -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
''',
]),
],
}

134
test/integration_test_wrapper.py Executable file
View file

@ -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()

View file

@ -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