//! gen_statem lifetime parity with gen_server: a machine lives until it //! stops, is shut down, or is killed — its refs are addresses. And //! `gen_statem::run_named` runs a machine inline as the current actor, so it //! is a direct `ChildSpec` child addressed by name. use smarm::gen_statem::{self, GenStatemName, Reply}; use smarm::supervisor::{ChildSpec, OneForOne, Restart, Shutdown}; use smarm::{monitor, request_shutdown, run, sleep, spawn, DownReason}; use std::sync::{Arc, Mutex}; use std::time::Duration; #[derive(Default, Clone)] struct Log(Arc>>); impl Log { fn push(&self, e: &'static str) { self.0.lock().unwrap().push(e); } fn get(&self) -> Vec<&'static str> { self.0.lock().unwrap().clone() } } #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum S { On, } struct D { log: Log, trap: bool, n: u64, } enum Cast { Inc, StopNow, } enum Call { Get(Reply), } smarm::gen_statem! { machine: Sm { state: S, data: D }; event: Ev { cast: Cast, call: Call, info: () }; context(data, prev, cx); enter { S::On => { data.log.push("enter"); if data.trap { cx.trap_exit() } }, } on S::On => { cast Cast::Inc => { data.n += 1; prev }, cast Cast::StopNow => stop, call Call::Get(r) => { r.reply(data.n); prev }, shutdown => { data.log.push("shutdown"); cx.stop(); prev }, state_timeout => unhandled, timeout _ => unhandled, } terminate { data.log.push("terminate"); } } fn d(log: &Log, trap: bool) -> D { D { log: log.clone(), trap, n: 0, } } #[test] fn dropping_last_ref_does_not_end_machine() { let log = Log::default(); let l = log.clone(); run(move || { let m = Sm::start(S::On, d(&l, true)); let pid = m.pid(); m.send(Ev::Cast(Cast::Inc)).unwrap(); assert_eq!(m.call(|r| Ev::Call(Call::Get(r))).unwrap(), 1); let mon = monitor(pid); drop(m); sleep(Duration::from_millis(30)); assert!( mon.rx.try_recv().unwrap().is_none(), "machine must outlive its refs" ); assert_eq!(l.get(), vec!["enter"]); request_shutdown(pid); assert_eq!(mon.rx.recv().unwrap().reason, DownReason::Exit); }); assert_eq!(log.get(), vec!["enter", "shutdown", "terminate"]); } const SM: GenStatemName = GenStatemName::new("lifetime-sm"); #[test] fn run_named_is_a_direct_supervised_child() { let log = Log::default(); let l = log.clone(); run(move || { let l2 = l.clone(); let sup = spawn(move || { let l3 = l2.clone(); OneForOne::new() .child( ChildSpec::new(Restart::Permanent, move || { gen_statem::run_named(SM, Sm::new(S::On, d(&l3, true))).expect("name free"); }) .shutdown(Shutdown::Infinity), ) .run(); }); sleep(Duration::from_millis(10)); gen_statem::send(SM, Ev::Cast(Cast::Inc)).unwrap(); assert_eq!(gen_statem::call(SM, |r| Ev::Call(Call::Get(r))).unwrap(), 1); // Normal self-exit → Permanent restart → fresh data, same name. gen_statem::send(SM, Ev::Cast(Cast::StopNow)).unwrap(); sleep(Duration::from_millis(30)); assert_eq!(gen_statem::call(SM, |r| Ev::Call(Call::Get(r))).unwrap(), 0); request_shutdown(sup.pid()); sup.join().unwrap(); assert!(gen_statem::whereis_machine(SM).is_none()); }); assert_eq!( log.get(), vec!["enter", "terminate", "enter", "shutdown", "terminate"] ); }