143 lines
4.4 KiB
Rust
143 lines
4.4 KiB
Rust
//! 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)));
|
|
}
|