Files
clipbin-desktop/scripts/validate/linux-session.sh
T

183 lines
7.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# End-to-end desktop/Steam-process/session test in the private Wayland rig.
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)"
out="${1:-${TMPDIR:-/tmp}/clipbin-session-validation}"
mkdir -p "${out}"
cargo build --locked -p clipbin-desktop --manifest-path "${repo}/Cargo.toml"
export CLIPBIN_SESSION_REPO="${repo}" CLIPBIN_SESSION_OUT="${out}"
"${repo}/scripts/rig/headless.sh" bash <<'INNER'
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}"
portal_config="${XDG_CONFIG_HOME}"
export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config"
mkdir -p "${XDG_DATA_HOME}" "${XDG_CONFIG_HOME}"
"${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/desktop.log" 2>&1 &
app=$!
trap 'kill "${app}" "${game:-}" "${portal:-}" 2>/dev/null || true' EXIT
sleep 3
swaymsg '[title="clipbin-test-game"] fullscreen disable' > /dev/null || true
swaymsg '[app_id="dev.clipbin.Clipbin"] floating enable, resize set 1360 860, move position center, focus' > /dev/null
bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' &
game=$!
python3 - "${game}" <<'PY'
import sys,time
for _ in range(20):
argv = open(f'/proc/{sys.argv[1]}/cmdline', 'rb').read().split(b'\0')
if argv[0] == b'reaper': break
time.sleep(.1)
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
PY
wait_log 'recorder_status=Recording' "${out}/desktop.log" 1 30
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
sleep 0.5
grim "${out}/recording.png"
if [[ "${CLIPBIN_VALIDATE_RECOVERY:-0}" == 1 ]]; then
kill "$(cat "${CLIPBIN_RIG_DIR}/xdpw.pid")"
wait_log 'recorder_status=Failed' "${out}/desktop.log" 1 15
grim "${out}/capture-failed.png"
XDG_CONFIG_HOME="${portal_config}" /usr/lib/xdg-desktop-portal-wlr > "${out}/recovered-portal.log" 2>&1 &
portal=$!
wait_log 'recorder_status=Recording' "${out}/desktop.log" 2 30
python3 - "${XDG_DATA_HOME}" <<'PY'
import pathlib,json,sys
p=max(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'), key=lambda p:int(p.parent.name))
m=json.loads(p.read_text())
assert any(s['path'].startswith('run-002/') for s in m['segments'])
assert len([s for s in m['session']['spans'] if s['kind']=='Missing']) >= 2
print('PASS capture recovery resumed in the same session with a Missing gap')
PY
fi
# Closing the sole window must leave the process and footage running.
swaymsg '[app_id="dev.clipbin.Clipbin"] kill' > /dev/null
kill -0 "${app}"
sleep 3
kill -0 "${app}"
kill "${game}"
wait "${game}" 2>/dev/null || true
finalised=0
for _ in $(seq 20); do
status=0
python3 - "${XDG_DATA_HOME}" <<'PY' || status=$?
import pathlib, json, sys
files = list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'))
if not files: sys.exit(2)
path = max(files, key=lambda p: int(p.parent.name))
m = json.loads(path.read_text())
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 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'])
print('PASS session finalised with footage')
PY
case "${status}" in
0) finalised=1; break ;;
2) sleep 1 ;;
*) echo "FAIL session finalisation check exited ${status}" >&2; exit 1 ;;
esac
done
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'
import pathlib,json,subprocess,sys
path=max(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'), key=lambda p:int(p.parent.name))
m=json.loads(path.read_text())
ns=lambda d:d['secs']*1_000_000_000+d['nanos']
runs={}
for segment in m['segments']:
run=segment['path'].split('/')[0]
base,packets=runs.get(run,(ns(segment['start']),0))
assert segment['dropped_frames']==0, 'Dropped frames make count-derived offsets uncertain'
expected=base+packets*1_000_000_000//segment['frames_per_second']
assert ns(segment['start'])==expected, (segment['path'],ns(segment['start']),expected)
if not segment['available']:
assert ns(segment['end'])==ns(segment['start'])
runs[run]=(base,packets)
continue
result=json.loads(subprocess.check_output(['ffprobe','-v','error','-select_streams','v:0','-count_packets','-show_entries','stream=nb_read_packets','-of','json',str(path.parent/segment['path'])]))
count=int(result['streams'][0]['nb_read_packets'])
assert count>0
runs[run]=(base,packets+count)
print('PASS segment offsets exactly match cumulative earlier video packet counts')
PY
python3 - "${XDG_DATA_HOME}" <<'PY'
import pathlib,json,sys
files=list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'))
assert files and all(not json.loads(p.read_text())['session']['active'] for p in files)
PY
for _ in $(seq 10); do
if ! kill -0 "${app}" 2>/dev/null; then break; fi
sleep 1
done
if kill -0 "${app}" 2>/dev/null; then echo 'FAIL closed app kept discovering after game exit' >&2; exit 1; fi
wait "${app}"
kill "$(cat "${CLIPBIN_RIG_DIR}/game.pid")"
# Reopen to prove the library loads from disk, then Quit cleanly with Ctrl+Q.
"${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/reopened.log" 2>&1 &
app=$!
sleep 4
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
sleep 0.5
grim "${out}/library.png"
bash -c 'exec -a reaper python3 -c "import time; time.sleep(120)" SteamLaunch AppId=480 --' &
game=$!
wait_log 'recorder_status=Recording' "${out}/reopened.log" 1 30
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
wtype -M ctrl -k q -m ctrl
for _ in $(seq 20); do
if ! kill -0 "${app}" 2>/dev/null; then break; fi
sleep 1
done
if kill -0 "${app}" 2>/dev/null; then echo 'FAIL explicit Quit did not exit' >&2; exit 1; fi
wait "${app}"
python3 - "${XDG_DATA_HOME}" <<'PY'
import pathlib,json,sys
files=list(pathlib.Path(sys.argv[1]).glob('clipbin/sessions/*/session.json'))
assert len(files) >= 2
assert all(not json.loads(p.read_text())['session']['active'] for p in files)
PY
echo "PASS automatic recording, window close, game exit, library reload and Quit"
INNER