Godot has no Unix socket API, so TCP localhost is required for client-server IPC. TcpBridge implements SimBridge with the same framing protocol as LocalBridge. Includes accept/connect methods and three integration tests over TCP. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
202 lines
6.6 KiB
Rust
202 lines
6.6 KiB
Rust
//! Integration tests for TcpBridge over TCP localhost (D-030 Layer 2: IPC roundtrip).
|
|
|
|
use settled_reach_server::bridge::framing::{read_framed, write_framed};
|
|
use settled_reach_server::bridge::tcp::TcpBridge;
|
|
use settled_reach_server::bridge::types::*;
|
|
use settled_reach_server::bridge::SimBridge;
|
|
use std::net::{TcpListener, TcpStream};
|
|
use std::sync::mpsc;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
/// Helper to bind a TCP listener and return its address.
|
|
/// This allows tests to discover the OS-assigned port before calling TcpBridge::accept().
|
|
fn bind_listener() -> (TcpListener, std::net::SocketAddr) {
|
|
let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind");
|
|
let addr = listener.local_addr().expect("failed to get local address");
|
|
(listener, addr)
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_roundtrip_over_tcp() {
|
|
// Pre-bind listener to discover the port
|
|
let (listener, server_addr) = bind_listener();
|
|
let addr_str = server_addr.to_string();
|
|
|
|
// Channel to signal when server is ready to accept
|
|
let (ready_tx, ready_rx) = mpsc::channel();
|
|
|
|
// Server thread: accept connection and send snapshot
|
|
let server_handle = thread::spawn(move || {
|
|
// Drop the pre-bound listener since TcpBridge::accept will bind its own
|
|
drop(listener);
|
|
|
|
// Signal we're about to accept
|
|
ready_tx.send(()).expect("failed to send ready signal");
|
|
|
|
let bridge = TcpBridge::accept(&addr_str).expect("failed to bind and accept");
|
|
|
|
let snapshot = ObserverSnapshot {
|
|
tick: 42,
|
|
entities: vec![VisibleEntity {
|
|
entity_id: 100,
|
|
x: 10.5,
|
|
y: 20.3,
|
|
z: 0,
|
|
kind: EntityKind::Npc,
|
|
}],
|
|
};
|
|
|
|
bridge
|
|
.send_snapshot(&snapshot)
|
|
.expect("failed to send snapshot");
|
|
});
|
|
|
|
// Wait for server thread to start
|
|
ready_rx
|
|
.recv_timeout(Duration::from_secs(2))
|
|
.expect("server did not become ready");
|
|
|
|
// Give server time to bind and call accept()
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
// Client: connect and receive snapshot
|
|
let stream = TcpStream::connect(server_addr).expect("failed to connect");
|
|
let mut reader = std::io::BufReader::new(stream);
|
|
|
|
let payload = read_framed(&mut reader)
|
|
.expect("failed to read frame")
|
|
.expect("unexpected EOF");
|
|
|
|
let snapshot: ObserverSnapshot =
|
|
rmp_serde::from_slice(&payload).expect("failed to deserialize");
|
|
|
|
assert_eq!(snapshot.tick, 42);
|
|
assert_eq!(snapshot.entities.len(), 1);
|
|
assert_eq!(snapshot.entities[0].entity_id, 100);
|
|
assert_eq!(snapshot.entities[0].x, 10.5);
|
|
assert_eq!(snapshot.entities[0].y, 20.3);
|
|
|
|
server_handle.join().expect("server thread panicked");
|
|
}
|
|
|
|
#[test]
|
|
fn input_roundtrip_over_tcp() {
|
|
// Pre-bind listener to discover the port
|
|
let (listener, server_addr) = bind_listener();
|
|
let addr_str = server_addr.to_string();
|
|
|
|
// Channel to signal when server is ready to accept
|
|
let (ready_tx, ready_rx) = mpsc::channel();
|
|
|
|
// Server thread: accept connection and receive inputs
|
|
let server_handle = thread::spawn(move || {
|
|
// Drop the pre-bound listener since TcpBridge::accept will bind its own
|
|
drop(listener);
|
|
|
|
// Signal we're about to accept
|
|
ready_tx.send(()).expect("failed to send ready signal");
|
|
|
|
let bridge = TcpBridge::accept(&addr_str).expect("failed to bind and accept");
|
|
|
|
let inputs = bridge.receive_inputs().expect("failed to receive inputs");
|
|
|
|
assert_eq!(inputs.len(), 2);
|
|
assert_eq!(inputs[0].tick, 10);
|
|
assert_eq!(inputs[1].tick, 11);
|
|
|
|
inputs
|
|
});
|
|
|
|
// Wait for server thread to start
|
|
ready_rx
|
|
.recv_timeout(Duration::from_secs(2))
|
|
.expect("server did not become ready");
|
|
|
|
// Give server time to bind and call accept()
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
// Client: connect and send inputs
|
|
let stream = TcpStream::connect(server_addr).expect("failed to connect");
|
|
let mut writer = std::io::BufWriter::new(stream);
|
|
|
|
let inputs = vec![
|
|
PlayerInput {
|
|
tick: 10,
|
|
action: PlayerAction::MoveNorth,
|
|
},
|
|
PlayerInput {
|
|
tick: 11,
|
|
action: PlayerAction::Interact,
|
|
},
|
|
];
|
|
|
|
let payload = rmp_serde::to_vec_named(&inputs).expect("failed to serialize");
|
|
write_framed(&mut writer, &payload).expect("failed to write frame");
|
|
|
|
// Drop writer to close connection and signal EOF to server
|
|
drop(writer);
|
|
|
|
let received_inputs = server_handle.join().expect("server thread panicked");
|
|
|
|
// Verify actions survived the round-trip
|
|
match &received_inputs[0].action {
|
|
PlayerAction::MoveNorth => {}
|
|
_ => panic!("expected MoveNorth action"),
|
|
}
|
|
match &received_inputs[1].action {
|
|
PlayerAction::Interact => {}
|
|
_ => panic!("expected Interact action"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tcp_bridge_eof_returns_error() {
|
|
// Pre-bind listener to discover the port
|
|
let (listener, server_addr) = bind_listener();
|
|
let addr_str = server_addr.to_string();
|
|
|
|
// Channel to signal when server is ready to accept
|
|
let (ready_tx, ready_rx) = mpsc::channel();
|
|
|
|
// Server thread: accept connection and receive EOF
|
|
let server_handle = thread::spawn(move || {
|
|
// Drop the pre-bound listener since TcpBridge::accept will bind its own
|
|
drop(listener);
|
|
|
|
// Signal we're about to accept
|
|
ready_tx.send(()).expect("failed to send ready signal");
|
|
|
|
let bridge = TcpBridge::accept(&addr_str).expect("failed to bind and accept");
|
|
|
|
// Attempt to receive inputs - should get EOF error
|
|
let result = bridge.receive_inputs();
|
|
|
|
assert!(result.is_err(), "expected error on EOF");
|
|
match result {
|
|
Err(settled_reach_server::bridge::BridgeError::Transport(msg)) => {
|
|
assert!(
|
|
msg.contains("disconnected") || msg.contains("EOF"),
|
|
"expected disconnect/EOF error, got: {}",
|
|
msg
|
|
);
|
|
}
|
|
_ => panic!("expected Transport error with disconnect/EOF message"),
|
|
}
|
|
});
|
|
|
|
// Wait for server thread to start
|
|
ready_rx
|
|
.recv_timeout(Duration::from_secs(2))
|
|
.expect("server did not become ready");
|
|
|
|
// Give server time to bind and call accept()
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
// Client: connect and immediately disconnect without sending data
|
|
let stream = TcpStream::connect(server_addr).expect("failed to connect");
|
|
drop(stream); // Close connection immediately
|
|
|
|
server_handle.join().expect("server thread panicked");
|
|
}
|