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 164 additions and 85 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

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

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,62 @@ 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
exclude_regex = re.compile('/(bash|sleep|systemd-notify|test-execute|test(-usr)?-dump)')
coredumps = json.loads(
subprocess.run(
[
args.mkosi,
'--directory',
os.fspath(args.meson_source_dir),
'--extra-search-path',
os.fspath(args.meson_build_dir),
'--forward-journal',
journal_file,
'coredumpctl',
'--json=short',
],
check=True,
stdout=subprocess.PIPE,
text=True,
).stdout
)
coredumps = [coredump for coredump in coredumps if not exclude_regex.search(coredump['exe'])]
if not coredumps:
return False
for coredump in coredumps:
print()
subprocess.run(
[
args.mkosi,
'--directory',
os.fspath(args.meson_source_dir),
'--forward-journal',
journal_file,
'coredumpctl',
'info',
coredump['exe'],
],
check=True,
)
print()
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 +100,43 @@ 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('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(
"""
@ -128,47 +190,55 @@ def main():
cmd = [
args.mkosi,
'--directory', os.fspath(args.meson_source_dir),
'--output-dir', os.fspath(args.meson_build_dir / 'mkosi.output'),
'--extra-search-path', os.fspath(args.meson_build_dir),
'--machine', name,
'--directory',
os.fspath(args.meson_source_dir),
'--output-dir',
os.fspath(args.meson_build_dir / 'mkosi.output'),
'--extra-search-path',
os.fspath(args.meson_build_dir),
'--machine',
name,
'--ephemeral',
*(['--forward-journal', journal_file] if journal_file else []),
*(
[
'--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',
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()
else []
),
'--credential',
f"systemd.unit-dropin.{args.unit}={shlex.quote(dropin)}",
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([
'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 []),
'systemd.mask=systemd-networkd-wait-online.service',
*(
[
"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}",
' '.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 []),
'systemd.mask=systemd-networkd-wait-online.service',
*(
[
'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',
]
@ -177,46 +247,53 @@ def main():
# 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:
journal_file.unlink(missing_ok=True)
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 = []
ops = []
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",
],
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")
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',
],
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')
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)