mod common;
use std::sync::Arc;
use std::time::Duration;
use common::*;
use vertep_runtime::journal::EntryBody;
use vertep_runtime::{Address, Envelope, Node, NodeConfig, VirtualClock};
use vertep_store::MemoryStore;
const REMIND: &str = r##"<state id="s">
<transition event="arm"><send event="remind" delay="5s" id="r1"/></transition>
<transition event="stop"><cancel sendid="r1"/></transition>
<transition event="remind"/>
</state>"##;
fn names(node: &Node, a: &Address) -> Vec<String> {
node.journal(a)
.unwrap()
.iter()
.filter_map(|e| match &e.body {
EntryBody::Input { input, .. } => Some(input.event.name.clone()),
_ => None,
})
.collect()
}
fn armed() -> (Node, VirtualClock, Arc<MemoryStore>, Address) {
let (mut node, clock, store) = fresh();
node.register_type(&ty("r"), scxml("s", REMIND)).unwrap();
let a = va("r", "a");
node.send(&a, ev("arm")).unwrap();
node.run_until_idle();
(node, clock, store, a)
}
#[test]
fn timer_recorded_with_its_macrostep() {
let (node, _clock, _store, a) = armed();
let j = node.journal(&a).unwrap();
let eff = j[0].effects().unwrap();
assert_eq!(eff.timers.len(), 1);
let t = &eff.timers[0];
assert_eq!(t.send_id, "r1");
assert_eq!(t.due_ms, j[0].timestamp_ms + 5_000);
assert_eq!(t.event.name, "remind");
assert_eq!(t.recipient, a.as_str());
assert!(eff.sends.is_empty(), "held as a timer, not dispatched");
}
#[test]
fn timer_survives_a_crash() {
let store = Arc::new(MemoryStore::new());
let clock = VirtualClock::new(T0);
let mut n1 = node_on(store.clone(), &clock, NodeConfig::default());
n1.register_type(&ty("r"), scxml("s", REMIND)).unwrap();
let a = va("r", "a");
n1.send(&a, ev("arm")).unwrap();
n1.run_until_idle();
drop(n1); clock.advance(Duration::from_secs(2));
let mut n2 = node_on(store.clone(), &clock, NodeConfig::default());
n2.register_type(&ty("r"), scxml("s", REMIND)).unwrap();
n2.run_until_idle();
assert_eq!(names(&n2, &a), vec!["arm"], "not due yet");
clock.advance(Duration::from_secs(3));
n2.run_until_idle();
assert_eq!(names(&n2, &a), vec!["arm", "remind"]);
assert_eq!(n2.journal(&a).unwrap()[1].timestamp_ms, T0 + 5_000);
}
#[test]
fn due_timer_delivered() {
let (mut node, clock, _store, a) = armed();
clock.advance(Duration::from_secs(5));
node.run_until_idle();
let j = node.journal(&a).unwrap();
assert_eq!(j.len(), 2);
let EntryBody::Input { input, .. } = &j[1].body else {
panic!()
};
assert_eq!(input.event.name, "remind");
let timer = &j[0].effects().unwrap().timers[0];
assert_eq!(input.timer.as_deref(), Some(timer.iri.as_str()));
assert_eq!(
input.message_id, timer.iri,
"message id derived from the send id"
);
}
#[test]
fn timer_fires_once() {
let (mut node, clock, _store, a) = armed();
let timer = node.journal(&a).unwrap()[0].effects().unwrap().timers[0].clone();
clock.advance(Duration::from_secs(5));
node.run_until_idle();
let mut env = Envelope::new(timer.message_id(0), &a, timer.event.clone());
env.timer = Some(timer.iri.clone());
node.send_envelope(env).unwrap();
node.run_until_idle();
assert_eq!(names(&node, &a), vec!["arm", "remind"]);
}
#[test]
fn cancel_before_due() {
let (mut node, clock, _store, a) = armed();
node.send(&a, ev("stop")).unwrap();
node.run_until_idle();
let j = node.journal(&a).unwrap();
let timer = j[0].effects().unwrap().timers[0].iri.clone();
assert_eq!(
j[1].effects().unwrap().cancels,
vec![timer],
"the cancellation is journaled"
);
clock.advance(Duration::from_secs(60));
node.run_until_idle();
assert_eq!(names(&node, &a), vec!["arm", "stop"], "never delivered");
assert!(node.pending_timers().is_empty());
}
#[test]
fn cancel_after_fired() {
let (mut node, clock, _store, a) = armed();
clock.advance(Duration::from_secs(5));
node.run_until_idle();
node.send(&a, ev("stop")).unwrap();
node.run_until_idle();
assert_eq!(
names(&node, &a),
vec!["arm", "remind", "stop"],
"no error event"
);
assert!(
node.journal(&a).unwrap()[2]
.effects()
.unwrap()
.cancels
.is_empty()
);
}
#[test]
fn reminder_wakes_an_idle_actor() {
let store = Arc::new(MemoryStore::new());
let clock = VirtualClock::new(T0);
let mut node = node_on(
store,
&clock,
NodeConfig {
idle_timeout_ms: 60_000,
..NodeConfig::default()
},
);
node.register_type(
&ty("sleeper"),
scxml(
"s",
r##"<state id="s"><transition event="arm"><send event="wake" delay="3600s"/></transition>
<transition event="wake" target="awake"/></state><state id="awake"/>"##,
),
)
.unwrap();
let a = va("sleeper", "a");
node.send(&a, ev("arm")).unwrap();
node.run_until_idle();
clock.advance(Duration::from_secs(120));
node.run_until_idle();
assert!(!node.is_active(&a), "deactivated for idleness");
clock.advance(Duration::from_secs(3600 - 120));
node.run_until_idle();
assert!(node.is_active(&a));
assert_eq!(node.state(&a).unwrap().configuration, vec!["awake"]);
}
#[test]
fn replay_reproduces_due_times() {
let (mut node, clock, _store, a) = armed();
let due = node.pending_timers()[0].0;
clock.advance(Duration::from_secs(2));
node.deactivate(&a);
node.activate_actor(&a).unwrap();
assert_eq!(node.pending_timers().len(), 1);
assert_eq!(
node.pending_timers()[0].0,
due,
"computed from the journaled timestamp"
);
clock.advance(Duration::from_secs(3));
node.run_until_idle();
assert_eq!(names(&node, &a), vec!["arm", "remind"]);
node.deactivate(&a);
node.activate_actor(&a).unwrap();
node.run_until_idle();
assert_eq!(names(&node, &a), vec!["arm", "remind"]);
assert!(node.pending_timers().is_empty());
}
const POLLER: &str = r##"<state id="polling">
<onentry><vt:reminder name="poll" period="10min" event="poll.tick"/></onentry>
<transition event="poll.tick"/>
<transition event="stop" target="done"/></state>
<final id="done"/>"##;
#[test]
fn periodic_reminder() {
let (mut node, clock, _store) = fresh();
node.register_type(&ty("poller"), scxml("polling", POLLER))
.unwrap();
let a = va("poller", "a");
node.send(&a, ev("start")).unwrap();
node.run_until_idle();
for _ in 0..3 {
clock.advance(Duration::from_secs(600));
node.run_until_idle();
}
assert_eq!(
names(&node, &a),
vec!["start", "poll.tick", "poll.tick", "poll.tick"]
);
let j = node.journal(&a).unwrap();
let stamps: Vec<i64> = j[1..].iter().map(|e| e.timestamp_ms).collect();
assert_eq!(stamps, vec![T0 + 600_000, T0 + 1_200_000, T0 + 1_800_000]);
let ids: std::collections::BTreeSet<String> =
j[1..].iter().map(|e| e.message_id.clone()).collect();
assert_eq!(ids.len(), 3, "each occurrence is a separate input");
}
#[test]
fn reminder_stops_at_final() {
let (mut node, clock, _store) = fresh();
node.register_type(&ty("poller"), scxml("polling", POLLER))
.unwrap();
let a = va("poller", "a");
node.send(&a, ev("start")).unwrap();
node.run_until_idle();
clock.advance(Duration::from_secs(600));
node.run_until_idle();
node.send(&a, ev("stop")).unwrap();
node.run_until_idle();
assert!(node.state(&a).unwrap().terminated);
assert!(
!node.journal(&a).unwrap()[2]
.effects()
.unwrap()
.cancels
.is_empty()
);
clock.advance(Duration::from_secs(3_600));
node.run_until_idle();
assert_eq!(names(&node, &a), vec!["start", "poll.tick", "stop"]);
assert!(node.pending_timers().is_empty());
}
#[test]
fn catching_up_after_downtime() {
let store = Arc::new(MemoryStore::new());
let clock = VirtualClock::new(T0);
let chart = || {
scxml(
"s",
r##"<state id="s"><onentry>
<vt:reminder name="beat" period="2s" event="beat"/>
<send event="second" delay="7s"/><send event="first" delay="5s"/></onentry>
<transition event="*"/></state>"##,
)
};
let mut n1 = node_on(store.clone(), &clock, NodeConfig::default());
n1.register_type(&ty("c"), chart()).unwrap();
let a = va("c", "a");
n1.send(&a, ev("start")).unwrap();
n1.run_until_idle();
drop(n1);
clock.advance(Duration::from_millis(10_500));
let mut n2 = node_on(store.clone(), &clock, NodeConfig::default());
n2.register_type(&ty("c"), chart()).unwrap();
n2.run_until_idle();
assert_eq!(names(&n2, &a), vec!["start", "first", "second", "beat"]);
clock.advance(Duration::from_millis(1_500));
n2.run_until_idle();
assert_eq!(
names(&n2, &a),
vec!["start", "first", "second", "beat", "beat"],
"back on period"
);
}