Files
hsmc/hsmc/tests/tokio_nested_alternation.rs
T
swaits bc7e17e58e chore: prepare 0.1.0 for crates.io publish
Add LICENSE-MIT and LICENSE-APACHE at workspace root and in each member package so the dual MIT OR Apache-2.0 license ships in the published .crate.

Fill in publish metadata (authors, repository, homepage, readme, keywords, categories) across the workspace and both members.

Add CHANGELOG.md with a 0.1.0 initial release entry.

Fix clippy lints: unused enumerate index, collapsible if, Default impls for TimerTable, SimRadio, and Ctx, use RangeInclusive::contains.

Exclude examples/microwave_tui.rs from the published package. The file stays in-tree but has known unawaited async log futures that need a dedicated pass before it ships.

Apply cargo fmt across the workspace.
2026-04-22 09:13:04 -06:00

143 lines
4.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Regression: a nested `default`-chain toggle (Splash.{LearnMore, Info}) must
//! keep alternating after the parent subtree has been exited to a sibling
//! (Dashboard) and re-entered via the parent's default.
//!
//! The firmware saw: on fresh boot, alternation between the two substates
//! worked perfectly. After one round-trip out to Dashboard and back,
//! alternation stalled. This test reproduces the scenario in async `run()`
//! so we can see it fail without hardware.
#![cfg(feature = "tokio")]
use hsmc::{statechart, Duration};
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct Ctx {
pub log: Arc<Mutex<Vec<&'static str>>>,
}
#[derive(Debug, Clone)]
pub enum Ev {
Go,
Back,
Halt,
}
statechart! {
M {
context: Ctx;
events: Ev;
default(On);
terminate(Halt);
state On {
on(Go) => Listening;
on(Back) => Splash;
default(Splash);
state Splash {
default(LearnMore);
state LearnMore {
entry: enter_lm;
on(after Duration::from_secs(5)) => Info;
}
state Info {
entry: enter_info;
on(after Duration::from_secs(5)) => LearnMore;
}
}
state Dashboard {
default(Listening);
state Listening {
entry: enter_listening;
// Repeating tick that matches the firmware's
// `on(every 1s) => advance_sparkline, paint_listening`.
on(every Duration::from_millis(1000)) => tick_listen;
}
}
}
}
}
impl MActions for MActionContext<'_> {
async fn enter_lm(&mut self) {
self.log.lock().unwrap().push("LM");
}
async fn enter_info(&mut self) {
self.log.lock().unwrap().push("INFO");
}
async fn enter_listening(&mut self) {
self.log.lock().unwrap().push("LISTEN");
}
async fn tick_listen(&mut self) {
// Intentionally mirror the firmware: the tick handler does a
// short async await (simulating a display flush). This is the
// kind of thing that lengthens `step()` and exposes subtle
// timing bugs in the run loop.
tokio::time::sleep(Duration::from_millis(20)).await;
}
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn nested_alternation_survives_round_trip() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut m = M::new(Ctx { log: log.clone() });
let sender = m.sender();
// Driver: exercise the full scenario, then halt.
tokio::spawn(async move {
// Let LM alternate with INFO for three cycles.
tokio::time::sleep(Duration::from_secs(5)).await; // LM→INFO
tokio::time::sleep(Duration::from_secs(5)).await; // INFO→LM
tokio::time::sleep(Duration::from_secs(5)).await; // LM→INFO
// Jump out to Listening. The machine will now tick every 1s
// with a 20ms-await tick handler — the firmware's actual shape.
let _ = sender.send(Ev::Go);
// Let several ticks fire (simulates a short RX session).
tokio::time::sleep(Duration::from_millis(3500)).await;
// Back to Splash. Splash's default should land us in LM.
let _ = sender.send(Ev::Back);
tokio::time::sleep(Duration::from_millis(50)).await;
// Regression: alternation must continue after re-entry.
tokio::time::sleep(Duration::from_secs(5)).await; // LM→INFO
tokio::time::sleep(Duration::from_secs(5)).await; // INFO→LM
// Second round-trip to be sure it wasn't a one-off.
let _ = sender.send(Ev::Go);
tokio::time::sleep(Duration::from_millis(2500)).await;
let _ = sender.send(Ev::Back);
tokio::time::sleep(Duration::from_millis(50)).await;
tokio::time::sleep(Duration::from_secs(5)).await; // LM→INFO
tokio::time::sleep(Duration::from_secs(5)).await; // INFO→LM
let _ = sender.send(Ev::Halt);
});
let res = tokio::time::timeout(Duration::from_secs(300), m.run())
.await
.expect("run() hung");
assert!(res.is_ok(), "run() returned error: {:?}", res);
let log = log.lock().unwrap();
// Between LISTEN events, LM and INFO must alternate perfectly.
// Expected overall: LM INFO LM INFO LISTEN LM INFO LM LISTEN LM INFO LM.
assert_eq!(
&log[..],
&[
"LM", "INFO", "LM", "INFO", // fresh-boot alternation ×3
"LISTEN", // Go #1
"LM", "INFO", "LM", // Back #1 + two 5s ticks
"LISTEN", // Go #2
"LM", "INFO", "LM", // Back #2 + two 5s ticks
][..],
"log did not match expected sequence: {:?}",
&log[..]
);
}