use std::time::Duration;
use bytes::Bytes;
use qmux::{Config, Error, Session, Transport, Version};
use tokio::sync::mpsc;
use web_transport_trait::{RecvStream as _, SendStream as _, Session as _};
struct ThrottledTransport {
tx: mpsc::Sender<Bytes>,
rx: mpsc::Receiver<Bytes>,
delay: Duration,
}
impl Transport for ThrottledTransport {
async fn send(&mut self, data: Bytes) -> Result<(), Error> {
tokio::time::sleep(self.delay).await;
self.tx.send(data).await.map_err(|_| Error::Closed)
}
async fn recv(&mut self) -> Result<Bytes, Error> {
self.rx.recv().await.ok_or(Error::Closed)
}
async fn close(&mut self) -> Result<(), Error> {
Ok(())
}
}
fn pair(delay: Duration) -> (Session, Session) {
let (c2s_tx, c2s_rx) = mpsc::channel(64);
let (s2c_tx, s2c_rx) = mpsc::channel(64);
let client_transport = ThrottledTransport {
tx: c2s_tx,
rx: s2c_rx,
delay,
};
let server_transport = ThrottledTransport {
tx: s2c_tx,
rx: c2s_rx,
delay,
};
let config = Config::new(Version::QMux01, None);
let client = Session::connect(client_transport, config.clone());
let server = Session::accept(server_transport, config);
(client, server)
}
#[tokio::test]
async fn higher_priority_completes_first() {
let (client, server) = pair(Duration::from_millis(5));
const LO_LEN: usize = 200 * 1024;
const HI_LEN: usize = 4 * 1024;
let writer = tokio::spawn(async move {
let mut lo = client.open_uni().await.unwrap();
let mut hi = client.open_uni().await.unwrap();
lo.set_priority(10);
hi.set_priority(200);
lo.write(&vec![b'L'; LO_LEN]).await.unwrap();
lo.finish().unwrap();
hi.write(&vec![b'H'; HI_LEN]).await.unwrap();
hi.finish().unwrap();
client
});
let lo = server.accept_uni().await.unwrap();
let mut hi = server.accept_uni().await.unwrap();
let lo_task = tokio::spawn(async move {
let mut lo = lo;
let mut total = 0usize;
while let Some(chunk) = lo.read_chunk(64 * 1024).await.unwrap() {
assert!(chunk.iter().all(|&b| b == b'L'));
total += chunk.len();
}
total
});
let hi_data = hi.read_all().await.unwrap();
assert_eq!(hi_data.len(), HI_LEN);
assert!(hi_data.iter().all(|&b| b == b'H'));
assert!(
!lo_task.is_finished(),
"low-priority stream should still be in-flight when the high-priority stream completes"
);
let lo_total = lo_task.await.unwrap();
assert_eq!(
lo_total, LO_LEN,
"low-priority stream must still deliver all bytes"
);
let _client = tokio::time::timeout(Duration::from_secs(2), writer)
.await
.expect("writer task should complete")
.expect("writer task panicked");
}
#[tokio::test]
async fn equal_priority_interleaves() {
let (client, server) = pair(Duration::from_millis(2));
const LEN: usize = 128 * 1024;
let writer = tokio::spawn(async move {
let mut a = client.open_uni().await.unwrap();
let mut b = client.open_uni().await.unwrap();
a.set_priority(50);
b.set_priority(50);
let wa = async {
a.write(&vec![b'A'; LEN]).await.unwrap();
a.finish().unwrap();
};
let wb = async {
b.write(&vec![b'B'; LEN]).await.unwrap();
b.finish().unwrap();
};
tokio::join!(wa, wb);
client
});
let mut a = server.accept_uni().await.unwrap();
let mut b = server.accept_uni().await.unwrap();
let first_a = a.read_chunk(8 * 1024).await.unwrap().unwrap();
assert!(!first_a.is_empty());
let first_b = b.read_chunk(8 * 1024).await.unwrap().unwrap();
assert!(
!first_b.is_empty(),
"second equal-priority stream must start before the first drains"
);
let mut a_total = first_a.len();
while let Some(c) = a.read_chunk(64 * 1024).await.unwrap() {
a_total += c.len();
}
let mut b_total = first_b.len();
while let Some(c) = b.read_chunk(64 * 1024).await.unwrap() {
b_total += c.len();
}
assert_eq!(a_total, LEN);
assert_eq!(b_total, LEN);
let _client = tokio::time::timeout(Duration::from_secs(2), writer)
.await
.expect("writer task should complete")
.expect("writer task panicked");
}
#[tokio::test]
async fn control_precedes_data_backlog() {
let (client, server) = pair(Duration::from_millis(5));
const LO_LEN: usize = 200 * 1024;
let mut bulk = client.open_uni().await.unwrap();
let mut signal = client.open_uni().await.unwrap();
bulk.set_priority(10);
signal.write(b"x").await.unwrap();
let bulk_writer = tokio::spawn(async move {
bulk.write(&vec![b'B'; LO_LEN]).await.ok();
client.closed().await;
});
tokio::time::sleep(Duration::from_millis(30)).await;
signal.reset(7);
let mut signal_recv = server.accept_uni().await.unwrap();
let reset_result = tokio::time::timeout(Duration::from_millis(500), signal_recv.closed()).await;
assert!(
reset_result.is_ok(),
"RESET_STREAM should arrive ahead of the data backlog"
);
assert!(matches!(reset_result.unwrap(), Err(Error::StreamReset(_))));
bulk_writer.abort();
}
#[tokio::test]
async fn mid_stream_set_priority_preserves_order() {
let (client, server) = pair(Duration::from_millis(2));
let payload: Vec<u8> = (0..200 * 1024).map(|i| (i % 251) as u8).collect();
let expected = payload.clone();
let writer = tokio::spawn(async move {
let mut filler = client.open_uni().await.unwrap();
filler.set_priority(1);
let mut s = client.open_uni().await.unwrap();
s.set_priority(5);
filler.write(&vec![b'F'; 200 * 1024]).await.unwrap();
filler.finish().unwrap();
let mid = payload.len() / 2;
s.write(&payload[..mid]).await.unwrap();
s.set_priority(250); s.write(&payload[mid..]).await.unwrap();
s.finish().unwrap();
client
});
let mut filler = server.accept_uni().await.unwrap();
let mut s = server.accept_uni().await.unwrap();
let got = s.read_all().await.unwrap();
assert_eq!(
got.as_ref(),
expected.as_slice(),
"byte sequence must be intact and in order"
);
let _ = filler.read_all().await;
let _client = tokio::time::timeout(Duration::from_secs(2), writer)
.await
.expect("writer task should complete")
.expect("writer task panicked");
}
#[tokio::test]
async fn teardown_unblocks_blocked_writer() {
let (client, server) = pair(Duration::from_millis(500));
let mut s = client.open_uni().await.unwrap();
let handle = tokio::spawn(async move {
s.write(&vec![b'Z'; 1024 * 1024]).await
});
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(
!handle.is_finished(),
"writer should be blocked on a full queue"
);
drop(client);
drop(server);
let result = tokio::time::timeout(Duration::from_secs(2), handle)
.await
.expect("writer should unblock on teardown")
.unwrap();
assert!(matches!(result, Err(Error::Closed)), "got {result:?}");
}