gen_statem: GenStatem* type prefix + cleanup

- 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
This commit is contained in:
smarm-agent
2026-06-20 10:48:33 +00:00
parent 3e316066c3
commit 0cf6b80396
6 changed files with 177 additions and 288 deletions
+108
View File
@@ -0,0 +1,108 @@
//! 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)));
}
-142
View File
@@ -1,142 +0,0 @@
//! 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::gen_statem` primitives directly, the same way a
//! `statem!`-generated machine eventually will.
use smarm::run;
use smarm::gen_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<u32>),
GetEnters(Reply<u32>),
Boom(Reply<u32>),
}
enum Ev {
Cast(Cast),
Call(Call),
}
struct Sm {
state: Switch,
data: Counts,
}
impl Sm {
fn start(init: Switch) -> StatemRef<Sm> {
gen_statem::spawn(Sm { state: init, data: Counts { flips: 0, enters: 0 } })
}
fn enter(&mut self, _s: Switch, _cx: &mut Cx<Ev>) {
self.data.enters += 1;
}
}
impl Machine for Sm {
type Ev = Ev;
fn on_start(&mut self, cx: &mut Cx<Ev>) {
let s = self.state;
self.enter(s, cx);
}
fn handle(&mut self, ev: Ev, cx: &mut Cx<Ev>) {
let prev = self.state;
let next: Resolution<Switch> = 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::<Result<u32, CallError>>));
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)));
}