#![cfg(feature = "async")]
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use lazily::{AsyncContext, AsyncStateMachine};
use tokio::sync::Barrier;
#[derive(PartialEq, Clone, Debug)]
enum Light {
Red,
Green,
Yellow,
}
#[derive(Debug)]
enum Tick {
Advance,
}
#[tokio::test]
async fn async_machine_transitions_through_all_states() {
let ctx = AsyncContext::new();
let m = AsyncStateMachine::new(&ctx, Light::Red, |s, _: &Tick| match s {
Light::Red => Some(Light::Green),
Light::Green => Some(Light::Yellow),
Light::Yellow => Some(Light::Red),
});
assert_eq!(m.state(&ctx), Light::Red);
assert!(m.send(&ctx, Tick::Advance));
assert_eq!(m.state(&ctx), Light::Green);
assert!(m.send(&ctx, Tick::Advance));
assert_eq!(m.state(&ctx), Light::Yellow);
assert!(m.send(&ctx, Tick::Advance));
assert_eq!(m.state(&ctx), Light::Red);
}
#[derive(PartialEq, Clone, Debug)]
enum Door {
Closed,
Opening,
Open,
Closing,
}
#[derive(Debug)]
enum DoorEvent {
ButtonPressed,
FullyOpen,
FullyClosed,
}
fn garage_door(ctx: &AsyncContext) -> AsyncStateMachine<Door, DoorEvent> {
AsyncStateMachine::new(ctx, Door::Closed, |s, e| match (s, e) {
(Door::Closed, DoorEvent::ButtonPressed) => Some(Door::Opening),
(Door::Opening, DoorEvent::FullyOpen) => Some(Door::Open),
(Door::Open, DoorEvent::ButtonPressed) => Some(Door::Closing),
(Door::Closing, DoorEvent::FullyClosed) => Some(Door::Closed),
(Door::Closing, DoorEvent::ButtonPressed) => Some(Door::Opening),
_ => None,
})
}
#[tokio::test]
async fn async_machine_rejects_invalid_transition() {
let ctx = AsyncContext::new();
let m = garage_door(&ctx);
assert_eq!(m.state(&ctx), Door::Closed);
assert!(!m.send(&ctx, DoorEvent::FullyOpen));
assert_eq!(m.state(&ctx), Door::Closed);
assert!(m.send(&ctx, DoorEvent::ButtonPressed));
assert_eq!(m.state(&ctx), Door::Opening);
assert!(!m.send(&ctx, DoorEvent::FullyClosed));
assert_eq!(m.state(&ctx), Door::Opening);
}
#[tokio::test]
async fn async_machine_self_transition_is_accepted_but_no_invalidation() {
let ctx = AsyncContext::new();
let call_count = Arc::new(Mutex::new(0usize));
let call_count_inner = call_count.clone();
let m = AsyncStateMachine::new(&ctx, 0i32, move |s, _: &()| {
*call_count_inner.lock().unwrap() += 1;
Some(*s)
});
let state = m.state_handle();
let recomputes = Arc::new(Mutex::new(0usize));
let recomputes_inner = recomputes.clone();
let _watch = ctx.computed_async(move |cctx| {
let r = recomputes_inner.clone();
async move {
*r.lock().unwrap() += 1;
cctx.get(&state)
}
});
let _ = ctx.get_async(&_watch).await;
let baseline = *recomputes.lock().unwrap();
assert!(m.send(&ctx, ()));
assert_eq!(m.state(&ctx), 0);
assert_eq!(*recomputes.lock().unwrap(), baseline);
}
#[tokio::test]
async fn async_derived_slot_updates_on_transition() {
let ctx = AsyncContext::new();
let m = garage_door(&ctx);
let state = m.state_handle();
let label = ctx.computed_async(move |cctx| {
let s = state;
async move {
match cctx.get(&s) {
Door::Closed => "closed",
Door::Opening => "opening",
Door::Open => "open",
Door::Closing => "closing",
}
}
});
assert_eq!(ctx.get_async(&label).await, "closed");
m.send(&ctx, DoorEvent::ButtonPressed);
assert_eq!(ctx.get_async(&label).await, "opening");
}
#[derive(PartialEq, Clone, Debug)]
enum ActiveDoor {
Primary,
Secondary,
}
#[derive(Debug)]
enum ActiveDoorEvent {
Toggle,
}
#[tokio::test]
async fn async_derived_slot_drops_stale_machine_dependency_after_branch_switch() {
let ctx = AsyncContext::new();
let active = AsyncStateMachine::new(
&ctx,
ActiveDoor::Primary,
|s, _: &ActiveDoorEvent| match s {
ActiveDoor::Primary => Some(ActiveDoor::Secondary),
ActiveDoor::Secondary => Some(ActiveDoor::Primary),
},
);
let primary = garage_door(&ctx);
let secondary = garage_door(&ctx);
let active_state = active.state_handle();
let primary_state = primary.state_handle();
let secondary_state = secondary.state_handle();
let recomputes = Arc::new(Mutex::new(0usize));
let recomputes_inner = recomputes.clone();
let selected_label = ctx.computed_async(move |cctx| {
let recomputes = recomputes_inner.clone();
async move {
*recomputes.lock().unwrap() += 1;
match cctx.get(&active_state) {
ActiveDoor::Primary => match cctx.get(&primary_state) {
Door::Closed => "primary:closed",
Door::Opening => "primary:opening",
Door::Open => "primary:open",
Door::Closing => "primary:closing",
},
ActiveDoor::Secondary => match cctx.get(&secondary_state) {
Door::Closed => "secondary:closed",
Door::Opening => "secondary:opening",
Door::Open => "secondary:open",
Door::Closing => "secondary:closing",
},
}
}
});
assert_eq!(ctx.get_async(&selected_label).await, "primary:closed");
primary.send(&ctx, DoorEvent::ButtonPressed);
assert_eq!(ctx.get_async(&selected_label).await, "primary:opening");
active.send(&ctx, ActiveDoorEvent::Toggle);
assert_eq!(ctx.get_async(&selected_label).await, "secondary:closed");
let after_switch = *recomputes.lock().unwrap();
primary.send(&ctx, DoorEvent::FullyOpen);
assert_eq!(ctx.get_async(&selected_label).await, "secondary:closed");
assert_eq!(
*recomputes.lock().unwrap(),
after_switch,
"branch switch must remove the stale primary state dependency"
);
secondary.send(&ctx, DoorEvent::ButtonPressed);
assert_eq!(ctx.get_async(&selected_label).await, "secondary:opening");
assert_eq!(*recomputes.lock().unwrap(), after_switch + 1);
}
#[tokio::test]
async fn async_eager_signal_tracks_machine_state() {
let ctx = AsyncContext::new();
let m = garage_door(&ctx);
let state = m.state_handle();
let observed = Arc::new(Mutex::new(Vec::<Door>::new()));
let observed_inner = observed.clone();
let _sig = ctx.signal_async(move |cctx| {
let o = observed_inner.clone();
async move {
let s = cctx.get(&state);
o.lock().unwrap().push(s.clone());
s
}
});
tokio::time::sleep(Duration::from_millis(50)).await;
m.send(&ctx, DoorEvent::ButtonPressed);
tokio::time::sleep(Duration::from_millis(50)).await;
m.send(&ctx, DoorEvent::FullyOpen);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(
observed.lock().unwrap().clone(),
vec![Door::Closed, Door::Opening, Door::Open]
);
}
#[tokio::test]
async fn async_on_transition_fires_with_old_and_new() {
let ctx = AsyncContext::new();
let m = garage_door(&ctx);
let transitions = Arc::new(Mutex::new(Vec::<(Door, Door)>::new()));
let transitions_inner = transitions.clone();
let _observer = m.on_transition(&ctx, move |old, new| {
transitions_inner
.lock()
.unwrap()
.push((old.clone(), new.clone()));
});
tokio::time::sleep(Duration::from_millis(50)).await;
m.send(&ctx, DoorEvent::ButtonPressed);
tokio::time::sleep(Duration::from_millis(50)).await;
m.send(&ctx, DoorEvent::FullyOpen);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(
transitions.lock().unwrap().clone(),
vec![(Door::Closed, Door::Opening), (Door::Opening, Door::Open)]
);
}
#[tokio::test]
async fn async_on_transition_does_not_fire_on_rejected_event() {
let ctx = AsyncContext::new();
let m = garage_door(&ctx);
let count = Arc::new(Mutex::new(0usize));
let count_inner = count.clone();
let _observer = m.on_transition(&ctx, move |_, _| {
*count_inner.lock().unwrap() += 1;
});
tokio::time::sleep(Duration::from_millis(50)).await;
m.send(&ctx, DoorEvent::FullyOpen);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(*count.lock().unwrap(), 0);
m.send(&ctx, DoorEvent::ButtonPressed);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(*count.lock().unwrap(), 1);
}
#[tokio::test]
async fn async_state_is_signal_reflects_current_state() {
let ctx = AsyncContext::new();
let m = garage_door(&ctx);
let is_open = m.state_is(&ctx, Door::Open);
let is_closed = m.state_is(&ctx, Door::Closed);
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(!is_open.get(&ctx).unwrap_or(false));
assert!(is_closed.get(&ctx).unwrap_or(false));
m.send(&ctx, DoorEvent::ButtonPressed);
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(!is_open.get(&ctx).unwrap_or(false));
assert!(!is_closed.get(&ctx).unwrap_or(false));
m.send(&ctx, DoorEvent::FullyOpen);
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(is_open.get(&ctx).unwrap_or(false));
assert!(!is_closed.get(&ctx).unwrap_or(false));
}
#[tokio::test]
async fn async_disposing_on_transition_stops_observing() {
let ctx = AsyncContext::new();
let m = garage_door(&ctx);
let count = Arc::new(Mutex::new(0usize));
let count_inner = count.clone();
let observer = m.on_transition(&ctx, move |_, _| {
*count_inner.lock().unwrap() += 1;
});
tokio::time::sleep(Duration::from_millis(50)).await;
m.send(&ctx, DoorEvent::ButtonPressed);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(*count.lock().unwrap(), 1);
ctx.dispose_async_effect(&observer);
m.send(&ctx, DoorEvent::FullyOpen);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(*count.lock().unwrap(), 1);
}
#[tokio::test]
async fn async_recreating_on_transition_observer_starts_fresh_after_dispose() {
let ctx = AsyncContext::new();
let m = garage_door(&ctx);
let first = Arc::new(Mutex::new(Vec::<(Door, Door)>::new()));
let first_inner = first.clone();
let observer = m.on_transition(&ctx, move |old, new| {
first_inner.lock().unwrap().push((old.clone(), new.clone()));
});
tokio::time::sleep(Duration::from_millis(50)).await;
m.send(&ctx, DoorEvent::ButtonPressed);
tokio::time::sleep(Duration::from_millis(50)).await;
ctx.dispose_async_effect(&observer);
let second = Arc::new(Mutex::new(Vec::<(Door, Door)>::new()));
let second_inner = second.clone();
let _observer = m.on_transition(&ctx, move |old, new| {
second_inner
.lock()
.unwrap()
.push((old.clone(), new.clone()));
});
tokio::time::sleep(Duration::from_millis(50)).await;
m.send(&ctx, DoorEvent::FullyOpen);
tokio::time::sleep(Duration::from_millis(50)).await;
m.send(&ctx, DoorEvent::ButtonPressed);
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(
first.lock().unwrap().clone(),
vec![(Door::Closed, Door::Opening)],
"disposed observer must not leak callbacks after recreation"
);
assert_eq!(
second.lock().unwrap().clone(),
vec![(Door::Opening, Door::Open), (Door::Open, Door::Closing)],
"new observer should seed from current state and only see future transitions"
);
}
#[tokio::test]
async fn async_batched_transactions_settle_to_final_state() {
let ctx = AsyncContext::new();
let m = garage_door(&ctx);
ctx.batch(|ctx| {
m.send(ctx, DoorEvent::ButtonPressed);
m.send(ctx, DoorEvent::FullyOpen);
});
assert_eq!(m.state(&ctx), Door::Open);
}
#[tokio::test]
async fn async_machine_handles_concurrent_send_and_state_reads() {
let ctx = Arc::new(AsyncContext::new());
let m = AsyncStateMachine::new(&ctx, 0usize, |_, next: &usize| Some(*next));
let start = Arc::new(Barrier::new(5));
let writer_ctx = Arc::clone(&ctx);
let writer_machine = m.clone();
let writer_start = Arc::clone(&start);
let writer = tokio::spawn(async move {
writer_start.wait().await;
for value in 1..=250 {
assert!(writer_machine.send(&writer_ctx, value));
}
});
let mut readers = (0..4)
.map(|_| {
let reader_ctx = Arc::clone(&ctx);
let reader_machine = m.clone();
let reader_start = Arc::clone(&start);
tokio::spawn(async move {
reader_start.wait().await;
let mut last_seen = 0usize;
for _ in 0..250 {
let current = reader_machine.state(&reader_ctx);
assert!(current <= 250);
last_seen = last_seen.max(current);
tokio::task::yield_now().await;
}
last_seen
})
})
.collect::<Vec<_>>();
writer.await.unwrap();
let mut max_seen = 0usize;
for reader in readers.drain(..) {
max_seen = max_seen.max(reader.await.unwrap());
}
assert_eq!(m.state(&ctx), 250);
assert!(max_seen <= 250);
}