//! Link + trap_exit tests (roadmap item #3). //! //! A *link* is bidirectional and persistent (contrast a monitor, which is //! unidirectional and one-shot). When a linked actor dies *abnormally* — a //! panic or a cooperative `request_stop` — the death propagates to the peer: //! //! - a peer that has NOT trapped exits is cooperatively `request_stop`'d, so //! a crash fate-shares across the link set (Erlang's "let it crash"); //! - a peer that HAS called `trap_exit()` instead receives an [`ExitSignal`] //! *message* on its trap inbox and survives. //! //! A *normal* exit never propagates — not even to a trapping peer. Linking an //! already-dead pid delivers an immediate exit signal (`NoProc`): a message to //! a trapping caller, a `request_stop` to a non-trapping one. //! //! These cases are deterministic under the single-thread runtime: an actor //! runs until it parks/yields, so the relative order of link setup, the //! triggering death, and the propagated signal is fixed. use smarm::{ channel, link, monitor, run, self_pid, spawn, trap_exit, unlink, yield_now, DownReason, }; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; /// Sets its flag when dropped — proves a propagated stop unwound the peer's /// stack (running Drop) rather than leaking it. struct DropFlag(Arc); impl Drop for DropFlag { fn drop(&mut self) { self.0.store(true, Ordering::SeqCst); } } /// Abnormal death of one linked actor cooperatively stops its non-trapping /// peer, and the peer's Drop guards run during the propagated unwind. #[test] fn linked_pair_one_panics_other_is_stopped() { let dropped = Arc::new(AtomicBool::new(false)); let saw_stopped = Arc::new(AtomicBool::new(false)); let (d, s) = (dropped.clone(), saw_stopped.clone()); run(move || { // B parks on recv (kept open by a retained sender) so it is alive and // suspended when the propagated stop arrives. let hb = spawn(move || { let _g = DropFlag(d); let (tx, rx) = channel::(); let _keep = tx; let _ = rx.recv(); // parks until the link-propagated stop unwinds us }); let b = hb.pid(); let down_b = monitor(b); // A links B, then panics. The panic propagates along the link to B. let ha = spawn(move || { link(b); panic!("boom"); }); let dn = down_b.rx.recv().expect("monitor channel closed before Down"); assert_eq!(dn.pid, b, "Down reported the wrong pid"); if matches!(dn.reason, DownReason::Stopped) { s.store(true, Ordering::SeqCst); } drop(ha); let _ = hb.join(); }); assert!( saw_stopped.load(Ordering::SeqCst), "peer should be Stopped by the propagated abnormal exit" ); assert!( dropped.load(Ordering::SeqCst), "peer's Drop guard must run during the propagated cancellation unwind" ); } /// A trapping peer receives an abnormal death as an `ExitSignal` message and /// keeps running instead of being torn down. #[test] fn trap_exit_turns_abnormal_death_into_a_message() { let ok = Arc::new(AtomicBool::new(false)); let o = ok.clone(); run(move || { let h = spawn(move || { let inbox = trap_exit(); // Spawn the peer from here so we control ordering: it is enqueued // behind us and runs once we park on `inbox.recv()`. let ha = spawn(|| panic!("peer down")); let a = ha.pid(); link(a); // Parks here; A then runs, panics, and the trap message wakes us. let sig = inbox.recv().expect("trap inbox closed without a signal"); if sig.from == a && matches!(sig.reason, DownReason::Panic) { o.store(true, Ordering::SeqCst); } drop(ha); }); let _ = h.join(); }); assert!( ok.load(Ordering::SeqCst), "trapping peer should receive ExitSignal{{from, Panic}} and survive" ); } /// A normal exit never propagates — a trapping linked peer sees no message and /// stays alive. #[test] fn normal_exit_does_not_propagate() { let empty = Arc::new(AtomicBool::new(false)); let e = empty.clone(); run(move || { let h = spawn(move || { let inbox = trap_exit(); let ha = spawn(|| { /* exits normally, immediately */ }); let a = ha.pid(); link(a); yield_now(); // let A run to completion and finalize // A exited normally: nothing should have landed on the inbox. if let Ok(None) = inbox.try_recv() { e.store(true, Ordering::SeqCst); } drop(ha); }); let _ = h.join(); }); assert!( empty.load(Ordering::SeqCst), "normal exit must not deliver any ExitSignal to a trapping peer" ); } /// Linking an already-dead pid from a non-trapping actor stops the caller /// (no silent no-op). #[test] fn link_to_dead_pid_stops_a_nontrapping_caller() { let dropped = Arc::new(AtomicBool::new(false)); let saw_stopped = Arc::new(AtomicBool::new(false)); let (d, s) = (dropped.clone(), saw_stopped.clone()); run(move || { let ha = spawn(|| {}); let a = ha.pid(); ha.join().unwrap(); // A finishes and is reclaimed → `a` is now stale let hb = spawn(move || { let _g = DropFlag(d); link(a); // dead target, not trapping → request_stop(self) loop { smarm::check!(); // observation point to realize the stop } }); let b = hb.pid(); let down_b = monitor(b); let dn = down_b.rx.recv().expect("monitor channel closed before Down"); if matches!(dn.reason, DownReason::Stopped) { s.store(true, Ordering::SeqCst); } let _ = hb.join(); }); assert!( saw_stopped.load(Ordering::SeqCst), "linking a dead pid (non-trapping) should stop the caller" ); assert!(dropped.load(Ordering::SeqCst), "Drop guard must run"); } /// Linking an already-dead pid from a trapping actor delivers an immediate /// `NoProc` exit message instead of killing it. #[test] fn link_to_dead_pid_messages_a_trapping_caller() { let ok = Arc::new(AtomicBool::new(false)); let o = ok.clone(); run(move || { let ha = spawn(|| {}); let a = ha.pid(); ha.join().unwrap(); // `a` is now stale let hb = spawn(move || { let inbox = trap_exit(); link(a); // dead target, trapping → immediate NoProc message let sig = inbox.recv().expect("trap inbox closed without a signal"); if sig.from == a && matches!(sig.reason, DownReason::NoProc) { o.store(true, Ordering::SeqCst); } }); let _ = hb.join(); }); assert!( ok.load(Ordering::SeqCst), "linking a dead pid (trapping) should deliver a NoProc ExitSignal" ); } /// `unlink` removes the relationship in both directions: an abnormal death no /// longer reaches the formerly-linked peer. #[test] fn unlink_prevents_propagation() { let survived = Arc::new(AtomicBool::new(false)); let sv = survived.clone(); run(move || { let h = spawn(move || { let b = self_pid(); let inbox = trap_exit(); let ha = spawn(move || { link(b); unlink(b); panic!("boom"); // abnormal, but the link is gone }); yield_now(); // let A link, unlink, and panic // Unlinked before death → no ExitSignal should have arrived. if let Ok(None) = inbox.try_recv() { sv.store(true, Ordering::SeqCst); } drop(ha); }); let _ = h.join(); }); assert!( survived.load(Ordering::SeqCst), "unlinked peer must not receive a propagated exit" ); }