#!/usr/bin/env bash # End-to-end check in the current PipeWire session (spec 9.3): # examples/fake_gamescope + gamescope-hyperhdr + examples/mock_hyperhdr. # # The fake node gets a unique name, so a real gamescope node is never touched. # Requires: pw-link, pw-dump, jq; gst-launch-1.0 with pipewiresink for scenario 4. set -uo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$root" cargo build --release --bin gamescope-hyperhdr --example fake_gamescope --example mock_hyperhdr || exit 1 bin="$root/target/release" logs="$(mktemp -d /tmp/gshdr-e2e.XXXXXX)" name="gamescope-e2e-$$" client_node="gamescope-hyperhdr" port=$((20000 + RANDOM % 20000)) failures=0 mock_pid="" fake_pid="" client_pid="" gst_pid="" pass() { printf ' \033[32mPASS\033[0m %s\n' "$*"; } fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; failures=$((failures + 1)); } skip() { printf ' \033[33mSKIP\033[0m %s\n' "$*"; } check() { local desc=$1 shift if "$@"; then pass "$desc"; else fail "$desc"; fi } # wait_pid PID TIMEOUT_S: returns the exit status, or kills the process and returns 124. wait_pid() { local pid=$1 end=$((SECONDS + $2)) while [[ -e /proc/$pid && "$(awk '{ print $3 }' "/proc/$pid/stat" 2>/dev/null)" != Z ]]; do if ((SECONDS >= end)); then kill -9 "$pid" 2>/dev/null wait "$pid" 2>/dev/null return 124 fi sleep 0.05 done wait "$pid" 2>/dev/null } cleanup() { for pid in $client_pid $fake_pid $gst_pid $mock_pid; do kill "$pid" 2>/dev/null wait_pid "$pid" 3 done echo "logs: $logs" } trap cleanup EXIT # wait_count FILE REGEX MIN_COUNT TIMEOUT_S [FROM_LINE] wait_count() { local file=$1 re=$2 min=$3 from=${5:-1} end=$((SECONDS + $4)) while :; do local n n=$(tail -n "+$from" "$file" 2>/dev/null | grep -Ec -- "$re") ((n >= min)) && return 0 ((SECONDS >= end)) && return 1 sleep 0.2 done } wait_log() { wait_count "$1" "$2" 1 "${3:-10}" "${4:-1}"; } next_line() { echo $(($(wc -l <"$1") + 1)); } absent_since() { ! tail -n "+$3" "$1" | grep -Eq -- "$2"; } start_mock() { "$bin/examples/mock_hyperhdr" --listen "127.0.0.1:$port" --timeout-secs 5 >>"$logs/mock.log" 2>&1 & mock_pid=$! } start_fake() { echo "--- start $*" >>"$logs/fake.log" "$bin/examples/fake_gamescope" --name "$name" "$@" >>"$logs/fake.log" 2>&1 & fake_pid=$! } stop_fake() { kill "$fake_pid" 2>/dev/null wait_pid "$fake_pid" 3 fake_pid="" } start_client() { local log=$1 shift "$bin/gamescope-hyperhdr" --host 127.0.0.1 --port "$port" --target "$name" "$@" >"$log" 2>&1 & client_pid=$! } stop_client() { kill -TERM "$client_pid" 2>/dev/null wait_pid "$client_pid" 3 client_pid="" } # " -> " for every link. links() { pw-link -l 2>/dev/null | awk '/^[^ \t]/ { port = $0 } /\|->/ { sub(/^[ \t]*\|->[ \t]*/, ""); print port " -> " $0 }' } linked_to_fake() { links | grep -q "^$name:.* -> $client_node:"; } foreign_links() { links | grep " -> $client_node:" | grep -v "^$name:"; } node_exists() { pw-dump 2>/dev/null | jq -e --arg n "$1" \ 'any(.[]; .type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n)' >/dev/null } # Negotiated Format of the client's ports, e.g. "BGRx 320x180". client_port_format() { pw-dump 2>/dev/null | jq -r --arg n "$client_node" ' [.[] | select(.type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n) | .id] as $ids | .[] | select(.type == "PipeWire:Interface:Port" and (.info.props["node.id"] as $id | $ids | index($id))) | .info.params.Format[]? | "\(.format) \(.size.width)x\(.size.height)"' } echo "== setup (fake node '$name', mock on port $port, logs in $logs)" start_mock wait_log "$logs/mock.log" "listening on" 5 || { fail "mock did not start"; exit 1; } start_client "$logs/client.log" wait_log "$logs/client.log" "waiting for a '$name'" 5 || { fail "client did not start"; exit 1; } start_fake --width 3840 --height 2160 --fps 60 echo "== 1. requested_size renegotiation" check "1st Format event is the output size 3840x2160" wait_log "$logs/client.log" 'format: BGRx 3840x2160.*event=1' check "2nd Format event is 320x180" wait_log "$logs/client.log" 'format: BGRx 320x180.*event=2' check "frames arrive as 320x180" wait_log "$logs/client.log" 'receiving frames: 320x180 .*sending 320x180' check "registered with priority 150" wait_log "$logs/client.log" 'registered with priority 150' check "mock receives 320x180 RGB" wait_log "$logs/mock.log" 'image 320x180 len=172800 size_ok=true' echo "== 2. graph" sleep 1 links >"$logs/links.txt" client_port_format >"$logs/format.txt" check "client is linked to the fake node" linked_to_fake check "nothing else is linked to the client" test -z "$(foreign_links)" check "pw-dump: negotiated $(tr '\n' ' ' <"$logs/format.txt")" grep -q "BGRx 320x180" "$logs/format.txt" echo "== 3. source disappears and returns" mock_mark=$(next_line "$logs/mock.log") client_mark=$(next_line "$logs/client.log") stop_fake check "client sends Clear when the node disappears" wait_log "$logs/client.log" 'source gone: sent Clear' 5 "$client_mark" check "mock gets Clear{150}" wait_log "$logs/mock.log" 'Clear \{ priority: 150 \}' 5 "$mock_mark" check "mock sees the client close the connection" wait_log "$logs/mock.log" 'Disconnected\(ClosedByClient\)' 5 "$mock_mark" sleep 1 mock_mark=$(next_line "$logs/mock.log") start_fake --width 3840 --height 2160 --fps 60 check "client re-registers without a restart" wait_log "$logs/mock.log" 'Register \{ origin: "gamescope", priority: 150 \}' 10 "$mock_mark" check "frames flow again" wait_log "$logs/mock.log" 'image 320x180 len=172800 size_ok=true' 10 "$mock_mark" echo "== 4. foreign Video/Source is ignored" client_mark=$(next_line "$logs/client.log") stop_fake wait_log "$logs/client.log" 'destroying stream' 5 "$client_mark" client_mark=$(next_line "$logs/client.log") if command -v gst-launch-1.0 >/dev/null; then foreign="e2e-foreign-$$" gst-launch-1.0 -q videotestsrc is-live=true \ ! video/x-raw,format=BGRx,width=320,height=180,framerate=30/1 \ ! pipewiresink mode=provide "stream-properties=props,media.class=Video/Source,node.name=$foreign" \ >"$logs/gst.log" 2>&1 & gst_pid=$! sleep 3 if node_exists "$foreign"; then check "client does not create a stream" absent_since "$logs/client.log" 'stream created' "$client_mark" check "client node does not exist" test "$(node_exists "$client_node" && echo yes)" != yes check "nothing is linked to the client" test -z "$(links | grep " -> $client_node:")" else skip "pipewiresink did not create node '$foreign'" fi kill "$gst_pid" 2>/dev/null wait_pid "$gst_pid" 3 gst_pid="" else skip "gst-launch-1.0 not installed" fi echo "== 5. SIGTERM" mock_mark=$(next_line "$logs/mock.log") start_fake --width 3840 --height 2160 --fps 60 wait_log "$logs/mock.log" 'image 320x180' 10 "$mock_mark" started=$(date +%s%N) kill -TERM "$client_pid" wait_pid "$client_pid" 3 status=$? elapsed_ms=$((($(date +%s%N) - started) / 1000000)) client_pid="" check "exit code 0 (got $status)" test "$status" -eq 0 check "exits within 1 s (${elapsed_ms} ms)" test "$elapsed_ms" -lt 1000 check "sends Clear on shutdown" grep -q 'shutting down: sent Clear' "$logs/client.log" check "mock gets Clear{150}" wait_log "$logs/mock.log" 'Clear \{ priority: 150 \}' 2 "$mock_mark" echo "== 6. --supersample 2" mock_mark=$(next_line "$logs/mock.log") start_client "$logs/client-supersample.log" --supersample 2 check "requests 640x360" wait_log "$logs/client-supersample.log" 'format: BGRx 640x360.*requested_size 640x360' check "downscales 640x360 to 320x180" wait_log "$logs/client-supersample.log" 'receiving frames: 640x360 .*sending 320x180' check "mock receives 320x180" wait_log "$logs/mock.log" 'image 320x180 len=172800 size_ok=true' 10 "$mock_mark" stop_client echo "== 7. static picture: keepalive holds the connection" stop_fake start_fake --width 3840 --height 2160 --fps 60 --static mock_mark=$(next_line "$logs/mock.log") start_client "$logs/client-static.log" wait_log "$logs/mock.log" 'image 320x180' 10 "$mock_mark" sleep 8 check "no idle timeout on the server (5 s)" absent_since "$logs/mock.log" 'Disconnected' "$mock_mark" check "keepalive images keep arriving" wait_count "$logs/mock.log" 'images in' 3 1 "$mock_mark" stop_client echo if ((failures == 0)); then echo "all scenarios passed" else echo "$failures check(s) failed" fi exit $((failures > 0))