mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
test(multiplayer): verify allocator worker lifecycle
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
container_name="cosmic-clash-allocator-integration"
|
||||
database="cosmic_clash_test"
|
||||
user="cosmic_clash_test"
|
||||
password="cosmic_clash_test"
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$container_name" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cleanup
|
||||
docker run --rm -d --name "$container_name" \
|
||||
-e POSTGRES_DB="$database" \
|
||||
-e POSTGRES_USER="$user" \
|
||||
-e POSTGRES_PASSWORD="$password" \
|
||||
-p 55436: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
|
||||
|
||||
cd "$repo_root/server"
|
||||
COSMIC_CLASH_POSTGRES_DSN="postgres://${user}:${password}@127.0.0.1:55436/${database}?sslmode=disable" \
|
||||
go test -tags integration ./allocator -count=1
|
||||
@@ -0,0 +1,114 @@
|
||||
//go:build integration
|
||||
|
||||
package allocator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmic-clash/cosmic-clash/server/agones"
|
||||
"github.com/cosmic-clash/cosmic-clash/server/migrations"
|
||||
"github.com/cosmic-clash/cosmic-clash/server/store"
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
func TestRealAllocatorWorkerReconcilesAgonesAllocationAndBindsMatch(t *testing.T) {
|
||||
dsn := os.Getenv("COSMIC_CLASH_POSTGRES_DSN")
|
||||
if dsn == "" {
|
||||
t.Skip("COSMIC_CLASH_POSTGRES_DSN is not set")
|
||||
}
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
ctx := context.Background()
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `DROP TABLE IF EXISTS schema_migrations, assignments, audit_events, outbox, result_receipts, ranked_season_rollovers, penalties, seasons, ratings, match_participants, matches, proposal_participants, proposals, allocations, game_servers, queue_tickets, idempotency_keys, sessions, identities CASCADE`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := migrations.Apply(ctx, db, filepath.Join("..", "migrations")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
for index, player := range []string{"allocator-worker-a", "allocator-worker-b"} {
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO identities (player_id, steam_id) VALUES ($1, $1)`, player); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO queue_tickets (ticket_id, player_id, playlist, state, client_build, protocol_version, enqueued_at, expires_at) VALUES ($1, $2, 'casual', 'ACCEPTED', 'build-1', 1, $3, $4)`, fmt.Sprintf("allocator-worker-ticket-%d", index), player, now, now.Add(time.Minute)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO matches (match_id, playlist, state, region, protocol_version) VALUES ('allocator-worker-match', 'casual', 'ALLOCATING', 'EU', 1)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for index, player := range []string{"allocator-worker-a", "allocator-worker-b"} {
|
||||
if _, err := db.ExecContext(ctx, `INSERT INTO match_participants (match_id, player_id, ticket_id, slot, team) VALUES ('allocator-worker-match', $1, $2, $3, $4)`, player, fmt.Sprintf("allocator-worker-ticket-%d", index), index, index); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var allocationCalls int
|
||||
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
_, _ = w.Write([]byte(`{"items":[{"metadata":{"name":"agones-ready-1","labels":{"cosmic-clash.io/region":"EU","cosmic-clash.io/build":"build-1","cosmic-clash.io/protocol":"1","cosmic-clash.io/transport":"enet"}},"status":{"state":"Ready"}}]}`))
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
t.Fatalf("provider method = %s", r.Method)
|
||||
}
|
||||
allocationCalls++
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["kind"] != "GameServerAllocation" {
|
||||
t.Fatalf("provider body kind = %v", body["kind"])
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"status":{"state":"Allocated","gameServerName":"agones-ready-1","address":"127.0.0.1","ports":[{"name":"default","port":7777}]}}`))
|
||||
}))
|
||||
defer provider.Close()
|
||||
|
||||
agonesClient := agones.Client{BaseURL: provider.URL, Namespace: "games", HTTP: provider.Client()}
|
||||
ready, err := agonesClient.ListReadyServers(ctx)
|
||||
if err != nil || len(ready) != 1 {
|
||||
t.Fatalf("ready projection = %+v, err=%v", ready, err)
|
||||
}
|
||||
if err := store.RegisterReadyServer(ctx, db, ready[0], now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
worker := Worker{
|
||||
Claims: store.AllocatingMatchClaims{DB: db, Transport: "enet"},
|
||||
Service: Service{Provider: agonesClient, Durable: store.AllocationRegistry{DB: db}, Now: func() time.Time { return now }},
|
||||
Now: func() time.Time { return now },
|
||||
}
|
||||
processed, err := worker.RunOnce(ctx)
|
||||
if err != nil || !processed || allocationCalls != 1 {
|
||||
t.Fatalf("worker processed=%t err=%v provider calls=%d", processed, err, allocationCalls)
|
||||
}
|
||||
var serverID, matchState, ticketState string
|
||||
if err := db.QueryRowContext(ctx, `SELECT server_id, state FROM matches WHERE match_id = 'allocator-worker-match'`).Scan(&serverID, &matchState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.QueryRowContext(ctx, `SELECT state FROM queue_tickets WHERE ticket_id = 'allocator-worker-ticket-0'`).Scan(&ticketState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if serverID != "agones-ready-1" || matchState != "ALLOCATING" || ticketState != "ALLOCATING" {
|
||||
t.Fatalf("durable lifecycle server=%q match=%q ticket=%q", serverID, matchState, ticketState)
|
||||
}
|
||||
var recorded int
|
||||
if err := db.QueryRowContext(ctx, `SELECT count(*) FROM allocations WHERE allocation_id = 'allocation-allocator-worker-match' AND server_id = 'agones-ready-1' AND state = 'ALLOCATED'`).Scan(&recorded); err != nil || recorded != 1 {
|
||||
t.Fatalf("recorded allocations=%d err=%v", recorded, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user