tools: add a test wrapper that replays crashing tests under gdb

meson test --wrapper hook to print a gdb backtrace inline in the test
log when a test exits with an actual crash signal (SIGSEGV, SIGABRT,
SIGBUS, SIGFPE, SIGILL). Wired into the default add_test_setup() so it
runs automatically on every `meson test`.

Environmental terminations (SIGTERM/SIGKILL/SIGPIPE/SIGALRM) are passed
through without replay, and the original signal is re-raised so the
parent's wait() observes WIFSIGNALED rather than a plain exit code.
This commit is contained in:
Daan De Meyer
2026-05-19 23:14:32 +02:00
committed by Daan De Meyer
parent 450a5a4740
commit b8b2316b0a
3 changed files with 74 additions and 5 deletions
+17 -5
View File
@@ -28,12 +28,24 @@ actions:
jobs:
- job: copr_build
trigger: pull_request
# Install gdb into the buildroot so post-failure hooks can pull backtraces from any cores the
# %check phase leaves behind.
targets:
- fedora-rawhide-aarch64
- fedora-rawhide-i386
- fedora-rawhide-ppc64le
- fedora-rawhide-s390x
- fedora-rawhide-x86_64
fedora-rawhide-aarch64:
additional_packages:
- gdb
fedora-rawhide-i386:
additional_packages:
- gdb
fedora-rawhide-ppc64le:
additional_packages:
- gdb
fedora-rawhide-s390x:
additional_packages:
- gdb
fedora-rawhide-x86_64:
additional_packages:
- gdb
- job: tests
trigger: pull_request
+1
View File
@@ -16,6 +16,7 @@ project('systemd', 'c',
add_test_setup(
'default',
exclude_suites : ['clang-tidy', 'unused-symbols', 'integration-tests'],
exe_wrapper : find_program('tools/test-crash-trace.sh'),
is_default : true,
)
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# meson test wrapper that replays crashing tests under gdb and prints a
# backtrace, so SIGSEGV/SIGABRT/etc. failures in CI show a stack trace inline
# in the test log instead of just an exit code.
#
# Usage: meson test --wrapper=$PWD/tools/test-crash-trace.sh
set -euo pipefail
if [[ $# -eq 0 ]]; then
echo "usage: $0 <command> [args...]" >&2
exit 2
fi
rc=0
"$@" || rc=$?
# Replay only on actual crash signals (128 + signal): SIGILL=132, SIGABRT=134,
# SIGBUS=135, SIGFPE=136, SIGSEGV=139. SIGTERM/SIGKILL/SIGPIPE/SIGALRM mean the
# test was killed by the environment (timeout, etc.), not that it crashed —
# replaying those just makes gdb hit the same kill.
case "$rc" in
132|134|135|136|139)
if command -v gdb >/dev/null 2>&1; then
echo "===== exit $rc — replaying under gdb =====" >&2
style_args=()
if [[ -t 2 ]]; then
style_args=(--ex 'set style enabled on')
fi
gdb --batch \
--ex 'set pagination off' \
${style_args[@]+"${style_args[@]}"} \
--ex 'run' \
--ex 'thread apply all bt full' \
--args "$@" >&2 || true
else
echo "===== exit $rc — install gdb for a backtrace =====" >&2
fi
;;
esac
# If the child died by signal, re-raise it so the parent's wait() observes
# WIFSIGNALED instead of a plain exit code. Best-effort: SIGKILL can't be
# delivered to ourselves and would have killed us already.
if [[ $rc -gt 128 ]]; then
sig=$((rc - 128))
trap - "$sig" 2>/dev/null || true
# Suppress the wrapper bash's own core dump from the re-raised crash signal;
# the original test binary already had its chance to dump core on line 18.
ulimit -c 0
kill -s "$sig" $$ 2>/dev/null || true
fi
exit "$rc"