Files
Waybar/test/smoke/state.sh
T
AlexandClaude Opus 4.8 4222d5a213 ci(smoke): fix state tier hang from bare wait on a lingering daemon
The state tier started actually running mpd in 74f0d33, which exposed a
deadlock in teardown: state.sh launches `mpd --no-daemon &` and never kills
it during the tier, and its cleanup() called smoke::stop *before* killing mpd.
smoke::stop ended in a bare `wait`, which reaps *every* background job of the
shell -- including the still-running mpd -- so it blocked until GitHub's 6h
job timeout. continue-on-error doesn't help: it catches failures, not hangs.

- lib.sh: smoke::stop now waits only on the PIDs it owns (waybar, compositor),
  so an unrelated daemon left running by a tier can't deadlock teardown.
- state.sh: cleanup() tears down mpd/pulseaudio before smoke::stop.
- smoke.yml: add timeout-minutes: 20 so a future hang fails fast instead of
  burning the default 6h runner budget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 23:10:07 +02:00

127 lines
4.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Tier — state-transition coverage with real daemons. Steady-state rendering
# never hits the empty/edge states where modules crash. Drive an actual mpd and
# PulseAudio through those states under ASan:
# - mpd: connect with an EMPTY queue (no current song -> null song_, #5183),
# then clear/add/stop/play while waybar is live
# - pulseaudio + pulseaudio/slider against a null sink
#
# Best-effort: each sub-tier is skipped (not failed) if its daemon isn't
# installed. Run continue-on-error in CI while the fixtures settle.
#
# Usage: state.sh
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib.sh
source "$DIR/lib.sh"
fail=0
MPD_PID=""
PA_STARTED=""
cleanup() {
# Tear our daemons down first: smoke::stop waits on the compositor/waybar,
# and a still-running mpd/pulseaudio must not be left for anything to block on.
[ -n "$MPD_PID" ] && kill "$MPD_PID" 2>/dev/null || true
[ -n "$PA_STARTED" ] && pulseaudio --kill 2>/dev/null || true
smoke::stop
return 0
}
smoke::setup
smoke::start_compositor
trap cleanup EXIT
# ---------------------------------------------------------------- mpd -----
mpd_tier() {
if ! command -v mpd >/dev/null || ! command -v mpc >/dev/null; then
echo "(mpd/mpc not installed, skipping mpd tier)"; return 0
fi
echo "::group::mpd: empty queue + transitions (#5183)"
local d conf; d="$(mktemp -d)"; conf="$d/mpd.conf"
mkdir -p "$d/music" "$d/playlists"
cat > "$conf" <<EOF
music_directory "$d/music"
playlist_directory "$d/playlists"
db_file "$d/db"
state_file "$d/state"
pid_file "$d/pid"
bind_to_address "127.0.0.1"
port "6600"
audio_output {
type "null"
name "null"
}
EOF
mpd --no-daemon "$conf" >/tmp/mpd.log 2>&1 &
MPD_PID=$!
local up=0 _
for _ in $(seq 1 20); do
mpc -h 127.0.0.1 -p 6600 status >/dev/null 2>&1 && { up=1; break; }
sleep 0.5
done
if [ "$up" != 1 ]; then
echo "::warning::mpd did not come up; skipping"; cat /tmp/mpd.log || true; echo "::endgroup::"; return 0
fi
mpc -h 127.0.0.1 -p 6600 clear >/dev/null 2>&1 || true # empty queue: the crash trigger
local cfg; cfg="$(mktemp --suffix=.json)"
cat > "$cfg" <<'EOF'
{
"layer": "top", "position": "top", "height": 30,
"modules-center": ["mpd"],
"mpd": { "server": "127.0.0.1", "port": 6600, "interval": 1,
"format": "{album} - {artist} - {title}", "format-stopped": "stopped" }
}
EOF
smoke::launch_waybar "$cfg" "$DIR/style.css" 5
smoke::assert_alive || fail=1 # would have segfaulted pre-#5183 fix
smoke::assert_clean || fail=1
# Churn state while live: clear again, then stop/play.
mpc -h 127.0.0.1 -p 6600 clear >/dev/null 2>&1 || true; sleep 2
mpc -h 127.0.0.1 -p 6600 stop >/dev/null 2>&1 || true; sleep 2
smoke::assert_alive || fail=1
smoke::assert_clean_exit || fail=1
echo "::endgroup::"
}
# -------------------------------------------------------- pulseaudio ------
pulse_tier() {
if ! command -v pulseaudio >/dev/null; then
echo "(pulseaudio not installed, skipping pulse tier)"; return 0
fi
echo "::group::pulseaudio + pulseaudio/slider (null sink)"
export PULSE_RUNTIME_PATH="$XDG_RUNTIME_DIR/pulse"
if pulseaudio --start --exit-idle-time=-1 --log-target=stderr >/tmp/pa.log 2>&1; then
PA_STARTED=1
else
echo "::warning::pulseaudio failed to start; skipping"; cat /tmp/pa.log || true; echo "::endgroup::"; return 0
fi
pactl load-module module-null-sink sink_name=smoke_null >/dev/null 2>&1 || true
local cfg; cfg="$(mktemp --suffix=.json)"
cat > "$cfg" <<'EOF'
{
"layer": "top", "position": "top", "height": 30,
"modules-center": ["pulseaudio", "pulseaudio/slider"],
"pulseaudio": { "format": "{volume}%" },
"pulseaudio/slider": { "min": 0, "max": 100, "orientation": "horizontal" }
}
EOF
smoke::launch_waybar "$cfg" "$DIR/style.css" 5
smoke::assert_alive || fail=1
smoke::assert_clean || fail=1
pactl set-sink-volume @DEFAULT_SINK@ 40% >/dev/null 2>&1 || true; sleep 1
pactl set-sink-mute @DEFAULT_SINK@ 1 >/dev/null 2>&1 || true; sleep 1
smoke::assert_alive || fail=1
smoke::assert_clean_exit || fail=1
echo "::endgroup::"
}
mpd_tier
pulse_tier
[ "$fail" = 0 ] || { echo "::error::state tier failed"; exit 1; }
echo "✓ state-transition tier passed"