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:
Alex
2026-07-05 22:21:46 +02:00
parent 445e2aec1f
commit 9a10949938
10 changed files with 558 additions and 13 deletions
+64 -2
View File
@@ -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