## 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)