Compare commits
7 Commits
b927c5b352
...
82872ab468
Author | SHA1 | Date |
---|---|---|
Daan De Meyer | 82872ab468 | |
Daan De Meyer | 86b273093f | |
Daan De Meyer | 90a2f9a28d | |
Daan De Meyer | ff51ac6d6f | |
Daan De Meyer | a1bb1e8076 | |
Daan De Meyer | 037b7e117c | |
Daan De Meyer | fcc82e6e27 |
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,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 = 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 +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(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(
|
||||||
"""
|
"""
|
||||||
|
@ -128,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',
|
||||||
]
|
]
|
||||||
|
@ -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.
|
# 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)
|
||||||
|
|
||||||
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',
|
||||||
"--json",
|
os.fspath(args.meson_source_dir),
|
||||||
"summary",
|
'--json',
|
||||||
],
|
'summary',
|
||||||
stdout=subprocess.PIPE,
|
],
|
||||||
text=True,
|
stdout=subprocess.PIPE,
|
||||||
).stdout
|
text=True,
|
||||||
)
|
).stdout
|
||||||
distribution = j["Images"][-1]["Distribution"]
|
)
|
||||||
release = j["Images"][-1]["Release"]
|
distribution = j['Images'][-1]['Distribution']
|
||||||
artifact = f"ci-mkosi-{id}-{iteration}-{distribution}-{release}-failed-test-journals"
|
release = j['Images'][-1]['Release']
|
||||||
ops += [f"gh run download {id} --name {artifact} -D ci/{artifact}"]
|
artifact = f'ci-mkosi-{id}-{iteration}-{distribution}-{release}-failed-test-journals'
|
||||||
journal_file = Path(f"ci/{artifact}/test/journal/{name}.journal")
|
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"
|
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)
|
||||||
|
|
Loading…
Reference in New Issue