feat: full runtime redesign (v0.6)
Complete rewrite with improved architecture & correctness: - src/runtime.rs: Simplified task scheduling with proper state transitions - src/scheduler.rs: Decoupled from runtime, pure task queue logic - src/io.rs, src/mutex.rs: Refactored for clarity & performance - New actor model framework (src/actor.rs, src/context.rs) - Channel primitives (src/channel.rs) & process IDs (src/pid.rs) - Preemption framework (src/preempt.rs) for fair timeslicing - Expanded benchmarks & tests (multi_scheduler, primes, runtime)
This commit is contained in:
207
tests/timer.rs
Normal file
207
tests/timer.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
//! 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, u64)>>,
|
||||
}
|
||||
impl TimerTarget for RecordingTarget {
|
||||
fn on_timeout(&self, pid: Pid, seq: u64) {
|
||||
self.calls.lock().unwrap().push((pid, seq));
|
||||
}
|
||||
}
|
||||
|
||||
#[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));
|
||||
t.insert_sleep(now + Duration::from_millis(10), Pid::new(1, 0));
|
||||
t.insert_sleep(now + Duration::from_millis(20), Pid::new(2, 0));
|
||||
|
||||
// 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));
|
||||
t.insert_sleep(now + Duration::from_millis(100), Pid::new(1, 0));
|
||||
|
||||
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));
|
||||
t.insert(
|
||||
now + Duration::from_millis(10),
|
||||
Pid::new(1, 0),
|
||||
Reason::WaitTimeout { target: target.clone(), wait_seq: 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 { wait_seq, .. } => assert_eq!(*wait_seq, 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));
|
||||
t.insert_sleep(d, Pid::new(1, 0));
|
||||
t.insert_sleep(d, Pid::new(2, 0));
|
||||
|
||||
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]);
|
||||
}
|
||||
Reference in New Issue
Block a user