Compare commits

...

10 Commits

Author SHA1 Message Date
Daan De Meyer 9aa7f22f1f test: Dump coredumps from journal in the integration test wrapper
Fixes #35277
2024-11-24 00:13:22 +01:00
Daan De Meyer af8c54fa79 test: Lint integration-test-wrapper.py 2024-11-24 00:13:22 +01:00
Daan De Meyer a98517d91e test: Fix typing errors in integration-test-wrapper.py 2024-11-24 00:13:22 +01:00
Daan De Meyer 5c7f7c42ae test: Format integration-test-wrapper.py 2024-11-24 00:13:22 +01:00
Daan De Meyer d0b9d245d6 ukify: Fix typing error 2024-11-24 00:13:22 +01:00
Daan De Meyer 88f531cee3 Move mypy.ini and ruff.toml to top level
This allows reusing them for integration-test-wrapper.py as well.
2024-11-24 00:13:22 +01:00
Daan De Meyer b27e4eee6f integration-test-wrapper: Remove unneeded format strings 2024-11-24 00:13:22 +01:00
Daan De Meyer 70dd6760ce 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:13:22 +01:00
Daan De Meyer b7a0fe5362 mkosi: Add dnf and dnf5 to sanitizer workaround list 2024-11-24 00:13:22 +01:00
Daan De Meyer 6d93dc972c mkosi: Install clangd everywhere 2024-11-24 00:13:19 +01:00
17 changed files with 167 additions and 86 deletions

View File

@ -37,7 +37,7 @@ jobs:
VALIDATE_GITHUB_ACTIONS: true
- 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
run: |
@ -47,14 +47,14 @@ jobs:
- name: Run mypy
run: |
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
run: |
ruff --version
ruff check src/ukify/ukify.py
ruff check src/ukify/ukify.py test/integration-test-wrapper.py
- name: Run ruff format
run: |
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>
<para>On success, 0 is returned; otherwise, a non-zero failure
code is returned. Not finding any matching core dumps is treated as
failure.
failure unless JSON output is enabled.
</para>
</refsect1>

View File

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

View File

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

View File

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

View File

@ -57,6 +57,8 @@ wrap=(
delv
dhcpd
dig
dnf
dnf5
dmsetup
dnsmasq
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_quiet)
log_notice("No coredumps found.");
if (!sd_json_format_enabled(arg_json_format_flags))
return -ESRCH;
}
}

View File

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

View File

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

View File

@ -4,7 +4,7 @@ integration_tests += [
integration_test_template + {
'name' : fs.name(meson.current_source_dir()),
'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 + {
'name' : fs.name(meson.current_source_dir()),
'vm' : true,
'coredump-exclude-regex' : '/(sleep|udevadm)$',
},
]

View File

@ -3,5 +3,6 @@
integration_tests += [
integration_test_template + {
'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()),
'storage': 'persistent',
'vm' : true,
'coredump-exclude-regex' : '/(test-usr-dump|test-dump|bash)$',
},
]

View File

@ -1,19 +1,18 @@
#!/usr/bin/python3
# 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 json
import os
import re
import shlex
import subprocess
import sys
import textwrap
from pathlib import Path
EMERGENCY_EXIT_DROPIN = """\
[Unit]
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.add_argument('--mkosi', required=True)
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('--vm', action=argparse.BooleanOptionalAction)
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()
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)
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,
)
exit(77)
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)
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,
)
exit(77)
if args.vm and bool(int(os.getenv("TEST_NO_QEMU", "0"))):
print(f"TEST_NO_QEMU=1, skipping {args.name}", file=sys.stderr)
if args.vm and bool(int(os.getenv('TEST_NO_QEMU', '0'))):
print(f'TEST_NO_QEMU=1, skipping {args.name}', file=sys.stderr)
exit(77)
for s in os.getenv("TEST_SKIP", "").split():
for s in os.getenv('TEST_SKIP', '').split():
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)
keep_journal = os.getenv("TEST_SAVE_JOURNAL", "fail")
shell = bool(int(os.getenv("TEST_SHELL", "0")))
keep_journal = os.getenv('TEST_SAVE_JOURNAL', 'fail')
shell = bool(int(os.getenv('TEST_SHELL', '0')))
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)
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(
"""\
@ -84,14 +147,14 @@ def main():
if not shell:
dropin += textwrap.dedent(
f"""
"""
[Unit]
SuccessAction=exit
SuccessActionExitStatus=123
"""
)
if os.getenv("TEST_MATCH_SUBTEST"):
if os.getenv('TEST_MATCH_SUBTEST'):
dropin += textwrap.dedent(
f"""
[Service]
@ -99,7 +162,7 @@ def main():
"""
)
if os.getenv("TEST_MATCH_TESTCASE"):
if os.getenv('TEST_MATCH_TESTCASE'):
dropin += textwrap.dedent(
f"""
[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():
dropin += textwrap.dedent(
"""
@ -115,9 +180,6 @@ def main():
FailureAction=exit
"""
)
journal_file = (args.meson_build_dir / (f"test/journal/{name}.journal")).absolute()
journal_file.unlink(missing_ok=True)
elif not shell:
dropin += textwrap.dedent(
"""
@ -136,87 +198,93 @@ def main():
*(['--forward-journal', journal_file] if journal_file else []),
*(
[
'--credential',
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.extra-unit.emergency-exit.service={shlex.quote(EMERGENCY_EXIT_SERVICE)}', # noqa: E501
'--credential', f'systemd.unit-dropin.emergency.target={shlex.quote(EMERGENCY_EXIT_DROPIN)}',
]
if not sys.stderr.isatty()
else []
),
'--credential',
f"systemd.unit-dropin.{args.unit}={shlex.quote(dropin)}",
'--credential', f'systemd.unit-dropin.{args.unit}={shlex.quote(dropin)}',
'--runtime-network=none',
'--runtime-scratch=no',
*args.mkosi_args,
'--qemu-firmware', args.firmware,
*(['--qemu-kvm', 'no'] if int(os.getenv("TEST_NO_KVM", "0")) else []),
'--qemu-firmware',
args.firmware,
*(['--qemu-kvm', 'no'] if int(os.getenv('TEST_NO_KVM', '0')) else []),
'--kernel-command-line-extra',
' '.join([
' '.join(
[
'systemd.hostname=H',
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_PATH=/usr/lib/systemd/tests/testdata/{args.name}.units:/usr/lib/systemd/tests/testdata/units:',
*([f'systemd.unit={args.unit}'] if not shell else []),
'systemd.mask=systemd-networkd-wait-online.service',
*(
[
"systemd.mask=serial-getty@.service",
"systemd.show_status=error",
"systemd.crash_shell=0",
"systemd.crash_action=poweroff",
'systemd.mask=serial-getty@.service',
'systemd.show_status=error',
'systemd.crash_shell=0',
'systemd.crash_action=poweroff',
]
if not sys.stderr.isatty()
else []
),
]),
]
),
'--credential', f"journal.storage={'persistent' if sys.stderr.isatty() else args.storage}",
*(['--runtime-build-sources=no'] if not sys.stderr.isatty() else []),
'qemu' if args.vm or os.getuid() != 0 else 'boot',
]
] # fmt: skip
result = subprocess.run(cmd)
# 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 journal_file:
journal_file.unlink(missing_ok=True)
result = subprocess.run(cmd)
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)
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)
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)
if journal_file:
ops = []
if os.getenv("GITHUB_ACTIONS"):
id = os.environ["GITHUB_RUN_ID"]
iteration = os.environ["GITHUB_RUN_ATTEMPT"]
if os.getenv('GITHUB_ACTIONS'):
id = os.environ['GITHUB_RUN_ID']
iteration = os.environ['GITHUB_RUN_ATTEMPT']
j = json.loads(
subprocess.run(
[
args.mkosi,
"--directory", os.fspath(args.meson_source_dir),
"--json",
"summary",
'--directory', os.fspath(args.meson_source_dir),
'--json',
'summary',
],
stdout=subprocess.PIPE,
text=True,
).stdout
)
distribution = j["Images"][-1]["Distribution"]
release = j["Images"][-1]["Release"]
artifact = f"ci-mkosi-{id}-{iteration}-{distribution}-{release}-failed-test-journals"
ops += [f"gh run download {id} --name {artifact} -D ci/{artifact}"]
journal_file = Path(f"ci/{artifact}/test/journal/{name}.journal")
) # fmt: skip
distribution = j['Images'][-1]['Distribution']
release = j['Images'][-1]['Release']
artifact = f'ci-mkosi-{id}-{iteration}-{distribution}-{release}-failed-test-journals'
ops += [f'gh run download {id} --name {artifact} -D ci/{artifact}']
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"
f"{(' && '.join(ops))}\n", file=sys.stderr)
print("Test failed, relevant logs can be viewed with: \n\n" 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.
exit(result.returncode or 1)

View File

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