//! Shared test scaffolding for the channels module tree: the toy //! pipe-delimited codec (`TP`) and the conn-side `Harness` that fakes a //! websocket transport. `cfg(test)` only. use super::*; use crate::ws::duplex::Outbound; use std::time::Duration; /// Toy payload + codec: `jref|ref|topic|kind|event-or-status|payload` /// with `-` for absent refs. Enough to drive the machinery. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct TP(pub(crate) String); pub(crate) fn part(o: Option<&str>) -> &str { o.unwrap_or("-") } impl Encode for TP { fn encode(f: FrameRef<'_, Self>) -> Message { let s = match f.message { MessageRef::Event { event, payload } => format!( "{}|{}|{}|event|{}|{}", part(f.join_ref), part(f.reference), f.topic, event, payload.map(|p| p.0.as_str()).unwrap_or("") ), MessageRef::Reply { status, payload } => format!( "{}|{}|{}|reply|{}|{}", part(f.join_ref), part(f.reference), f.topic, match status { Status::Ok => "ok", Status::Error => "error", }, payload.map(|p| p.0.as_str()).unwrap_or("") ), }; Message::Text(s) } } impl Decode for TP { fn decode(msg: &Message) -> Result, DecodeError> { let Message::Text(s) = msg else { return Err(DecodeError("binary".into())); }; let p: Vec<&str> = s.splitn(6, '|').collect(); if p.len() != 6 || p[3] != "event" { return Err(DecodeError(format!("bad frame: {s}"))); } let opt = |x: &str| (x != "-").then(|| x.to_string()); Ok(ChannelFrame { join_ref: opt(p[0]), reference: opt(p[1]), topic: p[2].to_string(), message: ChannelMessage::Event { event: p[4].to_string(), payload: TP(p[5].into()) }, }) } } /// A fake socket: the conn-side handler plus the raw Outbound channel /// a real duplex loop would drain. `recv_text` pulls the next encoded /// frame the "client" would see. pub(crate) struct Harness { handler: SocketHandler, sender: WsSender, out_rx: smarm::Receiver, } impl Harness { pub(crate) fn new(hub: &ChannelHub) -> Self { let (tx, out_rx) = smarm::channel(); Self { handler: SocketHandler { bus: hub.bus.clone(), router: hub.router.clone(), joined: HashMap::new(), }, sender: WsSender { tx }, out_rx, } } /// Feed one wire line in, as the duplex loop would. pub(crate) fn send(&mut self, line: &str) { self.handler .on_message(Message::Text(line.to_string()), &self.sender.clone()); } /// Assert nothing arrives for this client within a short window — /// for "the broadcast must NOT reach the evicted transport" cases. pub(crate) fn assert_silence(&self) { if let Ok(Outbound::Msg(Message::Text(s))) = self.out_rx.recv_timeout(Duration::from_millis(100)) { panic!("expected silence, got frame: {s}"); } } /// Next encoded frame the client would see (2s budget — channel /// actors answer asynchronously). pub(crate) fn recv(&self) -> String { match self.out_rx.recv_timeout(Duration::from_secs(2)) { Ok(Outbound::Msg(Message::Text(s))) => s, other => panic!("expected outbound text frame, got {:?}", other.is_ok()), } } } pub(crate) fn eventually(mut f: impl FnMut() -> bool) -> bool { for _ in 0..200 { if f() { return true; } smarm::sleep(Duration::from_millis(10)); } false }