#![cfg(test)]
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use tokio::sync::{mpsc, Mutex};
use tokio::time::timeout;
use crate::api::session::{ConnectionState, PhantomSession, SessionTransport};
use crate::errors::CoreError;
use crate::test_harness::fault_transport::{FaultControl, LossyTransport};
use crate::transport::handshake::{ClientHello, HandshakeResponse, HandshakeServer, ServerReply};
struct ChannelTransport {
tx: mpsc::Sender<Vec<u8>>,
rx: Mutex<mpsc::Receiver<Vec<u8>>>,
}
impl ChannelTransport {
fn pair() -> (Self, Self) {
let (a_tx, b_rx) = mpsc::channel(64);
let (b_tx, a_rx) = mpsc::channel(64);
(
Self {
tx: a_tx,
rx: Mutex::new(a_rx),
},
Self {
tx: b_tx,
rx: Mutex::new(b_rx),
},
)
}
}
impl SessionTransport for ChannelTransport {
async fn send_bytes(&self, data: &[u8]) -> Result<(), CoreError> {
self.tx
.send(data.to_vec())
.await
.map_err(|_| CoreError::NetworkError("channel closed".into()))
}
async fn recv_bytes(&self) -> Result<Bytes, CoreError> {
let mut rx = self.rx.lock().await;
let v = rx
.recv()
.await
.ok_or_else(|| CoreError::NetworkError("channel closed".into()))?;
Ok(Bytes::from(v))
}
}
async fn run_lossy_round_trips(
seed: u64,
loss_prob: f64,
reorder_prob: f64,
n: usize,
budget: Duration,
) {
let server_hs = HandshakeServer::new().expect("HandshakeServer::new");
let server_pinned_key = server_hs.verifying_key().clone();
let (client_channel, server_channel) = ChannelTransport::pair();
let faults = FaultControl::with_seed(seed, loss_prob, 0.0, reorder_prob, 0);
faults.arm_stochastic(false);
let lossy_client = LossyTransport::new(client_channel, faults.clone());
let client =
PhantomSession::connect_with_transport("test-server:9000", lossy_client, server_pinned_key);
let server_session_handle = tokio::spawn(async move {
let client_ip = "127.0.0.1".parse().expect("parse IP");
let hello_bytes = server_channel
.recv_bytes()
.await
.expect("server recv ClientHello");
let client_hello =
borsh::from_slice::<ClientHello>(&hello_bytes).expect("deserialize ClientHello");
let inner_session = loop {
match server_hs.process_client_hello(&client_hello, 0, client_ip) {
HandshakeResponse::Retry(retry) => {
let retry_bytes = ServerReply::Retry(retry)
.to_wire()
.expect("serialize retry");
server_channel
.send_bytes(&retry_bytes)
.await
.expect("server send retry");
let next_bytes = server_channel
.recv_bytes()
.await
.expect("server recv retry ClientHello");
let next_hello = borsh::from_slice::<ClientHello>(&next_bytes)
.expect("deserialize retry ClientHello");
match server_hs.process_client_hello(&next_hello, 0, client_ip) {
HandshakeResponse::Success(server_hello, session, _) => {
let b = ServerReply::Hello(server_hello)
.to_wire()
.expect("serialize ServerHello");
server_channel
.send_bytes(&b)
.await
.expect("server send ServerHello");
break session;
}
other => panic!("expected Success after retry, got {:?}", other),
}
}
HandshakeResponse::Success(server_hello, session, _) => {
let b = ServerReply::Hello(server_hello)
.to_wire()
.expect("serialize ServerHello");
server_channel
.send_bytes(&b)
.await
.expect("server send ServerHello");
break session;
}
HandshakeResponse::Reject(r) => panic!("unexpected Reject: {:?}", r),
HandshakeResponse::Fail(e) => panic!("handshake failed: {:?}", e),
}
};
let server_phantom = PhantomSession::from_accepted_server_session(
"test-client".into(),
server_channel,
Arc::new(inner_session),
);
for _ in 0..n {
let msg = timeout(budget, server_phantom.recv())
.await
.expect("server recv timed out — client retransmit never arrived")
.expect("server recv error");
server_phantom.send(msg).await.expect("server echo");
}
tokio::time::sleep(Duration::from_millis(200)).await;
server_phantom
});
let mut established = false;
for _ in 0..200 {
if client.connection_state() == ConnectionState::Connected {
established = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(established, "client session never became established");
faults.arm_stochastic(true);
for i in 0..n {
let payload = format!("loss-msg-{i:05}").into_bytes();
client.send(payload.clone()).await.expect("client send");
let reply = timeout(budget, client.recv())
.await
.unwrap_or_else(|_| {
panic!("client recv timed out on message {i} — loss not recovered within budget")
})
.expect("client recv error");
assert_eq!(
reply,
Bytes::from(payload),
"echo {i} must round-trip byte-exact and in order under seeded loss"
);
}
let server = server_session_handle.await.expect("server task panicked");
server.disconnect().await.expect("server clean disconnect");
client.disconnect().await.expect("client clean disconnect");
}
async fn run_pipelined_echo(
seed: u64,
loss_prob: f64,
reorder_prob: f64,
n: usize,
budget: Duration,
) -> Duration {
let server_hs = HandshakeServer::new().expect("HandshakeServer::new");
let server_pinned_key = server_hs.verifying_key().clone();
let (client_channel, server_channel) = ChannelTransport::pair();
let faults = FaultControl::with_seed(seed, loss_prob, 0.0, reorder_prob, 0);
faults.arm_stochastic(false);
let lossy_client = LossyTransport::new(client_channel, faults.clone());
let client =
PhantomSession::connect_with_transport("test-server:9000", lossy_client, server_pinned_key);
let server_handle = tokio::spawn(async move {
let client_ip = "127.0.0.1".parse().expect("parse IP");
let hello_bytes = server_channel
.recv_bytes()
.await
.expect("server recv ClientHello");
let client_hello =
borsh::from_slice::<ClientHello>(&hello_bytes).expect("deserialize ClientHello");
let inner = loop {
match server_hs.process_client_hello(&client_hello, 0, client_ip) {
HandshakeResponse::Retry(retry) => {
let retry_bytes = ServerReply::Retry(retry)
.to_wire()
.expect("serialize retry");
server_channel
.send_bytes(&retry_bytes)
.await
.expect("server send retry");
let next_bytes = server_channel
.recv_bytes()
.await
.expect("server recv retry ClientHello");
let next_hello = borsh::from_slice::<ClientHello>(&next_bytes)
.expect("deserialize retry ClientHello");
match server_hs.process_client_hello(&next_hello, 0, client_ip) {
HandshakeResponse::Success(server_hello, session, _) => {
let b = ServerReply::Hello(server_hello)
.to_wire()
.expect("serialize ServerHello");
server_channel
.send_bytes(&b)
.await
.expect("server send ServerHello");
break session;
}
other => panic!("expected Success after retry, got {other:?}"),
}
}
HandshakeResponse::Success(server_hello, session, _) => {
let b = ServerReply::Hello(server_hello)
.to_wire()
.expect("serialize ServerHello");
server_channel
.send_bytes(&b)
.await
.expect("server send ServerHello");
break session;
}
HandshakeResponse::Reject(r) => panic!("unexpected Reject: {r:?}"),
HandshakeResponse::Fail(e) => panic!("handshake failed: {e:?}"),
}
};
let server = PhantomSession::from_accepted_server_session(
"test-client".into(),
server_channel,
Arc::new(inner),
);
for _ in 0..n {
let m = timeout(budget, server.recv())
.await
.expect("server recv timed out — client retransmit never arrived")
.expect("server recv error");
server.send(m).await.expect("server echo");
}
tokio::time::sleep(Duration::from_millis(200)).await;
server
});
let mut established = false;
for _ in 0..200 {
if client.connection_state() == ConnectionState::Connected {
established = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(established, "client session never became established");
faults.arm_stochastic(true);
let start = tokio::time::Instant::now();
let mut sent: Vec<Vec<u8>> = Vec::with_capacity(n);
for i in 0..n {
let payload = format!("pipe-{i:05}").into_bytes();
client.send(payload.clone()).await.expect("client send");
sent.push(payload);
}
let mut got: Vec<Vec<u8>> = Vec::with_capacity(n);
for i in 0..n {
let echo = timeout(budget, client.recv())
.await
.unwrap_or_else(|_| panic!("client recv timed out on echo {i} — loss not recovered"))
.expect("client recv error");
got.push(echo);
}
let elapsed = start.elapsed();
assert_eq!(
got, sent,
"every echo must round-trip byte-exact and IN ORDER under pipelined loss+reorder"
);
let server = server_handle.await.expect("server task panicked");
server.disconnect().await.expect("server clean disconnect");
client.disconnect().await.expect("client clean disconnect");
elapsed
}
#[tokio::test]
async fn session_survives_seeded_loss_and_reorder() {
run_lossy_round_trips(
0x0A11_CE5E_ED0F_F00D,
0.03, 0.01, 40,
Duration::from_secs(60),
)
.await;
}
#[tokio::test]
async fn session_survives_seeded_loss_second_seed() {
run_lossy_round_trips(
0x1234_5678_9ABC_DEF0,
0.03,
0.01,
40,
Duration::from_secs(60),
)
.await;
}
#[tokio::test]
#[ignore = "documents the RTO-only latency cliff: 20% loss recovers losslessly but \
slowly (geometric 1s/2s/4s backoff). Phase 2/L1 SACK + fast-retransmit \
must flatten it. High-variance wall-clock — not an always-green gate."]
async fn loss_recovery_high_loss_recovers_but_is_slow() {
run_lossy_round_trips(
0xDEAD_BEEF_F00D_CAFE,
0.20, 0.02,
30,
Duration::from_secs(90),
)
.await;
}
#[tokio::test]
#[ignore = "measurement: prints pipelined recovery latency across loss rates"]
async fn pipelined_loss_sweep() {
for (loss, reorder) in [
(0.05, 0.02),
(0.10, 0.03),
(0.15, 0.05),
(0.20, 0.05),
(0.30, 0.05),
] {
let mut worst = Duration::ZERO;
for seed in [
0x1111_2222_3333_4444u64,
0xAAAA_BBBB_CCCC_DDDD,
0x0F0F_0F0F_0F0F_0F0F,
] {
let t = run_pipelined_echo(seed, loss, reorder, 40, Duration::from_secs(90)).await;
if t > worst {
worst = t;
}
}
println!(
"PIPELINED loss={:>2.0}% reorder={:>2.0}% n=40 → worst-seed {:?}",
loss * 100.0,
reorder * 100.0,
worst
);
}
}
#[tokio::test]
async fn pipelined_recovers_at_15pct_loss_with_reorder() {
run_pipelined_echo(
0xACCE_5515_0000_0001,
0.15,
0.05,
30,
Duration::from_secs(30),
)
.await;
}
#[tokio::test]
async fn pipelined_recovers_at_15pct_loss_second_seed() {
run_pipelined_echo(
0x5EED_0015_2222_3333,
0.15,
0.05,
30,
Duration::from_secs(30),
)
.await;
}
#[tokio::test]
async fn pipelined_recovers_at_20pct_loss() {
run_pipelined_echo(
0x2020_2020_ABCD_0001,
0.20,
0.05,
30,
Duration::from_secs(30),
)
.await;
}