//! Timer / sleep tests. These are time-sensitive and use generous //! tolerances — we care about ordering and "didn't return instantly / //! didn't take forever," not microsecond-precise scheduling. use smarm::{run, sleep, spawn}; use std::sync::Arc; use std::sync::Mutex; use std::time::{Duration, Instant}; #[test] fn sleep_returns_after_at_least_the_requested_duration() { run(|| { let t0 = Instant::now(); sleep(Duration::from_millis(50)); let elapsed = t0.elapsed(); assert!( elapsed >= Duration::from_millis(45), "slept only {:?}, expected ≥ ~50ms", elapsed ); // Loose upper bound — anything wildly slow indicates a bug. assert!( elapsed < Duration::from_millis(500), "slept {:?}, far longer than the 50ms request", elapsed ); }); } #[test] fn shorter_sleep_wakes_first() { let log: Arc>> = Arc::new(Mutex::new(Vec::new())); let l1 = log.clone(); let l2 = log.clone(); run(move || { let h1 = spawn(move || { sleep(Duration::from_millis(60)); l1.lock().unwrap().push(1); }); let h2 = spawn(move || { sleep(Duration::from_millis(20)); l2.lock().unwrap().push(2); }); h1.join().unwrap(); h2.join().unwrap(); }); // 2 (shorter sleep) wakes before 1. assert_eq!(*log.lock().unwrap(), vec![2, 1]); } #[test] fn one_sleeping_actor_does_not_block_other_runnable_actors() { let log: Arc>> = Arc::new(Mutex::new(Vec::new())); let l1 = log.clone(); let l2 = log.clone(); run(move || { let h1 = spawn(move || { sleep(Duration::from_millis(100)); l1.lock().unwrap().push(1); }); let h2 = spawn(move || { // Doesn't sleep. Should be able to run while h1 is parked. for _ in 0..3 { l2.lock().unwrap().push(2); smarm::yield_now(); } }); h2.join().unwrap(); h1.join().unwrap(); }); let v = log.lock().unwrap(); // h2 finishes long before h1's 100ms timer. let h2_count = v.iter().filter(|&&x| x == 2).count(); let h1_pos = v.iter().position(|&x| x == 1); assert_eq!(h2_count, 3); // h1's push should land after h2 is fully done. if let Some(p) = h1_pos { assert!(p >= h2_count, "h1 woke before h2 finished: log = {:?}", *v); } } #[test] fn zero_duration_sleep_yields_but_does_not_park_forever() { // A zero-duration sleep should behave like yield_now: control returns // promptly without hanging. run(|| { let t0 = Instant::now(); sleep(Duration::from_millis(0)); assert!(t0.elapsed() < Duration::from_millis(100)); }); } #[test] fn many_concurrent_sleepers_all_wake() { let counter = Arc::new(std::sync::atomic::AtomicU32::new(0)); let c = counter.clone(); run(move || { let mut handles = Vec::new(); for i in 0..20u64 { let cc = c.clone(); handles.push(spawn(move || { // Stagger so they don't all coalesce to the same wake. sleep(Duration::from_millis(5 + i * 2)); cc.fetch_add(1, std::sync::atomic::Ordering::SeqCst); })); } for h in handles { h.join().unwrap(); } }); assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 20); } // --------------------------------------------------------------------------- // Direct tests on the Timers data structure. No scheduler involved — these // cover the new Reason machinery without needing a Mutex implementation. // --------------------------------------------------------------------------- use smarm::pid::Pid; use smarm::timer::{Reason, TimerTarget, Timers}; struct RecordingTarget { calls: Mutex>, } impl TimerTarget for RecordingTarget { fn on_timeout(&self, pid: Pid, epoch: u32) { self.calls.lock().unwrap().push((pid, epoch)); } } #[test] fn timers_pop_due_returns_entries_in_deadline_order() { let mut t = Timers::new(); let now = Instant::now(); // Insert out of order; pop_due should hand them back sorted by deadline. t.insert_sleep(now + Duration::from_millis(30), Pid::new(0, 0), 1); t.insert_sleep(now + Duration::from_millis(10), Pid::new(1, 0), 1); t.insert_sleep(now + Duration::from_millis(20), Pid::new(2, 0), 1); // Advance past all of them. let due = t.pop_due(now + Duration::from_millis(50)); let pids: Vec = due.iter().map(|e| e.pid.index()).collect(); assert_eq!(pids, vec![1, 2, 0]); assert!(t.is_empty()); } #[test] fn timers_only_pop_entries_whose_deadline_has_passed() { let mut t = Timers::new(); let now = Instant::now(); t.insert_sleep(now + Duration::from_millis(5), Pid::new(0, 0), 1); t.insert_sleep(now + Duration::from_millis(100), Pid::new(1, 0), 1); let due = t.pop_due(now + Duration::from_millis(20)); assert_eq!(due.len(), 1); assert_eq!(due[0].pid.index(), 0); assert!(!t.is_empty()); // The unpopped entry's deadline is still visible. assert!(t.peek_deadline().is_some()); } #[test] fn timers_mix_sleep_and_wait_timeout_reasons() { let mut t = Timers::new(); let target = Arc::new(RecordingTarget { calls: Mutex::new(Vec::new()) }); let now = Instant::now(); t.insert_sleep(now + Duration::from_millis(5), Pid::new(0, 0), 1); t.insert( now + Duration::from_millis(10), Pid::new(1, 0), Reason::WaitTimeout { target: target.clone(), epoch: 42 }, ); let due = t.pop_due(now + Duration::from_millis(20)); assert_eq!(due.len(), 2); // Order: Sleep (5ms) first, WaitTimeout (10ms) second. match &due[0].reason { Reason::Sleep { .. } => {} _ => panic!("first entry should be a Sleep"), } match &due[1].reason { Reason::WaitTimeout { epoch, .. } => assert_eq!(*epoch, 42), _ => panic!("second entry should be a WaitTimeout"), } } #[test] fn same_deadline_entries_pop_in_insertion_order() { // The `seq` tiebreaker means inserting two entries with the same // deadline preserves the order they were inserted. let mut t = Timers::new(); let now = Instant::now(); let d = now + Duration::from_millis(10); t.insert_sleep(d, Pid::new(0, 0), 1); t.insert_sleep(d, Pid::new(1, 0), 1); t.insert_sleep(d, Pid::new(2, 0), 1); let due = t.pop_due(now + Duration::from_millis(20)); let pids: Vec = due.iter().map(|e| e.pid.index()).collect(); assert_eq!(pids, vec![0, 1, 2]); } // --------------------------------------------------------------------------- // send_after / cancel_timer — the message-delivery timer substrate. // // Unit tests drive `Timers` directly with a flag-flipping fire thunk (no // runtime needed, mirroring RecordingTarget above). Integration tests drive // the public scheduler API and assert real registry-resolved delivery. // --------------------------------------------------------------------------- use std::sync::atomic::{AtomicBool, Ordering}; // Pull the Send fire thunk out of a popped entry and run it. fn run_fire(entry: smarm::timer::Entry) { match entry.reason { Reason::Send { fire } => fire(), _ => panic!("expected a Send entry"), } } #[test] fn armed_send_timer_is_returned_and_fires() { let mut t = Timers::new(); let now = Instant::now(); let fired = Arc::new(AtomicBool::new(false)); let f = fired.clone(); let _id = t.insert_send( now + Duration::from_millis(10), Pid::new(0, 0), Box::new(move || f.store(true, Ordering::SeqCst)), ); let mut due = t.pop_due(now + Duration::from_millis(20)); assert_eq!(due.len(), 1, "an armed send timer should pop when due"); assert!(!fired.load(Ordering::SeqCst), "pop must not fire on its own"); run_fire(due.pop().unwrap()); assert!(fired.load(Ordering::SeqCst), "running the thunk delivers"); assert!(t.is_empty()); } #[test] fn cancelled_send_timer_is_discarded_not_returned() { let mut t = Timers::new(); let now = Instant::now(); let fired = Arc::new(AtomicBool::new(false)); let f = fired.clone(); let id = t.insert_send( now + Duration::from_millis(10), Pid::new(0, 0), Box::new(move || f.store(true, Ordering::SeqCst)), ); assert!(t.cancel(id), "cancel before fire returns true"); let due = t.pop_due(now + Duration::from_millis(20)); assert!(due.is_empty(), "a cancelled send timer must not pop"); assert!(!fired.load(Ordering::SeqCst)); } #[test] fn cancel_after_fire_returns_false() { // The race signal Mark wanted: cancelling a timer that already fired tells // you it was too late. let mut t = Timers::new(); let now = Instant::now(); let id = t.insert_send( now + Duration::from_millis(5), Pid::new(0, 0), Box::new(|| {}), ); let due = t.pop_due(now + Duration::from_millis(10)); assert_eq!(due.len(), 1); assert!(!t.cancel(id), "cancel after the timer fired returns false"); } #[test] fn cancel_unknown_id_returns_false() { let mut t = Timers::new(); let now = Instant::now(); let id = t.insert_send(now + Duration::from_millis(5), Pid::new(0, 0), Box::new(|| {})); assert!(t.cancel(id)); // Second cancel of the same id: already gone. assert!(!t.cancel(id)); } #[test] fn send_timers_interleave_with_sleep_in_deadline_order() { let mut t = Timers::new(); let now = Instant::now(); t.insert_sleep(now + Duration::from_millis(30), Pid::new(0, 0), 1); let _id = t.insert_send(now + Duration::from_millis(10), Pid::new(1, 0), Box::new(|| {})); t.insert_sleep(now + Duration::from_millis(20), Pid::new(2, 0), 1); let due = t.pop_due(now + Duration::from_millis(50)); assert_eq!(due.len(), 3); // 10ms Send, then 20ms Sleep, then 30ms Sleep. assert!(matches!(due[0].reason, Reason::Send { .. })); assert_eq!(due[1].pid.index(), 2); assert_eq!(due[2].pid.index(), 0); } #[test] fn clear_drops_armed_send_timers() { let mut t = Timers::new(); let now = Instant::now(); let id = t.insert_send(now + Duration::from_millis(10), Pid::new(0, 0), Box::new(|| {})); t.clear(); assert!(t.is_empty()); // The arm record is gone too: cancelling reports nothing to cancel. assert!(!t.cancel(id)); } // --- Integration: real delivery through the scheduler + registry. --- use smarm::{cancel_timer, channel, register, send_after_named, Name}; #[test] fn send_after_named_delivers_after_the_delay() { const PING: Name = Name::new("send_after_ping"); run(|| { let (tx, rx) = channel::(); register(PING, tx).unwrap(); let t0 = Instant::now(); let _id = send_after_named(Duration::from_millis(30), PING, 99); assert_eq!(rx.recv().unwrap(), 99); assert!( t0.elapsed() >= Duration::from_millis(25), "delivered too early: {:?}", t0.elapsed() ); }); } #[test] fn cancel_timer_prevents_delivery() { const C: Name = Name::new("send_after_cancel"); run(|| { let (tx, rx) = channel::(); register(C, tx).unwrap(); let id = send_after_named(Duration::from_millis(50), C, 7); assert!(cancel_timer(id), "cancel before fire returns true"); sleep(Duration::from_millis(90)); assert_eq!(rx.try_recv(), Ok(None), "cancelled timer delivered anyway"); }); } #[test] fn send_after_to_unresolved_name_is_silent() { const NOPE: Name = Name::new("send_after_nobody_home"); run(|| { // Nobody registered NOPE; firing resolves to nothing and is dropped. let _id = send_after_named(Duration::from_millis(10), NOPE, 1); sleep(Duration::from_millis(40)); // let it fire and no-op // Reaching here without a panic is the assertion. }); } // --- Integration: typed Pid delivery (exercises send_to on fire). --- use smarm::{send_after, send_to, spawn_addr, Addressable, Receiver}; struct Sink; impl Addressable for Sink { type Msg = u64; } #[test] fn send_after_delivers_to_typed_pid() { run(|| { // A reply channel so the test actor learns what Sink received. let (report_tx, report_rx) = channel::(); let sink: Pid = spawn_addr::(move |rx: Receiver| { if let Ok(v) = rx.recv() { let _ = report_tx.send(v); } }); let _id = send_after(Duration::from_millis(25), sink, 1234); assert_eq!(report_rx.recv().unwrap(), 1234); }); } #[test] fn send_after_to_dead_typed_pid_is_silent() { run(|| { // Sink exits immediately after handling one message; arm a second // delivery for after it's gone. The fire-time send_to returns Dead and // is dropped — no panic. let (report_tx, report_rx) = channel::(); let sink: Pid = spawn_addr::(move |rx: Receiver| { if let Ok(v) = rx.recv() { let _ = report_tx.send(v); } // body returns -> actor exits }); send_to(sink, 1).unwrap(); assert_eq!(report_rx.recv().unwrap(), 1); // sink has now exited let _id = send_after(Duration::from_millis(15), sink, 2); sleep(Duration::from_millis(45)); // let it fire against the dead pid // No panic; the sink is gone, so its report sender dropped with it — // closed+empty is Err (documented), which also proves nothing // further was delivered. assert!(report_rx.try_recv().is_err(), "nothing further delivered"); }); } // --------------------------------------------------------------------------- // Wall-anchored send_after (RFC 007 user-facing opt-out). The API exists in // both feature configs; featureless it is behaviourally identical to // `send_after` — these tests pin exactly that. // --------------------------------------------------------------------------- #[test] fn armed_wall_send_timer_is_returned_and_fires() { let mut t = Timers::new(); let now = Instant::now(); let fired = Arc::new(AtomicBool::new(false)); let f = fired.clone(); let _id = t.insert_send_wall( now + Duration::from_millis(10), Pid::new(0, 0), Box::new(move || f.store(true, Ordering::SeqCst)), ); let mut due = t.pop_due(now + Duration::from_millis(20)); assert_eq!(due.len(), 1, "an armed wall send timer should pop when due"); run_fire(due.pop().unwrap()); assert!(fired.load(Ordering::SeqCst), "running the thunk delivers"); assert!(t.is_empty()); } use smarm::send_after_named_wall; #[test] fn send_after_named_wall_delivers_after_the_delay() { const WPING: Name = Name::new("send_after_wall_ping"); run(|| { let (tx, rx) = channel::(); register(WPING, tx).unwrap(); let t0 = Instant::now(); let _id = send_after_named_wall(Duration::from_millis(30), WPING, 99); assert_eq!(rx.recv().unwrap(), 99); assert!( t0.elapsed() >= Duration::from_millis(25), "delivered too early: {:?}", t0.elapsed() ); }); } #[test] fn send_after_named_wall_cancels() { const WC: Name = Name::new("send_after_wall_cancel"); run(|| { let (tx, rx) = channel::(); register(WC, tx).unwrap(); let id = send_after_named_wall(Duration::from_millis(50), WC, 7); assert!(cancel_timer(id), "cancel before fire returns true"); sleep(Duration::from_millis(90)); assert_eq!(rx.try_recv(), Ok(None), "cancelled wall timer delivered"); }); }