183 lines
7.8 KiB
Bash
Executable File
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
|