#!/usr/bin/env bash # Shared helpers for the manual desktop smoke test. Sourced by # scripts/smoke/desktop-smoke.sh inside the captioneer-dev Distrobox. set -Eeuo pipefail SMOKE_SINK_NAME="captioneer-smoke" SMOKE_SINK_MODULE="" SMOKE_PREVIOUS_SINK="" SMOKE_COMPOSITOR_PID="" SMOKE_APP_PID="" SMOKE_PLAYER_PID="" SMOKE_FAILURES=() smoke_log() { printf 'smoke: %s\n' "$*" >&2 } smoke_die() { printf 'smoke: FATAL: %s\n' "$*" >&2 exit 1 } smoke_fail() { SMOKE_FAILURES+=("$1") smoke_log "FAIL: $1" } smoke_pass() { smoke_log "ok: $1" } # smoke_wait_for_line FILE PATTERN TIMEOUT_SECONDS smoke_wait_for_line() { local file=$1 pattern=$2 timeout=$3 waited=0 until grep -q -- "$pattern" "$file" 2>/dev/null; do if (( waited >= timeout )); then return 1 fi sleep 1 waited=$((waited + 1)) done return 0 } # smoke_wait_for_exit PID TIMEOUT_SECONDS; returns 1 on timeout smoke_wait_for_exit() { local pid=$1 timeout=$2 waited=0 while kill -0 "$pid" 2>/dev/null; do if (( waited >= timeout )); then return 1 fi sleep 1 waited=$((waited + 1)) done return 0 } smoke_setup_audio() { local workdir=$1 command -v pactl >/dev/null 2>&1 || smoke_die "pactl is required" command -v paplay >/dev/null 2>&1 || smoke_die "paplay is required" SMOKE_PREVIOUS_SINK="$(pactl get-default-sink 2>/dev/null || true)" SMOKE_SINK_MODULE="$(pactl load-module module-null-sink \ "sink_name=$SMOKE_SINK_NAME" \ "sink_properties=device.description=Captioneer-Smoke")" pactl set-default-sink "$SMOKE_SINK_NAME" smoke_log "null sink $SMOKE_SINK_NAME loaded (module $SMOKE_SINK_MODULE, previous default: ${SMOKE_PREVIOUS_SINK:-none})" command -v espeak-ng >/dev/null 2>&1 || smoke_die "espeak-ng is required" espeak-ng -v en-us -s 140 -w "$workdir/speech.wav" \ "The quick brown fox jumps over the lazy dog. Live captions should appear for this sentence." [[ -s "$workdir/speech.wav" ]] || smoke_die "espeak-ng produced no audio fixture" } smoke_teardown_audio() { if [[ -n "$SMOKE_PLAYER_PID" ]] && kill -0 "$SMOKE_PLAYER_PID" 2>/dev/null; then kill "$SMOKE_PLAYER_PID" 2>/dev/null || true fi if [[ -n "$SMOKE_PREVIOUS_SINK" ]]; then pactl set-default-sink "$SMOKE_PREVIOUS_SINK" 2>/dev/null || true fi if [[ -n "$SMOKE_SINK_MODULE" ]]; then pactl unload-module "$SMOKE_SINK_MODULE" 2>/dev/null || true SMOKE_SINK_MODULE="" fi } # smoke_play_speech WORKDIR SECONDS: loop the fixture into the null sink. smoke_play_speech() { local workdir=$1 seconds=$2 ( end=$((SECONDS + seconds)) while (( SECONDS < end )); do paplay --device="$SMOKE_SINK_NAME" "$workdir/speech.wav" 2>/dev/null || sleep 1 done ) & SMOKE_PLAYER_PID=$! } # smoke_start_compositor WORKDIR: starts headless sway; prints new socket name. # sway is wlroots-based (Layer Shell capable) and, unlike labwc, can disable # Xwayland, whose sockets are not creatable inside a rootless Distrobox. smoke_start_compositor() { local workdir=$1 candidate socket="" waited=0 command -v sway >/dev/null 2>&1 || smoke_die "sway is required" local before="$workdir/sockets-before.txt" find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'wayland-[0-9]*' ! -name '*.lock' \ -printf '%f\n' 2>/dev/null | sort > "$before" printf 'xwayland disable\n' > "$workdir/sway-config" WLR_BACKENDS=headless \ WLR_LIBINPUT_NO_DEVICES=1 \ WLR_RENDERER=pixman \ sway -c "$workdir/sway-config" > "$workdir/compositor.log" 2>&1 & SMOKE_COMPOSITOR_PID=$! while (( waited < 15 )); do while IFS= read -r candidate; do socket="$candidate" done < <(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'wayland-[0-9]*' ! -name '*.lock' \ -printf '%f\n' 2>/dev/null | sort | comm -13 "$before" -) if [[ -n "$socket" ]]; then printf '%s\n' "$socket" return 0 fi kill -0 "$SMOKE_COMPOSITOR_PID" 2>/dev/null || smoke_die "sway exited early; see $workdir/compositor.log" sleep 1 waited=$((waited + 1)) done smoke_die "sway did not create a Wayland socket; see $workdir/compositor.log" } smoke_teardown_compositor() { if [[ -n "$SMOKE_COMPOSITOR_PID" ]] && kill -0 "$SMOKE_COMPOSITOR_PID" 2>/dev/null; then kill "$SMOKE_COMPOSITOR_PID" 2>/dev/null || true fi } smoke_stop_app() { if [[ -n "$SMOKE_APP_PID" ]] && kill -0 "$SMOKE_APP_PID" 2>/dev/null; then kill -INT "$SMOKE_APP_PID" 2>/dev/null || true fi } smoke_cleanup() { smoke_stop_app if [[ -n "$SMOKE_APP_PID" ]]; then smoke_wait_for_exit "$SMOKE_APP_PID" 10 || kill -KILL "$SMOKE_APP_PID" 2>/dev/null || true fi smoke_teardown_audio smoke_teardown_compositor }