synvael/crates/net/src/tests/chunk.rs

95 lines
4.2 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-only
//! Loopback integration test for the chunk-stream transport.
//!
//! Binds a real QUIC server endpoint, completes the handshake, and drives the server-side [`chunk_stream_task`] end-to-end: a client-sent `ChunkSubscribe` must surface on the simulation-loop events channel as [`ServerEvent::ChunkSubscribe`], and a `ChunkMessage` pushed through the [`ChunkSink`] must be received by the client on the chunk stream.
use std::time::Duration;
use crate::chunk::{ChunkSink, chunk_stream_task};
use crate::codec::{MAX_CHUNK_FRAME_LEN, read_frame, write_frame};
use crate::endpoint::{client_endpoint, server_endpoint};
use crate::handshake::{accept_connection, connect};
use crate::runtime::ServerEvent;
use shared::protocol::chunk::{ChunkMessage, ChunkSubscribe};
use shared::protocol::{ClientHello, FeatureFlags, PROTOCOL_VERSION, PlayerIdentity};
use shared::world::{ChunkData, ChunkPos};
/// Builds a minimal `ClientHello` advertising the current protocol version.
fn hello(display_name: &str) -> ClientHello {
ClientHello {
protocol_version: PROTOCOL_VERSION,
client_build: "synvael-client-test".to_owned(),
player_identity: PlayerIdentity {
display_name: display_name.to_owned(),
},
installed_packs: vec![],
requested_features: FeatureFlags(0),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn chunk_subscribe_and_delivery_round_trip()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let server = server_endpoint("127.0.0.1:0".parse()?)?;
let server_addr = server.local_addr()?;
// Stand in for the simulation loop's channels: the events channel the task forwards subscriptions to, and the outbound sink it drains deliveries from.
let (events_tx, events_rx) = crossbeam_channel::unbounded::<ServerEvent>();
let (chunk_tx, chunk_rx) = tokio::sync::mpsc::unbounded_channel::<ChunkMessage>();
let sink = ChunkSink::new(chunk_tx);
// Server side: accept one connection, complete the handshake, then run the chunk pump until the client closes.
let server_task = tokio::spawn(async move {
let incoming = server.accept().await.ok_or("server endpoint closed")?;
let conn = accept_connection(incoming, "synvael-server-test".to_owned(), 20).await?;
chunk_stream_task(conn.connection, 7, events_tx, chunk_rx).await;
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(())
});
// Client side: connect, then open the chunk stream and send a subscription.
let client = client_endpoint()?;
let connected = connect(&client, server_addr, "localhost", hello("Tester")).await?;
let (mut client_send, mut client_recv) = connected.connection.open_bi().await?;
let subscribe = ChunkSubscribe {
center: ChunkPos::new(1, 2, 3),
radius: 4,
};
write_frame(&mut client_send, &subscribe).await?;
// The task must forward the subscription to the events channel. The crossbeam receiver is blocking, so it is polled on a blocking thread to avoid stalling the runtime.
let event = tokio::task::spawn_blocking(move || {
events_rx
.recv_timeout(Duration::from_secs(5))
.map(|e| (e, events_rx))
})
.await?;
let (event, events_rx) = event?;
match event {
ServerEvent::ChunkSubscribe { id, request } => {
assert_eq!(id, 7, "the subscribe must carry the session id");
assert_eq!(request, subscribe, "the subscribe must round-trip intact");
}
other => return Err(format!("expected ChunkSubscribe, got {other:?}").into()),
}
// The simulation loop hands a chunk back through the sink; the client must receive it on the stream.
let data = ChunkData::new(ChunkPos::new(1, 2, 3), 0);
let delivered = ChunkMessage::Chunk {
pos: ChunkPos::new(1, 2, 3),
data: data.clone(),
};
sink.send(delivered.clone());
let received = read_frame::<ChunkMessage>(&mut client_recv, MAX_CHUNK_FRAME_LEN).await?;
assert_eq!(received, delivered, "the chunk must round-trip intact");
// Close the client so the server task's pump ends and the endpoint winds down cleanly.
drop(events_rx);
drop(sink);
drop(connected);
server_task.await??;
Ok(())
}