//! gen_statem (RFC 017) chunk-1 tests: call/cast round-trip against a //! hand-written machine, `enter` firing on start and on every real transition //! (but not on a stay), and the machine-down path when a handler panics. //! //! These drive the `smarm::statem` primitives directly, the same way a //! `statem!`-generated machine eventually will. use smarm::run; use smarm::statem::{self, CallError, Cx, Machine, Reply, Resolution, StatemRef}; use std::sync::{Arc, Mutex}; // --------------------------------------------------------------------------- // A hand-written two-state machine: Flip toggles, calls read, Boom panics. // --------------------------------------------------------------------------- #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Switch { Off, On, } struct Counts { flips: u32, enters: u32, } enum Cast { Flip, } enum Call { GetFlips(Reply), GetEnters(Reply), Boom(Reply), } enum Ev { Cast(Cast), Call(Call), } struct Sm { state: Switch, data: Counts, } impl Sm { fn start(init: Switch) -> StatemRef { statem::spawn(Sm { state: init, data: Counts { flips: 0, enters: 0 } }) } fn enter(&mut self, _s: Switch, _cx: &mut Cx) { self.data.enters += 1; } } impl Machine for Sm { type Ev = Ev; fn on_start(&mut self, cx: &mut Cx) { let s = self.state; self.enter(s, cx); } fn handle(&mut self, ev: Ev, cx: &mut Cx) { let prev = self.state; let next: Resolution = match (self.state, ev) { (Switch::Off, Ev::Cast(Cast::Flip)) => { self.data.flips += 1; Switch::On.into() } (Switch::On, Ev::Cast(Cast::Flip)) => Switch::Off.into(), (s, Ev::Call(Call::GetFlips(r))) => { r.reply(self.data.flips); s.into() // stay } (s, Ev::Call(Call::GetEnters(r))) => { r.reply(self.data.enters); s.into() // stay } (_, Ev::Call(Call::Boom(_r))) => panic!("boom"), }; match next { Resolution::To(s) if s == prev => {} Resolution::To(s) => { self.state = s; self.enter(s, cx); } Resolution::Postpone => {} Resolution::Unhandled => cx.on_unhandled(), } } } // Casts are applied in order and a later call observes the accumulated data. #[test] fn cast_then_call_roundtrip() { let got = Arc::new(Mutex::new(0u32)); let got2 = got.clone(); run(move || { let sw = Sm::start(Switch::Off); sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On sw.send(Ev::Cast(Cast::Flip)).unwrap(); // On -> Off let flips = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap(); *got2.lock().unwrap() = flips; }); assert_eq!(*got.lock().unwrap(), 1, "turned On once across the two flips"); } // `enter` fires once on start and once per *real* transition; a stay (a call // that returns the current tag) does not re-enter. #[test] fn enter_on_start_and_each_transition_but_not_stay() { let got = Arc::new(Mutex::new((0u32, 0u32, 0u32))); let got2 = got.clone(); run(move || { let sw = Sm::start(Switch::Off); // on_start enter -> enters = 1 let after_start = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap(); // Two stays (the reads above + below) must not bump enters. let _ = sw.call(|r| Ev::Call(Call::GetFlips(r))).unwrap(); let still = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap(); sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On -> enter -> enters = 2 let after_flip = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap(); *got2.lock().unwrap() = (after_start, still, after_flip); }); assert_eq!(*got.lock().unwrap(), (1, 1, 2)); } // A handler that panics tears the loop down; the in-flight call's reply channel // closes as the stack unwinds, so the parked caller wakes with Down (mirrors // gen_server's panicking-handler path). #[test] fn call_to_panicking_handler_is_down() { let got = Arc::new(Mutex::new(None::>)); let got2 = got.clone(); run(move || { let sw = Sm::start(Switch::Off); let r = sw.call(|rep| Ev::Call(Call::Boom(rep))); *got2.lock().unwrap() = Some(r); }); assert_eq!(*got.lock().unwrap(), Some(Err(CallError::Down))); }