95 lines
4.2 KiB
Rust
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(())
|
|
}
|