From 0cf6b80396611d369b6c5c86305e32f823e2307c Mon Sep 17 00:00:00 2001 From: smarm-agent Date: Sat, 20 Jun 2026 10:48:33 +0000 Subject: [PATCH] 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 --- .../{statem_fused.rs => gen_statem_fused.rs} | 24 ++- .../{statem_macro.rs => gen_statem_macro.rs} | 22 +-- src/gen_statem.rs | 165 +++++------------- src/lib.rs | 4 +- tests/gen_statem.rs | 108 ++++++++++++ tests/statem.rs | 142 --------------- 6 files changed, 177 insertions(+), 288 deletions(-) rename examples/{statem_fused.rs => gen_statem_fused.rs} (90%) rename examples/{statem_macro.rs => gen_statem_macro.rs} (88%) create mode 100644 tests/gen_statem.rs delete mode 100644 tests/statem.rs diff --git a/examples/statem_fused.rs b/examples/gen_statem_fused.rs similarity index 90% rename from examples/statem_fused.rs rename to examples/gen_statem_fused.rs index 4897093..4d1531f 100644 --- a/examples/statem_fused.rs +++ b/examples/gen_statem_fused.rs @@ -1,11 +1,9 @@ -//! SPIKE (throwaway) — RFC 017 "fused" approach. +//! The hand-written **expansion target** of the `gen_statem!` macro: the same +//! machine as `examples/gen_statem_macro.rs`, written out in full so the +//! primitives can be judged standing on their own. The macro generates exactly +//! this shape; nothing here needs the macro to be correct or safe. //! -//! This is the hand-written *expansion target* of the eventual `statem!` macro, -//! written out in full so the primitives can be judged standing on their own. -//! A macro would generate exactly this from a `transitions { … }` block; nothing -//! here needs the macro to be correct or safe. -//! -//! The design, as settled over the prior iterations: +//! The design: //! //! * States and events are real enums. An invalid state is unrepresentable; //! there are no bitflags, no `u32` superpositions, no unsafe unions. @@ -27,18 +25,18 @@ //! wired in is dead code — and dead_code is denied below, making an orphan //! handler a compile error. //! -//! * Drops onto the committed `Machine` / `Resolution` / `Cx` primitives with -//! no change to `src/statem.rs`. +//! * Drops onto the `Machine` / `Resolution` / `Cx` primitives with no change +//! to `src/gen_statem.rs`. //! //! Default build is clean and runs. A BREAK-CASE MENU at the bottom documents //! how to make each of the four guarantees fire. //! -//! Run: `cargo run --example statem_fused` +//! Run: `cargo run --example gen_statem_fused` #![deny(dead_code, unreachable_patterns)] use smarm::run; -use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, StatemRef}; +use smarm::gen_statem::{spawn, Cx, Machine, Reply, Resolution, GenStatemRef}; // === user types ============================================================ @@ -114,7 +112,7 @@ struct DoorSm { } impl DoorSm { - fn start(init: Door) -> StatemRef { + fn start(init: Door) -> GenStatemRef { spawn(DoorSm { state: init, data: Data { enters: 0, pushes: 0 }, @@ -184,7 +182,7 @@ impl Machine for DoorSm { self.state = s; // sole writer of the state cell self.enter(cx); } - Resolution::Postpone => unreachable!("postpone lands in chunk 3"), + Resolution::Postpone => unreachable!("postpone is not generated yet"), Resolution::Unhandled => cx.on_unhandled(), } } diff --git a/examples/statem_macro.rs b/examples/gen_statem_macro.rs similarity index 88% rename from examples/statem_macro.rs rename to examples/gen_statem_macro.rs index 6b961e4..660325c 100644 --- a/examples/statem_macro.rs +++ b/examples/gen_statem_macro.rs @@ -1,18 +1,18 @@ -//! RFC 017 — the **same** machine as `examples/statem_fused.rs`, written through -//! the `gen_statem!` macro. Diff this file against that one to see exactly what -//! the macro buys: every `// ===` section there that was boilerplate (the `Ev` +//! The **same** machine as `examples/gen_statem_fused.rs`, written through the +//! `gen_statem!` macro. Diff this file against that one to see exactly what the +//! macro buys: every `// ===` section there that was boilerplate (the `Ev` //! enum, the `DoorSm` struct, `start`, the whole `Machine` impl, the `enter` //! dispatch, the stay/transition apply-tail) collapses into the invocation //! below. What stays hand-written is what carries meaning: the four types, the //! per-state successor enum, and the handler fns. //! //! The point of the exercise is that the macro is *pure sugar*: the four -//! compile-time guarantees the fused spike demonstrates are properties of the -//! emitted code, not of the macro, so they survive expansion unchanged. The +//! compile-time guarantees the hand-written form demonstrates are properties of +//! the emitted code, not of the macro, so they survive expansion unchanged. The //! BREAK-CASE MENU at the bottom is the same four cases, re-expressed against //! the macro surface — flip any one on and the compiler fires identically. //! -//! Run: `cargo run --example statem_macro` +//! Run: `cargo run --example gen_statem_macro` #![deny(dead_code)] // guarantee #3 (orphan handlers); the macro denies the // dispatch's own unreachable_patterns internally. @@ -21,7 +21,7 @@ use smarm::gen_statem; use smarm::run; use smarm::gen_statem::Reply; -// === user types (identical to statem_fused.rs) ============================= +// === user types (identical to gen_statem_fused.rs) ========================= #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Door { @@ -91,7 +91,7 @@ gen_statem! { // You name the bindings the bodies use; the macro can't lend you its own // `self`/`cx` across macro hygiene. `data` = &mut Data, `prev` = current - // state tag, `cx` = context handle (unused in chunk 1). + // state tag, `cx` = context handle (unused here). context(data, prev, cx); enter { @@ -145,7 +145,7 @@ fn main() { // =========================================================================== // BREAK-CASE MENU — the four guarantees, through the macro. Each fires exactly -// as it does in the hand-written statem_fused.rs. +// as it does in the hand-written gen_statem_fused.rs. // // 1. ORPHAN HANDLER (dead_code -> error): // add `fn on_slam(_d: &mut Data) {}` and don't reference it. @@ -157,8 +157,8 @@ fn main() { // NOTE: this example is a separate crate from `smarm`, so rustc's // in_external_macro rule SILENCES this lint here even though the macro // denies it — the duplicate compiles. The guarantee is real only for -// machines defined inside the smarm crate (see the unit test in -// src/statem.rs). This is the documented macro_rules! limitation. +// machines defined inside the `smarm` crate itself. This is the documented +// macro_rules! limitation. // // 3. MISSING PAIR (non-exhaustive match, E0004): // delete the `cast Cast::Push | Cast::Pull | Cast::Lock => unhandled,` diff --git a/src/gen_statem.rs b/src/gen_statem.rs index 18e4f24..0dbfd41 100644 --- a/src/gen_statem.rs +++ b/src/gen_statem.rs @@ -1,50 +1,45 @@ -//! gen_statem — generic finite state machine behaviour (RFC 017). +//! gen_statem — generic finite state machine behaviour. //! //! The sibling of [`gen_server`](crate::gen_server): where a `gen_server` //! carries one undifferentiated blob of state and a single `handle` that //! re-derives "what mode am I in" on every message, a `gen_statem` makes the -//! state an explicit **tag**, routes event handling by it, and (in later -//! chunks) adds the machinery state machines need — state-entry callbacks, a -//! timeout taxonomy, and event postponement. +//! state an explicit **tag** and routes event handling by it. //! //! ## What this layer is //! -//! This module is the **runtime support** a state machine runs on, *not* the -//! authoring surface. A machine is any type implementing [`Machine`]: it owns -//! its state tag and data, and its [`handle`](Machine::handle) reduces a -//! `(state, event)` pair to a [`Resolution`]. The loop here drives it — spawn, +//! This module is the **runtime support** a state machine runs on. A machine is +//! any type implementing [`Machine`]: it owns its state tag and data, and its +//! [`handle`](Machine::handle) reduces a `(state, event)` pair to a +//! [`Resolution`]. The loop here drives it — spawn, //! [`on_start`](Machine::on_start), then one [`handle`](Machine::handle) per //! inbox event — mirroring `gen_server`'s spawn/teardown idioms. //! -//! The RFC's `statem!` macro (deferred) would *generate* a `Machine` impl from -//! a declarative `transitions { … }` graph plus per-state handler blocks, and -//! add the expansion-time edge-lint. Until then a machine is hand-written -//! against these primitives; see `examples/statem_switch.rs` for the shape the -//! macro would target. +//! The [`gen_statem!`](crate::gen_statem) macro is the authoring surface: it +//! *generates* a `Machine` impl from per-state handler blocks. Edge-validity is +//! not a separate check — it falls out of the generated total `match (state, +//! event)` under denied lints (a forgotten or duplicated pair is a compile +//! error), so no proc-macro is needed. See `examples/gen_statem_macro.rs` for +//! the macro form and `examples/gen_statem_fused.rs` for the hand-written shape +//! it expands to. //! //! ## The unified event //! -//! A machine's [`Ev`](Machine::Ev) is the single payload its inbox carries. -//! By convention (and in the macro's desugaring) it folds the user's `cast` -//! and `call` enums together with the runtime's own internal events: +//! A machine's [`Ev`](Machine::Ev) is the single payload its inbox carries. By +//! convention (and in the macro's desugaring) it folds the user's `cast` and +//! `call` enums together: //! //! ```ignore -//! enum Ev { Cast(MyCast), Call(MyCall) /* later: StateTimeout, Timeout(name) */ } +//! enum Ev { Cast(MyCast), Call(MyCall) } //! ``` //! -//! [`StatemRef::send`] pushes any event (a cast is just a `send`); -//! [`StatemRef::call`] builds a one-shot [`Reply`] channel, hands it to a +//! [`GenStatemRef::send`] pushes any event (a cast is just a `send`); +//! [`GenStatemRef::call`] builds a one-shot [`Reply`] channel, hands it to a //! `call` variant, and parks until the machine answers — exactly the //! `gen_server` call round-trip, but with the reply handle riding *inside* the -//! user's own event so a handler can answer (or, later, postpone) it. +//! user's own event so a handler can answer it. //! -//! ## Chunk status (RFC 017 §Sequencing) -//! -//! This is **chunk 1**: macro-free dispatch against real time — spawn, -//! `on_start`, stay/transition, and the `enter` callback (a method on the -//! machine, run on entry to a state). [`Resolution::Postpone`] and the timeout -//! arming on [`Cx`] are part of the type surface but are not yet acted on; they -//! land in chunks 2–3. +//! [`Resolution::Postpone`] and the timeout arming on [`Cx`] are part of the +//! type surface but are not yet wired up. use crate::channel::{channel, Receiver, Sender}; use crate::pid::Pid; @@ -85,7 +80,7 @@ pub trait Machine: Send + 'static { // --------------------------------------------------------------------------- /// The outcome of handling one event, before the loop/handler acts on it: -/// dispatch reduces to `(state, event) -> Resolution` (RFC §Semantics). +/// dispatch reduces to `(state, event) -> Resolution`. /// /// [`From`](From) is why a bare state tag works as an arm tail and why /// "stay" needs no keyword — `Tag.into()` is `To(Tag)`, and the handler treats @@ -95,7 +90,7 @@ pub enum Resolution { /// run `enter`); a **stay** when `s == current` (no `enter`). To(S), /// Defer the current event onto the postpone queue, to be replayed after - /// the next real transition. Produced by `cx.postpone()` (chunk 3); present + /// the next real transition. Produced by `cx.postpone()`; present /// here for forward-compatibility but not yet generated. Postpone, /// No arm matched `(state, event)`: log-and-drop via @@ -116,11 +111,11 @@ impl From for Resolution { /// The context handle injected into [`Machine::on_start`] and /// [`Machine::handle`]. Non-state outcomes (postpone, timeout arming) live here -/// as method calls rather than keywords (RFC §"Non-state outcomes live on cx"). +/// as method calls rather than keywords. /// -/// In chunk 1 it carries only the [`on_unhandled`](Self::on_unhandled) hook; -/// `cx.state_timeout(d)` / `cx.timeout(name, d)` (chunk 2) and `cx.postpone()` -/// (chunk 3) attach here as those chunks land. It lives only on the actor's own +/// For now it carries only the [`on_unhandled`](Self::on_unhandled) hook; +/// `cx.state_timeout(d)` / `cx.timeout(name, d)` and `cx.postpone()` will attach +/// here when implemented. It lives only on the actor's own /// stack and is never sent. pub struct Cx { _ev: PhantomData Ev>, @@ -132,7 +127,7 @@ impl Cx { } /// The default for an event no arm matched: **log-and-drop**. Overridable - /// hook wiring is a follow-on (RFC Open Q5); for now an unmatched event is + /// hook wiring is a follow-on; for now an unmatched event is /// silently dropped, as `gen_server` does with unexpected messages. pub fn on_unhandled(&mut self) {} } @@ -143,14 +138,14 @@ impl Cx { /// The reply side of a synchronous `call`, carried *inside* the machine's own /// `call` event variant (`GetCount(Reply)`). Move-only: answering consumes -/// it, so a handler replies at most once. Built by [`StatemRef::call`]; the +/// it, so a handler replies at most once. Built by [`GenStatemRef::call`]; the /// caller parks on the matching receiver until `reply` is invoked (or the /// machine dies, closing the channel). /// /// Carrying the handle in the event — rather than the loop owning a reply slot /// — is what lets a later chunk **postpone a call**: the whole event, reply /// handle included, moves onto the postpone queue and is answered by a later -/// state (RFC §Postpone). +/// state. pub struct Reply { tx: Sender, } @@ -168,14 +163,14 @@ impl Reply { // Client handle // --------------------------------------------------------------------------- -/// Returned by [`StatemRef::call`] when the machine is unreachable. +/// Returned by [`GenStatemRef::call`] when the machine is unreachable. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum CallError { /// The machine was already gone, or died before replying. Down, } -/// Returned by [`StatemRef::send`] when the machine's inbox is closed. +/// Returned by [`GenStatemRef::send`] when the machine's inbox is closed. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum SendError { /// The machine is gone (its inbox is closed). @@ -183,20 +178,20 @@ pub enum SendError { } /// A clonable handle to a running machine. Cloning yields another sender to the -/// same inbox; the machine lives until the last `StatemRef` is dropped, at which +/// same inbox; the machine lives until the last `GenStatemRef` is dropped, at which /// point its inbox closes and the loop exits. -pub struct StatemRef { +pub struct GenStatemRef { tx: Sender, pid: Pid, } -impl Clone for StatemRef { +impl Clone for GenStatemRef { fn clone(&self) -> Self { - StatemRef { tx: self.tx.clone(), pid: self.pid } + GenStatemRef { tx: self.tx.clone(), pid: self.pid } } } -impl StatemRef { +impl GenStatemRef { /// The machine actor's pid — usable with `monitor`, `request_stop`, `link`. pub fn pid(&self) -> Pid { self.pid @@ -235,15 +230,15 @@ impl StatemRef { // Spawn + loop // --------------------------------------------------------------------------- -/// Spawn `machine` as an actor and hand back its [`StatemRef`]. Shape mirrors +/// Spawn `machine` as an actor and hand back its [`GenStatemRef`]. Shape mirrors /// `gen_server::start`: make the inbox, spawn the loop, return the ref; the /// backing join handle is dropped (lifetime is governed by refs, not joining). /// /// Panics if called outside `Runtime::run()`. -pub fn spawn(machine: M) -> StatemRef { +pub fn spawn(machine: M) -> GenStatemRef { let (tx, rx) = channel::(); let handle = spawn_actor(move || statem_loop(rx, machine)); - StatemRef { tx, pid: handle.pid() } + GenStatemRef { tx, pid: handle.pid() } } /// The machine actor body: `on_start`, then one `handle` per inbox event until @@ -266,7 +261,7 @@ fn statem_loop(rx: Receiver, mut machine: M) { } // --------------------------------------------------------------------------- -// gen_statem! — the authoring macro (RFC 017 §Surface, fused variant) +// gen_statem! — the authoring macro (fused total-match variant) // --------------------------------------------------------------------------- /// Assemble a complete [`Machine`] from hand-written types, a glanceable @@ -393,7 +388,7 @@ fn statem_loop(rx: Receiver, mut machine: M) { /// # What it emits /// /// `enum $Ev { Cast($Cast), Call($Call) }`, `struct $Sm { state, data }`, -/// `$Sm::start(init, data) -> StatemRef<$Sm>`, the `Machine` impl (`on_start` +/// `$Sm::start(init, data) -> GenStatemRef<$Sm>`, the `Machine` impl (`on_start` /// running the initial `enter`; `handle` = the dispatch match + the /// stay/transition/unhandled apply-tail, the cell's sole writer), and the /// `enter` dispatch. Chunk 1: real time, no timers, no postpone. @@ -426,7 +421,7 @@ macro_rules! gen_statem { } impl $sm { - fn start(init: $State, data: $Data) -> $crate::gen_statem::StatemRef<$sm> { + fn start(init: $State, data: $Data) -> $crate::gen_statem::GenStatemRef<$sm> { $crate::gen_statem::spawn($sm { state: init, data }) } @@ -466,7 +461,7 @@ macro_rules! gen_statem { self.enter(s, $cx); } $crate::gen_statem::Resolution::Postpone => { - unreachable!("postpone is unreachable until chunk 3") + unreachable!("postpone is not generated yet") } $crate::gen_statem::Resolution::Unhandled => $cx.on_unhandled(), } @@ -527,73 +522,3 @@ macro_rules! gen_statem { $crate::gen_statem!(@arms ($Ev) ($ss, $se) [ $($arms)* ] $($more)*) }; } - -#[cfg(test)] -mod gen_statem_tests { - use crate::gen_statem::Reply; - - #[derive(Clone, Copy, PartialEq, Eq, Debug)] - enum Switch { - Off, - On, - } - struct Counts { - flips: u32, - enters: u32, - } - enum Cast { - Flip, - Nudge, - } - enum Call { - GetFlips(Reply), - GetEnters(Reply), - } - - fn on_flip(data: &mut Counts) { - data.flips += 1; - } - - crate::gen_statem! { - machine: SwitchSm { state: Switch, data: Counts }; - event: Ev { cast: Cast, call: Call }; - context(data, prev, cx); - - enter { - _ => data.enters += 1, - } - - on Switch::Off => { - cast Cast::Flip => { on_flip(data); Switch::On }, - cast Cast::Nudge => unhandled, - } - on Switch::On => { - cast Cast::Flip => { on_flip(data); Switch::Off }, - cast Cast::Nudge => prev, // explicit stay - } - on _ => { - call Call::GetFlips(r) => { r.reply(data.flips); prev }, - call Call::GetEnters(r) => { r.reply(data.enters); prev }, - } - } - - // NOTE (guarantee #2): because this machine lives in the SAME crate as - // `gen_statem!`, adding a duplicate row here — e.g. a second - // `cast Cast::Nudge => unhandled,` under `on Switch::Off` — is a hard - // `unreachable pattern` error. (From a downstream crate it is silently - // suppressed by rustc's in_external_macro rule; see the macro docs.) - #[test] - fn macro_machine_drives_and_counts() { - crate::run(|| { - let sm = SwitchSm::start(Switch::Off, Counts { flips: 0, enters: 0 }); - sm.send(Ev::Cast(Cast::Flip)).unwrap(); // Off -> On (flip=1, enter) - sm.send(Ev::Cast(Cast::Nudge)).unwrap(); // On: stay (no enter) - sm.send(Ev::Cast(Cast::Flip)).unwrap(); // On -> Off (flip=2, enter) - - let flips = sm.call(|r| Ev::Call(Call::GetFlips(r))).unwrap(); - let enters = sm.call(|r| Ev::Call(Call::GetEnters(r))).unwrap(); - assert_eq!(flips, 2); - assert_eq!(enters, 3); // Off(start) + On + Off - }); - } -} diff --git a/src/lib.rs b/src/lib.rs index e22afd2..e3016be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,8 +59,8 @@ pub use gen_server::{ NamedGenServerBuilder, GenServerBuilder, GenServerCtx, GenServerName, GenServerRef, TimerHandle, Watcher, }; pub use gen_statem::{ - CallError as StatemCallError, Cx, Machine, Reply, Resolution, SendError as StatemSendError, - StatemRef, + CallError as GenStatemCallError, Cx, Machine, Reply, Resolution, SendError as GenStatemSendError, + GenStatemRef, }; pub use introspect::{ actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree, diff --git a/tests/gen_statem.rs b/tests/gen_statem.rs new file mode 100644 index 0000000..3efafe8 --- /dev/null +++ b/tests/gen_statem.rs @@ -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), + 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))); +} diff --git a/tests/statem.rs b/tests/statem.rs deleted file mode 100644 index 6c7631b..0000000 --- a/tests/statem.rs +++ /dev/null @@ -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), - GetEnters(Reply), - Boom(Reply), -} - -enum Ev { - Cast(Cast), - Call(Call), -} - -struct Sm { - state: Switch, - data: Counts, -} - -impl Sm { - fn start(init: Switch) -> StatemRef { - gen_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))); -}