ci: add ENet integration coverage

This commit is contained in:
Josh Creek
2026-08-21 19:57:59 +01:00
parent 72944a03e9
commit 23c1c3d231
7 changed files with 132 additions and 2 deletions
+16
View File
@@ -0,0 +1,16 @@
name: ENet Integration Tests
on:
push:
pull_request:
jobs:
enet-integration:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Build the pinned Godot test image
run: docker build --target exporter -t cosmic-clash-enet-tests .
- name: Run multi-process ENet smoke tests
run: docker run --rm cosmic-clash-enet-tests bash scripts/verify_enet_integration.sh
+3
View File
@@ -6,6 +6,9 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends libfontconfig1 \
&& rm -rf /var/lib/apt/lists/*
COPY Game /workspace/Game
# The ENet integration workflow runs its multi-process harness inside this
# pinned Godot image so source and CI use the same engine version.
COPY scripts/verify_enet_integration.sh /workspace/scripts/verify_enet_integration.sh
# Godot dedicated exports disallow command-line scene overrides. Bake the
# server scene into this export (the interactive project's source stays
# unchanged), then generate the global-script/autoload metadata it needs.
+5
View File
@@ -22,6 +22,10 @@ extends Node
const PORT := 7801
const RUN_SECONDS := 6.0
# The client starts after the host, so its identical run window ends later.
# Keep the host alive through that tail to avoid polling a deliberately
# closed transport during a successful clock test.
const HOST_GRACE_SECONDS := 1.0
const CONVERGE_BY_SEC := 2.0
const TICK_MS := 1000.0 / 60.0 # SimConstants.TICK_HZ, kept literal to avoid pulling in the whole project for one constant in a throwaway diagnostic
const EPOCH_FILE := "/tmp/cosmicclash_clock_smoke_epoch_offset.txt"
@@ -89,6 +93,7 @@ func _on_clock_updated(rtt_ms: float, offset_ms: float) -> void:
func _on_run_complete() -> void:
if _role != "client":
await get_tree().create_timer(HOST_GRACE_SECONDS).timeout
_finish(true, "host ran for %.1fs" % RUN_SECONDS)
return
+4 -1
View File
@@ -1,7 +1,10 @@
.PHONY: verify-phase6 verify-steam-templates
.PHONY: verify-phase6 verify-enet-integration verify-steam-templates
verify-phase6:
bash scripts/verify_phase6.sh
verify-enet-integration:
bash scripts/verify_enet_integration.sh
verify-steam-templates:
bash scripts/verify_steam_templates.sh
+1 -1
View File
@@ -1106,7 +1106,7 @@ New `--role=client-reconnect` grades the returning player: not a spectator, owns
| 6.4 `[P]` | **DONE.** Structured logging (join, leave, goal, kick, rate-limit, tick overrun) with `--log-level` | Greppable stdout/stderr events exercised in the smoke |
| 6.5 `[P]` | **DONE.** Arena rotation between matches; `--max-matches N` drain-and-exit | Smoke asserts two different arenas and `server_draining` |
| 6.6 `[P]` | **DONE.** systemd unit, Dockerfile, `SERVER.md` (ports, firewall, sizing per §1.4, and the SIGTERM caveat) | A third party can host from the docs alone |
| 6.7 `[D:3.6]` `[P]` | **DONE.** CI builds the server export and runs the smoke test against the **exported binary**, not source | `.github/workflows/phase6.yml` runs `make verify-phase6` on clean checkout |
| 6.7 `[D:3.6]` `[P]` | **DONE.** CI builds the server export and runs the smoke test against the **exported binary**, not source | `.github/workflows/dedicated-server-smoke.yml` runs `make verify-phase6` on clean checkout |
> `dedicated_server=true` enables Godot's strip-visuals export mode, which replaces meshes and textures with placeholders per resource. Every relevant site is already headless-guarded — `ship.gd:167`, `ball.gd:25`, `goal.gd`, `arena_boundary.gd` — so the code should be safe. **Verify it against a real stripped build anyway**; this is the kind of thing that fails silently.
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -euo pipefail
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$root_dir"
godot_bin="${GODOT_BIN:-godot}"
logs_dir="$(mktemp -d "${TMPDIR:-/tmp}/cosmic-clash-enet.XXXXXX")"
pids=()
# Comma-separated selection for local debugging; CI leaves this unset and
# therefore runs the complete suite.
selected_cases=",${VERIFY_ENET_CASES:-net,match-net,clock,lobby,networked-match},"
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
for pid in "${pids[@]}"; do
kill "$pid" 2>/dev/null || true
done
echo "ENet integration logs: $logs_dir"
}
trap cleanup EXIT
start_role() {
local log_file="$1"
local scene="$2"
shift 2
"$godot_bin" --headless --path Game "$scene" -- "$@" >"$log_file" 2>&1 &
pids+=("$!")
}
wait_for_role() {
local pid="$1"
wait "$pid"
}
assert_clean_logs() {
local label="$1"
shift
if grep -E "(SCRIPT ERROR|ERROR:|SMOKE FAIL)" "$@"; then
echo "$label emitted an engine or smoke error" >&2
return 1
fi
}
run_pair() {
local label="$1"
local scene="$2"
local host_log="$logs_dir/${label}-host.log"
local client_log="$logs_dir/${label}-client.log"
echo "ENet integration: $label"
start_role "$host_log" "$scene" --role=host
local host_pid="${pids[${#pids[@]} - 1]}"
sleep 0.5
start_role "$client_log" "$scene" --role=client
local client_pid="${pids[${#pids[@]} - 1]}"
wait_for_role "$client_pid"
wait_for_role "$host_pid"
assert_clean_logs "$label" "$host_log" "$client_log"
}
run_networked_match() {
local host_log="$logs_dir/networked-match-host.log"
local client_one_log="$logs_dir/networked-match-client-one.log"
local client_two_log="$logs_dir/networked-match-client-two.log"
echo "ENet integration: networked match"
start_role "$host_log" res://tests/networked_match_ci.tscn --role=host
local host_pid="${pids[${#pids[@]} - 1]}"
sleep 0.5
start_role "$client_one_log" res://tests/networked_match_ci.tscn --role=client-bot --test-bot
local client_one_pid="${pids[${#pids[@]} - 1]}"
sleep 0.2
start_role "$client_two_log" res://tests/networked_match_ci.tscn --role=client-bot --test-bot
local client_two_pid="${pids[${#pids[@]} - 1]}"
wait_for_role "$client_one_pid"
wait_for_role "$client_two_pid"
wait_for_role "$host_pid"
assert_clean_logs "networked match" "$host_log" "$client_one_log" "$client_two_log"
}
if [[ "$selected_cases" == *",net,"* ]]; then
run_pair net res://tests/net_smoke.tscn
fi
if [[ "$selected_cases" == *",match-net,"* ]]; then
run_pair match-net res://tests/match_net_smoke.tscn
fi
if [[ "$selected_cases" == *",clock,"* ]]; then
run_pair clock res://tests/clock_smoke.tscn
fi
if [[ "$selected_cases" == *",lobby,"* ]]; then
run_pair lobby res://tests/lobby_smoke.tscn
fi
if [[ "$selected_cases" == *",networked-match,"* ]]; then
run_networked_match
fi
echo "ENet integration verification passed"