added a new test for comm

This commit is contained in:
2026-04-24 02:40:05 +00:00
parent 0bc7097c88
commit 759497ee70
7 changed files with 234 additions and 210 deletions

View File

@@ -62,7 +62,7 @@ mod test {
#[test] #[test]
fn not_called_if_finished() { fn not_called_if_finished() {
spin_on(false, async { spin_on(async {
let (mut req_in, mut req_out) = mpsc::channel(0); let (mut req_in, mut req_out) = mpsc::channel(0);
let (mut rep_in, mut rep_out) = mpsc::channel(0); let (mut rep_in, mut rep_out) = mpsc::channel(0);
join( join(

View File

@@ -143,15 +143,26 @@ pub fn eprint_stream_events<'a, S: Stream + 'a>(
) )
} }
thread_local! {
static WAKE_LODUD: RefCell<bool> = const { RefCell::new(false) };
}
/// Equivalent to [spin_on], but also logs on wake
pub fn spin_on_loud<Fut: Future>(fut: Fut) -> Fut::Output {
let prev = WAKE_LODUD.replace(true);
let ret = spin_on(fut);
WAKE_LODUD.set(prev);
ret
}
struct SpinWaker { struct SpinWaker {
repeat: AtomicBool, repeat: AtomicBool,
loud: bool,
} }
impl Wake for SpinWaker { impl Wake for SpinWaker {
fn wake(self: Arc<Self>) { fn wake(self: Arc<Self>) {
self.repeat.store(true, Ordering::SeqCst); self.repeat.store(true, Ordering::SeqCst);
if self.loud { if WAKE_LODUD.with_borrow(|k| *k) {
eprintln!("Triggered repeat for spin_on") eprintln!("{Label} Triggered repeat for spin_on")
} }
} }
} }
@@ -160,11 +171,13 @@ impl Wake for SpinWaker {
/// keeps synchronously waking itself. This is useful for deterministic tests /// keeps synchronously waking itself. This is useful for deterministic tests
/// that don't contain side effects or threading. /// that don't contain side effects or threading.
/// ///
/// Use [spin_on_loud] to get messages on wake for debugging
///
/// # Panics /// # Panics
/// ///
/// If the future doesn't wake itself and doesn't settle. /// If the future doesn't wake itself and doesn't settle.
pub fn spin_on<Fut: Future>(loud: bool, f: Fut) -> Fut::Output { pub fn spin_on<Fut: Future>(f: Fut) -> Fut::Output {
let spin_waker = Arc::new(SpinWaker { repeat: AtomicBool::new(false), loud }); let spin_waker = Arc::new(SpinWaker { repeat: AtomicBool::new(false) });
let mut f = pin!(f); let mut f = pin!(f);
let waker = spin_waker.clone().into(); let waker = spin_waker.clone().into();
let mut cx = Context::from_waker(&waker); let mut cx = Context::from_waker(&waker);

View File

@@ -354,7 +354,7 @@ struct ReplySub {
cb: oneshot::Sender<ReplyRecord>, cb: oneshot::Sender<ReplyRecord>,
} }
struct IoClient { pub struct IoClient {
output: IoLock<dyn AsyncWrite>, output: IoLock<dyn AsyncWrite>,
id: Rc<RefCell<u64>>, id: Rc<RefCell<u64>>,
subscribe: Rc<Sender<ReplySub>>, subscribe: Rc<Sender<ReplySub>>,
@@ -491,30 +491,33 @@ impl MsgWriter for IoNotifWriter {
} }
} }
pub struct CommCtx { pub struct CommCx {
exit: Sender<()>, exit: Sender<()>,
} }
impl CommCtx { impl CommCx {
pub async fn exit(self) -> io::Result<()> { pub async fn exit(self) -> io::Result<()> {
self.exit.clone().send(()).await.expect("quit channel dropped"); self.exit.clone().send(()).await.expect("quit channel dropped");
Ok(()) Ok(())
} }
} }
pub struct IoComm {
pub client: IoClient,
pub cx: CommCx,
pub srv: IoCommServer,
}
/// Establish bidirectional request-notification communication over a duplex /// Establish bidirectional request-notification communication over a duplex
/// channel. The returned [IoClient] can be used for notifications immediately, /// channel. The returned [IoClient] can be used for notifications immediately,
/// but requests can only be received while the future is running. The future /// but requests can only be received while the future is running. The future
/// will only resolve when [CommCtx::exit] is called. /// will only resolve when [CommCtx::exit] is called.
pub fn io_comm( pub fn io_comm(o: Pin<Box<dyn AsyncWrite>>, i: Pin<Box<dyn AsyncRead>>) -> IoComm {
o: Pin<Box<dyn AsyncWrite>>,
i: Pin<Box<dyn AsyncRead>>,
) -> (impl Client + 'static, CommCtx, IoCommServer) {
let i = Rc::new(Mutex::new(i)); let i = Rc::new(Mutex::new(i));
let o = Rc::new(Mutex::new(o)); let o = Rc::new(Mutex::new(o));
let (onsub, client) = IoClient::new(o.clone()); let (onsub, client) = IoClient::new(o.clone());
let (exit, onexit) = channel(1); let (exit, onexit) = channel(1);
(client, CommCtx { exit }, IoCommServer { o, i, onsub, onexit }) IoComm { client, cx: CommCx { exit }, srv: IoCommServer { o, i, onsub, onexit } }
} }
pub struct IoCommServer { pub struct IoCommServer {
o: Rc<Mutex<Pin<Box<dyn AsyncWrite>>>>, o: Rc<Mutex<Pin<Box<dyn AsyncWrite>>>>,
@@ -657,53 +660,61 @@ impl IoCommServer {
} }
#[cfg(test)] #[cfg(test)]
mod test { pub mod test {
use std::cell::RefCell; use std::cell::RefCell;
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::{FutureExt, SinkExt, StreamExt, join, select}; use futures::future::{join3, select};
use futures::{SinkExt, StreamExt};
use orchid_api_derive::{Coding, Hierarchy}; use orchid_api_derive::{Coding, Hierarchy};
use orchid_api_traits::Request; use orchid_api_traits::Request;
use orchid_async_utils::debug::{spin_on, with_label}; use orchid_async_utils::debug::{spin_on, with_label};
use unsync_pipe::pipe; use unsync_pipe::pipe;
use crate::comm::{ClientExt, MsgReaderExt, ReqReaderExt, io_comm}; use super::*;
use crate::with_stash; use crate::with_stash;
pub fn test_pair() -> (IoComm, IoComm) {
let (right_in, left_out) = pipe(1024);
let (left_in, right_out) = pipe(1024);
(
io_comm(Box::pin(left_in), Box::pin(left_out)),
io_comm(Box::pin(right_in), Box::pin(right_out)),
)
}
pub async fn listen_no_ingress(srv: IoCommServer) {
srv
.listen(
async |_| panic!("Not expecting ingress notif"),
async |_| panic!("Not expecting ingress req"),
)
.await
.unwrap()
}
#[derive(Clone, Debug, PartialEq, Coding, Hierarchy)] #[derive(Clone, Debug, PartialEq, Coding, Hierarchy)]
#[extendable] #[extendable]
struct TestNotif(u64); struct TestNotif(u64);
#[test] #[test]
fn notification() { fn notification() {
spin_on(false, async { let (left, right) = test_pair();
let (in1, out2) = pipe(1024);
let (in2, out1) = pipe(1024);
let (received, mut on_receive) = mpsc::channel(2); let (received, mut on_receive) = mpsc::channel(2);
let (_, recv_ctx, recv_srv) = io_comm(Box::pin(in2), Box::pin(out2)); let right_srv_fut = right.srv.listen(
let (sender, ..) = io_comm(Box::pin(in1), Box::pin(out1));
join!(
async {
recv_srv
.listen(
async |notif| { async |notif| {
received.clone().send(notif.read::<TestNotif>().await?).await.unwrap(); received.clone().send(notif.read::<TestNotif>().await?).await.unwrap();
Ok(()) Ok(())
}, },
async |_| panic!("Should receive notif, not request"), async |_| panic!("Should receive notif, not request"),
)
.await
.unwrap()
},
async {
sender.notify(TestNotif(3)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(3)));
sender.notify(TestNotif(4)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(4)));
recv_ctx.exit().await.unwrap();
}
); );
}) spin_on(join(async { right_srv_fut.await.unwrap() }, async {
left.client.notify(TestNotif(3)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(3)));
left.client.notify(TestNotif(4)).await.unwrap();
assert_eq!(on_receive.next().await, Some(TestNotif(4)));
right.cx.exit().await.unwrap();
}));
} }
#[derive(Clone, Debug, Coding, Hierarchy)] #[derive(Clone, Debug, Coding, Hierarchy)]
@@ -715,57 +726,36 @@ mod test {
#[test] #[test]
fn request() { fn request() {
spin_on(false, async { let (left, right) = test_pair();
let (in1, out2) = pipe(1024); let right_srv_fut = right.srv.listen(
let (in2, out1) = pipe(1024);
let (_, srv_ctx, srv) = io_comm(Box::pin(in2), Box::pin(out2));
let (client, client_ctx, client_srv) = io_comm(Box::pin(in1), Box::pin(out1));
join!(
async {
srv
.listen(
async |_| panic!("No notifs expected"), async |_| panic!("No notifs expected"),
async |mut req| { async |mut req| {
let val = req.read_req::<DummyRequest>().await?; let val = req.read_req::<DummyRequest>().await?;
req.reply(&val, val.0 + 1).await req.reply(&val, val.0 + 1).await
}, },
) );
.await let left_srv_fut = left.srv.listen(
.unwrap()
},
async {
client_srv
.listen(
async |_| panic!("Not expecting ingress notif"), async |_| panic!("Not expecting ingress notif"),
async |_| panic!("Not expecting ingress req"), async |_| panic!("Not expecting ingress req"),
)
.await
.unwrap()
},
async {
let response = client.request(DummyRequest(5)).await.unwrap();
assert_eq!(response, 6);
srv_ctx.exit().await.unwrap();
client_ctx.exit().await.unwrap();
}
); );
}) spin_on(join3(
async { right_srv_fut.await.unwrap() },
async { left_srv_fut.await.unwrap() },
async {
let response = left.client.request(DummyRequest(5)).await.unwrap();
assert_eq!(response, 6);
right.cx.exit().await.unwrap();
left.cx.exit().await.unwrap();
},
));
} }
#[test] #[test]
fn exit() { fn exit() {
spin_on(false, async { let (left, right) = test_pair();
let (input1, output1) = pipe(1024); let reply_context = RefCell::new(Some(right.cx));
let (input2, output2) = pipe(1024); let (exit, onexit) = oneshot::channel::<()>();
let (reply_client, reply_context, reply_server) = let right_srv_fut = right.srv.listen(
io_comm(Box::pin(input1), Box::pin(output2));
let (req_client, req_context, req_server) = io_comm(Box::pin(input2), Box::pin(output1));
let reply_context = RefCell::new(Some(reply_context));
let (exit, onexit) = futures::channel::oneshot::channel::<()>();
join!(
with_label("reply", async move {
reply_server
.listen(
async |hand| { async |hand| {
let _notif = hand.read::<TestNotif>().await.unwrap(); let _notif = hand.read::<TestNotif>().await.unwrap();
let context = reply_context.borrow_mut().take().unwrap(); let context = reply_context.borrow_mut().take().unwrap();
@@ -776,43 +766,34 @@ mod test {
let req = hand.read_req::<DummyRequest>().await?; let req = hand.read_req::<DummyRequest>().await?;
hand.reply(&req, req.0 + 1).await hand.reply(&req, req.0 + 1).await
}, },
) );
.await let left_srv_fut = left.srv.listen(
.unwrap();
exit.send(()).unwrap();
let _client = reply_client;
}),
with_label("client", async move {
req_server
.listen(
async |_| panic!("Only the other server expected notifs"), async |_| panic!("Only the other server expected notifs"),
async |_| panic!("Only the other server expected requests"), async |_| panic!("Only the other server expected requests"),
) );
.await spin_on(join3(
.unwrap(); with_label("reply", async move {
let _ctx = req_context; right_srv_fut.await.unwrap();
exit.send(()).unwrap();
let _client = right.client;
}),
with_label("client", async move {
left_srv_fut.await.unwrap();
let _ctx = left.cx;
}), }),
async move { async move {
req_client.request(DummyRequest(0)).await.unwrap(); left.client.request(DummyRequest(0)).await.unwrap();
req_client.notify(TestNotif(0)).await.unwrap(); left.client.notify(TestNotif(0)).await.unwrap();
onexit.await.unwrap(); onexit.await.unwrap();
} },
) ));
});
} }
#[test] #[test]
fn timely_cancel() { fn timely_cancel() {
spin_on(false, async { let (left, right) = test_pair();
let (in1, out2) = pipe(1024);
let (in2, out1) = pipe(1024);
let (wait_in, mut wait_out) = mpsc::channel(0); let (wait_in, mut wait_out) = mpsc::channel(0);
let (_, srv_ctx, srv) = io_comm(Box::pin(in2), Box::pin(out2)); let right_srv_fut = right.srv.listen(
let (client, client_ctx, client_srv) = io_comm(Box::pin(in1), Box::pin(out1));
join!(
with_label("server", async {
srv
.listen(
async |_| panic!("No notifs expected"), async |_| panic!("No notifs expected"),
async |mut req| { async |mut req| {
let _ = req.read_req::<DummyRequest>().await?; let _ = req.read_req::<DummyRequest>().await?;
@@ -822,33 +803,67 @@ mod test {
// the loop // the loop
futures::future::pending().await futures::future::pending().await
}, },
) );
.await spin_on(join3(
.unwrap(); with_label("server", async { right_srv_fut.await.unwrap() }),
}), with_label("client", listen_no_ingress(left.srv)),
with_label("client", async { with_label(
client_srv "outer_stash",
.listen(
async |_| panic!("Not expecting ingress notif"),
async |_| panic!("Not expecting ingress req"),
)
.await
.unwrap();
}),
with_stash(async { with_stash(async {
with_stash(async { with_stash(async {
select! { select(
_ = client.request(DummyRequest(5)).fuse() => { Box::pin(async {
panic!("This one should not run") left.client.request(DummyRequest(5)).await.unwrap();
}, panic!("This one should not run");
rep = wait_out.next() => rep.expect("something?"), }),
} Box::pin(async {
wait_out.next().await.expect("something?");
}),
)
.await;
}) })
.await; .await;
srv_ctx.exit().await.unwrap(); right.cx.exit().await.unwrap();
client_ctx.exit().await.unwrap(); left.cx.exit().await.unwrap();
}) }),
),
));
}
#[test]
fn late_cancel() {
let (left, right) = test_pair();
let (send, mut recv) = mpsc::channel(0);
let right_srv_fut = right.srv.listen(
async |_| panic!("Expected a request"),
async |mut req| {
req.read_req::<DummyRequest>().await?;
let mut reply_writer = req.start_reply().await?;
let (stop_wait, wait) = oneshot::channel();
send.clone().send(stop_wait).await.unwrap();
wait.await.unwrap();
(1 as <DummyRequest as Request>::Response).encode(reply_writer.writer()).await?;
reply_writer.finish().await
},
); );
}) spin_on(join3(
async { right_srv_fut.await.unwrap() },
with_label("client", listen_no_ingress(left.srv)),
async {
with_stash(Box::pin(async {
select(
Box::pin(async {
left.client.request(DummyRequest(5)).await.unwrap();
panic!("This one should not run");
}),
Box::pin(async { recv.next().await.unwrap().send(()) }),
)
.await;
}))
.await;
right.cx.exit().await.unwrap();
left.cx.exit().await.unwrap();
},
));
} }
} }

View File

@@ -148,9 +148,7 @@ mod test {
#[test] #[test]
fn run_stashed_future() { fn run_stashed_future() {
let (mut send, recv) = mpsc::channel(0); let (mut send, recv) = mpsc::channel(0);
spin_on( spin_on(join(
false,
join(
with_stash(async { with_stash(async {
let mut send1 = send.clone(); let mut send1 = send.clone();
stash(async move { stash(async move {
@@ -176,13 +174,8 @@ mod test {
async { async {
let mut results = recv.take(6).collect::<Vec<_>>().await; let mut results = recv.take(6).collect::<Vec<_>>().await;
results.sort(); results.sort();
assert_eq!( assert_eq!(&results, &[1, 2, 3, 4, 5, 6], "all variations completed in unspecified order");
&results,
&[1, 2, 3, 4, 5, 6],
"all variations completed in unspecified order"
);
}, },
), ));
);
} }
} }

View File

@@ -15,7 +15,7 @@ use itertools::Itertools;
use orchid_api_traits::{Decode, Encode, Request, UnderRoot, enc_vec}; use orchid_api_traits::{Decode, Encode, Request, UnderRoot, enc_vec};
use orchid_async_utils::{Handle, JoinError, to_task}; use orchid_async_utils::{Handle, JoinError, to_task};
use orchid_base::{ use orchid_base::{
Client, ClientExt, CommCtx, Comment, MsgReader, MsgReaderExt, ReqHandleExt, ReqReaderExt, Client, ClientExt, CommCx, Comment, IoComm, MsgReader, MsgReaderExt, ReqHandleExt, ReqReaderExt,
Snippet, Sym, TokenVariant, Witness, char_filter_match, char_filter_union, es, io_comm, is, log, Snippet, Sym, TokenVariant, Witness, char_filter_match, char_filter_union, es, io_comm, is, log,
mk_char_filter, try_with_reporter, ttv_from_api, with_interner, with_logger, with_stash, mk_char_filter, try_with_reporter, ttv_from_api, with_interner, with_logger, with_stash,
}; };
@@ -37,7 +37,7 @@ use crate::{
task_local::task_local! { task_local::task_local! {
static CLIENT: Rc<dyn Client>; static CLIENT: Rc<dyn Client>;
static CTX: Rc<RefCell<Option<CommCtx>>>; static CTX: Rc<RefCell<Option<CommCx>>>;
} }
fn get_client() -> Rc<dyn Client> { CLIENT.get() } fn get_client() -> Rc<dyn Client> { CLIENT.get() }
@@ -51,7 +51,7 @@ pub async fn exit() {
/// Set the client used for global [request] and [notify] functions within the /// Set the client used for global [request] and [notify] functions within the
/// runtime of this future /// runtime of this future
pub async fn with_comm<F: Future>(c: Rc<dyn Client>, ctx: CommCtx, fut: F) -> F::Output { pub async fn with_comm<F: Future>(c: Rc<dyn Client>, ctx: CommCx, fut: F) -> F::Output {
CLIENT.scope(c, CTX.scope(Rc::new(RefCell::new(Some(ctx))), fut)).await CLIENT.scope(c, CTX.scope(Rc::new(RefCell::new(Some(ctx))), fut)).await
} }
@@ -194,9 +194,9 @@ impl ExtensionBuilder {
ctx.output.as_mut().flush().await.unwrap(); ctx.output.as_mut().flush().await.unwrap();
let logger1 = LoggerImpl::from_api(&host_header.logger); let logger1 = LoggerImpl::from_api(&host_header.logger);
let logger2 = logger1.clone(); let logger2 = logger1.clone();
let (client, comm_ctx, extension_srv) = io_comm(ctx.output, ctx.input); let IoComm { client, cx: comm_ctx, srv } = io_comm(ctx.output, ctx.input);
// this future will be ready once the extension cleanly exits // this future will be ready once the extension cleanly exits
let extension_fut = extension_srv.listen( let extension_fut = srv.listen(
async |n: Box<dyn MsgReader<'_>>| { async |n: Box<dyn MsgReader<'_>>| {
let notif = n.read().await.unwrap(); let notif = n.read().await.unwrap();
match notif { match notif {

View File

@@ -17,7 +17,7 @@ use hashbrown::{HashMap, HashSet};
use itertools::Itertools; use itertools::Itertools;
use orchid_api_traits::{Decode, Encode, Request}; use orchid_api_traits::{Decode, Encode, Request};
use orchid_base::{ use orchid_base::{
AtomRepr, Client, ClientExt, CommCtx, FmtCtxImpl, Format, IStr, IStrv, MsgReaderExt, Pos, AtomRepr, Client, ClientExt, CommCx, FmtCtxImpl, Format, IStr, IStrv, IoComm, MsgReaderExt, Pos,
ReqHandleExt, ReqReaderExt, Sym, Witness, es, ev, io_comm, is, iv, log, stash, with_stash, ReqHandleExt, ReqReaderExt, Sym, Witness, es, ev, io_comm, is, iv, log, stash, with_stash,
}; };
@@ -45,7 +45,7 @@ pub struct ReqPair<R: Request>(R, Sender<R::Response>);
pub struct ExtensionData { pub struct ExtensionData {
name: String, name: String,
ctx: Ctx, ctx: Ctx,
comm_cx: Option<CommCtx>, comm_cx: Option<CommCx>,
join_ext: Option<Box<dyn JoinHandle>>, join_ext: Option<Box<dyn JoinHandle>>,
client: Rc<dyn Client>, client: Rc<dyn Client>,
systems: Vec<SystemCtor>, systems: Vec<SystemCtor>,
@@ -82,12 +82,12 @@ impl Extension {
let header2 = header.clone(); let header2 = header.clone();
Ok(Self(Rc::new_cyclic(|weak: &Weak<ExtensionData>| { Ok(Self(Rc::new_cyclic(|weak: &Weak<ExtensionData>| {
// context not needed because exit is extension-initiated // context not needed because exit is extension-initiated
let (client, comm_cx, comm) = io_comm(init.input, init.output); let IoComm { client, cx: comm_cx, srv } = io_comm(init.input, init.output);
let weak2 = weak; let weak2 = weak;
let weak = weak.clone(); let weak = weak.clone();
let ctx2 = ctx.clone(); let ctx2 = ctx.clone();
let join_ext = ctx.clone().spawn(Duration::ZERO, async move { let join_ext = ctx.clone().spawn(Duration::ZERO, async move {
comm srv
.listen( .listen(
async |reader| { async |reader| {
with_stash(async { with_stash(async {

View File

@@ -47,6 +47,9 @@
"--logs=msg>stderr", "--logs=msg>stderr",
"exec", "exec",
"1 + 1" "1 + 1"
],
"initCommands": [
"settings set target.disable-aslr false"
] ]
}, },
{ {