#![cfg(feature = "connector-session")]
use std::sync::{Arc, Mutex};
use std::time::Duration;
use futures::StreamExt;
use aimdb_core::session::{
run_client, serve, AuthError, BoxFut, BoxStream, ClientConfig, CodecError, Connection, Dialer,
Dispatch, EnvelopeCodec, Inbound, Listener, OneShotDialer, OneShotListener, Outbound, Payload,
PeerInfo, RpcError, Session, SessionConfig, SessionCtx, SessionLimits, SubUpdate,
TransportError, TransportResult,
};
use aimdb_core::executor::test_support::NoopRuntimeOps as TestClock;
struct ChannelConn {
tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
rx: tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>,
peer: PeerInfo,
}
fn conn_pair() -> (ChannelConn, ChannelConn) {
let (a_tx, a_rx) = tokio::sync::mpsc::unbounded_channel();
let (b_tx, b_rx) = tokio::sync::mpsc::unbounded_channel();
(
ChannelConn {
tx: a_tx,
rx: b_rx,
peer: PeerInfo::default(),
},
ChannelConn {
tx: b_tx,
rx: a_rx,
peer: PeerInfo::default(),
},
)
}
impl Connection for ChannelConn {
fn recv(&mut self) -> BoxFut<'_, TransportResult<Option<Vec<u8>>>> {
Box::pin(async move { Ok(self.rx.recv().await) }) }
fn send<'a>(&'a mut self, frame: &'a [u8]) -> BoxFut<'a, TransportResult<()>> {
let tx = self.tx.clone();
let bytes = frame.to_vec();
Box::pin(async move { tx.send(bytes).map_err(|_| TransportError::Closed) })
}
fn peer(&self) -> &PeerInfo {
&self.peer
}
}
struct ChannelListener {
incoming: tokio::sync::mpsc::UnboundedReceiver<Box<dyn Connection>>,
}
impl Listener for ChannelListener {
fn accept(&mut self) -> BoxFut<'_, TransportResult<Box<dyn Connection>>> {
Box::pin(async move { self.incoming.recv().await.ok_or(TransportError::Closed) })
}
}
struct ChannelDialer {
connect_tx: tokio::sync::mpsc::UnboundedSender<Box<dyn Connection>>,
}
impl Dialer for ChannelDialer {
fn connect(&self) -> BoxFut<'_, TransportResult<Box<dyn Connection>>> {
let connect_tx = self.connect_tx.clone();
Box::pin(async move {
let (server_side, client_side) = conn_pair();
connect_tx
.send(Box::new(server_side) as Box<dyn Connection>)
.map_err(|_| TransportError::Closed)?;
Ok(Box::new(client_side) as Box<dyn Connection>)
})
}
}
fn transport_pair() -> (ChannelListener, ChannelDialer) {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
(
ChannelListener { incoming: rx },
ChannelDialer { connect_tx: tx },
)
}
struct LineCodec;
fn payload_from(s: &str) -> Payload {
Arc::from(s.as_bytes())
}
fn utf8(b: &[u8]) -> Result<&str, CodecError> {
std::str::from_utf8(b).map_err(|_| CodecError::Malformed)
}
fn rpc_code(e: &RpcError) -> &'static str {
match e {
RpcError::NotFound => "notfound",
RpcError::Denied => "denied",
_ => "internal",
}
}
fn code_rpc(s: &str) -> RpcError {
match s {
"notfound" => RpcError::NotFound,
"denied" => RpcError::Denied,
_ => RpcError::Internal,
}
}
impl EnvelopeCodec for LineCodec {
fn decode(&self, frame: &[u8]) -> Result<Inbound, CodecError> {
let s = utf8(frame)?;
let (tag, rest) = s.split_once('\n').unwrap_or((s, ""));
match tag {
"REQ" => {
let (id, r) = rest.split_once('\n').ok_or(CodecError::Malformed)?;
let (method, params) = r.split_once('\n').unwrap_or((r, ""));
Ok(Inbound::Request {
id: id.parse().map_err(|_| CodecError::Malformed)?,
method: method.to_string(),
params: payload_from(params),
})
}
"SUB" => {
let (id, topic) = rest.split_once('\n').ok_or(CodecError::Malformed)?;
Ok(Inbound::Subscribe {
id: id.parse().map_err(|_| CodecError::Malformed)?,
topic: topic.to_string(),
})
}
"UNSUB" => Ok(Inbound::Unsubscribe {
sub: rest.to_string(),
}),
"WRITE" => {
let (topic, payload) = rest.split_once('\n').unwrap_or((rest, ""));
Ok(Inbound::Write {
topic: topic.to_string(),
payload: payload_from(payload),
})
}
"PING" => Ok(Inbound::Ping),
_ => Err(CodecError::Malformed),
}
}
fn encode(&self, msg: Outbound<'_>, out: &mut Vec<u8>) -> Result<(), CodecError> {
let s = match msg {
Outbound::Reply { id, result } => match result {
Ok(data) => format!("REPLY\n{}\nOK\n{}", id, utf8(&data)?),
Err(e) => format!("REPLY\n{}\nERR\n{}", id, rpc_code(&e)),
},
Outbound::Event {
sub,
seq,
topic,
data,
} => {
format!(
"EVENT\n{}\n{}\n{}\n{}",
sub,
seq,
topic.unwrap_or("-"),
utf8(&data)?
)
}
Outbound::Snapshot {
sub,
seq,
last,
topic,
data,
} => {
format!(
"SNAP\n{}\n{}\n{}\n{}\n{}",
sub,
seq,
if last { "L" } else { "-" },
topic,
utf8(&data)?
)
}
Outbound::Pong => "PONG".to_string(),
Outbound::Subscribed { sub } => format!("SUBSCRIBED\n{}", sub),
};
out.extend_from_slice(s.as_bytes());
Ok(())
}
fn encode_inbound(&self, msg: Inbound, out: &mut Vec<u8>) -> Result<(), CodecError> {
let s = match msg {
Inbound::Request { id, method, params } => {
format!("REQ\n{}\n{}\n{}", id, method, utf8(¶ms)?)
}
Inbound::Subscribe { id, topic } => format!("SUB\n{}\n{}", id, topic),
Inbound::Unsubscribe { sub } => format!("UNSUB\n{}", sub),
Inbound::Write { topic, payload } => format!("WRITE\n{}\n{}", topic, utf8(&payload)?),
Inbound::Ping => "PING".to_string(),
};
out.extend_from_slice(s.as_bytes());
Ok(())
}
fn decode_outbound<'a>(&self, frame: &'a [u8]) -> Result<Outbound<'a>, CodecError> {
let s = utf8(frame)?;
let (tag, rest) = s.split_once('\n').unwrap_or((s, ""));
match tag {
"REPLY" => {
let (id, r) = rest.split_once('\n').ok_or(CodecError::Malformed)?;
let (kind, tail) = r.split_once('\n').unwrap_or((r, ""));
let result = match kind {
"OK" => Ok(payload_from(tail)),
"ERR" => Err(code_rpc(tail)),
_ => return Err(CodecError::Malformed),
};
Ok(Outbound::Reply {
id: id.parse().map_err(|_| CodecError::Malformed)?,
result,
})
}
"EVENT" => {
let (sub, r) = rest.split_once('\n').ok_or(CodecError::Malformed)?;
let (seq, r) = r.split_once('\n').ok_or(CodecError::Malformed)?;
let (topic, data) = r.split_once('\n').unwrap_or((r, ""));
Ok(Outbound::Event {
sub,
seq: seq.parse().map_err(|_| CodecError::Malformed)?,
topic: (topic != "-").then_some(topic),
data: payload_from(data),
})
}
"SNAP" => {
let (sub, r) = rest.split_once('\n').ok_or(CodecError::Malformed)?;
let (seq, r) = r.split_once('\n').ok_or(CodecError::Malformed)?;
let (last, r) = r.split_once('\n').ok_or(CodecError::Malformed)?;
let (topic, data) = r.split_once('\n').unwrap_or((r, ""));
Ok(Outbound::Snapshot {
sub,
seq: seq.parse().map_err(|_| CodecError::Malformed)?,
last: last == "L",
topic,
data: payload_from(data),
})
}
"PONG" => Ok(Outbound::Pong),
_ => Err(CodecError::Malformed),
}
}
}
type WriteLog = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
struct EchoDispatch {
writes: WriteLog,
}
impl Dispatch for EchoDispatch {
fn authenticate<'a>(
&'a self,
_peer: &'a PeerInfo,
_first: Option<&'a [u8]>,
) -> BoxFut<'a, Result<SessionCtx, AuthError>> {
Box::pin(async { Ok(SessionCtx::default()) })
}
fn open(&self, _ctx: &SessionCtx) -> Box<dyn Session> {
Box::new(EchoSession {
writes: self.writes.clone(),
})
}
}
struct EchoSession {
writes: WriteLog,
}
impl Session for EchoSession {
fn call<'a>(
&'a mut self,
_method: &'a str,
params: Payload,
) -> BoxFut<'a, Result<Payload, RpcError>> {
Box::pin(async move { Ok(params) })
}
fn subscribe<'a>(
&'a mut self,
topic: &'a str,
) -> BoxFut<'a, Result<BoxStream<'static, SubUpdate>, RpcError>> {
let topic = topic.to_string();
Box::pin(async move {
if topic == "bad" {
return Err(RpcError::NotFound);
}
let items: Vec<SubUpdate> = (1..=3)
.map(|i| SubUpdate::new(payload_from(&format!("{topic}#{i}"))))
.collect();
Ok(Box::pin(futures::stream::iter(items)) as BoxStream<'static, SubUpdate>)
})
}
fn write<'a>(
&'a mut self,
topic: &'a str,
payload: Payload,
) -> BoxFut<'a, Result<(), RpcError>> {
let writes = self.writes.clone();
let topic = topic.to_string();
Box::pin(async move {
writes.lock().unwrap().push((topic, payload.to_vec()));
Ok(())
})
}
}
const SNAPSHOT_RECORDS: usize = 300;
type EventFeed = Arc<Mutex<Option<tokio::sync::mpsc::UnboundedReceiver<SubUpdate>>>>;
struct SnapshotDispatch {
feed: EventFeed,
}
impl Dispatch for SnapshotDispatch {
fn authenticate<'a>(
&'a self,
_peer: &'a PeerInfo,
_first: Option<&'a [u8]>,
) -> BoxFut<'a, Result<SessionCtx, AuthError>> {
Box::pin(async { Ok(SessionCtx::default()) })
}
fn open(&self, _ctx: &SessionCtx) -> Box<dyn Session> {
Box::new(SnapshotSession {
feed: self.feed.clone(),
})
}
}
struct SnapshotSession {
feed: EventFeed,
}
impl Session for SnapshotSession {
fn call<'a>(
&'a mut self,
_method: &'a str,
params: Payload,
) -> BoxFut<'a, Result<Payload, RpcError>> {
Box::pin(async move { Ok(params) })
}
fn snapshots(&mut self, _topic: &str) -> Vec<(String, Payload)> {
(1..=SNAPSHOT_RECORDS)
.map(|i| (format!("rec.{i}"), payload_from(&format!("snap#{i}"))))
.collect()
}
fn subscribe<'a>(
&'a mut self,
_topic: &'a str,
) -> BoxFut<'a, Result<BoxStream<'static, SubUpdate>, RpcError>> {
let feed = self.feed.clone();
Box::pin(async move {
let rx = feed.lock().unwrap().take().ok_or(RpcError::Internal)?;
let stream =
futures::stream::unfold(
rx,
|mut rx| async move { rx.recv().await.map(|u| (u, rx)) },
);
Ok(Box::pin(stream) as BoxStream<'static, SubUpdate>)
})
}
fn write<'a>(
&'a mut self,
_topic: &'a str,
_payload: Payload,
) -> BoxFut<'a, Result<(), RpcError>> {
Box::pin(async { Ok(()) })
}
}
#[tokio::test]
async fn echo_roundtrip_rpc_streaming_and_write() {
let (listener, dialer) = transport_pair();
let writes = Arc::new(Mutex::new(Vec::new()));
let dispatch = Arc::new(EchoDispatch {
writes: writes.clone(),
});
let server = tokio::spawn(serve(
listener,
Arc::new(LineCodec),
dispatch,
SessionConfig::default(),
));
let (handle, client_fut) = run_client(
dialer,
LineCodec,
ClientConfig {
reconnect: false,
reconnect_delay: 10,
max_reconnect_delay: 10,
max_reconnect_attempts: 0,
keepalive_interval: None,
max_offline_queue: 1024,
sends_hello: true,
},
Arc::new(TestClock),
);
let client = tokio::spawn(client_fut);
let reply = handle.call("echo", payload_from("hello")).await.unwrap();
assert_eq!(&*reply, b"hello", "RPC reply should echo the params");
let mut stream = handle.subscribe("temp").unwrap();
let e1 = stream.next().await.expect("event 1").expect("ok event 1");
let e2 = stream.next().await.expect("event 2").expect("ok event 2");
let e3 = stream.next().await.expect("event 3").expect("ok event 3");
assert_eq!(&*e1.data, b"temp#1");
assert_eq!(&*e2.data, b"temp#2");
assert_eq!(&*e3.data, b"temp#3");
handle.write("room", payload_from("on")).unwrap();
let _ = handle.call("noop", payload_from("x")).await.unwrap();
let got = writes.lock().unwrap().clone();
assert_eq!(
got,
vec![("room".to_string(), b"on".to_vec())],
"server should have received the write"
);
drop(handle);
drop(stream);
client
.await
.expect("client engine should stop cleanly when handles drop");
server.abort();
}
#[tokio::test]
async fn failed_subscribe_ends_stream_via_ack() {
let (listener, dialer) = transport_pair();
let dispatch = Arc::new(EchoDispatch {
writes: Arc::new(Mutex::new(Vec::new())),
});
let server = tokio::spawn(serve(
listener,
Arc::new(LineCodec),
dispatch,
SessionConfig::default(),
));
let (handle, client_fut) = run_client(
dialer,
LineCodec,
ClientConfig {
reconnect: false,
reconnect_delay: 10,
max_reconnect_delay: 10,
max_reconnect_attempts: 0,
keepalive_interval: None,
max_offline_queue: 1024,
sends_hello: false,
},
Arc::new(TestClock),
);
let client = tokio::spawn(client_fut);
let mut stream = handle.subscribe("bad").unwrap();
let rejected = tokio::time::timeout(Duration::from_secs(2), stream.next())
.await
.expect("failed subscribe must yield an item, not hang")
.expect("rejected subscribe should yield a terminal item");
assert_eq!(
rejected.unwrap_err(),
RpcError::NotFound,
"rejection should carry the server's error code"
);
let ended = tokio::time::timeout(Duration::from_secs(2), stream.next())
.await
.expect("stream must end after the rejection, not hang");
assert!(
ended.is_none(),
"stream should end after the terminal error"
);
drop(handle);
drop(stream);
let _ = client.await;
server.abort();
}
#[tokio::test]
async fn oversized_snapshot_burst_completes_without_a_live_event() {
let (listener, dialer) = transport_pair();
let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel::<SubUpdate>();
let dispatch = Arc::new(SnapshotDispatch {
feed: Arc::new(Mutex::new(Some(event_rx))),
});
let server = tokio::spawn(serve(
listener,
Arc::new(LineCodec),
dispatch,
SessionConfig::default(),
));
let (handle, client_fut) = run_client(
dialer,
LineCodec,
ClientConfig {
reconnect: false,
reconnect_delay: 10,
max_reconnect_delay: 10,
max_reconnect_attempts: 0,
keepalive_interval: None,
max_offline_queue: 1024,
sends_hello: false,
},
Arc::new(TestClock),
);
let client = tokio::spawn(client_fut);
let mut stream = handle.subscribe("rec.#").unwrap();
let reply = handle.call("echo", payload_from("barrier")).await.unwrap();
assert_eq!(&*reply, b"barrier");
let mut delivered = Vec::new();
while let Ok(next) = tokio::time::timeout(Duration::from_millis(50), stream.next()).await {
let item = next.expect("stream must stay open while the subscription lives");
delivered.push(item.expect("a snapshot burst must not be a terminal error"));
}
assert!(
delivered.len() < SNAPSHOT_RECORDS,
"burst must overrun the sink for this regression to mean anything; \
all {} snapshots fit — raise SNAPSHOT_RECORDS above SUBSCRIBE_CHANNEL_CAP",
delivered.len()
);
let (end_idx, end) = delivered
.iter()
.enumerate()
.find(|(_, u)| u.snapshot_end)
.expect(
"the burst must deliver its final snapshot even when it overran the sink — \
otherwise a static subscription can never tell truncated from complete",
);
assert_eq!(
end_idx,
delivered.len() - 1,
"the snapshot_end update must be the last one delivered"
);
assert_eq!(
delivered.iter().filter(|u| u.snapshot_end).count(),
1,
"exactly one update may close the burst"
);
assert_eq!(
end.topic.as_deref(),
Some(format!("rec.{SNAPSHOT_RECORDS}").as_str())
);
assert_eq!(&*end.data, format!("snap#{SNAPSHOT_RECORDS}").as_bytes());
for (i, update) in delivered[..end_idx].iter().enumerate() {
assert_eq!(
update.topic.as_deref(),
Some(format!("rec.{}", i + 1).as_str()),
"snapshot {i} should carry its concrete record topic, in burst order"
);
assert_eq!(&*update.data, format!("snap#{}", i + 1).as_bytes());
assert_eq!(
update.skipped, 0,
"the delivered head of the burst is contiguous"
);
}
assert!(
end.skipped > 0,
"this burst overran the sink, so the closing update must report the loss"
);
let reported: u64 = delivered.iter().map(|u| u.skipped).sum();
assert_eq!(
delivered.len() as u64 + reported,
SNAPSHOT_RECORDS as u64,
"every matched record must be either delivered or reported lost"
);
event_tx
.send(SubUpdate::new(payload_from("live")))
.expect("subscription stream should still be feeding");
let event = tokio::time::timeout(Duration::from_secs(2), stream.next())
.await
.expect("the live event must arrive")
.expect("stream must still be open")
.expect("a live event must not be a terminal error");
assert_eq!(&*event.data, b"live");
assert!(!event.snapshot_end, "a live event does not close a burst");
assert_eq!(
event.skipped, 0,
"the closing snapshot already accounted for the burst; the event adds no phantom gap"
);
drop(handle);
drop(stream);
let _ = client.await;
server.abort();
}
#[tokio::test]
async fn ended_subscription_frees_its_cap_slot() {
let (listener, dialer) = transport_pair();
let dispatch = Arc::new(EchoDispatch {
writes: Arc::new(Mutex::new(Vec::new())),
});
let server = tokio::spawn(serve(
listener,
Arc::new(LineCodec),
dispatch,
SessionConfig {
limits: SessionLimits {
max_connections: 16,
max_subs_per_connection: 2,
},
reads_hello: false,
acks_subscribe: false,
},
));
let (handle, client_fut) = run_client(
dialer,
LineCodec,
ClientConfig {
reconnect: false,
reconnect_delay: 10,
max_reconnect_delay: 10,
max_reconnect_attempts: 0,
keepalive_interval: None,
max_offline_queue: 1024,
sends_hello: false,
},
Arc::new(TestClock),
);
let client = tokio::spawn(client_fut);
async fn drain_three(
stream: &mut BoxStream<'static, Result<SubUpdate, RpcError>>,
topic: &str,
) {
for i in 1..=3 {
let ev = tokio::time::timeout(Duration::from_secs(2), stream.next())
.await
.expect("event should arrive")
.expect("an accepted subscription must yield its events")
.expect("an accepted subscription must not be rejected");
assert_eq!(&*ev.data, format!("{topic}#{i}").as_bytes());
}
}
let mut a = handle.subscribe("a").unwrap();
drain_three(&mut a, "a").await;
let mut b = handle.subscribe("b").unwrap();
drain_three(&mut b, "b").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let mut c = handle.subscribe("c").unwrap();
let first = tokio::time::timeout(Duration::from_secs(2), c.next())
.await
.expect("third subscribe must not hang");
assert_eq!(
first.map(|u| u.expect("third subscribe must be accepted").data.to_vec()),
Some(b"c#1".to_vec()),
"an ended subscription must free its cap slot; the third subscribe was refused"
);
drop(handle);
drop(a);
drop(b);
drop(c);
let _ = client.await;
server.abort();
}
fn silent_peer_config() -> ClientConfig {
ClientConfig {
reconnect: false,
keepalive_interval: None,
..Default::default()
}
}
async fn read_request(peer: &mut Box<dyn Connection>) -> (u64, String, String) {
let frame = tokio::time::timeout(Duration::from_secs(2), peer.recv())
.await
.expect("the client must send its request")
.expect("the pipe must stay open")
.expect("the pipe must stay open");
let text = String::from_utf8(frame).expect("test frames are utf-8");
let mut parts = text.splitn(4, '\n');
assert_eq!(
parts.next(),
Some("REQ"),
"expected a request frame: {text}"
);
let id = parts
.next()
.and_then(|id| id.parse().ok())
.expect("request id");
let method = parts.next().expect("request method").to_string();
(id, method, parts.next().unwrap_or("").to_string())
}
#[tokio::test]
async fn repeated_call_timeouts_leave_the_connection_usable() {
let (mut listener, dialer) = transport_pair();
let (handle, client_fut) =
run_client(dialer, LineCodec, silent_peer_config(), Arc::new(TestClock));
let client = tokio::spawn(client_fut);
let mut peer = listener.accept().await.expect("the client must dial");
for i in 0..64 {
let call = handle.call("hang", payload_from(&format!("{i}")));
assert!(
tokio::time::timeout(Duration::from_millis(20), call)
.await
.is_err(),
"an unanswered call must not resolve"
);
let (_, method, _) = read_request(&mut peer).await;
assert_eq!(method, "hang", "each request must still reach the peer");
}
let answered = tokio::spawn({
let handle = handle.clone();
async move { handle.call("ping", payload_from("hi")).await }
});
let (id, method, params) = read_request(&mut peer).await;
assert_eq!((method.as_str(), params.as_str()), ("ping", "hi"));
peer.send(format!("REPLY\n{id}\nOK\npong").as_bytes())
.await
.expect("the pipe must stay open");
let reply = tokio::time::timeout(Duration::from_secs(2), answered)
.await
.expect("the answered call must resolve")
.expect("the call task must not panic")
.expect("the peer replied with OK");
assert_eq!(&*reply, b"pong");
drop(handle);
drop(peer);
let _ = client.await;
}
#[tokio::test]
async fn a_call_abandoned_before_dispatch_is_never_sent() {
let (mut listener, dialer) = transport_pair();
let (handle, client_fut) =
run_client(dialer, LineCodec, silent_peer_config(), Arc::new(TestClock));
assert!(
tokio::time::timeout(Duration::ZERO, handle.call("ghost", payload_from("x")))
.await
.is_err(),
"the call must be abandoned before the engine dequeues it"
);
let client = tokio::spawn(client_fut);
let mut peer = listener.accept().await.expect("the client must dial");
let answered = tokio::spawn({
let handle = handle.clone();
async move { handle.call("real", payload_from("hi")).await }
});
let (id, method, _) = read_request(&mut peer).await;
assert_eq!(
method, "real",
"an abandoned call must not be sent to the peer"
);
peer.send(format!("REPLY\n{id}\nOK\npong").as_bytes())
.await
.expect("the pipe must stay open");
let reply = tokio::time::timeout(Duration::from_secs(2), answered)
.await
.expect("the answered call must resolve")
.expect("the call task must not panic")
.expect("the peer replied with OK");
assert_eq!(&*reply, b"pong");
drop(handle);
drop(peer);
let _ = client.await;
}
#[tokio::test]
async fn a_concurrent_batch_of_timeouts_leaves_the_connection_usable() {
const BATCH: usize = 256;
let (mut listener, dialer) = transport_pair();
let (handle, client_fut) =
run_client(dialer, LineCodec, silent_peer_config(), Arc::new(TestClock));
let client = tokio::spawn(client_fut);
let mut peer = listener.accept().await.expect("the client must dial");
let batch: Vec<_> = (0..BATCH)
.map(|i| {
let handle = handle.clone();
tokio::spawn(async move {
tokio::time::timeout(
Duration::from_millis(50),
handle.call("hang", payload_from(&format!("{i}"))),
)
.await
})
})
.collect();
for _ in 0..BATCH {
let (_, method, _) = read_request(&mut peer).await;
assert_eq!(method, "hang", "each request must reach the peer");
}
for task in batch {
assert!(
task.await.expect("the call task must not panic").is_err(),
"an unanswered call must not resolve"
);
}
let answered = tokio::spawn({
let handle = handle.clone();
async move { handle.call("ping", payload_from("hi")).await }
});
let (id, method, params) = read_request(&mut peer).await;
assert_eq!((method.as_str(), params.as_str()), ("ping", "hi"));
peer.send(format!("REPLY\n{id}\nOK\npong").as_bytes())
.await
.expect("the pipe must stay open");
let reply = tokio::time::timeout(Duration::from_secs(2), answered)
.await
.expect("the answered call must resolve")
.expect("the call task must not panic")
.expect("the peer replied with OK");
assert_eq!(&*reply, b"pong");
drop(handle);
drop(peer);
let _ = client.await;
}
#[tokio::test]
async fn a_one_shot_dialer_hands_out_one_connection_then_reports_closed() {
let (a, _b) = conn_pair();
let dialer = OneShotDialer::new(a);
assert!(dialer.connect().await.is_ok(), "first connect hands it out");
assert_eq!(
dialer.connect().await.err(),
Some(TransportError::Closed),
"`Closed` is what stops the engine; `Io` would earn a backoff and retry"
);
}
#[tokio::test]
async fn a_one_shot_listener_parks_after_its_only_accept() {
let (a, _b) = conn_pair();
let mut listener = OneShotListener::new(a);
assert!(listener.accept().await.is_ok(), "first accept yields");
assert!(
tokio::time::timeout(Duration::from_millis(50), listener.accept())
.await
.is_err(),
"the second accept must park, not resolve"
);
}
#[tokio::test]
async fn serve_over_a_one_shot_listener_handles_the_peer_then_keeps_waiting() {
let writes: WriteLog = Arc::new(Mutex::new(Vec::new()));
let dispatch = Arc::new(EchoDispatch {
writes: writes.clone(),
});
let (server_end, mut client_end) = conn_pair();
let server = tokio::spawn(serve(
OneShotListener::new(server_end),
Arc::new(LineCodec),
dispatch,
SessionConfig::default(),
));
client_end
.send(
b"WRITE
topic
hello",
)
.await
.expect("send the write");
drop(client_end);
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
loop {
if !writes.lock().unwrap().is_empty() {
break;
}
assert!(
tokio::time::Instant::now() < deadline,
"the write never reached the dispatch"
);
tokio::task::yield_now().await;
}
assert!(
tokio::time::timeout(Duration::from_millis(100), server)
.await
.is_err(),
"serve must keep waiting on the parked accept, not return"
);
}