Compare commits

..

7 Commits

Author SHA1 Message Date
Daan De Meyer 82872ab468 test: Dump coredumps from journal in the integration test wrapper
Fixes #35277
2024-11-23 13:03:23 +01:00
Daan De Meyer 86b273093f test: Lint integration-test-wrapper.py 2024-11-23 13:03:21 +01:00
Daan De Meyer 90a2f9a28d test: Fix typing errors in integration-test-wrapper.py 2024-11-23 13:01:00 +01:00
Daan De Meyer ff51ac6d6f test: Format integration-test-wrapper.py 2024-11-23 12:59:56 +01:00
Daan De Meyer a1bb1e8076 Move mypy.ini and ruff.toml to top level
This allows reusing them for integration-test-wrapper.py as well.
2024-11-23 12:59:20 +01:00
Daan De Meyer 037b7e117c integration-test-wrapper: Remove unneeded format strings 2024-11-23 12:15:10 +01:00
Daan De Meyer fcc82e6e27 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-23 12:15:10 +01:00
6 changed files with 110 additions and 81 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

@ -965,7 +965,9 @@ 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.");
return -ESRCH;
if (!sd_json_format_enabled(arg_json_format_flags))
return -ESRCH;
} }
} }

View File

@ -1,20 +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
import re
from pathlib import Path from pathlib import Path
EMERGENCY_EXIT_DROPIN = """\ EMERGENCY_EXIT_DROPIN = """\
[Unit] [Unit]
Wants=emergency-exit.service Wants=emergency-exit.service
@ -42,16 +40,20 @@ def dump_coredumps(args: argparse.Namespace, journal_file: Path) -> bool:
# systemd-notify - intermittent (and intentional) SIGABRT caused by TEST-59 # systemd-notify - intermittent (and intentional) SIGABRT caused by TEST-59
# test-execute - intentional coredump in TEST-02 # test-execute - intentional coredump in TEST-02
# test(-usr)?-dump - intentional coredumps from systemd-coredump tests in TEST-74 # test(-usr)?-dump - intentional coredumps from systemd-coredump tests in TEST-74
exclude_regex = re.compile("/(bash|sleep|systemd-notify|test-execute|test(-usr)?-dump)") exclude_regex = re.compile('/(bash|sleep|systemd-notify|test-execute|test(-usr)?-dump)')
coredumps = json.loads( coredumps = json.loads(
subprocess.run( subprocess.run(
[ [
args.mkosi, args.mkosi,
"--directory", os.fspath(args.meson_source_dir), '--directory',
"--forward-journal", journal_file, os.fspath(args.meson_source_dir),
"coredumpctl", '--extra-search-path',
"--json=short", os.fspath(args.meson_build_dir),
'--forward-journal',
journal_file,
'coredumpctl',
'--json=short',
], ],
check=True, check=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@ -59,7 +61,7 @@ def dump_coredumps(args: argparse.Namespace, journal_file: Path) -> bool:
).stdout ).stdout
) )
coredumps = [coredump for coredump in coredumps if not exclude_regex.search(coredump["exe"])] coredumps = [coredump for coredump in coredumps if not exclude_regex.search(coredump['exe'])]
if not coredumps: if not coredumps:
return False return False
@ -70,11 +72,13 @@ def dump_coredumps(args: argparse.Namespace, journal_file: Path) -> bool:
subprocess.run( subprocess.run(
[ [
args.mkosi, args.mkosi,
"--directory", os.fspath(args.meson_source_dir), '--directory',
"--forward-journal", journal_file, os.fspath(args.meson_source_dir),
"coredumpctl", '--forward-journal',
"info", journal_file,
coredump["exe"], 'coredumpctl',
'info',
coredump['exe'],
], ],
check=True, check=True,
) )
@ -84,7 +88,7 @@ def dump_coredumps(args: argparse.Namespace, journal_file: Path) -> bool:
return True return True
def main(): 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)
@ -96,34 +100,43 @@ 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('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("--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(
"""\ """\
@ -141,7 +154,7 @@ def main():
""" """
) )
if os.getenv("TEST_MATCH_SUBTEST"): if os.getenv('TEST_MATCH_SUBTEST'):
dropin += textwrap.dedent( dropin += textwrap.dedent(
f""" f"""
[Service] [Service]
@ -149,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]
@ -157,7 +170,7 @@ def main():
""" """
) )
journal_file = (args.meson_build_dir / (f"test/journal/{name}.journal")).absolute() journal_file = (args.meson_build_dir / (f'test/journal/{name}.journal')).absolute()
journal_file.unlink(missing_ok=True) journal_file.unlink(missing_ok=True)
if not sys.stderr.isatty(): if not sys.stderr.isatty():
@ -177,47 +190,55 @@ def main():
cmd = [ cmd = [
args.mkosi, args.mkosi,
'--directory', os.fspath(args.meson_source_dir), '--directory',
'--output-dir', os.fspath(args.meson_build_dir / 'mkosi.output'), os.fspath(args.meson_source_dir),
'--extra-search-path', os.fspath(args.meson_build_dir), '--output-dir',
'--machine', name, os.fspath(args.meson_build_dir / 'mkosi.output'),
'--extra-search-path',
os.fspath(args.meson_build_dir),
'--machine',
name,
'--ephemeral', '--ephemeral',
*(['--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)}", f'systemd.extra-unit.emergency-exit.service={shlex.quote(EMERGENCY_EXIT_SERVICE)}',
'--credential', '--credential',
f"systemd.unit-dropin.emergency.target={shlex.quote(EMERGENCY_EXIT_DROPIN)}", 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', [
f"SYSTEMD_UNIT_PATH=/usr/lib/systemd/tests/testdata/{args.name}.units:/usr/lib/systemd/tests/testdata/units:", 'systemd.hostname=H',
*([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:',
'systemd.mask=systemd-networkd-wait-online.service', *([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.mask=serial-getty@.service',
"systemd.crash_shell=0", 'systemd.show_status=error',
"systemd.crash_action=poweroff", 'systemd.crash_shell=0',
] 'systemd.crash_action=poweroff',
if not sys.stderr.isatty() ]
else [] if not sys.stderr.isatty()
), 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',
] ]
@ -226,15 +247,21 @@ def main():
# 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:
journal_file.unlink(missing_ok=True) if journal_file:
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)
coredumps = dump_coredumps(args, journal_file) 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): 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) and not coredumps): if shell or (result.returncode in (args.exit_code, 77) and not coredumps):
@ -242,31 +269,31 @@ def main():
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',
"--json", os.fspath(args.meson_source_dir),
"summary", '--json',
'summary',
], ],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, text=True,
).stdout ).stdout
) )
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)