Server proof room now has 3 NPCs instead of 1. Find player entity by kind instead of assuming entities[0]. Wall-hides test checks specific NPC position (16.5, 13.5) rather than asserting zero NPC count. Corner-reveal test searches for NPC1 by position. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
127 lines
4.1 KiB
GDScript
127 lines
4.1 KiB
GDScript
## D-030 Layer 3: End-to-end connection test
|
|
## Spawns the Rust simulation server, connects via LocalBridge,
|
|
## sends a MoveNorth input, and verifies the snapshot response.
|
|
## Requires: server binary built (cargo build in server/)
|
|
class_name TestE2EConnection
|
|
extends GdUnitTestSuite
|
|
|
|
const CONNECT_TIMEOUT: float = 3.0
|
|
const RESPONSE_TIMEOUT: float = 5.0
|
|
const MAX_PORT_ATTEMPTS: int = 5
|
|
|
|
var _server_pid: int = -1
|
|
var _bridge: LocalBridge = null
|
|
var _test_port: int = 0
|
|
|
|
|
|
func _server_binary_path() -> String:
|
|
var project_dir := ProjectSettings.globalize_path("res://")
|
|
return project_dir.path_join("../server/target/debug/settled-reach-server")
|
|
|
|
|
|
## Pick a random high port to avoid conflicts in parallel CI runs.
|
|
## Range 49152-65535 is the dynamic/ephemeral port range (IANA).
|
|
static func _random_test_port() -> int:
|
|
return 49152 + (randi() % (65535 - 49152 + 1))
|
|
|
|
|
|
## Spawn server with port rotation — if the port is in use, the server exits
|
|
## immediately (bind failure). Detect this and retry with a new random port.
|
|
func _spawn_server(server_path: String) -> bool:
|
|
for attempt in range(MAX_PORT_ATTEMPTS):
|
|
_test_port = _random_test_port()
|
|
var addr := "127.0.0.1:%d" % _test_port
|
|
_server_pid = OS.create_process(server_path, [addr])
|
|
if _server_pid <= 0:
|
|
continue
|
|
# Give server time to bind or fail
|
|
await get_tree().create_timer(0.15).timeout
|
|
if OS.is_process_running(_server_pid):
|
|
return true
|
|
# Server exited — port likely in use, try another
|
|
_server_pid = -1
|
|
return false
|
|
|
|
|
|
func after_test() -> void:
|
|
if _bridge != null:
|
|
_bridge.disconnect_from_server()
|
|
_bridge = null
|
|
if _server_pid > 0 and OS.is_process_running(_server_pid):
|
|
OS.kill(_server_pid)
|
|
_server_pid = -1
|
|
|
|
|
|
# -- E2E: full round-trip through server binary --------------------------------
|
|
|
|
func test_send_input_receive_snapshot() -> void:
|
|
var server_path := _server_binary_path()
|
|
if not FileAccess.file_exists(server_path):
|
|
push_warning("E2E test skipped: server binary not found at %s" % server_path)
|
|
return
|
|
|
|
# Spawn server with port rotation (retries if port is in use)
|
|
var spawned := await _spawn_server(server_path)
|
|
assert_bool(spawned).is_true()
|
|
|
|
# Connect with retries (server needs time to accept)
|
|
_bridge = LocalBridge.new()
|
|
var connected := false
|
|
var elapsed := 0.0
|
|
while elapsed < CONNECT_TIMEOUT:
|
|
if _bridge.get_status() == StreamPeerTCP.STATUS_NONE:
|
|
_bridge.connect_to_server("127.0.0.1", _test_port)
|
|
_bridge.poll()
|
|
if _bridge.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
|
connected = true
|
|
break
|
|
if _bridge.get_status() == StreamPeerTCP.STATUS_ERROR:
|
|
_bridge.disconnect_from_server()
|
|
_bridge.reset()
|
|
await get_tree().create_timer(0.1).timeout
|
|
elapsed += 0.1
|
|
|
|
assert_bool(connected).is_true()
|
|
|
|
# Send batch input: MoveNorth at tick 0 (matching game_loop.rs test)
|
|
var inputs: Array = [{"tick": 0, "action_name": "MoveNorth"}]
|
|
var encoded := Protocol.encode_player_inputs(inputs)
|
|
assert_that(encoded.size()).is_greater(0)
|
|
var send_err := _bridge.send_message(encoded)
|
|
assert_that(send_err).is_equal(OK)
|
|
|
|
# Poll for snapshot response
|
|
var snapshot_bytes := PackedByteArray()
|
|
elapsed = 0.0
|
|
while elapsed < RESPONSE_TIMEOUT:
|
|
_bridge.poll()
|
|
snapshot_bytes = _bridge.poll_message()
|
|
if snapshot_bytes.size() > 0:
|
|
break
|
|
await get_tree().create_timer(0.05).timeout
|
|
elapsed += 0.05
|
|
|
|
assert_that(snapshot_bytes.size()).is_greater(0)
|
|
|
|
# Decode snapshot
|
|
var snapshot: Variant = Protocol.decode_snapshot(snapshot_bytes)
|
|
assert_that(snapshot).is_not_null()
|
|
|
|
# Server starts at tick 0, snapshot reflects state after processing
|
|
assert_that(snapshot.tick).is_equal(0)
|
|
assert_that(snapshot.entities.size()).is_greater(0)
|
|
|
|
# Find the player entity by kind (entity order is not guaranteed)
|
|
var player: Dictionary = {}
|
|
for entity in snapshot.entities:
|
|
if entity.kind.variant == "Player":
|
|
player = entity
|
|
break
|
|
assert_that(player.size()).is_greater(0)
|
|
|
|
# Player started at (16, 16, 0), moved north (y-1) to (16, 15, 0)
|
|
# Render coords: tile center offset -> (16.5, 15.5, 0)
|
|
assert_float(player.x).is_equal_approx(16.5, 0.001)
|
|
assert_float(player.y).is_equal_approx(15.5, 0.001)
|
|
assert_that(player.z).is_equal(0)
|