ci(smoke): sanitizer build + module/interaction/a11y/layout coverage
Turn the smoke test into a real runtime safety net. Waybar is now built with AddressSanitizer and exercised end to end in a headless compositor: - Tier 0: run everything under ASan; fail on ASan reports and Gtk/GLib criticals in the log (lib.sh assert_clean) - Tier 1: per-module render matrix (modules.sh) — each headless-safe module rendered in isolation - Tier 2: pointer interaction (interact.sh) — inject clicks via sway, assert on-click side effect and format-alt toggle; AT-SPI assertions (a11y.sh/.py) check module labels semantically instead of by pixels - Tier 3: layout matrix (positions.sh) — top/bottom/left + HiDPI scale 2; second compositor run under labwc Shared helpers extracted to lib.sh. Adds a workflow_dispatch `bless` input to regenerate the golden reference in one click. Trigger push only on master to avoid duplicate PR runs. AT-SPI and labwc steps are continue-on-error.
This commit is contained in:
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers for the headless Waybar smoke/render/interaction tests.
|
||||
#
|
||||
# A test sources this file, then typically:
|
||||
# smoke::setup
|
||||
# smoke::start_compositor
|
||||
# smoke::launch_waybar <config> [style]
|
||||
# smoke::assert_alive
|
||||
# smoke::assert_clean # ASan/UBSan/Gtk-CRITICAL/segfault gate
|
||||
# smoke::screenshot out.png
|
||||
# smoke::stop
|
||||
#
|
||||
# Environment knobs (optional): WIDTH HEIGHT SCALE COMPOSITOR WAYBAR_BIN
|
||||
set -uo pipefail
|
||||
|
||||
WIDTH="${WIDTH:-1920}"
|
||||
HEIGHT="${HEIGHT:-1080}"
|
||||
SCALE="${SCALE:-1}"
|
||||
COMPOSITOR="${COMPOSITOR:-sway}"
|
||||
WAYBAR_BIN="${WAYBAR_BIN:-waybar}"
|
||||
|
||||
SMOKE_LOG=""
|
||||
COMP_PID=""
|
||||
WAYBAR_PID=""
|
||||
|
||||
smoke::setup() {
|
||||
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
||||
chmod 700 "$XDG_RUNTIME_DIR"
|
||||
export WLR_BACKENDS=headless
|
||||
export WLR_RENDERER=pixman
|
||||
export WLR_LIBINPUT_NO_DEVICES=1
|
||||
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"
|
||||
export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1"
|
||||
}
|
||||
|
||||
smoke::start_compositor() {
|
||||
case "$COMPOSITOR" in
|
||||
sway)
|
||||
local cfg; cfg="$(mktemp)"
|
||||
cat > "$cfg" <<EOF
|
||||
output HEADLESS-1 resolution ${WIDTH}x${HEIGHT} scale ${SCALE} background #000000 solid_color
|
||||
EOF
|
||||
sway -c "$cfg" >/tmp/compositor.log 2>&1 &
|
||||
COMP_PID=$!
|
||||
;;
|
||||
labwc)
|
||||
# labwc reads outputs from the wlroots headless backend directly.
|
||||
labwc >/tmp/compositor.log 2>&1 &
|
||||
COMP_PID=$!
|
||||
;;
|
||||
*)
|
||||
echo "::error::unknown compositor '$COMPOSITOR'"; return 1 ;;
|
||||
esac
|
||||
|
||||
local sock=""
|
||||
for _ in $(seq 1 40); do
|
||||
sock="$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'wayland-*' ! -name '*.lock' 2>/dev/null | head -1 || true)"
|
||||
[ -n "$sock" ] && break
|
||||
sleep 0.5
|
||||
done
|
||||
if [ -z "$sock" ]; then
|
||||
echo "::error::$COMPOSITOR did not create a Wayland socket"; cat /tmp/compositor.log || true; return 1
|
||||
fi
|
||||
export WAYLAND_DISPLAY="$(basename "$sock")"
|
||||
|
||||
# sway IPC socket, so swaymsg (cursor injection, clean exit) can reach it.
|
||||
if [ "$COMPOSITOR" = sway ]; then
|
||||
for _ in $(seq 1 10); do
|
||||
SWAYSOCK="$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'sway-ipc.*.sock' 2>/dev/null | head -1 || true)"
|
||||
[ -n "$SWAYSOCK" ] && break
|
||||
sleep 0.3
|
||||
done
|
||||
export SWAYSOCK
|
||||
fi
|
||||
echo "compositor '$COMPOSITOR' up on WAYLAND_DISPLAY=$WAYLAND_DISPLAY (${WIDTH}x${HEIGHT} scale ${SCALE})"
|
||||
}
|
||||
|
||||
# smoke::launch_waybar <config> [style] [settle_seconds]
|
||||
smoke::launch_waybar() {
|
||||
local config="$1" style="${2:-}" settle="${3:-6}"
|
||||
SMOKE_LOG="$(mktemp)"
|
||||
local args=(-c "$config" -l debug)
|
||||
[ -n "$style" ] && args+=(-s "$style")
|
||||
"$WAYBAR_BIN" "${args[@]}" >"$SMOKE_LOG" 2>&1 &
|
||||
WAYBAR_PID=$!
|
||||
sleep "$settle"
|
||||
}
|
||||
|
||||
smoke::log() { cat "$SMOKE_LOG" 2>/dev/null || true; }
|
||||
|
||||
smoke::assert_alive() {
|
||||
if ! kill -0 "$WAYBAR_PID" 2>/dev/null; then
|
||||
echo "::error::waybar exited early (crashed on startup)"
|
||||
echo "::group::waybar log"; smoke::log; echo "::endgroup::"
|
||||
return 1
|
||||
fi
|
||||
echo "✓ waybar is alive"
|
||||
}
|
||||
|
||||
# Fail on sanitizer reports and GTK/GLib criticals in the log.
|
||||
smoke::assert_clean() {
|
||||
local bad
|
||||
bad="$(grep -iE 'AddressSanitizer|runtime error:|LeakSanitizer|Gtk-CRITICAL|GLib-CRITICAL|GLib-GObject-CRITICAL|assertion .*failed|segfault|terminate called|SUMMARY: .*Sanitizer' "$SMOKE_LOG" || true)"
|
||||
if [ -n "$bad" ]; then
|
||||
echo "::error::waybar reported sanitizer/critical issues:"
|
||||
echo "$bad"
|
||||
echo "::group::full waybar log"; smoke::log; echo "::endgroup::"
|
||||
return 1
|
||||
fi
|
||||
echo "✓ no sanitizer/critical issues in log"
|
||||
}
|
||||
|
||||
smoke::screenshot() {
|
||||
local out="$1"
|
||||
grim "$out"
|
||||
echo "✓ screenshot -> $out"
|
||||
}
|
||||
|
||||
# Assert the top bar strip actually rendered something (not a uniform color).
|
||||
smoke::assert_not_blank() {
|
||||
local img="$1" h="${2:-30}"
|
||||
local sd
|
||||
sd="$(convert "$img" -crop "${WIDTH}x${h}+0+0" +repage -colorspace Gray -format '%[fx:standard_deviation]' info:)"
|
||||
echo "bar strip stddev = $sd"
|
||||
awk "BEGIN{ exit !($sd > 0.01) }" || { echo "::error::bar strip is blank in $img"; return 1; }
|
||||
echo "✓ bar rendered content"
|
||||
}
|
||||
|
||||
smoke::stop() {
|
||||
kill "$WAYBAR_PID" 2>/dev/null || true
|
||||
swaymsg -q exit 2>/dev/null || kill "$COMP_PID" 2>/dev/null || true
|
||||
wait 2>/dev/null || true
|
||||
}
|
||||
Reference in New Issue
Block a user