- rename StatemRef/StatemCallError/StatemSendError -> GenStatem*
- move the inline unit test out of src; consolidate the Switch coverage
onto a single macro-driven harness in tests/gen_statem.rs
- drop the redundant hand-written Switch test machine and the two
untracked rejected-direction probes (succ_enums, typed_edges)
- rename examples statem_{fused,macro}.rs -> gen_statem_{fused,macro}.rs
- strip RFC/chunk/spike provenance and fix the mislabeled "throwaway"
example header and dead cross-references
109 lines
3.5 KiB
Rust
109 lines
3.5 KiB
Rust
//! 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<u32>),
|
|
GetEnters(Reply<u32>),
|
|
Boom(Reply<u32>),
|
|
}
|
|
|
|
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::<Result<u32, CallError>>));
|
|
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)));
|
|
}
|