use std::future::Future;
use std::io;
use std::path::PathBuf;
use std::pin::{Pin, pin};
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use futures_util::sink::Sink;
use futures_util::stream::Stream;
use tokio::io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _, ReadBuf};
use slither::constants::INITIAL_MAX_STREAM_DATA;
use slither::error::{ConnectionLost, DatagramError, ReadError, WriteError};
use slither::identity::Identity;
use slither::shell::{Intro, Notification};
use slither::testutil::{Pair, local, settle};
const PATIENCE: Duration = Duration::from_secs(5);
async fn within<F: Future>(fut: F, what: &str) -> F::Output {
match tokio::time::timeout(PATIENCE, fut).await {
Ok(v) => v,
Err(_) => panic!("{what}: still pending after {PATIENCE:?} of virtual time"),
}
}
fn noop_context() -> Context<'static> {
Context::from_waker(Waker::noop())
}
fn poll_stream_once<S: Stream + Unpin>(s: &mut S) -> Poll<Option<S::Item>> {
let mut cx = noop_context();
Pin::new(s).poll_next(&mut cx)
}
fn poll_future_once<F: Future>(mut f: Pin<&mut F>) -> Poll<F::Output> {
let mut cx = noop_context();
f.as_mut().poll(&mut cx)
}
fn poll_read_once<R: AsyncRead + Unpin>(r: &mut R, buf: &mut ReadBuf<'_>) -> Poll<io::Result<()>> {
let mut cx = noop_context();
Pin::new(r).poll_read(&mut cx, buf)
}
fn poll_write_once<W: AsyncWrite + Unpin>(w: &mut W, buf: &[u8]) -> Poll<io::Result<usize>> {
let mut cx = noop_context();
Pin::new(w).poll_write(&mut cx, buf)
}
fn poll_flush_once<W: AsyncWrite + Unpin>(w: &mut W) -> Poll<io::Result<()>> {
let mut cx = noop_context();
Pin::new(w).poll_flush(&mut cx)
}
fn poll_shutdown_once<W: AsyncWrite + Unpin>(w: &mut W) -> Poll<io::Result<()>> {
let mut cx = noop_context();
Pin::new(w).poll_shutdown(&mut cx)
}
fn read_into<R: AsyncRead + Unpin>(r: &mut R, store: &mut [u8]) -> Poll<io::Result<usize>> {
let mut rb = ReadBuf::new(store);
match poll_read_once(r, &mut rb) {
Poll::Pending => Poll::Pending,
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
Poll::Ready(Ok(())) => Poll::Ready(Ok(rb.filled().len())),
}
}
#[tokio::test(start_paused = true)]
async fn a_stream_adapter_claims_at_most_one_message_per_poll() {
local(async {
let pair = Pair::seeded(0x58);
let (a, b) = pair.establish().await;
for m in [&b"one"[..], &b"two"[..], &b"three"[..]] {
within(b.send_message(m), "send_message")
.await
.expect("send_message");
}
for _ in 0..4 {
settle().await;
}
let mut msgs = a.messages();
match poll_stream_once(&mut msgs) {
Poll::Ready(Some(Ok(m))) => assert_eq!(
m.as_slice(),
b"one",
"the adapter must yield the messages in the order the verb does"
),
Poll::Ready(Some(Err(e))) => panic!("messages() failed: {e:?}"),
Poll::Ready(None) => panic!("messages() ended on a live connection (ruling 226)"),
Poll::Pending => {
panic!(
"messages() parked with three messages queued — fixture problem, not ruling 58"
)
}
}
let recv = pin!(a.recv_message());
match poll_future_once(recv) {
Poll::Ready(Ok(m)) => assert_eq!(
m.as_slice(),
b"two",
"ruling 58: one `poll_next` must claim exactly one item, so the verb \
underneath still owns messages two and three"
),
Poll::Pending => panic!(
"RULING 58 VIOLATED: one `poll_next` claimed more than one item. \
`recv_message()` parked, so the adapter has swallowed messages two \
and three into an intermediate queue — the unbounded shell queue \
§10.6 forbids, which is what ruling 58 exists to prevent."
),
Poll::Ready(Err(e)) => panic!("recv_message() failed: {e:?}"),
}
drop(msgs);
})
.await;
}
#[tokio::test(start_paused = true)]
async fn a_stream_adapter_claims_at_most_one_datagram_per_poll() {
local(async {
let pair = Pair::seeded(0x59);
let (a, b) = pair.establish().await;
for d in [&b"alpha"[..], &b"bravo"[..], &b"charlie"[..]] {
b.send_datagram(d).expect("send_datagram");
}
for _ in 0..4 {
settle().await;
}
let mut dgrams = a.datagrams();
let first = match poll_stream_once(&mut dgrams) {
Poll::Ready(Some(Ok(d))) => d,
Poll::Ready(Some(Err(e))) => panic!("datagrams() failed: {e:?}"),
Poll::Ready(None) => panic!("datagrams() ended on a live connection (ruling 226)"),
Poll::Pending => panic!("datagrams() parked with three queued — fixture problem"),
};
let recv = pin!(a.recv_datagram());
match poll_future_once(recv) {
Poll::Ready(Ok(d)) => assert_ne!(
d, first,
"the verb re-delivered the datagram the adapter already claimed"
),
Poll::Pending => panic!(
"RULING 58 VIOLATED on `datagrams()`: `recv_datagram()` parked, so one \
`poll_next` claimed more than one datagram."
),
Poll::Ready(Err(e)) => panic!("recv_datagram() failed: {e:?}"),
}
drop(dgrams);
})
.await;
}
#[tokio::test(start_paused = true)]
async fn a_stream_adapter_claims_at_most_one_incoming_stream_per_poll() {
local(async {
let pair = Pair::seeded(0x5A);
let (a, b) = pair.establish().await;
let mut openers = Vec::new();
for tag in [1u8, 2, 3] {
let mut s = within(b.open_bi(), "open_bi").await.expect("open_bi");
within(s.write_all(&[tag]), "write_all")
.await
.expect("write_all");
openers.push(s);
}
for _ in 0..4 {
settle().await;
}
let mut incoming = a.incoming_bi();
match poll_stream_once(&mut incoming) {
Poll::Ready(Some(Ok(_first))) => {}
Poll::Ready(Some(Err(e))) => panic!("incoming_bi() failed: {e:?}"),
Poll::Ready(None) => panic!("incoming_bi() ended on a live connection (ruling 226)"),
Poll::Pending => panic!("incoming_bi() parked with three streams pending — fixture"),
}
let accept = pin!(a.accept_bi());
match poll_future_once(accept) {
Poll::Ready(Ok(_second)) => {}
Poll::Pending => panic!(
"RULING 58 VIOLATED on `incoming_bi()`: `accept_bi()` parked, so one \
`poll_next` claimed more than one stream. A claimed-but-unconsumed \
`BiStream` is a live handle whose `Drop` resets the stream."
),
Poll::Ready(Err(e)) => panic!("accept_bi() failed: {e:?}"),
}
drop(incoming);
drop(openers);
})
.await;
}
fn all_connection_lost() -> Vec<ConnectionLost> {
vec![
ConnectionLost::TimedOut,
ConnectionLost::NonceExhausted,
ConnectionLost::LocallyClosed,
ConnectionLost::PeerClosed {
code: 7,
reason: b"bye".to_vec(),
},
ConnectionLost::ProtocolViolation { code: 9 },
ConnectionLost::Replaced,
ConnectionLost::EndpointDropped,
]
}
fn read_table() -> Vec<(ReadError, io::ErrorKind)> {
use io::ErrorKind as K;
vec![
(ReadError::Reset(7), K::ConnectionReset),
(ConnectionLost::TimedOut.into(), K::TimedOut),
(ConnectionLost::NonceExhausted.into(), K::ConnectionAborted),
(ConnectionLost::LocallyClosed.into(), K::NotConnected),
(
ConnectionLost::PeerClosed {
code: 7,
reason: b"bye".to_vec(),
}
.into(),
K::ConnectionAborted,
),
(
ConnectionLost::ProtocolViolation { code: 9 }.into(),
K::ConnectionAborted,
),
(ConnectionLost::Replaced.into(), K::ConnectionAborted),
(ConnectionLost::EndpointDropped.into(), K::NotConnected),
]
}
fn write_table() -> Vec<(WriteError, io::ErrorKind)> {
use io::ErrorKind as K;
vec![
(WriteError::Reset(7), K::ConnectionReset),
(WriteError::Finished, K::BrokenPipe),
(ConnectionLost::TimedOut.into(), K::TimedOut),
(ConnectionLost::NonceExhausted.into(), K::BrokenPipe),
(ConnectionLost::LocallyClosed.into(), K::NotConnected),
(
ConnectionLost::PeerClosed {
code: 7,
reason: b"bye".to_vec(),
}
.into(),
K::BrokenPipe,
),
(
ConnectionLost::ProtocolViolation { code: 9 }.into(),
K::BrokenPipe,
),
(ConnectionLost::Replaced.into(), K::BrokenPipe),
(ConnectionLost::EndpointDropped.into(), K::NotConnected),
]
}
#[test]
fn read_error_kinds_match_the_ratified_table() {
for (err, want) in read_table() {
let got = io::Error::from(err.clone());
assert_eq!(
got.kind(),
want,
"§16.11.1 read row for {err:?}: expected {want:?}, got {:?}",
got.kind()
);
}
}
#[test]
fn write_error_kinds_match_the_ratified_table() {
for (err, want) in write_table() {
let got = io::Error::from(err.clone());
assert_eq!(
got.kind(),
want,
"§16.11.1 write row for {err:?}: expected {want:?}, got {:?}",
got.kind()
);
}
}
#[test]
fn the_kind_split_is_by_variant_not_by_direction() {
use io::ErrorKind as K;
let same = [
(ConnectionLost::TimedOut, K::TimedOut),
(ConnectionLost::LocallyClosed, K::NotConnected),
(ConnectionLost::EndpointDropped, K::NotConnected),
];
for (lost, want) in same {
let r = io::Error::from(ReadError::from(lost.clone()));
let w = io::Error::from(WriteError::from(lost.clone()));
assert_eq!(r.kind(), want, "§16.11.1: read {lost:?}");
assert_eq!(
w.kind(),
want,
"§16.11.1: {lost:?} maps identically in both directions — ruling 227 reads \
§16.11's slash as a variant split, so a direction split is wrong here"
);
}
assert_eq!(
io::Error::from(ReadError::Reset(1)).kind(),
K::ConnectionReset
);
assert_eq!(
io::Error::from(WriteError::Reset(1)).kind(),
K::ConnectionReset
);
let differ = [
ConnectionLost::NonceExhausted,
ConnectionLost::PeerClosed {
code: 3,
reason: Vec::new(),
},
ConnectionLost::ProtocolViolation { code: 4 },
ConnectionLost::Replaced,
];
for lost in differ {
let r = io::Error::from(ReadError::from(lost.clone()));
let w = io::Error::from(WriteError::from(lost.clone()));
assert_eq!(r.kind(), K::ConnectionAborted, "§16.11.1: read {lost:?}");
assert_eq!(w.kind(), K::BrokenPipe, "§16.11.1: write {lost:?}");
assert_ne!(
r.kind(),
w.kind(),
"§16.11.1: {lost:?} must differ by direction — a single shared conversion, or \
one `From<ConnectionLost>` both arms delegate to, collapses this row"
);
}
}
#[test]
fn the_inner_read_error_is_preserved_with_its_payload() {
for (err, _) in read_table() {
let io_err = io::Error::from(err.clone());
let inner = io_err
.into_inner()
.unwrap_or_else(|| panic!("§8.3: {err:?} lost its inner error — `io::Error::new` ?"));
let recovered = *inner
.downcast::<ReadError>()
.unwrap_or_else(|_| panic!("§8.3: {err:?} did not downcast back to `ReadError`"));
assert_eq!(
recovered, err,
"§8.3: the payload must survive the round trip"
);
}
let io_err = io::Error::from(ReadError::Reset(u64::MAX));
let inner = *io_err
.into_inner()
.expect("inner error")
.downcast::<ReadError>()
.expect("downcast");
assert_eq!(inner, ReadError::Reset(u64::MAX));
}
#[test]
fn the_inner_write_error_is_preserved_with_its_payload() {
for (err, _) in write_table() {
let io_err = io::Error::from(err.clone());
let inner = io_err
.into_inner()
.unwrap_or_else(|| panic!("§8.3: {err:?} lost its inner error"));
let recovered = *inner
.downcast::<WriteError>()
.unwrap_or_else(|_| panic!("§8.3: {err:?} did not downcast back to `WriteError`"));
assert_eq!(
recovered, err,
"§8.3: the payload must survive the round trip"
);
}
let io_err = io::Error::from(WriteError::Reset(u64::MAX));
let inner = *io_err
.into_inner()
.expect("inner error")
.downcast::<WriteError>()
.expect("downcast");
assert_eq!(inner, WriteError::Reset(u64::MAX));
}
#[test]
fn the_conversions_are_total_and_not_confusable() {
for lost in all_connection_lost() {
let r = io::Error::from(ReadError::from(lost.clone()));
assert!(
r.into_inner()
.expect("inner")
.downcast::<WriteError>()
.is_err(),
"a `ReadError` conversion boxed a `WriteError` for {lost:?}"
);
let w = io::Error::from(WriteError::from(lost.clone()));
assert!(
w.into_inner()
.expect("inner")
.downcast::<ReadError>()
.is_err(),
"a `WriteError` conversion boxed a `ReadError` for {lost:?}"
);
}
assert_eq!(
all_connection_lost().len(),
7,
"§16.11.1 has seven `ConnectionLost` rows"
);
assert_eq!(
read_table().len(),
8,
"§16.11.1's read column has eight rows"
);
assert_eq!(
write_table().len(),
9,
"§16.11.1's write column has nine rows"
);
}
#[tokio::test(start_paused = true)]
async fn poll_flush_is_ready_with_unacknowledged_bytes_outstanding() {
local(async {
let pair = Pair::seeded(0x56);
let (a, _b) = pair.establish().await;
let mut s = within(a.open_uni(), "open_uni").await.expect("open_uni");
let n = match poll_write_once(&mut s, b"unacknowledged") {
Poll::Ready(Ok(n)) => n,
other => panic!("poll_write: {other:?}"),
};
assert_eq!(n, b"unacknowledged".len());
match poll_flush_once(&mut s) {
Poll::Ready(Ok(())) => {}
Poll::Pending => panic!(
"RULING 56 VIOLATED: `poll_flush` parked with bytes unacknowledged, so it \
is a second, weaker `acked()`. `AsyncWrite::flush` is not delivery \
confirmation; `SendStream::acked` is the verb that means acknowledged."
),
Poll::Ready(Err(e)) => panic!("RULING 56 VIOLATED: `poll_flush` failed: {e}"),
}
})
.await;
}
#[tokio::test(start_paused = true)]
async fn poll_flush_is_ready_ok_even_after_the_connection_dies() {
local(async {
let pair = Pair::seeded(0x561);
let (a, _b) = pair.establish().await;
let mut s = within(a.open_uni(), "open_uni").await.expect("open_uni");
assert!(matches!(
poll_write_once(&mut s, b"before the end"),
Poll::Ready(Ok(_))
));
a.close(0, b"").await;
settle().await;
match poll_flush_once(&mut s) {
Poll::Ready(Ok(())) => {}
Poll::Pending => panic!("RULING 56: `poll_flush` parked on a dead connection"),
Poll::Ready(Err(e)) => panic!(
"RULING 56 / CONTRACT-8 §3.1: `poll_flush` is `Ready(Ok(()))` \
*unconditionally* and touches neither the cell nor the core, so it \
cannot observe the death — got {e} ({:?})",
e.kind()
),
}
})
.await;
}
#[tokio::test(start_paused = true)]
async fn poll_shutdown_waits_for_the_acknowledgement_not_just_the_fin() {
local(async {
let pair = Pair::seeded(0x57);
let (a, b) = pair.establish().await;
let mut s = within(a.open_uni(), "open_uni").await.expect("open_uni");
let payload = b"the tail of the transfer";
assert_eq!(
match poll_write_once(&mut s, payload) {
Poll::Ready(Ok(n)) => n,
other => panic!("poll_write: {other:?}"),
},
payload.len()
);
match poll_shutdown_once(&mut s) {
Poll::Pending => {}
Poll::Ready(Ok(())) => panic!(
"RULING 57 VIOLATED: `poll_shutdown` resolved before the peer could \
acknowledge — it is `finish()` alone. `copy(..).await; shutdown().await` \
then loses its tail at the path's loss rate, silently (S28)."
),
Poll::Ready(Err(e)) => panic!("poll_shutdown failed early: {e}"),
}
let mut r = within(b.accept_uni(), "accept_uni")
.await
.expect("accept_uni");
within(s.shutdown(), "shutdown")
.await
.expect("shutdown resolved Ok");
let mut got = Vec::new();
within(r.read_to_end(&mut got), "read_to_end")
.await
.expect("read_to_end");
assert_eq!(
got.as_slice(),
payload,
"the tail must survive the shutdown that waited for it"
);
})
.await;
}
#[tokio::test(start_paused = true)]
async fn poll_shutdown_resolves_in_error_when_the_connection_dies() {
local(async {
let pair = Pair::seeded(0x571);
let (a, b) = pair.establish().await;
let mut s = within(a.open_uni(), "open_uni").await.expect("open_uni");
assert!(matches!(
poll_write_once(&mut s, b"never acknowledged"),
Poll::Ready(Ok(_))
));
assert!(
matches!(poll_shutdown_once(&mut s), Poll::Pending),
"precondition: the shutdown is waiting for an acknowledgement"
);
b.close(7, b"bye").await;
settle().await;
let e = within(s.shutdown(), "shutdown after the connection died")
.await
.expect_err("RULING 57: a dying connection must resolve the wait in error");
assert_eq!(
e.kind(),
io::ErrorKind::BrokenPipe,
"§16.11.1: `ConnectionLost(PeerClosed)` on the write side is `BrokenPipe` — \
a peer that went away under a writer"
);
})
.await;
}
#[tokio::test(start_paused = true)]
async fn poll_shutdown_is_idempotent_across_repeated_polls() {
local(async {
let pair = Pair::seeded(0x572);
let (a, b) = pair.establish().await;
let mut s = within(a.open_uni(), "open_uni").await.expect("open_uni");
assert!(matches!(
poll_write_once(&mut s, b"polled repeatedly"),
Poll::Ready(Ok(_))
));
for i in 0..5 {
match poll_shutdown_once(&mut s) {
Poll::Pending => {}
Poll::Ready(Ok(())) => panic!(
"poll {i}: resolved before any driver turn — ruling 57's `acked()` half \
is missing"
),
Poll::Ready(Err(e)) => panic!(
"poll {i}: CONTRACT-8 §3.1 — re-entering `poll_shutdown` calls \
`poll_finish` again *harmlessly*, because a second `finish()` is \
`Ok(())`. Got {e} ({:?}).",
e.kind()
),
}
}
let mut r = within(b.accept_uni(), "accept_uni")
.await
.expect("accept_uni");
within(s.shutdown(), "shutdown")
.await
.expect("shutdown resolved Ok");
let mut got = Vec::new();
within(r.read_to_end(&mut got), "read_to_end")
.await
.expect("read_to_end");
assert_eq!(got.as_slice(), b"polled repeatedly");
})
.await;
}
#[tokio::test(start_paused = true)]
async fn bistream_shutdown_closes_only_the_send_half() {
local(async {
let pair = Pair::seeded(0x573);
let (a, b) = pair.establish().await;
let request = b"GET /".as_slice();
let response = b"200 OK, after the requester half-closed".as_slice();
let mut ab = within(a.open_bi(), "open_bi").await.expect("open_bi");
within(ab.write_all(request), "write request")
.await
.expect("write_all");
let mut ba = within(b.accept_bi(), "accept_bi").await.expect("accept_bi");
within(ab.shutdown(), "requester shutdown")
.await
.expect("shutdown");
let mut got = Vec::new();
within(ba.read_to_end(&mut got), "responder read_to_end")
.await
.expect("read_to_end");
assert_eq!(got.as_slice(), request);
within(ba.write_all(response), "write response")
.await
.expect("write_all");
within(ba.shutdown(), "responder shutdown")
.await
.expect("shutdown");
let mut back = Vec::new();
within(ab.read_to_end(&mut back), "requester read_to_end")
.await
.expect(
"CONTRACT-8 §3.3: `poll_shutdown` on a `BiStream` shuts down the send half \
only; the receive half must survive it",
);
assert_eq!(
back.as_slice(),
response,
"the receive half was touched by the send half's shutdown — a half-closed \
`BiStream` is the shape every request/response protocol relies on"
);
})
.await;
}
fn assert_never_ends<T, S>(s: &mut S, what: &str)
where
S: Stream<Item = Result<T, ConnectionLost>> + Unpin,
{
let mut lost: Option<ConnectionLost> = None;
for _ in 0..8 {
match poll_stream_once(s) {
Poll::Ready(Some(Err(e))) => {
lost = Some(e);
break;
}
Poll::Ready(Some(Ok(_))) => continue,
Poll::Ready(None) => panic!(
"{what}: RULING 226 VIOLATED — yielded `None`. The `Result`-carrying faces \
never end; `None` destroys the death *reason*, which is the only thing a \
consumer draining this face ever learns about why it stopped."
),
Poll::Pending => panic!("{what}: parked on a dead connection"),
}
}
let lost = lost.unwrap_or_else(|| panic!("{what}: never reported the death at all"));
for i in 0..8 {
match poll_stream_once(s) {
Poll::Ready(Some(Err(e))) => assert_eq!(
e, lost,
"{what}: poll {i} reported a different death — `ConnectionLost` is `Clone` \
so that the same value is re-reported indefinitely"
),
Poll::Ready(None) => panic!(
"{what}: RULING 226 VIOLATED — ended at repeat poll {i}. This is the \
`Some(Err(..))`-once-then-`None` build: it passes any single-poll test."
),
Poll::Ready(Some(Ok(_))) => panic!("{what}: yielded an item after the death"),
Poll::Pending => panic!("{what}: parked at repeat poll {i} on a dead connection"),
}
}
}
#[tokio::test(start_paused = true)]
async fn the_result_carrying_faces_never_end() {
local(async {
let pair = Pair::seeded(0x226);
let (a, _b) = pair.establish().await;
a.close(0, b"").await;
settle().await;
let mut messages = a.messages();
assert_never_ends(&mut messages, "messages()");
let mut datagrams = a.datagrams();
assert_never_ends(&mut datagrams, "datagrams()");
let mut incoming_bi = a.incoming_bi();
assert_never_ends(&mut incoming_bi, "incoming_bi()");
let mut notifications = a.notifications();
assert_never_ends(&mut notifications, "notifications()");
})
.await;
}
#[tokio::test(start_paused = true)]
async fn incoming_uni_never_ends() {
local(async {
let pair = Pair::seeded(0x2261);
let (a, _b) = pair.establish().await;
a.close(0, b"").await;
settle().await;
let mut incoming_uni = a.incoming_uni();
assert_never_ends(&mut incoming_uni, "incoming_uni()");
})
.await;
}
#[allow(dead_code)]
mod adapter_item_types {
use super::*;
use slither::packet::Handshake;
fn is_stream_of<I, S: Stream<Item = I>>(_: S) {}
fn messages<S: Handshake>(m: slither::compat::Messages<'_, S>) {
is_stream_of::<Result<Vec<u8>, ConnectionLost>, _>(m);
}
fn datagrams<S: Handshake>(d: slither::compat::Datagrams<'_, S>) {
is_stream_of::<Result<Vec<u8>, ConnectionLost>, _>(d);
}
fn incoming_bi<S: Handshake>(i: slither::compat::IncomingBi<'_, S>) {
is_stream_of::<Result<slither::shell::BiStream<S>, ConnectionLost>, _>(i);
}
fn incoming_uni<S: Handshake>(i: slither::compat::IncomingUni<'_, S>) {
is_stream_of::<Result<slither::shell::RecvStream<S>, ConnectionLost>, _>(i);
}
fn notifications<S: Handshake>(n: slither::compat::Notifications<'_, S>) {
is_stream_of::<Result<Notification, ConnectionLost>, _>(n);
}
fn incoming<I: Identity>(i: slither::compat::Incoming<'_, I>) {
is_stream_of::<Intro<I>, _>(i);
}
}
async fn uni_with_data(
conn_a: &slither::testutil::TestConnection,
conn_b: &slither::testutil::TestConnection,
data: &[u8],
) -> (
slither::testutil::TestSendStream,
slither::testutil::TestRecvStream,
) {
let mut s = within(conn_a.open_uni(), "open_uni")
.await
.expect("open_uni");
within(s.write_all(data), "write_all")
.await
.expect("write_all");
let r = within(conn_b.accept_uni(), "accept_uni")
.await
.expect("accept_uni");
for _ in 0..4 {
settle().await;
}
(s, r)
}
#[tokio::test(start_paused = true)]
async fn an_empty_read_buf_neither_consumes_nor_latches_end_of_file() {
local(async {
let pair = Pair::seeded(0x119);
let (a, b) = pair.establish().await;
let payload = b"still here after the empty read".as_slice();
let (_s, mut r) = uni_with_data(&a, &b, payload).await;
let mut nothing: [u8; 0] = [];
let mut empty = ReadBuf::new(&mut nothing);
match poll_read_once(&mut r, &mut empty) {
Poll::Ready(Ok(())) => assert_eq!(
empty.filled().len(),
0,
"an empty `ReadBuf` cannot have been filled"
),
Poll::Pending => panic!(
"CONTRACT-8 §3.2: an empty `buf` must short-circuit to `Ready`, never park — \
no arrival of data can unblock a reader with nowhere to put it (ruling 119, \
mirroring ruling 110)"
),
Poll::Ready(Err(e)) => panic!("empty read failed: {e}"),
}
let mut store = [0u8; 128];
match read_into(&mut r, &mut store) {
Poll::Ready(Ok(0)) => panic!(
"RULING 119 VIOLATED: the empty read was taken for end-of-stream. \
`Ok(Some(0))` means *park*; `Ok(None)` means end of stream. Collapsing \
them loses every byte still queued, and reports it as a clean EOF."
),
Poll::Ready(Ok(n)) => assert_eq!(
&store[..n],
payload,
"the empty read consumed part of the payload"
),
Poll::Pending => panic!("the payload was delivered; the read must not park"),
Poll::Ready(Err(e)) => panic!("read failed: {e}"),
}
})
.await;
}
#[tokio::test(start_paused = true)]
async fn a_drained_but_open_stream_parks_and_does_not_report_end_of_file() {
local(async {
let pair = Pair::seeded(0x1191);
let (a, b) = pair.establish().await;
let payload = b"drain me".as_slice();
let (_s, mut r) = uni_with_data(&a, &b, payload).await;
let mut store = [0u8; 128];
match read_into(&mut r, &mut store) {
Poll::Ready(Ok(n)) => assert_eq!(&store[..n], payload),
other => panic!("first read: {other:?}"),
}
let mut nothing: [u8; 0] = [];
let mut empty = ReadBuf::new(&mut nothing);
assert!(
matches!(poll_read_once(&mut r, &mut empty), Poll::Ready(Ok(()))),
"CONTRACT-8 §3.2: an empty `buf` short-circuits to `Ready`"
);
match read_into(&mut r, &mut store) {
Poll::Pending => {}
Poll::Ready(Ok(0)) => panic!(
"RULING 119 VIOLATED: a live, drained stream reported end-of-file. \
`read_to_end` returns *successfully* under this build, so a truncated \
transfer is presented as a complete one."
),
Poll::Ready(Ok(n)) => panic!("the stream was drained; got {n} more bytes"),
Poll::Ready(Err(e)) => panic!("read failed: {e}"),
}
})
.await;
}
#[tokio::test(start_paused = true)]
async fn end_of_stream_is_a_sticky_zero_fill_and_survives_the_connection_death() {
local(async {
let pair = Pair::seeded(0x121);
let (a, b) = pair.establish().await;
let payload = b"complete transfer".as_slice();
let mut s = within(a.open_uni(), "open_uni").await.expect("open_uni");
within(s.write_all(payload), "write_all")
.await
.expect("write_all");
within(s.finish(), "finish").await.expect("finish");
let mut r = within(b.accept_uni(), "accept_uni")
.await
.expect("accept_uni");
let mut got = Vec::new();
within(r.read_to_end(&mut got), "read_to_end")
.await
.expect("read_to_end");
assert_eq!(got.as_slice(), payload);
let mut store = [0u8; 64];
for i in 0..4 {
match read_into(&mut r, &mut store) {
Poll::Ready(Ok(0)) => {}
Poll::Ready(Ok(n)) => panic!("read {i}: {n} bytes past end-of-stream"),
Poll::Pending => panic!(
"read {i}: parked past end-of-stream — the EOF latch is not sticky, and \
`read_to_end` hangs for ever under this build"
),
Poll::Ready(Err(e)) => panic!("read {i}: end-of-stream reported as {e}"),
}
}
a.close(0, b"").await;
settle().await;
match read_into(&mut r, &mut store) {
Poll::Ready(Ok(0)) => {}
Poll::Ready(Err(e)) => panic!(
"SPEC.md:4805 — the stream's end-of-file latch sits *ahead of* the \
connection's death latch, so a connection dying after the FIN must not \
surface a spurious `io::Error` to `read_to_end`. Got {e} ({:?}).",
e.kind()
),
other => panic!("read after death: {other:?}"),
}
})
.await;
}
#[tokio::test(start_paused = true)]
async fn an_empty_write_is_ok_zero_and_a_blocked_write_parks() {
local(async {
let pair = Pair::seeded(0x110);
let (a, _b) = pair.establish().await;
let mut s = within(a.open_uni(), "open_uni").await.expect("open_uni");
match poll_write_once(&mut s, &[]) {
Poll::Ready(Ok(0)) => {}
Poll::Ready(Ok(n)) => panic!("a zero-length write reported {n} bytes"),
Poll::Pending => panic!(
"RULING 110 VIOLATED: an empty write parked. No credit arrival can ever \
unblock a writer with nothing to send, so this hangs for ever."
),
Poll::Ready(Err(e)) => panic!(
"RULING 110 VIOLATED: an empty write failed with {e} ({:?}). Ordinary \
`tokio::io` combinators hand `AsyncWrite` empty buffers.",
e.kind()
),
}
let chunk = vec![0xA5u8; 1024];
let rounds = (INITIAL_MAX_STREAM_DATA as usize / chunk.len()) + 64;
let mut accepted = 0usize;
let mut parked = false;
for _ in 0..rounds {
match poll_write_once(&mut s, &chunk) {
Poll::Ready(Ok(0)) => panic!(
"CONTRACT-8 §3.2 / ruling 110: `Ok(0)` for a **non-empty** buffer. A \
blocked write parks; `Ok(0)` here makes `write_all` fail with \
`ErrorKind::WriteZero` instead of waiting for credit."
),
Poll::Ready(Ok(n)) => accepted += n,
Poll::Pending => {
parked = true;
break;
}
Poll::Ready(Err(e)) => panic!("write failed: {e}"),
}
}
assert!(
parked,
"the writer never parked after {accepted} B with no driver turn to grant more \
credit — a build that grants credit for ever passes every upper-bound test"
);
assert!(accepted > 0, "no bytes were accepted at all");
})
.await;
}
#[tokio::test(start_paused = true)]
async fn message_sink_poll_close_does_not_close_the_connection() {
local(async {
let pair = Pair::seeded(0x44);
let (a, b) = pair.establish().await;
{
let mut sink = a.message_sink();
assert!(
matches!(
Pin::new(&mut sink).poll_ready(&mut noop_context()),
Poll::Ready(Ok(()))
),
"a fresh `MessageSink` has an empty slot, so `poll_ready` is `Ready`"
);
Pin::new(&mut sink)
.start_send(b"through the sink".to_vec())
.expect("start_send");
within(
std::future::poll_fn(|cx| Pin::new(&mut sink).poll_close(cx)),
"poll_close",
)
.await
.expect("poll_close");
}
settle().await;
let first = within(b.recv_message(), "recv_message")
.await
.expect("recv_message");
assert_eq!(first.as_slice(), b"through the sink");
within(
a.send_message(b"the connection is still up"),
"send_message",
)
.await
.expect(
"CONTRACT-8 §4.4 / §9.12: `Sink::poll_close` must not close the connection — \
a sink is one face of a multiplexer",
);
let second = within(b.recv_message(), "recv_message")
.await
.expect("recv_message");
assert_eq!(second.as_slice(), b"the connection is still up");
})
.await;
}
#[tokio::test(start_paused = true)]
async fn datagram_sink_buffers_nothing_and_is_always_ready() {
local(async {
let pair = Pair::seeded(0x441);
let (a, b) = pair.establish().await;
let mut sink = a.datagram_sink();
assert!(matches!(
Pin::new(&mut sink).poll_ready(&mut noop_context()),
Poll::Ready(Ok(()))
));
Pin::new(&mut sink)
.start_send(b"unbuffered".to_vec())
.expect("start_send");
assert!(
matches!(
Pin::new(&mut sink).poll_ready(&mut noop_context()),
Poll::Ready(Ok(()))
),
"CONTRACT-8 §4.4: `DatagramSink` buffers nothing, so `poll_ready` is `Ready` \
immediately after a `start_send` — a one-item slot copied from `MessageSink` \
parks here"
);
assert!(matches!(
Pin::new(&mut sink).poll_flush(&mut noop_context()),
Poll::Ready(Ok(()))
));
settle().await;
let got = within(b.recv_datagram(), "recv_datagram")
.await
.expect("recv_datagram");
assert_eq!(got.as_slice(), b"unbuffered");
a.close(0, b"").await;
settle().await;
assert!(
matches!(
Pin::new(&mut sink).poll_ready(&mut noop_context()),
Poll::Ready(Ok(()))
),
"CONTRACT-8 §4.4: `poll_ready` is `Ready(Ok(()))` **always** — it ignores its \
`Context` entirely and never consults the connection"
);
let err = Pin::new(&mut sink)
.start_send(b"after the end".to_vec())
.expect_err("the death is reported by `start_send`, which is `send_datagram`");
assert!(
matches!(err, DatagramError::ConnectionLost(_)),
"expected `ConnectionLost`, got {err:?}"
);
})
.await;
}
fn compat_sources() -> Vec<(String, String)> {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/compat");
let mut out = Vec::new();
let mut stack = vec![root.clone()];
while let Some(dir) = stack.pop() {
let entries = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", dir.display()));
for entry in entries {
let path = entry.expect("directory entry").path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "rs") {
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
let code: String = text
.lines()
.map(|line| match line.find("//") {
Some(i) => &line[..i],
None => line,
})
.collect::<Vec<_>>()
.join("\n");
out.push((path.display().to_string(), code));
}
}
}
assert!(
!out.is_empty(),
"no sources found under src/compat — this suite pins that module"
);
out
}
#[test]
fn compat_source_scan() {
const BANNED: &[(&str, &str)] = &[
(
"unreachable!",
"ruling 227 and CONTRACT-8 §3.2 both forbid it: the `#[non_exhaustive]` \
fallback maps to `ErrorKind::Other`, and the `Ok(Some(0))` row maps to a \
zero fill. Neither is a panic.",
),
(
"todo!",
"an unfinished arm in a shipped conversion is a panic in a consumer's process",
),
(
"unimplemented!",
"same as `todo!`: CONTRACT-8 §8.2's fallback arm is `ErrorKind::Other`",
),
(
"VecDeque",
"CONTRACT-8 §9.1 / ruling 58: no intermediate queue. The single-item \
`MessageSink` slot is the only buffer permitted in `compat/`, and it exists \
because `Sink`'s own protocol requires it on the *send* side.",
),
(
"tokio::spawn",
"CONTRACT-8 §9.6 / S21: the driver is `!Send` by requirement. `spawn_local` \
is the substitute.",
),
];
let mut findings = Vec::new();
for (path, code) in compat_sources() {
for (needle, why) in BANNED {
if code.contains(needle) {
findings.push(format!("{path}: `{needle}` — {why}"));
}
}
}
assert!(
findings.is_empty(),
"CONTRACT-8 §9's checklist is violated:\n {}",
findings.join("\n ")
);
}