fix: fail session validation promptly without missing tools

This commit is contained in:
2026-10-08 17:38:56 +02:00
parent b51c64eee3
commit c1cac1ba46
2 changed files with 59 additions and 26 deletions
+3
View File
@@ -196,6 +196,9 @@ Wayland rig. It verifies Recording status, non-empty segment files and metadata,
closes the only window while recording continues, kills the game process, closes the only window while recording continues, kills the game process,
checks finalisation, reopens the app to load its library from disk, captures checks finalisation, reopens the app to load its library from disk, captures
screenshots and uses Ctrl+Q during a second recording for clean shutdown. screenshots and uses Ctrl+Q during a second recording for clean shutdown.
The script checks required tools before starting and distinguishes pending
status from failed checks. The recovery run also passed with `PATH=/usr/bin:/bin`
without ripgrep installed (2026-10-08); scripts use grep instead.
The recording and library screenshots were inspected; storage totals came from The recording and library screenshots were inspected; storage totals came from
real Matroska file sizes. The existing Linux rig also passed all nine scenarios real Matroska file sizes. The existing Linux rig also passed all nine scenarios
after integration (2026-10-08). Its unwritable-output assertion accepts both after integration (2026-10-08). Its unwritable-output assertion accepts both
+56 -26
View File
@@ -1,6 +1,22 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# End-to-end desktop/Steam-process/session test in the private Wayland rig. # End-to-end desktop/Steam-process/session test in the private Wayland rig.
set -euo pipefail set -euo pipefail
# Check before building or starting the rig; missing tools must not look like
# a status that has simply not appeared yet.
for tool in bash cargo grep python3 ffprobe grim wtype swaymsg dirname mkdir sleep cat seq \
env dbus-run-session mktemp chmod rm basename head dbus-update-activation-environment \
pipewire wireplumber pipewire-pulse pactl sway Xwayland ffplay fusermount3; do
if ! command -v "${tool}" > /dev/null; then
echo "FAIL required tool '${tool}' is missing from PATH" >&2
exit 127
fi
done
for tool in /usr/lib/xdg-desktop-portal /usr/lib/xdg-desktop-portal-wlr; do
if [[ ! -x "${tool}" ]]; then
echo "FAIL required tool '${tool}' is missing or not executable" >&2
exit 127
fi
done
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
out="${1:-${TMPDIR:-/tmp}/clipbin-session-validation}" out="${1:-${TMPDIR:-/tmp}/clipbin-session-validation}"
mkdir -p "${out}" mkdir -p "${out}"
@@ -8,6 +24,28 @@ cargo build --locked -p clipbin-desktop --manifest-path "${repo}/Cargo.toml"
export CLIPBIN_SESSION_REPO="${repo}" CLIPBIN_SESSION_OUT="${out}" export CLIPBIN_SESSION_REPO="${repo}" CLIPBIN_SESSION_OUT="${out}"
"${repo}/scripts/rig/headless.sh" bash <<'INNER' "${repo}/scripts/rig/headless.sh" bash <<'INNER'
set -euo pipefail set -euo pipefail
# grep returns 1 for no matches; every other error must fail immediately.
wait_log() {
local pattern="$1" log="$2" minimum="$3" attempts="$4" count status attempt
for ((attempt = 0; attempt < attempts; attempt++)); do
if count=$(grep -c "${pattern}" "${log}"); then
if ((count >= minimum)); then return 0; fi
else
status=$?
if [[ ${status} != 1 ]]; then
echo "FAIL could not check '${pattern}' in ${log} (grep exited ${status})" >&2
exit 1
fi
fi
if ! kill -0 "${app}" 2>/dev/null; then
echo "FAIL desktop exited while waiting for '${pattern}'; log: ${log}" >&2
exit 1
fi
sleep 1
done
echo "FAIL timed out waiting for ${minimum} '${pattern}' entries; log: ${log}" >&2
exit 1
}
out="${CLIPBIN_SESSION_OUT}" out="${CLIPBIN_SESSION_OUT}"
portal_config="${XDG_CONFIG_HOME}" portal_config="${XDG_CONFIG_HOME}"
export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config" export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config"
@@ -29,29 +67,17 @@ for _ in range(20):
argv = open(f'/proc/{sys.argv[1]}/cmdline', 'rb').read().split(b'\0') argv = open(f'/proc/{sys.argv[1]}/cmdline', 'rb').read().split(b'\0')
assert argv[0] == b'reaper' and b'SteamLaunch' in argv and b'AppId=480' in argv, argv assert argv[0] == b'reaper' and b'SteamLaunch' in argv and b'AppId=480' in argv, argv
PY PY
for _ in $(seq 30); do wait_log 'recorder_status=Recording' "${out}/desktop.log" 1 30
if rg -q 'recorder_status=Recording' "${out}/desktop.log"; then break; fi
sleep 1
done
rg -q 'recorder_status=Recording' "${out}/desktop.log"
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
sleep 0.5 sleep 0.5
grim "${out}/recording.png" grim "${out}/recording.png"
if [[ "${CLIPBIN_VALIDATE_RECOVERY:-0}" == 1 ]]; then if [[ "${CLIPBIN_VALIDATE_RECOVERY:-0}" == 1 ]]; then
kill "$(cat "${CLIPBIN_RIG_DIR}/xdpw.pid")" kill "$(cat "${CLIPBIN_RIG_DIR}/xdpw.pid")"
for _ in $(seq 15); do wait_log 'recorder_status=Failed' "${out}/desktop.log" 1 15
if rg -q 'recorder_status=Failed' "${out}/desktop.log"; then break; fi
sleep 1
done
rg -q 'recorder_status=Failed' "${out}/desktop.log"
grim "${out}/capture-failed.png" grim "${out}/capture-failed.png"
XDG_CONFIG_HOME="${portal_config}" /usr/lib/xdg-desktop-portal-wlr > "${out}/recovered-portal.log" 2>&1 & XDG_CONFIG_HOME="${portal_config}" /usr/lib/xdg-desktop-portal-wlr > "${out}/recovered-portal.log" 2>&1 &
portal=$! portal=$!
for _ in $(seq 30); do wait_log 'recorder_status=Recording' "${out}/desktop.log" 2 30
if [[ $(rg -c 'recorder_status=Recording' "${out}/desktop.log") -ge 2 ]]; then break; fi
sleep 1
done
[[ $(rg -c 'recorder_status=Recording' "${out}/desktop.log") -ge 2 ]]
python3 - "${XDG_DATA_HOME}" <<'PY' python3 - "${XDG_DATA_HOME}" <<'PY'
import pathlib,json,sys import pathlib,json,sys
p=max(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'), key=lambda p:int(p.parent.name)) p=max(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'), key=lambda p:int(p.parent.name))
@@ -70,22 +96,30 @@ kill "${game}"
wait "${game}" 2>/dev/null || true wait "${game}" 2>/dev/null || true
finalised=0 finalised=0
for _ in $(seq 20); do for _ in $(seq 20); do
if python3 - "${XDG_DATA_HOME}" 2>/dev/null <<'PY' status=0
python3 - "${XDG_DATA_HOME}" <<'PY' || status=$?
import pathlib, json, sys import pathlib, json, sys
files = list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json')) files = list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'))
assert files if not files: sys.exit(2)
path = max(files, key=lambda p: int(p.parent.name)) path = max(files, key=lambda p: int(p.parent.name))
m = json.loads(path.read_text()) m = json.loads(path.read_text())
assert m['app_id'] == 480 and not m['session']['active'] if m['session']['active']: sys.exit(2)
assert m['app_id'] == 480
assert m['segments'] and all(s['complete'] for s in m['segments']) assert m['segments'] and all(s['complete'] for s in m['segments'])
assert all((path.parent / s['path']).stat().st_size > 0 for s in m['segments']) assert all((path.parent / s['path']).stat().st_size > 0 for s in m['segments'])
assert any(s['kind'] == 'Footage' for s in m['session']['spans']) assert any(s['kind'] == 'Footage' for s in m['session']['spans'])
print('PASS session finalised with footage') print('PASS session finalised with footage')
PY PY
then finalised=1; break; fi case "${status}" in
sleep 1 0) finalised=1; break ;;
2) sleep 1 ;;
*) echo "FAIL session finalisation check exited ${status}" >&2; exit 1 ;;
esac
done done
[[ ${finalised} == 1 ]] if [[ ${finalised} != 1 ]]; then
echo "FAIL timed out waiting for session finalisation; log: ${out}/desktop.log" >&2
exit 1
fi
python3 - "${XDG_DATA_HOME}" <<'PY' python3 - "${XDG_DATA_HOME}" <<'PY'
import pathlib,json,subprocess,sys import pathlib,json,subprocess,sys
path=max(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'), key=lambda p:int(p.parent.name)) path=max(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'), key=lambda p:int(p.parent.name))
@@ -129,11 +163,7 @@ sleep 0.5
grim "${out}/library.png" grim "${out}/library.png"
bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' & bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' &
game=$! game=$!
for _ in $(seq 30); do wait_log 'recorder_status=Recording' "${out}/reopened.log" 1 30
if rg -q 'recorder_status=Recording' "${out}/reopened.log"; then break; fi
sleep 1
done
rg -q 'recorder_status=Recording' "${out}/reopened.log"
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
wtype -M ctrl -k q -m ctrl wtype -M ctrl -k q -m ctrl
for _ in $(seq 20); do for _ in $(seq 20); do