Compare commits

...

10 Commits

Author SHA1 Message Date
Daan De Meyer c24cfff558 test: Dump coredumps from journal in the integration test wrapper
Fixes #35277
2024-11-24 00:04:27 +01:00
Daan De Meyer f15e6f0699 test: Lint integration-test-wrapper.py 2024-11-24 00:04:27 +01:00
Daan De Meyer 5f25417acb test: Fix typing errors in integration-test-wrapper.py 2024-11-24 00:04:27 +01:00
Daan De Meyer 4bfd8c825d test: Format integration-test-wrapper.py 2024-11-24 00:04:27 +01:00
Daan De Meyer a54b191d59 ukify: Fix typing error 2024-11-24 00:04:27 +01:00
Daan De Meyer 3f32239f32 Move mypy.ini and ruff.toml to top level
This allows reusing them for integration-test-wrapper.py as well.
2024-11-24 00:04:27 +01:00
Daan De Meyer 7f7c1fab74 integration-test-wrapper: Remove unneeded format strings 2024-11-24 00:04:27 +01:00
Daan De Meyer c07852ab72 coredumpctl: Don't treat no coredumps as failure if JSON is enabled
Having to deal with a process that fails or doesn't fail depending on
whether there are coredumps or not is incredibly annoying for users.
So let's compromise and not fail if JSON output is enabled and there
are no coredumps.
2024-11-24 00:04:27 +01:00
Daan De Meyer 4b7514d10c mkosi: Add dnf and dnf5 to sanitizer workaround list 2024-11-24 00:04:27 +01:00
Daan De Meyer 9ebec661f9 mkosi: Install clangd everywhere 2024-11-23 23:36:17 +01:00
17 changed files with 167 additions and 86 deletions

View File

@ -37,7 +37,7 @@ jobs:
VALIDATE_GITHUB_ACTIONS: true VALIDATE_GITHUB_ACTIONS: true
- name: Check that tabs are not used in Python code - name: Check that tabs are not used in Python code
run: sh -c '! git grep -P "\\t" -- src/ukify/ukify.py' run: sh -c '! git grep -P "\\t" -- src/ukify/ukify.py test/integration-test-wrapper.py'
- name: Install ruff and mypy - name: Install ruff and mypy
run: | run: |
@ -47,14 +47,14 @@ jobs:
- name: Run mypy - name: Run mypy
run: | run: |
python3 -m mypy --version python3 -m mypy --version
python3 -m mypy src/ukify/ukify.py python3 -m mypy src/ukify/ukify.py test/integration-test-wrapper.py
- name: Run ruff check - name: Run ruff check
run: | run: |
ruff --version ruff --version
ruff check src/ukify/ukify.py ruff check src/ukify/ukify.py test/integration-test-wrapper.py
- name: Run ruff format - name: Run ruff format
run: | run: |
ruff --version ruff --version
ruff format --check src/ukify/ukify.py ruff format --check src/ukify/ukify.py test/integration-test-wrapper.py

View File

@ -391,7 +391,7 @@
<title>Exit status</title> <title>Exit status</title>
<para>On success, 0 is returned; otherwise, a non-zero failure <para>On success, 0 is returned; otherwise, a non-zero failure
code is returned. Not finding any matching core dumps is treated as code is returned. Not finding any matching core dumps is treated as
failure. failure unless JSON output is enabled.
</para> </para>
</refsect1> </refsect1>

View File

@ -13,6 +13,7 @@ Environment=
[Content] [Content]
Packages= Packages=
clangd
compiler-rt compiler-rt
gdb gdb
git-core git-core

View File

@ -15,6 +15,7 @@ Environment=
[Content] [Content]
Packages= Packages=
apt apt
clangd
erofs-utils erofs-utils
git-core git-core
libclang-rt-dev libclang-rt-dev

View File

@ -12,6 +12,7 @@ Environment=
[Content] [Content]
Packages= Packages=
clang
diffutils diffutils
erofs-utils erofs-utils
gcc-c++ gcc-c++

View File

@ -57,6 +57,8 @@ wrap=(
delv delv
dhcpd dhcpd
dig dig
dnf
dnf5
dmsetup dmsetup
dnsmasq dnsmasq
findmnt findmnt

View File

@ -965,6 +965,8 @@ static int dump_list(int argc, char **argv, void *userdata) {
if (!arg_field && n_found <= 0) { if (!arg_field && n_found <= 0) {
if (!arg_quiet) if (!arg_quiet)
log_notice("No coredumps found."); log_notice("No coredumps found.");
if (!sd_json_format_enabled(arg_json_format_flags))
return -ESRCH; return -ESRCH;
} }
} }

View File

@ -467,7 +467,7 @@ class SignTool:
raise NotImplementedError() raise NotImplementedError()
@staticmethod @staticmethod
def from_string(name) -> type['SignTool']: def from_string(name: str) -> type['SignTool']:
if name == 'pesign': if name == 'pesign':
return PeSign return PeSign
elif name == 'sbsign': elif name == 'sbsign':

View File

@ -3,6 +3,7 @@
integration_tests += [ integration_tests += [
integration_test_template + { integration_test_template + {
'name' : fs.name(meson.current_source_dir()), 'name' : fs.name(meson.current_source_dir()),
'coredump-exclude-regex' : '/(bash|python3.[0-9]+|systemd-executor)$',
'cmdline' : integration_test_template['cmdline'] + [ 'cmdline' : integration_test_template['cmdline'] + [
''' '''

View File

@ -4,7 +4,7 @@ integration_tests += [
integration_test_template + { integration_test_template + {
'name' : fs.name(meson.current_source_dir()), 'name' : fs.name(meson.current_source_dir()),
'unit' : files('TEST-16-EXTEND-TIMEOUT.service'), 'unit' : files('TEST-16-EXTEND-TIMEOUT.service'),
'coredump-exclude-regex' : '/(bash|sleep), 'coredump-exclude-regex' : '/(bash|sleep)$',
}, },
] ]

View File

@ -4,5 +4,6 @@ integration_tests += [
integration_test_template + { integration_test_template + {
'name' : fs.name(meson.current_source_dir()), 'name' : fs.name(meson.current_source_dir()),
'vm' : true, 'vm' : true,
'coredump-exclude-regex' : '/(sleep|udevadm)$',
}, },
] ]

View File

@ -3,5 +3,6 @@
integration_tests += [ integration_tests += [
integration_test_template + { integration_test_template + {
'name' : fs.name(meson.current_source_dir()), 'name' : fs.name(meson.current_source_dir()),
'coredump-exclude-regex' : '/(sleep|bash|systemd-notify)$',
}, },
] ]

View File

@ -5,6 +5,7 @@ integration_tests += [
'name' : fs.name(meson.current_source_dir()), 'name' : fs.name(meson.current_source_dir()),
'storage': 'persistent', 'storage': 'persistent',
'vm' : true, 'vm' : true,
'coredump-exclude-regex' : '/(test-usr-dump|test-dump|bash)$',
}, },
] ]

View File

@ -1,19 +1,18 @@
#!/usr/bin/python3 #!/usr/bin/python3
# SPDX-License-Identifier: LGPL-2.1-or-later # SPDX-License-Identifier: LGPL-2.1-or-later
'''Test wrapper command for driving integration tests. """Test wrapper command for driving integration tests."""
'''
import argparse import argparse
import json import json
import os import os
import re
import shlex import shlex
import subprocess import subprocess
import sys import sys
import textwrap import textwrap
from pathlib import Path from pathlib import Path
EMERGENCY_EXIT_DROPIN = """\ EMERGENCY_EXIT_DROPIN = """\
[Unit] [Unit]
Wants=emergency-exit.service Wants=emergency-exit.service
@ -34,7 +33,61 @@ ExecStart=false
""" """
def main(): def dump_coredumps(args: argparse.Namespace, journal_file: Path) -> bool:
# Collect executable paths of all coredumps and filter out the expected ones.
# The following are excluded:
# sleep/bash - intentional SIGABRT caused by TEST-57
# systemd-notify - intermittent (and intentional) SIGABRT caused by TEST-59
# test-execute - intentional coredump in TEST-02
# test(-usr)?-dump - intentional coredumps from systemd-coredump tests in TEST-74
if args.coredump_exclude_regex:
exclude_regex = re.compile(args.coredump_exclude_regex)
else:
exclude_regex = None
coredumps = json.loads(
subprocess.run(
[
args.mkosi,
'--directory', os.fspath(args.meson_source_dir),
'--extra-search-path', os.fspath(args.meson_build_dir),
'sandbox',
'coredumpctl',
'--file', journal_file,
'--json=short',
],
check=True,
stdout=subprocess.PIPE,
text=True,
).stdout
) # fmt: skip
coredumps = [
coredump for coredump in coredumps if not exclude_regex or not exclude_regex.search(coredump['exe'])
]
if not coredumps:
return False
subprocess.run(
[
args.mkosi,
'--directory', os.fspath(args.meson_source_dir),
'--extra-search-path', os.fspath(args.meson_build_dir),
'sandbox',
'coredumpctl',
'--file', journal_file,
'--no-pager',
'info',
*(coredump['exe'] for coredump in coredumps),
],
check=True,
) # fmt: skip
return True
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--mkosi', required=True) parser.add_argument('--mkosi', required=True)
parser.add_argument('--meson-source-dir', required=True, type=Path) parser.add_argument('--meson-source-dir', required=True, type=Path)
@ -46,34 +99,44 @@ def main():
parser.add_argument('--slow', action=argparse.BooleanOptionalAction) parser.add_argument('--slow', action=argparse.BooleanOptionalAction)
parser.add_argument('--vm', action=argparse.BooleanOptionalAction) parser.add_argument('--vm', action=argparse.BooleanOptionalAction)
parser.add_argument('--exit-code', required=True, type=int) parser.add_argument('--exit-code', required=True, type=int)
parser.add_argument('mkosi_args', nargs="*") parser.add_argument('--coredump-exclude-regex', required=True)
parser.add_argument('mkosi_args', nargs='*')
args = parser.parse_args() args = parser.parse_args()
if not bool(int(os.getenv("SYSTEMD_INTEGRATION_TESTS", "0"))): if not bool(int(os.getenv('SYSTEMD_INTEGRATION_TESTS', '0'))):
print(f"SYSTEMD_INTEGRATION_TESTS=1 not found in environment, skipping {args.name}", file=sys.stderr) print(
f'SYSTEMD_INTEGRATION_TESTS=1 not found in environment, skipping {args.name}',
file=sys.stderr,
)
exit(77) exit(77)
if args.slow and not bool(int(os.getenv("SYSTEMD_SLOW_TESTS", "0"))): if args.slow and not bool(int(os.getenv('SYSTEMD_SLOW_TESTS', '0'))):
print(f"SYSTEMD_SLOW_TESTS=1 not found in environment, skipping {args.name}", file=sys.stderr) print(
f'SYSTEMD_SLOW_TESTS=1 not found in environment, skipping {args.name}',
file=sys.stderr,
)
exit(77) exit(77)
if args.vm and bool(int(os.getenv("TEST_NO_QEMU", "0"))): if args.vm and bool(int(os.getenv('TEST_NO_QEMU', '0'))):
print(f"TEST_NO_QEMU=1, skipping {args.name}", file=sys.stderr) print(f'TEST_NO_QEMU=1, skipping {args.name}', file=sys.stderr)
exit(77) exit(77)
for s in os.getenv("TEST_SKIP", "").split(): for s in os.getenv('TEST_SKIP', '').split():
if s in args.name: if s in args.name:
print(f"Skipping {args.name} due to TEST_SKIP", file=sys.stderr) print(f'Skipping {args.name} due to TEST_SKIP', file=sys.stderr)
exit(77) exit(77)
keep_journal = os.getenv("TEST_SAVE_JOURNAL", "fail") keep_journal = os.getenv('TEST_SAVE_JOURNAL', 'fail')
shell = bool(int(os.getenv("TEST_SHELL", "0"))) shell = bool(int(os.getenv('TEST_SHELL', '0')))
if shell and not sys.stderr.isatty(): if shell and not sys.stderr.isatty():
print(f"--interactive must be passed to meson test to use TEST_SHELL=1", file=sys.stderr) print(
'--interactive must be passed to meson test to use TEST_SHELL=1',
file=sys.stderr,
)
exit(1) exit(1)
name = args.name + (f"-{i}" if (i := os.getenv("MESON_TEST_ITERATION")) else "") name = args.name + (f'-{i}' if (i := os.getenv('MESON_TEST_ITERATION')) else '')
dropin = textwrap.dedent( dropin = textwrap.dedent(
"""\ """\
@ -84,14 +147,14 @@ def main():
if not shell: if not shell:
dropin += textwrap.dedent( dropin += textwrap.dedent(
f""" """
[Unit] [Unit]
SuccessAction=exit SuccessAction=exit
SuccessActionExitStatus=123 SuccessActionExitStatus=123
""" """
) )
if os.getenv("TEST_MATCH_SUBTEST"): if os.getenv('TEST_MATCH_SUBTEST'):
dropin += textwrap.dedent( dropin += textwrap.dedent(
f""" f"""
[Service] [Service]
@ -99,7 +162,7 @@ def main():
""" """
) )
if os.getenv("TEST_MATCH_TESTCASE"): if os.getenv('TEST_MATCH_TESTCASE'):
dropin += textwrap.dedent( dropin += textwrap.dedent(
f""" f"""
[Service] [Service]
@ -107,7 +170,9 @@ def main():
""" """
) )
journal_file = None journal_file = (args.meson_build_dir / (f'test/journal/{name}.journal')).absolute()
journal_file.unlink(missing_ok=True)
if not sys.stderr.isatty(): if not sys.stderr.isatty():
dropin += textwrap.dedent( dropin += textwrap.dedent(
""" """
@ -115,9 +180,6 @@ def main():
FailureAction=exit FailureAction=exit
""" """
) )
journal_file = (args.meson_build_dir / (f"test/journal/{name}.journal")).absolute()
journal_file.unlink(missing_ok=True)
elif not shell: elif not shell:
dropin += textwrap.dedent( dropin += textwrap.dedent(
""" """
@ -136,87 +198,93 @@ def main():
*(['--forward-journal', journal_file] if journal_file else []), *(['--forward-journal', journal_file] if journal_file else []),
*( *(
[ [
'--credential', '--credential', f'systemd.extra-unit.emergency-exit.service={shlex.quote(EMERGENCY_EXIT_SERVICE)}', # noqa: E501
f"systemd.extra-unit.emergency-exit.service={shlex.quote(EMERGENCY_EXIT_SERVICE)}", '--credential', f'systemd.unit-dropin.emergency.target={shlex.quote(EMERGENCY_EXIT_DROPIN)}',
'--credential',
f"systemd.unit-dropin.emergency.target={shlex.quote(EMERGENCY_EXIT_DROPIN)}",
] ]
if not sys.stderr.isatty() if not sys.stderr.isatty()
else [] else []
), ),
'--credential', '--credential', f'systemd.unit-dropin.{args.unit}={shlex.quote(dropin)}',
f"systemd.unit-dropin.{args.unit}={shlex.quote(dropin)}",
'--runtime-network=none', '--runtime-network=none',
'--runtime-scratch=no', '--runtime-scratch=no',
*args.mkosi_args, *args.mkosi_args,
'--qemu-firmware', args.firmware, '--qemu-firmware',
*(['--qemu-kvm', 'no'] if int(os.getenv("TEST_NO_KVM", "0")) else []), args.firmware,
*(['--qemu-kvm', 'no'] if int(os.getenv('TEST_NO_KVM', '0')) else []),
'--kernel-command-line-extra', '--kernel-command-line-extra',
' '.join([ ' '.join(
[
'systemd.hostname=H', 'systemd.hostname=H',
f"SYSTEMD_UNIT_PATH=/usr/lib/systemd/tests/testdata/{args.name}.units:/usr/lib/systemd/tests/testdata/units:", f'SYSTEMD_UNIT_PATH=/usr/lib/systemd/tests/testdata/{args.name}.units:/usr/lib/systemd/tests/testdata/units:',
*([f"systemd.unit={args.unit}"] if not shell else []), *([f'systemd.unit={args.unit}'] if not shell else []),
'systemd.mask=systemd-networkd-wait-online.service', 'systemd.mask=systemd-networkd-wait-online.service',
*( *(
[ [
"systemd.mask=serial-getty@.service", 'systemd.mask=serial-getty@.service',
"systemd.show_status=error", 'systemd.show_status=error',
"systemd.crash_shell=0", 'systemd.crash_shell=0',
"systemd.crash_action=poweroff", 'systemd.crash_action=poweroff',
] ]
if not sys.stderr.isatty() if not sys.stderr.isatty()
else [] else []
), ),
]), ]
),
'--credential', f"journal.storage={'persistent' if sys.stderr.isatty() else args.storage}", '--credential', f"journal.storage={'persistent' if sys.stderr.isatty() else args.storage}",
*(['--runtime-build-sources=no'] if not sys.stderr.isatty() else []), *(['--runtime-build-sources=no'] if not sys.stderr.isatty() else []),
'qemu' if args.vm or os.getuid() != 0 else 'boot', 'qemu' if args.vm or os.getuid() != 0 else 'boot',
] ] # fmt: skip
result = subprocess.run(cmd) result = subprocess.run(cmd)
# On Debian/Ubuntu we get a lot of random QEMU crashes. Retry once, and then skip if it fails again. # On Debian/Ubuntu we get a lot of random QEMU crashes. Retry once, and then skip if it fails again.
if args.vm and result.returncode == 247 and args.exit_code != 247: if args.vm and result.returncode == 247 and args.exit_code != 247:
if journal_file:
journal_file.unlink(missing_ok=True) journal_file.unlink(missing_ok=True)
result = subprocess.run(cmd) result = subprocess.run(cmd)
if args.vm and result.returncode == 247 and args.exit_code != 247: if args.vm and result.returncode == 247 and args.exit_code != 247:
print(f"Test {args.name} failed due to QEMU crash (error 247), ignoring", file=sys.stderr) print(
f'Test {args.name} failed due to QEMU crash (error 247), ignoring',
file=sys.stderr,
)
exit(77) exit(77)
if journal_file and (keep_journal == "0" or (result.returncode in (args.exit_code, 77) and keep_journal == "fail")): coredumps = dump_coredumps(args, journal_file)
if keep_journal == '0' or (
keep_journal == 'fail' and result.returncode in (args.exit_code, 77) and not coredumps
):
journal_file.unlink(missing_ok=True) journal_file.unlink(missing_ok=True)
if shell or result.returncode in (args.exit_code, 77): if shell or (result.returncode in (args.exit_code, 77) and not coredumps):
exit(0 if shell or result.returncode == args.exit_code else 77) exit(0 if shell or result.returncode == args.exit_code else 77)
if journal_file:
ops = [] ops = []
if os.getenv("GITHUB_ACTIONS"): if os.getenv('GITHUB_ACTIONS'):
id = os.environ["GITHUB_RUN_ID"] id = os.environ['GITHUB_RUN_ID']
iteration = os.environ["GITHUB_RUN_ATTEMPT"] iteration = os.environ['GITHUB_RUN_ATTEMPT']
j = json.loads( j = json.loads(
subprocess.run( subprocess.run(
[ [
args.mkosi, args.mkosi,
"--directory", os.fspath(args.meson_source_dir), '--directory', os.fspath(args.meson_source_dir),
"--json", '--json',
"summary", 'summary',
], ],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, text=True,
).stdout ).stdout
) ) # fmt: skip
distribution = j["Images"][-1]["Distribution"] distribution = j['Images'][-1]['Distribution']
release = j["Images"][-1]["Release"] release = j['Images'][-1]['Release']
artifact = f"ci-mkosi-{id}-{iteration}-{distribution}-{release}-failed-test-journals" artifact = f'ci-mkosi-{id}-{iteration}-{distribution}-{release}-failed-test-journals'
ops += [f"gh run download {id} --name {artifact} -D ci/{artifact}"] ops += [f'gh run download {id} --name {artifact} -D ci/{artifact}']
journal_file = Path(f"ci/{artifact}/test/journal/{name}.journal") journal_file = Path(f'ci/{artifact}/test/journal/{name}.journal')
ops += [f"journalctl --file {journal_file} --no-hostname -o short-monotonic -u {args.unit} -p info"] ops += [f'journalctl --file {journal_file} --no-hostname -o short-monotonic -u {args.unit} -p info']
print("Test failed, relevant logs can be viewed with: \n\n" print("Test failed, relevant logs can be viewed with: \n\n" f"{(' && '.join(ops))}\n", file=sys.stderr)
f"{(' && '.join(ops))}\n", file=sys.stderr)
# 0 also means we failed so translate that to a non-zero exit code to mark the test as failed. # 0 also means we failed so translate that to a non-zero exit code to mark the test as failed.
exit(result.returncode or 1) exit(result.returncode or 1)

View File

@ -297,6 +297,7 @@ integration_test_template = {
'qemu-args' : [], 'qemu-args' : [],
'exit-code' : 123, 'exit-code' : 123,
'vm' : false, 'vm' : false,
'coredump-exclude-regex' : '',
} }
testdata_subdirs = [ testdata_subdirs = [
'auxv', 'auxv',
@ -391,6 +392,7 @@ foreach integration_test : integration_tests
'--storage', integration_test['storage'], '--storage', integration_test['storage'],
'--firmware', integration_test['firmware'], '--firmware', integration_test['firmware'],
'--exit-code', integration_test['exit-code'].to_string(), '--exit-code', integration_test['exit-code'].to_string(),
'--coredump-exclude-regex', integration_test['coredump-exclude-regex'],
] ]
if 'unit' in integration_test if 'unit' in integration_test