ci(smoke): cover teardown, state transitions and fake hardware
The smoke test only exercised steady-state rendering and killed waybar with SIGTERM, so whole crash classes were invisible: exit-time use-after-free (#5182), module state transitions (#5183) and hardware-backed modules (#5179). Add tiers that hit those paths under ASan (+ _GLIBCXX_ASSERTIONS): - lib.sh: assert_clean_exit (SIGINT teardown -> checks segfault/abort + ASan report emitted during destruction), output hotplug helpers, signal/reload, WAYBAR_WRAP hook, opt-in leak detection. - lifecycle.sh (gating): clean exit, runtime output hotplug (Bar/module destroy), toggle/reload churn, fast-interval teardown race (#5182 class). - fuzz.sh (gating): pathological custom-backend output (empty, nonzero exit, invalid JSON, huge, non-UTF8, empty format). - coverage.sh (gating): every Factory module must be classified for smoke coverage; a new unlisted module fails the job (#5179 slipped through). - modules.sh: also render the whole matrix inside a group; SIGINT teardown per module instead of SIGTERM. - state.sh (best-effort): real mpd driven through an empty queue (#5183) + stop/clear; pulseaudio + slider on a null sink. - hardware.sh (best-effort): backlight, backlight/slider (#5179) and battery via umockdev. - leakcheck.sh (report-only): clean-exit run under LSan.
This commit is contained in:
+64
-2
@@ -18,6 +18,9 @@ HEIGHT="${HEIGHT:-1080}"
|
||||
SCALE="${SCALE:-1}"
|
||||
COMPOSITOR="${COMPOSITOR:-sway}"
|
||||
WAYBAR_BIN="${WAYBAR_BIN:-waybar}"
|
||||
# Optional command prefix for the waybar launch (e.g. `umockdev-run -d dev --`),
|
||||
# used by the hardware tier to fake sysfs/udev devices. Empty by default.
|
||||
WAYBAR_WRAP="${WAYBAR_WRAP:-}"
|
||||
|
||||
SMOKE_LOG=""
|
||||
COMP_PID=""
|
||||
@@ -32,7 +35,9 @@ smoke::setup() {
|
||||
export LIBGL_ALWAYS_SOFTWARE=1
|
||||
# Keep ASan/UBSan noise low but fatal on real errors (leaks in GTK/glib are
|
||||
# too noisy to gate on, so only memory-corruption and UB abort the run).
|
||||
export ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:detect_odr_violation=0"
|
||||
# Leak detection is opt-in (leakcheck.sh sets SMOKE_DETECT_LEAKS=1) and needs
|
||||
# a clean exit for LSan's atexit hook to run.
|
||||
export ASAN_OPTIONS="detect_leaks=${SMOKE_DETECT_LEAKS:-0}:halt_on_error=1:abort_on_error=1:detect_odr_violation=0"
|
||||
export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1"
|
||||
}
|
||||
|
||||
@@ -84,7 +89,10 @@ smoke::launch_waybar() {
|
||||
SMOKE_LOG="$(mktemp)"
|
||||
local args=(-c "$config" -l debug)
|
||||
[ -n "$style" ] && args+=(-s "$style")
|
||||
"$WAYBAR_BIN" "${args[@]}" >"$SMOKE_LOG" 2>&1 &
|
||||
local run=()
|
||||
[ -n "$WAYBAR_WRAP" ] && read -ra run <<<"$WAYBAR_WRAP"
|
||||
run+=("$WAYBAR_BIN" "${args[@]}")
|
||||
"${run[@]}" >"$SMOKE_LOG" 2>&1 &
|
||||
WAYBAR_PID=$!
|
||||
sleep "$settle"
|
||||
}
|
||||
@@ -129,6 +137,60 @@ smoke::assert_not_blank() {
|
||||
echo "✓ bar rendered content"
|
||||
}
|
||||
|
||||
# Send a signal to waybar and give it a moment to act on it.
|
||||
# smoke::signal <SIGNAME|num> [settle_seconds]
|
||||
smoke::signal() {
|
||||
kill -"$1" "$WAYBAR_PID" 2>/dev/null || true
|
||||
sleep "${2:-2}"
|
||||
}
|
||||
|
||||
# Reload waybar's config in place (recreates bars + modules).
|
||||
smoke::reload() { smoke::signal SIGUSR2 "${1:-3}"; }
|
||||
|
||||
# Create a headless output at runtime (sway only). wlroots' headless backend
|
||||
# lets sway hotplug outputs, which makes waybar build a new Bar + module set.
|
||||
smoke::add_output() {
|
||||
[ "$COMPOSITOR" = sway ] || { echo "(add_output: sway only, skipped)"; return 0; }
|
||||
swaymsg create_output >/dev/null 2>&1 || echo "(create_output unsupported, skipped)"
|
||||
sleep 2
|
||||
}
|
||||
|
||||
# Remove every headless output except the primary HEADLESS-1. Destroying an
|
||||
# output tears down its Bar -> ~Bar -> GtkWindow unmap -> toggleSuspend(), i.e.
|
||||
# the exact runtime path that segfaulted in #5182 -- exercised here under ASan.
|
||||
smoke::remove_output() {
|
||||
[ "$COMPOSITOR" = sway ] || return 0
|
||||
local o
|
||||
for o in $(swaymsg -t get_outputs 2>/dev/null | jq -r '.[].name' 2>/dev/null | grep -v '^HEADLESS-1$' || true); do
|
||||
swaymsg output "$o" unplug >/dev/null 2>&1 || true
|
||||
done
|
||||
sleep 2
|
||||
}
|
||||
|
||||
# Shut waybar down the clean way (SIGINT -> Client::reset -> gtk quit ->
|
||||
# bars.clear -> ~Bar) and assert the destructor path neither crashed nor tripped
|
||||
# a sanitizer report. A plain SIGTERM/kill never runs this path, which is why the
|
||||
# exit-time use-after-free (#5182) went unnoticed. Call instead of smoke::stop's
|
||||
# kill when you want the teardown itself checked.
|
||||
smoke::assert_clean_exit() {
|
||||
[ -n "$WAYBAR_PID" ] || { echo "(no waybar running)"; return 0; }
|
||||
kill -INT "$WAYBAR_PID" 2>/dev/null || true
|
||||
local _ ; for _ in $(seq 1 20); do kill -0 "$WAYBAR_PID" 2>/dev/null || break; sleep 0.5; done
|
||||
if kill -0 "$WAYBAR_PID" 2>/dev/null; then
|
||||
echo "::error::waybar did not exit on SIGINT (hung during teardown)"
|
||||
echo "::group::waybar log"; smoke::log; echo "::endgroup::"
|
||||
kill -9 "$WAYBAR_PID" 2>/dev/null || true; WAYBAR_PID=""; return 1
|
||||
fi
|
||||
local st=0; wait "$WAYBAR_PID" 2>/dev/null || st=$?
|
||||
WAYBAR_PID=""
|
||||
if [ "$st" -ge 128 ]; then
|
||||
echo "::error::waybar crashed on exit (killed by signal $((st - 128)))"
|
||||
echo "::group::waybar log"; smoke::log; echo "::endgroup::"; return 1
|
||||
fi
|
||||
smoke::assert_clean || return 1
|
||||
echo "✓ clean exit (status $st)"
|
||||
}
|
||||
|
||||
smoke::stop() {
|
||||
kill "$WAYBAR_PID" 2>/dev/null || true
|
||||
swaymsg -q exit 2>/dev/null || kill "$COMP_PID" 2>/dev/null || true
|
||||
|
||||
Reference in New Issue
Block a user