The trampoline caught the root's panic, recorded it as Outcome::Panic on the slot, and run() dropped the initial handle without reading it — every assert inside run(), the standard test-suite pattern, was silently vacuous (found live: a failing-first test passed). run() now reads the root outcome before the handle drop and resume_unwinds the payload after full teardown, so a caller's catch_unwind leaves the Runtime reusable; Exit and Stopped return normally. The payload message is printed before re-raising (the throw-site hook output was suppressed in-actor). Correct the two tests this unmasked, both born failing and never run: the select loser-arm test kept a closed arm in the set (the documented closed-arm rule: a closed arm reports ready forever — observe the disconnect and drop it); the send_after-to-dead test expected Ok(None) from a closed+empty channel (documented: Err(RecvError), which proves nothing-delivered even more strongly).
467 lines
16 KiB
Rust
467 lines
16 KiB
Rust
//! 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<Mutex<Vec<u8>>> = 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<Mutex<Vec<u8>>> = 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<Vec<(Pid, u32)>>,
|
|
}
|
|
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<u32> = 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<u32> = 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<u64> = Name::new("send_after_ping");
|
|
run(|| {
|
|
let (tx, rx) = channel::<u64>();
|
|
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<u64> = Name::new("send_after_cancel");
|
|
run(|| {
|
|
let (tx, rx) = channel::<u64>();
|
|
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<u64> = 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<A> 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::<u64>();
|
|
let sink: Pid<Sink> = spawn_addr::<Sink>(move |rx: Receiver<u64>| {
|
|
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::<u64>();
|
|
let sink: Pid<Sink> = spawn_addr::<Sink>(move |rx: Receiver<u64>| {
|
|
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<u64> = Name::new("send_after_wall_ping");
|
|
run(|| {
|
|
let (tx, rx) = channel::<u64>();
|
|
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<u64> = Name::new("send_after_wall_cancel");
|
|
run(|| {
|
|
let (tx, rx) = channel::<u64>();
|
|
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");
|
|
});
|
|
}
|