Split monolithic test file into domain-specific test modules: - common/mod.rs: shared messages, actors, helpers - runtime_lifecycle.rs (57 tests): spawn, FIFO, threading, panic safety - runtime_mechanics.rs (10): placement, backpressure, cleanup, recovery - runtime_hooks.rs (12): on_start/on_stop, graceful stop - runtime_timers.rs (6): one-shot and interval timers - runtime_registry.rs (32): naming, monitoring, groups, ask pattern - runtime_supervision.rs (20): supervisor + router - runtime_hasher.rs (3): identity hasher correctness All 140 tests pass. Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
213 lines
6.7 KiB
Rust
213 lines
6.7 KiB
Rust
mod common;
|
|
use common::*;
|
|
|
|
// ── Timer Helpers ─────────────────────────────────────────────────────────
|
|
|
|
/// Actor that schedules a one-shot timer in on_start: sends a Ping to target after N ticks.
|
|
struct TimerStartActor {
|
|
target: ActorAddress,
|
|
delay_ticks: u64,
|
|
}
|
|
|
|
impl ActorInterface for TimerStartActor {
|
|
type Incoming = Ping;
|
|
type Response = Pong;
|
|
|
|
fn on_start(&mut self, ctx: &Ctx) {
|
|
ctx.send_after_ticks(self.target, Ping { reply_to: ctx.self_addr() }, self.delay_ticks);
|
|
}
|
|
|
|
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {}
|
|
}
|
|
|
|
/// Actor that schedules a one-shot timer when it receives a Forward message.
|
|
struct DelayPingPongActor;
|
|
|
|
impl ActorInterface for DelayPingPongActor {
|
|
type Incoming = Forward;
|
|
type Response = Done;
|
|
|
|
fn handle(&mut self, ctx: &Ctx, msg: Forward) {
|
|
ctx.send_after_ticks(msg.reply_to, Done(msg.value), 3);
|
|
}
|
|
}
|
|
|
|
/// Actor that schedules an interval timer on start: sends Ping every N ticks.
|
|
struct HeartbeatActor {
|
|
target: ActorAddress,
|
|
period: u64,
|
|
}
|
|
|
|
impl ActorInterface for HeartbeatActor {
|
|
type Incoming = Ping;
|
|
type Response = Pong;
|
|
|
|
fn on_start(&mut self, ctx: &Ctx) {
|
|
ctx.send_interval_ticks(self.target, Ping { reply_to: ctx.self_addr() }, self.period);
|
|
}
|
|
|
|
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {}
|
|
}
|
|
|
|
// ── Timer Tests ───────────────────────────────────────────────────────────
|
|
|
|
/// Given an actor that schedules a one-shot timer in on_start,
|
|
/// when enough ticks pass,
|
|
/// then the timer message is delivered to the target.
|
|
#[test]
|
|
fn one_shot_timer_fires_after_n_ticks() {
|
|
let rt = std_runtime(RuntimeConfig::default());
|
|
let inbox = rt.new_inbox::<Ping>().unwrap();
|
|
|
|
let _timer_actor = rt.spawn(TimerStartActor {
|
|
target: *inbox.addr(),
|
|
delay_ticks: 3,
|
|
}).unwrap();
|
|
|
|
// Tick 1: on_start schedules timer (fire_at = current_tick + 3 = 4)
|
|
rt.tick(); // tick 1: on_start, timer scheduled
|
|
assert!(inbox.try_recv().is_none(), "no delivery before delay");
|
|
|
|
rt.tick(); // tick 2
|
|
assert!(inbox.try_recv().is_none(), "no delivery on tick 2");
|
|
|
|
rt.tick(); // tick 3
|
|
assert!(inbox.try_recv().is_none(), "no delivery on tick 3");
|
|
|
|
rt.tick(); // tick 4: timer fires
|
|
let msg = inbox.try_recv();
|
|
assert!(msg.is_some(), "timer message delivered after 3-tick delay");
|
|
}
|
|
|
|
/// Given an actor that schedules a one-shot timer from a message handler,
|
|
/// when enough ticks pass after the triggering message,
|
|
/// then the delayed response arrives.
|
|
#[test]
|
|
fn handler_can_schedule_one_shot_timer() {
|
|
let rt = std_runtime(RuntimeConfig::default());
|
|
let inbox = rt.new_inbox::<Done>().unwrap();
|
|
|
|
let addr = rt.spawn(DelayPingPongActor).unwrap();
|
|
|
|
let _ = rt.send_to(addr, Forward { value: 42, reply_to: *inbox.addr() });
|
|
rt.tick(); // process Forward, schedule timer (delay=3)
|
|
|
|
assert!(inbox.try_recv().is_none(), "no immediate reply");
|
|
|
|
rt.tick(); // tick 2
|
|
rt.tick(); // tick 3
|
|
assert!(inbox.try_recv().is_none(), "not yet");
|
|
|
|
rt.tick(); // tick 4: timer fires
|
|
let reply = inbox.try_recv();
|
|
assert_eq!(reply, Some(Done(42)), "delayed reply arrives after 3 ticks");
|
|
}
|
|
|
|
/// Given a one-shot timer,
|
|
/// when it fires,
|
|
/// then it does NOT fire again on subsequent ticks (consumed).
|
|
#[test]
|
|
fn one_shot_timer_fires_only_once() {
|
|
let rt = std_runtime(RuntimeConfig::default());
|
|
let inbox = rt.new_inbox::<Ping>().unwrap();
|
|
|
|
let _timer_actor = rt.spawn(TimerStartActor {
|
|
target: *inbox.addr(),
|
|
delay_ticks: 1,
|
|
}).unwrap();
|
|
|
|
rt.tick(); // on_start schedules timer
|
|
rt.tick(); // timer fires
|
|
assert!(inbox.try_recv().is_some(), "first fire");
|
|
|
|
// Subsequent ticks should NOT fire again
|
|
for _ in 0..5 { rt.tick(); }
|
|
assert!(inbox.try_recv().is_none(), "one-shot does not repeat");
|
|
}
|
|
|
|
/// Given an interval timer with period 2,
|
|
/// when multiple ticks pass,
|
|
/// then the timer fires repeatedly every 2 ticks.
|
|
#[test]
|
|
fn interval_timer_fires_repeatedly() {
|
|
let rt = std_runtime(RuntimeConfig::default());
|
|
let inbox = rt.new_inbox::<Ping>().unwrap();
|
|
|
|
let _heartbeat = rt.spawn(HeartbeatActor {
|
|
target: *inbox.addr(),
|
|
period: 2,
|
|
}).unwrap();
|
|
|
|
rt.tick(); // tick 1: on_start, interval scheduled (next_fire = current + 2 = 3)
|
|
assert!(inbox.try_recv().is_none(), "no fire on tick 1");
|
|
|
|
rt.tick(); // tick 2
|
|
assert!(inbox.try_recv().is_none(), "no fire on tick 2");
|
|
|
|
rt.tick(); // tick 3: first fire
|
|
assert!(inbox.try_recv().is_some(), "fire on tick 3");
|
|
|
|
rt.tick(); // tick 4
|
|
assert!(inbox.try_recv().is_none(), "no fire on tick 4");
|
|
|
|
rt.tick(); // tick 5: second fire
|
|
assert!(inbox.try_recv().is_some(), "fire on tick 5");
|
|
|
|
rt.tick(); // tick 6
|
|
assert!(inbox.try_recv().is_none(), "no fire on tick 6");
|
|
|
|
rt.tick(); // tick 7: third fire
|
|
assert!(inbox.try_recv().is_some(), "fire on tick 7");
|
|
}
|
|
|
|
/// Given an interval timer targeting an actor that gets stopped,
|
|
/// when the actor is removed,
|
|
/// then the interval timer is cleaned up (no orphan timers).
|
|
#[test]
|
|
fn interval_timer_cleaned_up_when_actor_dies() {
|
|
let rt = std_runtime(RuntimeConfig::default());
|
|
let _inbox = rt.new_inbox::<Ping>().unwrap();
|
|
|
|
// Heartbeat sends to a counter that we'll kill
|
|
let counter_addr = rt.spawn(CounterActor { count: 0 }).unwrap();
|
|
|
|
// HeartbeatActor sends Ping to counter every tick
|
|
let _hb = rt.spawn(HeartbeatActor {
|
|
target: counter_addr,
|
|
period: 1,
|
|
}).unwrap();
|
|
|
|
// Let it run a few ticks
|
|
for _ in 0..3 { rt.tick(); }
|
|
|
|
// Stop the counter
|
|
rt.stop_actor(counter_addr).unwrap();
|
|
for _ in 0..5 { rt.tick(); }
|
|
|
|
// Counter is gone, interval timer should be GC'd.
|
|
let stats = rt.stats();
|
|
// Only the heartbeat actor should remain
|
|
assert_eq!(stats.workers[0].num_actors, 1);
|
|
}
|
|
|
|
/// Given a timer with delay 0,
|
|
/// when the next tick fires,
|
|
/// then the message is delivered immediately on the next tick.
|
|
#[test]
|
|
fn timer_with_zero_delay_fires_next_tick() {
|
|
let rt = std_runtime(RuntimeConfig::default());
|
|
let inbox = rt.new_inbox::<Ping>().unwrap();
|
|
|
|
let _timer_actor = rt.spawn(TimerStartActor {
|
|
target: *inbox.addr(),
|
|
delay_ticks: 0,
|
|
}).unwrap();
|
|
|
|
rt.tick(); // on_start schedules timer with delay=0
|
|
// Timer requests are processed after tick_all (phase 5.5)
|
|
// Timer fires on the NEXT tick (phase 2.5)
|
|
assert!(inbox.try_recv().is_none(), "not yet -- timer fires next tick");
|
|
|
|
rt.tick(); // timer fires
|
|
assert!(inbox.try_recv().is_some(), "zero-delay timer fires on next tick");
|
|
}
|