//! gen_statem behaviour tests, driven through the `gen_statem!` macro: cast/call //! round-trip, `enter` firing on start and on every real transition (but not on //! a stay), and the machine-down path when a handler panics. use smarm::gen_statem; use smarm::gen_statem::{CallError, Reply}; use smarm::run; use std::sync::{Arc, Mutex}; // A two-state machine: Flip toggles, calls read counters, 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), } gen_statem! { machine: Sm { state: Switch, data: Counts }; event: Ev { cast: Cast, call: Call }; context(data, prev, cx); enter { _ => data.enters += 1, } on Switch::Off => { cast Cast::Flip => { data.flips += 1; Switch::On }, } on Switch::On => { cast Cast::Flip => Switch::Off, } // State-independent queries: reply, then stay via `prev`. Boom panics // (`boom()` is typed as a state tag so the arm stays well-formed). on _ => { call Call::GetFlips(r) => { r.reply(data.flips); prev }, call Call::GetEnters(r) => { r.reply(data.enters); prev }, call Call::Boom(_r) => boom(), } } fn boom() -> Switch { panic!("boom") } // 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, Counts { flips: 0, enters: 0 }); sw.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On (flips = 1) sw.send(Ev::Cast(Cast::Flip)).unwrap(); // On -> Off (no flip count) 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, Counts { flips: 0, enters: 0 }); // enter -> 1 let after_start = sw.call(|r| Ev::Call(Call::GetEnters(r))).unwrap(); // Two stays (the reads) 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 -> 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, Counts { flips: 0, enters: 0 }); let r = sw.call(|rep| Ev::Call(Call::Boom(rep))); *got2.lock().unwrap() = Some(r); }); assert_eq!(*got.lock().unwrap(), Some(Err(CallError::Down))); }