//! One-for-one supervisor tests. //! //! A `OneForOne` runs as an ordinary actor: it spawns its children under //! itself, collects their termination `Signal`s on a single mailbox, and //! restarts them per policy until either every child is in a terminal, //! non-restartable state (then `run()` returns) or the restart intensity cap //! is tripped (then `run()` gives up and returns). //! //! Policies: //! - `Permanent` — restart on any termination (normal or panic). //! - `Transient` — restart only on panic; a normal exit is "done". //! - `Temporary` — never restart. //! //! All cases below are deterministic under the single-thread runtime: the //! supervisor parks in `recv()` while a child runs to completion, so signals //! arrive one at a time in a fixed order. use smarm::supervisor::{ChildSpec, OneForOne, Restart}; use smarm::{run, spawn}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; fn counting_child( counter: &Arc, behavior: impl Fn(usize) + Send + Sync + 'static, ) -> impl Fn() + Send + Sync + 'static { let c = counter.clone(); move || { let n = c.fetch_add(1, Ordering::SeqCst) + 1; behavior(n); } } #[test] fn transient_child_is_restarted_on_panic_then_settles() { let runs = Arc::new(AtomicUsize::new(0)); let r = runs.clone(); run(move || { let child = counting_child(&r, |n| { if n < 3 { panic!("crash {n}"); } // 3rd run returns normally -> Transient does not restart. }); let sup = spawn(move || { OneForOne::new() .intensity(10, Duration::from_secs(60)) .child(ChildSpec::new(Restart::Transient, child)) .run(); }); sup.join().unwrap(); }); assert_eq!(runs.load(Ordering::SeqCst), 3, "two restarts then a clean exit"); } #[test] fn transient_child_normal_exit_is_not_restarted() { let runs = Arc::new(AtomicUsize::new(0)); let r = runs.clone(); run(move || { let child = counting_child(&r, |_| { /* return normally */ }); let sup = spawn(move || { OneForOne::new() .child(ChildSpec::new(Restart::Transient, child)) .run(); }); sup.join().unwrap(); }); assert_eq!(runs.load(Ordering::SeqCst), 1); } #[test] fn temporary_child_is_not_restarted_on_panic() { let runs = Arc::new(AtomicUsize::new(0)); let r = runs.clone(); run(move || { let child = counting_child(&r, |_| panic!("once")); let sup = spawn(move || { OneForOne::new() .child(ChildSpec::new(Restart::Temporary, child)) .run(); }); sup.join().unwrap(); }); assert_eq!(runs.load(Ordering::SeqCst), 1); } #[test] fn intensity_cap_stops_a_permanent_crash_loop() { let runs = Arc::new(AtomicUsize::new(0)); let r = runs.clone(); run(move || { let child = counting_child(&r, |n| panic!("always {n}")); let sup = spawn(move || { OneForOne::new() .intensity(3, Duration::from_secs(60)) .child(ChildSpec::new(Restart::Permanent, child)) .run(); }); sup.join().unwrap(); }); // 1 initial run + 3 restarts, then the 4th failure trips the cap. assert_eq!(runs.load(Ordering::SeqCst), 4); } #[test] fn one_for_one_restarts_only_the_failed_child() { let a = Arc::new(AtomicUsize::new(0)); let b = Arc::new(AtomicUsize::new(0)); let (ra, rb) = (a.clone(), b.clone()); run(move || { let child_a = counting_child(&ra, |n| { if n < 2 { panic!("a crash {n}"); } }); let child_b = counting_child(&rb, |_| { /* runs once, normal */ }); let sup = spawn(move || { OneForOne::new() .intensity(10, Duration::from_secs(60)) .child(ChildSpec::new(Restart::Transient, child_a)) .child(ChildSpec::new(Restart::Temporary, child_b)) .run(); }); sup.join().unwrap(); }); assert_eq!(a.load(Ordering::SeqCst), 2, "A restarts once then settles"); assert_eq!(b.load(Ordering::SeqCst), 1, "B is untouched by A's restart"); }