fix(bridge): add Disconnected error variant, fix test race condition

Replace string-matching disconnect detection with explicit
BridgeError::Disconnected variant. Add TcpBridge::accept_on(listener)
that takes a pre-bound TcpListener, eliminating the 100ms sleep hack
in TCP tests. Send errors now also trigger ServerRunning=false.
Add trace logging to generate_snapshot for entity count visibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 21:15:56 +01:00
co-authored by Claude Opus 4.6
parent 058d0352ab
commit 2cd8786036
4 changed files with 65 additions and 94 deletions
+20 -90
View File
@@ -5,36 +5,17 @@ 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();
// Bind listener first — port is guaranteed ready before spawning threads
let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind");
let server_addr = listener.local_addr().expect("failed to get local address");
// Channel to signal when server is ready to accept
let (ready_tx, ready_rx) = mpsc::channel();
// Server thread: accept connection and send snapshot
// Server thread: accept on pre-bound listener 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 bridge = TcpBridge::accept_on(listener).expect("failed to accept");
let snapshot = ObserverSnapshot {
tick: 42,
@@ -52,15 +33,7 @@ fn snapshot_roundtrip_over_tcp() {
.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
// Client: connect and receive snapshot (no sleep needed — listener already bound)
let stream = TcpStream::connect(server_addr).expect("failed to connect");
let mut reader = std::io::BufReader::new(stream);
@@ -82,22 +55,11 @@ fn snapshot_roundtrip_over_tcp() {
#[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();
let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind");
let server_addr = listener.local_addr().expect("failed to get local address");
// 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 bridge = TcpBridge::accept_on(listener).expect("failed to accept");
let inputs = bridge.receive_inputs().expect("failed to receive inputs");
@@ -108,14 +70,6 @@ fn input_roundtrip_over_tcp() {
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);
@@ -139,7 +93,6 @@ fn input_roundtrip_over_tcp() {
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"),
@@ -152,50 +105,27 @@ fn input_roundtrip_over_tcp() {
#[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();
let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind");
let server_addr = listener.local_addr().expect("failed to get local address");
// 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);
let bridge = TcpBridge::accept_on(listener).expect("failed to accept");
// 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"),
}
assert!(
matches!(
result,
Err(settled_reach_server::bridge::BridgeError::Disconnected)
),
"expected Disconnected error"
);
});
// 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
drop(stream);
server_handle.join().expect("server thread panicked");
}