mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
161 lines
6.2 KiB
Bash
Executable File
161 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Two-player extension of verify_control_plane_integration.sh: a real
|
|
# PostgreSQL instance, the real api.Service (via testkit-api, see that
|
|
# script's header for why), a real server/cmd/matcher (the actual production
|
|
# binary -- it needs no fake, it only ever touches queue_tickets/proposals),
|
|
# and two real headless Godot clients each playing one player through
|
|
# login -> queue_create -> (real matcher pairs them) -> proposal accept.
|
|
# multiplayer-next.md 8.40 names this as the next scoped extension to the
|
|
# single-player harness.
|
|
|
|
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$root_dir"
|
|
|
|
godot_bin="${GODOT_BIN:-}"
|
|
godot_image="barichello/godot-ci@sha256:622e5ca81b54cd8038ecf7de5d157b47efc800d7cf635af2eec18a6aee4bab7e"
|
|
container_name="cosmic-clash-control-plane-proposal-integration"
|
|
database="cosmic_clash_test"
|
|
user="cosmic_clash_test"
|
|
password="cosmic_clash_test"
|
|
pg_port="55435"
|
|
api_port="18100"
|
|
logs_dir="$(mktemp -d "${TMPDIR:-/tmp}/cosmic-clash-control-plane-proposal.XXXXXX")"
|
|
|
|
testkit_pid=""
|
|
matcher_pid=""
|
|
if [[ -z "$godot_bin" ]]; then
|
|
if command -v godot >/dev/null 2>&1; then
|
|
godot_bin="$(command -v godot)"
|
|
elif [[ -x /Applications/Godot.app/Contents/MacOS/Godot ]]; then
|
|
godot_bin="/Applications/Godot.app/Contents/MacOS/Godot"
|
|
fi
|
|
fi
|
|
use_container_godot=0
|
|
if [[ -z "$godot_bin" || ! -x "$godot_bin" ]]; then
|
|
use_container_godot=1
|
|
fi
|
|
|
|
run_godot() {
|
|
if [[ "$use_container_godot" == 0 ]]; then
|
|
"$godot_bin" --headless --path Game res://tests/control_plane_proposal_smoke.tscn -- "$@"
|
|
return
|
|
fi
|
|
docker run --rm --platform linux/amd64 \
|
|
--mount "type=bind,src=$root_dir,dst=/workspace" -w /workspace "$godot_image" \
|
|
godot --headless --path Game res://tests/control_plane_proposal_smoke.tscn -- "$@"
|
|
}
|
|
cleanup() {
|
|
local status=$?
|
|
if (( status != 0 )); then
|
|
for log_file in "$logs_dir"/*.log; do
|
|
[[ -f "$log_file" ]] || continue
|
|
echo "--- $log_file" >&2
|
|
cat "$log_file" >&2
|
|
done
|
|
fi
|
|
[[ -n "$testkit_pid" ]] && kill "$testkit_pid" 2>/dev/null || true
|
|
[[ -n "$matcher_pid" ]] && kill "$matcher_pid" 2>/dev/null || true
|
|
lsof -ti "tcp:${api_port}" 2>/dev/null | xargs -r kill -9 2>/dev/null || true
|
|
docker rm -f "$container_name" >/dev/null 2>&1 || true
|
|
echo "Control-plane proposal integration logs: $logs_dir"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
docker rm -f "$container_name" >/dev/null 2>&1 || true
|
|
docker run --rm -d --name "$container_name" \
|
|
-e POSTGRES_DB="$database" \
|
|
-e POSTGRES_USER="$user" \
|
|
-e POSTGRES_PASSWORD="$password" \
|
|
-p "${pg_port}:5432" postgres:17-alpine >/dev/null
|
|
|
|
for attempt in $(seq 1 30); do
|
|
if docker exec "$container_name" pg_isready -U "$user" -d "$database" >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
if [ "$attempt" = 30 ]; then
|
|
echo "PostgreSQL did not become ready" >&2
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
dsn="postgres://${user}:${password}@127.0.0.1:${pg_port}/${database}?sslmode=disable"
|
|
|
|
go -C server build -o "$logs_dir/testkit-api" ./cmd/testkit-api
|
|
go -C server build -o "$logs_dir/matcher" ./cmd/matcher
|
|
|
|
COSMIC_CLASH_POSTGRES_DSN="$dsn" "$logs_dir/testkit-api" --listen="127.0.0.1:${api_port}" --migrations="$root_dir/server/migrations" \
|
|
>"$logs_dir/testkit-api.log" 2>&1 &
|
|
testkit_pid=$!
|
|
|
|
for attempt in $(seq 1 30); do
|
|
if curl -sSf "http://127.0.0.1:${api_port}/healthz" >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
if [ "$attempt" = 30 ]; then
|
|
echo "testkit-api did not become ready" >&2
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
# --interval=250ms: this is the whole test's own latency budget, not a
|
|
# production setting -- fast polling here just keeps the smoke test quick.
|
|
COSMIC_CLASH_POSTGRES_DSN="$dsn" "$logs_dir/matcher" --playlist=casual --size=2 --interval=250ms --migrations="$root_dir/server/migrations" \
|
|
>"$logs_dir/matcher.log" 2>&1 &
|
|
matcher_pid=$!
|
|
|
|
client_api_host="127.0.0.1"
|
|
if [[ "$use_container_godot" == 1 ]]; then
|
|
client_api_host="host.docker.internal"
|
|
fi
|
|
run_godot --control-plane-url="http://${client_api_host}:${api_port}" --role=player-a \
|
|
>"$logs_dir/godot-player-a.log" 2>&1 &
|
|
player_a_pid=$!
|
|
run_godot --control-plane-url="http://${client_api_host}:${api_port}" --role=player-b \
|
|
>"$logs_dir/godot-player-b.log" 2>&1 &
|
|
player_b_pid=$!
|
|
|
|
# The matcher only forms a match from candidates that share a verified
|
|
# region (server/domain/matcher.go's commonRegions, over each candidate's
|
|
# queue_tickets.predicted_rtt) -- populated for real only via the
|
|
# authenticated Steam-relay probe flow (task 8.15/8.16), which this harness
|
|
# has no real Steam access to drive. Seed it directly in the same database
|
|
# instead of building another fake auth boundary just for this: both real
|
|
# clients still go through the real queue/matcher/proposal path end to end,
|
|
# only the region-probe INPUT is synthetic, exactly like testkit-api's fake
|
|
# Steam login already is for identity. Bounded to the same overall timeout
|
|
# the Godot clients themselves use.
|
|
seed_deadline=$(( $(date +%s) + 20 ))
|
|
while [ "$(date +%s)" -lt "$seed_deadline" ]; do
|
|
if ! kill -0 "$player_a_pid" 2>/dev/null && ! kill -0 "$player_b_pid" 2>/dev/null; then
|
|
break
|
|
fi
|
|
docker exec "$container_name" psql -U "$user" -d "$database" -c \
|
|
"UPDATE queue_tickets SET predicted_rtt = '{\"EU\": 20}'::jsonb WHERE state = 'QUEUED' AND client_build = 'smoke-build'" \
|
|
>/dev/null 2>&1 || true
|
|
sleep 0.2
|
|
done
|
|
|
|
status_a=0
|
|
status_b=0
|
|
wait "$player_a_pid" || status_a=$?
|
|
wait "$player_b_pid" || status_b=$?
|
|
|
|
if [ "$status_a" -ne 0 ] || [ "$status_b" -ne 0 ] \
|
|
|| ! grep -q "^SMOKE PASS:" "$logs_dir/godot-player-a.log" \
|
|
|| ! grep -q "^SMOKE PASS:" "$logs_dir/godot-player-b.log"; then
|
|
echo "Control-plane proposal integration FAILED (player-a=$status_a player-b=$status_b)" >&2
|
|
docker exec "$container_name" psql -U "$user" -d "$database" -c \
|
|
"SELECT state, client_build, predicted_rtt, count(*) FROM queue_tickets GROUP BY state, client_build, predicted_rtt ORDER BY state" >&2 || true
|
|
docker exec "$container_name" psql -U "$user" -d "$database" -c \
|
|
"SELECT proposal_id, state, revision, count(*) AS participants FROM proposals LEFT JOIN proposal_participants USING (proposal_id) GROUP BY proposal_id, state, revision" >&2 || true
|
|
cat "$logs_dir/godot-player-a.log" >&2
|
|
cat "$logs_dir/godot-player-b.log" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Control-plane proposal integration PASS"
|