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::().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::().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::().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::().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::().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::().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"); }