Add native Linux SNI and Windows notification-area trays, single-instance reopening, background startup and login entries. Preserve recording and discovery after close, with clean Quit and reachable no-tray fallbacks. Track capture-source audio delivery independently of silent output tracks and warn without stopping video. Give clip metadata its own row and shape labels at final bounds to avoid GPUI 0.2.2 truncation cache and text-run bugs. Validated with strict workspace checks, 57 tests, Windows core/OBS/probe cross-clippy, all ten capture scenarios, tray/session/clips/playback rigs and narrow/wide screenshots. Document runtime validation gaps.
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-core --example sni-host
|
|
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
|
|
}
|
|
"${CLIPBIN_SESSION_REPO}/target/debug/examples/sni-host" > "${CLIPBIN_SESSION_OUT}/tray-host.log" 2>&1 &
|
|
host=$!
|
|
sleep 1
|
|
source "${CLIPBIN_SESSION_REPO}/scripts/rig/keys.sh"
|
|
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:-}" "${host}" 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
|
|
kill -0 "${app}"
|
|
kill "$(cat "${CLIPBIN_RIG_DIR}/game.pid")"
|
|
# A second launch signals the original instance and exits; background discovery survives.
|
|
"${CLIPBIN_SESSION_REPO}/target/debug/clipbin-desktop" > "${out}/second-launch.log" 2>&1
|
|
kill -0 "${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}/desktop.log" 2 30
|
|
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
|
|
rig_key q 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
|