//! RFC 010 c3 — transport conformance suite, run against both shipped impls //! (TCP and in-memory loopback), plus impl-specific cases. //! //! Shared suite (roadmap): frame roundtrips through the framed codec, framing //! across a split write, coalesced frames in one write, peer-close mid-frame //! (must error, not EOF), clean close at a frame boundary (EOF as `Ok(None)`). //! //! The TCP impl parks the calling actor, so its runs live inside `smarm::run`; //! loopback blocks the OS thread and runs as plain tests. #![cfg(feature = "cluster")] use smarm::cluster::envelope::Frame; use smarm::cluster::transport::loopback::LoopbackTransport; use smarm::cluster::transport::tcp::TcpTransport; use smarm::cluster::transport::{Conn, FramedConn, RecvError, Transport}; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /// Listener + dial + accept against one transport, both conns returned. /// Relies on dial not requiring a concurrent accept (TCP backlog / loopback /// queue), so a single thread or actor can hold both ends. fn pair(t: &dyn Transport, addr: &str) -> (Box, Box) { let mut l = t.listen(addr).unwrap(); let a = t.dial(&l.local_addr()).unwrap(); let b = l.accept().unwrap(); (a, b) } fn frames() -> Vec { vec![ Frame::Heartbeat, Frame::Send { index: 42, generation: 3, type_hash: 0x1234_5678_9ABC_DEF0, payload: vec![1, 2, 3, 4, 5], }, Frame::SendNamed { name: "the_counter".into(), type_hash: 0xFFFF_0000_FFFF_0000, payload: vec![], }, Frame::Demonitor { monitor_id: 77 }, ] } fn encode(f: &Frame) -> Vec { let mut out = Vec::new(); f.encode(&mut out).unwrap(); out } // --------------------------------------------------------------------------- // Shared conformance suite — generic over an established pair // --------------------------------------------------------------------------- fn suite_roundtrip(a: Box, b: Box) { let mut fa = FramedConn::new(a); let mut fb = FramedConn::new(b); // a -> b, then b -> a: both directions carry every frame shape. for f in frames() { fa.send(&f).unwrap(); assert_eq!(fb.recv().unwrap().unwrap(), f); } for f in frames() { fb.send(&f).unwrap(); assert_eq!(fa.recv().unwrap().unwrap(), f); } } fn suite_split_write(mut a: Box, b: Box) { let f = Frame::Send { index: 7, generation: 1, type_hash: 0xAB, payload: vec![9; 64], }; let bytes = encode(&f); // Split inside the length prefix, then inside the body: the reader must // reassemble regardless of where the boundary falls. a.write_all(&bytes[..2]).unwrap(); a.write_all(&bytes[2..10]).unwrap(); a.write_all(&bytes[10..]).unwrap(); let mut fb = FramedConn::new(b); assert_eq!(fb.recv().unwrap().unwrap(), f); } fn suite_coalesced(mut a: Box, b: Box) { let f1 = Frame::Heartbeat; let f2 = Frame::Demonitor { monitor_id: 5 }; let mut bytes = encode(&f1); bytes.extend_from_slice(&encode(&f2)); a.write_all(&bytes).unwrap(); let mut fb = FramedConn::new(b); assert_eq!(fb.recv().unwrap().unwrap(), f1); assert_eq!(fb.recv().unwrap().unwrap(), f2); } fn suite_close_mid_frame(mut a: Box, b: Box) { let bytes = encode(&Frame::Send { index: 1, generation: 1, type_hash: 1, payload: vec![0; 128], }); a.write_all(&bytes[..bytes.len() / 2]).unwrap(); a.close(); let mut fb = FramedConn::new(b); match fb.recv() { Err(RecvError::TruncatedByPeer) => {} other => panic!("expected TruncatedByPeer, got {other:?}"), } } fn suite_clean_close(mut a: Box, b: Box) { let f = Frame::Heartbeat; a.write_all(&encode(&f)).unwrap(); a.close(); let mut fb = FramedConn::new(b); // The buffered frame is still delivered, then EOF at the boundary. assert_eq!(fb.recv().unwrap().unwrap(), f); assert!(fb.recv().unwrap().is_none()); } fn run_suite(t: &dyn Transport, addr: &str) { let (a, b) = pair(t, addr); suite_roundtrip(a, b); let (a, b) = pair(t, addr); suite_split_write(a, b); let (a, b) = pair(t, addr); suite_coalesced(a, b); let (a, b) = pair(t, addr); suite_close_mid_frame(a, b); let (a, b) = pair(t, addr); suite_clean_close(a, b); } // --------------------------------------------------------------------------- // Loopback — plain tests, no runtime // --------------------------------------------------------------------------- #[test] fn loopback_conformance() { // Fresh transport per pair() call is fine, but one instance must also // support sequential re-listen on distinct addresses. let t = LoopbackTransport::default(); run_suite(&t, "alpha"); } #[test] fn loopback_dial_unknown_addr_refused() { let t = LoopbackTransport::default(); let err = t.dial("nobody-home").unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::ConnectionRefused); } #[test] fn loopback_addr_in_use() { let t = LoopbackTransport::default(); let _l = t.listen("alpha").unwrap(); let err = t.listen("alpha").unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::AddrInUse); } #[test] fn loopback_listener_drop_frees_addr_and_refuses_dial() { let t = LoopbackTransport::default(); let l = t.listen("alpha").unwrap(); drop(l); let err = t.dial("alpha").unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::ConnectionRefused); // Address is reusable after the listener is gone. let _l2 = t.listen("alpha").unwrap(); } #[test] fn loopback_write_after_peer_close_broken_pipe() { let t = LoopbackTransport::default(); let (mut a, mut b) = pair(&t, "alpha"); b.close(); let err = a.write_all(&[1, 2, 3]).unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::BrokenPipe); } #[test] fn loopback_cross_thread_blocking_read() { // Reader blocks on an empty pipe until the writer thread delivers. let t = LoopbackTransport::default(); let (a, b) = pair(&t, "alpha"); let mut fb = FramedConn::new(b); let writer = std::thread::spawn(move || { let mut a = a; std::thread::sleep(std::time::Duration::from_millis(30)); a.write_all(&encode(&Frame::Heartbeat)).unwrap(); }); assert_eq!(fb.recv().unwrap().unwrap(), Frame::Heartbeat); writer.join().unwrap(); } // --------------------------------------------------------------------------- // TCP — inside the runtime (read/write park the calling actor) // --------------------------------------------------------------------------- #[test] fn tcp_conformance() { smarm::run(|| { run_suite(&TcpTransport, "127.0.0.1:0"); }); } #[test] fn tcp_dial_refused() { smarm::run(|| { // Bind to an OS-assigned port, learn it, close the listener, dial it. let addr = { let l = TcpTransport.listen("127.0.0.1:0").unwrap(); l.local_addr() }; let err = TcpTransport.dial(&addr).unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::ConnectionRefused); }); } #[test] fn tcp_bad_addr_rejected_without_resolution() { // Addresses are opaque pre-resolved strings; the c9 seam resolves names. // A hostname is therefore invalid input here, not something to resolve. let err = TcpTransport.dial("localhost:1234").unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); } #[test] fn tcp_local_addr_reports_real_port() { let l = TcpTransport.listen("127.0.0.1:0").unwrap(); let addr = l.local_addr(); let port: u16 = addr.rsplit(':').next().unwrap().parse().unwrap(); assert_ne!(port, 0); } #[test] fn tcp_big_frame_across_socket_buffers() { // A payload far beyond socket buffer sizes forces genuine fragmentation // and write backpressure: writer and reader must run concurrently. smarm::run(|| { let (tx, rx) = smarm::channel::(); let payload = vec![0xA5u8; 4 * 1024 * 1024]; let f = Frame::Send { index: 9, generation: 2, type_hash: 0xC0FFEE, payload, }; let mut l = TcpTransport.listen("127.0.0.1:0").unwrap(); let addr = l.local_addr(); let fw = f.clone(); let writer = smarm::spawn(move || { let mut fa = FramedConn::new(TcpTransport.dial(&addr).unwrap()); fa.send(&fw).unwrap(); }); let reader = smarm::spawn(move || { let mut fb = FramedConn::new(l.accept().unwrap()); let got = fb.recv().unwrap().unwrap(); tx.send(got).unwrap(); }); let got = rx.recv().unwrap(); assert_eq!(got, f); writer.join().unwrap(); reader.join().unwrap(); }); }