## 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 TEST_PORT: int = 19876 const CONNECT_TIMEOUT: float = 3.0 const RESPONSE_TIMEOUT: float = 5.0 var _server_pid: int = -1 var _bridge: LocalBridge = null func _server_binary_path() -> String: # Resolve path relative to the Godot project root var project_dir := ProjectSettings.globalize_path("res://") return project_dir.path_join("../server/target/debug/settled-reach-server") func after_test() -> void: # Clean up regardless of test outcome 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 var addr := "127.0.0.1:%d" % TEST_PORT _server_pid = OS.create_process(server_path, [addr]) assert_that(_server_pid).is_greater(0) # Connect with retries (server needs time to bind port) _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: # Reset and retry _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_equal(1) # Player started at (16, 16, 0), moved north (y-1) to (16, 15, 0) # Render coords: tile center offset → (16.5, 15.5, 0) var player: Dictionary = snapshot.entities[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) assert_that(player.kind.variant).is_equal("Player")