chore: reset working tree (v0.5)
Temporary commit clearing working tree for v0.6 rebuild
This commit is contained in:
110
tests/channel.rs
110
tests/channel.rs
@@ -1,110 +0,0 @@
|
||||
//! Channel tests. These run under the scheduler because `recv()` needs to
|
||||
//! be able to park, which requires a live runtime.
|
||||
|
||||
use smarm::{channel, run, spawn};
|
||||
use std::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
static OUT: Cell<i64> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_then_recv_same_actor() {
|
||||
OUT.with(|c| c.set(0));
|
||||
run(|| {
|
||||
let (tx, rx) = channel::<i64>();
|
||||
tx.send(42).unwrap();
|
||||
let v = rx.recv().unwrap();
|
||||
OUT.with(|c| c.set(v));
|
||||
});
|
||||
assert_eq!(OUT.with(|c| c.get()), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recv_parks_until_send_from_other_actor() {
|
||||
OUT.with(|c| c.set(0));
|
||||
run(|| {
|
||||
let (tx, rx) = channel::<i64>();
|
||||
let h = spawn(move || {
|
||||
// This actor blocks on an empty channel.
|
||||
let v = rx.recv().unwrap();
|
||||
OUT.with(|c| c.set(v));
|
||||
});
|
||||
// Parent runs, then yields to let the child block,
|
||||
// then sends, then joins.
|
||||
smarm::yield_now();
|
||||
tx.send(7).unwrap();
|
||||
h.join().unwrap();
|
||||
});
|
||||
assert_eq!(OUT.with(|c| c.get()), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_messages_arrive_in_order() {
|
||||
let captured: std::sync::Arc<std::sync::Mutex<Vec<i64>>> =
|
||||
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let cap2 = captured.clone();
|
||||
|
||||
run(move || {
|
||||
let (tx, rx) = channel::<i64>();
|
||||
let h = spawn(move || {
|
||||
for _ in 0..3 {
|
||||
let v = rx.recv().unwrap();
|
||||
cap2.lock().unwrap().push(v);
|
||||
}
|
||||
});
|
||||
for v in 1..=3i64 {
|
||||
tx.send(v).unwrap();
|
||||
}
|
||||
h.join().unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(*captured.lock().unwrap(), vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloned_senders_both_deliver() {
|
||||
let captured: std::sync::Arc<std::sync::Mutex<Vec<i64>>> =
|
||||
std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let cap2 = captured.clone();
|
||||
|
||||
run(move || {
|
||||
let (tx, rx) = channel::<i64>();
|
||||
let tx2 = tx.clone();
|
||||
let h = spawn(move || {
|
||||
for _ in 0..2 {
|
||||
let v = rx.recv().unwrap();
|
||||
cap2.lock().unwrap().push(v);
|
||||
}
|
||||
});
|
||||
tx.send(10).unwrap();
|
||||
tx2.send(20).unwrap();
|
||||
h.join().unwrap();
|
||||
});
|
||||
|
||||
let mut got = captured.lock().unwrap().clone();
|
||||
got.sort();
|
||||
assert_eq!(got, vec![10, 20]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recv_returns_err_when_all_senders_dropped() {
|
||||
let saw_err: std::sync::Arc<std::sync::atomic::AtomicBool> =
|
||||
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let saw_err2 = saw_err.clone();
|
||||
|
||||
run(move || {
|
||||
let (tx, rx) = channel::<i64>();
|
||||
let h = spawn(move || {
|
||||
// Receiver waits; no message will ever come.
|
||||
if rx.recv().is_err() {
|
||||
saw_err2.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
smarm::yield_now();
|
||||
drop(tx); // last sender gone; rx.recv must return Err.
|
||||
h.join().unwrap();
|
||||
});
|
||||
|
||||
assert!(saw_err.load(std::sync::atomic::Ordering::SeqCst));
|
||||
}
|
||||
137
tests/context.rs
137
tests/context.rs
@@ -1,137 +0,0 @@
|
||||
//! Low-level context-switch tests. These poke `init_actor_stack` and the
|
||||
//! naked asm shims directly — no scheduler involved.
|
||||
|
||||
use smarm::context::{
|
||||
get_actor_sp, init_actor_stack, set_actor_sp, switch_to_actor, switch_to_scheduler,
|
||||
};
|
||||
use smarm::stack::Stack;
|
||||
use std::cell::Cell;
|
||||
|
||||
thread_local! {
|
||||
static LOG: Cell<u64> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
fn log(v: u64) { LOG.with(|c| c.set(c.get() | v)); }
|
||||
fn get_log() -> u64 { LOG.with(|c| c.get()) }
|
||||
fn reset_log() { LOG.with(|c| c.set(0)); }
|
||||
|
||||
extern "C-unwind" fn actor_simple() {
|
||||
log(0x1);
|
||||
unsafe { switch_to_scheduler() };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actor_runs_and_returns_to_scheduler() {
|
||||
reset_log();
|
||||
let stack = Stack::new(64 * 1024).unwrap();
|
||||
let sp = init_actor_stack(stack.top(), actor_simple);
|
||||
set_actor_sp(sp);
|
||||
unsafe { switch_to_actor() };
|
||||
assert_eq!(get_log(), 0x1);
|
||||
}
|
||||
|
||||
extern "C-unwind" fn actor_two_steps() {
|
||||
log(0x1);
|
||||
unsafe { switch_to_scheduler() };
|
||||
log(0x2);
|
||||
unsafe { switch_to_scheduler() };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actor_yields_and_resumes() {
|
||||
reset_log();
|
||||
let stack = Stack::new(64 * 1024).unwrap();
|
||||
let sp = init_actor_stack(stack.top(), actor_two_steps);
|
||||
set_actor_sp(sp);
|
||||
|
||||
unsafe { switch_to_actor() };
|
||||
assert_eq!(get_log(), 0x1, "after first resume");
|
||||
|
||||
unsafe { switch_to_actor() };
|
||||
assert_eq!(get_log(), 0x1 | 0x2, "after second resume");
|
||||
}
|
||||
|
||||
// Callee-saved registers must survive a yield.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static REG_BEFORE: OnceLock<[u64; 4]> = OnceLock::new();
|
||||
static REG_AFTER: OnceLock<[u64; 4]> = OnceLock::new();
|
||||
|
||||
extern "C-unwind" fn actor_reg_check() {
|
||||
unsafe {
|
||||
let s0: u64 = 0xAAAA_BBBB_0000_0001;
|
||||
let s1: u64 = 0xCCCC_DDDD_0000_0002;
|
||||
let s2: u64 = 0xEEEE_FFFF_0000_0003;
|
||||
let s3: u64 = 0x1111_2222_0000_0004;
|
||||
|
||||
core::arch::asm!(
|
||||
"mov r12, {s0}", "mov r13, {s1}", "mov r14, {s2}", "mov r15, {s3}",
|
||||
s0 = in(reg) s0, s1 = in(reg) s1, s2 = in(reg) s2, s3 = in(reg) s3,
|
||||
out("r12") _, out("r13") _, out("r14") _, out("r15") _,
|
||||
);
|
||||
REG_BEFORE.set([s0, s1, s2, s3]).ok();
|
||||
switch_to_scheduler();
|
||||
|
||||
let a0: u64; let a1: u64; let a2: u64; let a3: u64;
|
||||
core::arch::asm!(
|
||||
"mov {a0}, r12", "mov {a1}, r13", "mov {a2}, r14", "mov {a3}, r15",
|
||||
a0 = out(reg) a0, a1 = out(reg) a1, a2 = out(reg) a2, a3 = out(reg) a3,
|
||||
);
|
||||
REG_AFTER.set([a0, a1, a2, a3]).ok();
|
||||
switch_to_scheduler();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callee_saved_registers_survive_yield() {
|
||||
let stack = Stack::new(64 * 1024).unwrap();
|
||||
let sp = init_actor_stack(stack.top(), actor_reg_check);
|
||||
set_actor_sp(sp);
|
||||
unsafe { switch_to_actor(); switch_to_actor(); }
|
||||
assert_eq!(REG_BEFORE.get().copied().unwrap(), REG_AFTER.get().copied().unwrap());
|
||||
}
|
||||
|
||||
// Two actors, independent stacks.
|
||||
|
||||
thread_local! {
|
||||
static A_VAL: Cell<u64> = const { Cell::new(0) };
|
||||
static B_VAL: Cell<u64> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
extern "C-unwind" fn actor_a() {
|
||||
A_VAL.with(|c| c.set(0xAAAA));
|
||||
unsafe { switch_to_scheduler() };
|
||||
let v = A_VAL.with(|c| c.get());
|
||||
A_VAL.with(|c| c.set(if v == 0xAAAA { 0xA00D } else { 0xDEAD }));
|
||||
unsafe { switch_to_scheduler() };
|
||||
}
|
||||
|
||||
extern "C-unwind" fn actor_b() {
|
||||
B_VAL.with(|c| c.set(0xBBBB));
|
||||
unsafe { switch_to_scheduler() };
|
||||
let v = B_VAL.with(|c| c.get());
|
||||
B_VAL.with(|c| c.set(if v == 0xBBBB { 0xB00D } else { 0xDEAD }));
|
||||
unsafe { switch_to_scheduler() };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_actors_dont_corrupt_each_other() {
|
||||
let stack_a = Stack::new(64 * 1024).unwrap();
|
||||
let stack_b = Stack::new(64 * 1024).unwrap();
|
||||
|
||||
let sp_a = init_actor_stack(stack_a.top(), actor_a);
|
||||
let sp_b = init_actor_stack(stack_b.top(), actor_b);
|
||||
|
||||
set_actor_sp(sp_a); unsafe { switch_to_actor() };
|
||||
let sp_a = get_actor_sp();
|
||||
|
||||
set_actor_sp(sp_b); unsafe { switch_to_actor() };
|
||||
let sp_b = get_actor_sp();
|
||||
|
||||
set_actor_sp(sp_a); unsafe { switch_to_actor() };
|
||||
set_actor_sp(sp_b); unsafe { switch_to_actor() };
|
||||
|
||||
assert_eq!(A_VAL.with(|c| c.get()), 0xA00D);
|
||||
assert_eq!(B_VAL.with(|c| c.get()), 0xB00D);
|
||||
}
|
||||
99
tests/io.rs
99
tests/io.rs
@@ -1,99 +0,0 @@
|
||||
//! Tests for `block_on_io` — running a blocking closure on a worker OS
|
||||
//! thread while the calling actor is parked.
|
||||
|
||||
use smarm::{block_on_io, run, spawn, yield_now};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn block_on_io_returns_the_closures_value() {
|
||||
let captured: Arc<Mutex<Option<u64>>> = Arc::new(Mutex::new(None));
|
||||
let c = captured.clone();
|
||||
run(move || {
|
||||
let v: u64 = block_on_io(|| {
|
||||
// Burn a tiny bit of time so this actually crosses thread.
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
42
|
||||
});
|
||||
*c.lock().unwrap() = Some(v);
|
||||
});
|
||||
assert_eq!(*captured.lock().unwrap(), Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_actors_run_while_block_on_io_is_in_flight() {
|
||||
// While actor A is parked in block_on_io, actor B should be able to
|
||||
// make progress.
|
||||
let order: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let oa = order.clone();
|
||||
let ob = order.clone();
|
||||
|
||||
run(move || {
|
||||
let a = spawn(move || {
|
||||
oa.lock().unwrap().push(1); // A starts first.
|
||||
block_on_io(|| {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
});
|
||||
oa.lock().unwrap().push(4); // A resumes last.
|
||||
});
|
||||
let b = spawn(move || {
|
||||
// Make sure A enters block_on_io first.
|
||||
yield_now();
|
||||
ob.lock().unwrap().push(2);
|
||||
yield_now();
|
||||
ob.lock().unwrap().push(3);
|
||||
});
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
// Required interleaving: 1 (A starts) before 2,3 (B runs while A
|
||||
// is parked), and 4 (A resumes) after 2,3.
|
||||
let v = order.lock().unwrap();
|
||||
assert_eq!(v[0], 1, "log: {:?}", *v);
|
||||
assert_eq!(v[v.len() - 1], 4, "log: {:?}", *v);
|
||||
let pos_2 = v.iter().position(|&x| x == 2).unwrap();
|
||||
let pos_3 = v.iter().position(|&x| x == 3).unwrap();
|
||||
let pos_4 = v.iter().position(|&x| x == 4).unwrap();
|
||||
assert!(pos_2 < pos_4, "B's first step ran after A resumed: {:?}", *v);
|
||||
assert!(pos_3 < pos_4, "B's second step ran after A resumed: {:?}", *v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_concurrent_block_on_io_calls_all_complete() {
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
let c = counter.clone();
|
||||
run(move || {
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..10 {
|
||||
let cc = c.clone();
|
||||
handles.push(spawn(move || {
|
||||
let n: u32 = block_on_io(|| {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
1
|
||||
});
|
||||
cc.fetch_add(n, Ordering::SeqCst);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
});
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_on_io_panic_propagates_to_caller() {
|
||||
let saw_err = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let s = saw_err.clone();
|
||||
run(move || {
|
||||
let h = spawn(move || {
|
||||
// The closure panics on the worker thread; that should
|
||||
// resurface as a panic in this actor.
|
||||
let _: () = block_on_io(|| panic!("boom on io thread"));
|
||||
});
|
||||
if h.join().is_err() {
|
||||
s.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
assert!(saw_err.load(Ordering::SeqCst));
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
//! Tests for epoll-based fd readiness primitives: `wait_readable`,
|
||||
//! `wait_writable`, and the `read`/`write` sugar on top of them.
|
||||
//!
|
||||
//! Pipes are the convenient test target: cheap to create, easy to drive,
|
||||
//! and we already use `libc::pipe2` internally. Each pipe is one direction
|
||||
//! and respects `O_NONBLOCK` if we ask for it.
|
||||
|
||||
use smarm::{run, spawn, wait_readable, wait_writable, yield_now};
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::time::Duration;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pipe helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct Pipe {
|
||||
read: RawFd,
|
||||
write: RawFd,
|
||||
}
|
||||
|
||||
impl Pipe {
|
||||
fn new() -> Self {
|
||||
let mut fds: [libc::c_int; 2] = [0; 2];
|
||||
let r = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
||||
assert_eq!(r, 0, "pipe2 failed");
|
||||
Pipe {
|
||||
read: fds[0],
|
||||
write: fds[1],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Pipe {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
libc::close(self.read);
|
||||
libc::close(self.write);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_write(fd: RawFd, buf: &[u8]) -> isize {
|
||||
unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) }
|
||||
}
|
||||
|
||||
fn raw_read(fd: RawFd, buf: &mut [u8]) -> isize {
|
||||
unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wait_readable parks until data arrives, then libc::read succeeds.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn wait_readable_blocks_until_data_arrives_then_read_succeeds() {
|
||||
let captured: Arc<StdMutex<Vec<u8>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
let cap = captured.clone();
|
||||
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_reader = p.clone();
|
||||
let p_writer = p.clone();
|
||||
|
||||
run(move || {
|
||||
let reader = spawn(move || {
|
||||
// Initially the pipe is empty; this parks.
|
||||
wait_readable(p_reader.read).expect("wait_readable failed");
|
||||
// Now data should be readable.
|
||||
let mut buf = [0u8; 16];
|
||||
let n = raw_read(p_reader.read, &mut buf);
|
||||
assert!(n > 0, "read returned {}", n);
|
||||
cap.lock().unwrap().extend_from_slice(&buf[..n as usize]);
|
||||
});
|
||||
|
||||
let writer = spawn(move || {
|
||||
// Yield so the reader gets to park first.
|
||||
yield_now();
|
||||
yield_now();
|
||||
// Sleep a touch so the reader is definitely waiting in epoll.
|
||||
smarm::sleep(Duration::from_millis(5));
|
||||
let n = raw_write(p_writer.write, b"hello");
|
||||
assert_eq!(n, 5);
|
||||
});
|
||||
|
||||
reader.join().unwrap();
|
||||
writer.join().unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(*captured.lock().unwrap(), b"hello");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The smarm::scheduler::read sugar — wait_readable + libc::read in one call.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn read_sugar_returns_bytes_from_pipe() {
|
||||
let captured: Arc<StdMutex<Vec<u8>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
let cap = captured.clone();
|
||||
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_reader = p.clone();
|
||||
let p_writer = p.clone();
|
||||
|
||||
run(move || {
|
||||
let reader = spawn(move || {
|
||||
let mut buf = [0u8; 16];
|
||||
let n = smarm::scheduler::read(p_reader.read, &mut buf)
|
||||
.expect("smarm::scheduler::read failed");
|
||||
cap.lock().unwrap().extend_from_slice(&buf[..n]);
|
||||
});
|
||||
|
||||
let writer = spawn(move || {
|
||||
yield_now();
|
||||
smarm::sleep(Duration::from_millis(5));
|
||||
let _ = raw_write(p_writer.write, b"world");
|
||||
});
|
||||
|
||||
reader.join().unwrap();
|
||||
writer.join().unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(*captured.lock().unwrap(), b"world");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// wait_writable + write — though pipes are almost always writable; the
|
||||
// useful test here is that the call doesn't hang on a writable fd.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn write_sugar_sends_bytes_to_pipe() {
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
let c = counter.clone();
|
||||
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_writer = p.clone();
|
||||
let p_reader = p.clone();
|
||||
|
||||
run(move || {
|
||||
let writer = spawn(move || {
|
||||
// Pipe is empty + has buffer space, so this returns immediately
|
||||
// after wait_writable wakes (which happens fast because the
|
||||
// kernel marks an empty pipe as immediately writable).
|
||||
let n = smarm::scheduler::write(p_writer.write, b"smarm")
|
||||
.expect("write failed");
|
||||
assert_eq!(n, 5);
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let reader = spawn(move || {
|
||||
// Give the writer time.
|
||||
smarm::sleep(Duration::from_millis(10));
|
||||
let mut buf = [0u8; 16];
|
||||
let n = raw_read(p_reader.read, &mut buf);
|
||||
assert_eq!(n, 5);
|
||||
assert_eq!(&buf[..5], b"smarm");
|
||||
});
|
||||
|
||||
writer.join().unwrap();
|
||||
reader.join().unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// While an actor is parked on wait_readable, other actors keep running.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn other_actors_run_while_one_is_parked_on_wait_readable() {
|
||||
let log: Arc<StdMutex<Vec<u8>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
let la = log.clone();
|
||||
let lb = log.clone();
|
||||
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_a = p.clone();
|
||||
let p_b = p.clone();
|
||||
|
||||
run(move || {
|
||||
let a = spawn(move || {
|
||||
la.lock().unwrap().push(b'A');
|
||||
wait_readable(p_a.read).unwrap();
|
||||
la.lock().unwrap().push(b'a');
|
||||
});
|
||||
|
||||
let b = spawn(move || {
|
||||
// A starts parking on the empty pipe; B should be free to do
|
||||
// its work in the meantime.
|
||||
for _ in 0..3 {
|
||||
yield_now();
|
||||
lb.lock().unwrap().push(b'B');
|
||||
}
|
||||
// Now wake A.
|
||||
let _ = raw_write(p_b.write, b"x");
|
||||
});
|
||||
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
let v = log.lock().unwrap();
|
||||
// A goes first ('A'), then B makes progress (multiple 'B's) while A is
|
||||
// parked, then A wakes and finishes ('a').
|
||||
let pos_big_a = v.iter().position(|&c| c == b'A').unwrap();
|
||||
let pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
|
||||
let big_b_count = v.iter().filter(|&&c| c == b'B').count();
|
||||
assert_eq!(big_b_count, 3, "B should have made 3 steps: {:?}", *v);
|
||||
assert!(pos_big_a < pos_lit_a, "A pre-park before A post-park: {:?}", *v);
|
||||
// At least the last B step should be before A resumes.
|
||||
let last_big_b = v.iter().rposition(|&c| c == b'B').unwrap();
|
||||
assert!(last_big_b < pos_lit_a, "B should finish before A resumes: {:?}", *v);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Two-way pipe ping-pong via wait_readable.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn ping_pong_between_two_pipes_completes() {
|
||||
// a_to_b: actor A writes, actor B reads.
|
||||
// b_to_a: actor B writes, actor A reads.
|
||||
let a_to_b = Arc::new(Pipe::new());
|
||||
let b_to_a = Arc::new(Pipe::new());
|
||||
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
let ca = counter.clone();
|
||||
let cb = counter.clone();
|
||||
|
||||
let a_to_b_a = a_to_b.clone();
|
||||
let a_to_b_b = a_to_b.clone();
|
||||
let b_to_a_a = b_to_a.clone();
|
||||
let b_to_a_b = b_to_a.clone();
|
||||
|
||||
run(move || {
|
||||
let a = spawn(move || {
|
||||
for _ in 0..5 {
|
||||
let _ = raw_write(a_to_b_a.write, b"x");
|
||||
wait_readable(b_to_a_a.read).unwrap();
|
||||
let mut buf = [0u8; 4];
|
||||
let _ = raw_read(b_to_a_a.read, &mut buf);
|
||||
ca.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
let b = spawn(move || {
|
||||
for _ in 0..5 {
|
||||
wait_readable(a_to_b_b.read).unwrap();
|
||||
let mut buf = [0u8; 4];
|
||||
let _ = raw_read(a_to_b_b.read, &mut buf);
|
||||
let _ = raw_write(b_to_a_b.write, b"y");
|
||||
cb.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
// Both sides did 5 rounds; counter is incremented by both, so total = 10.
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Same fd reused across calls — DEL+ADD cycle works.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn same_fd_can_be_waited_on_repeatedly() {
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_r = p.clone();
|
||||
let p_w = p.clone();
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
let c = counter.clone();
|
||||
|
||||
run(move || {
|
||||
let reader = spawn(move || {
|
||||
for _ in 0..4 {
|
||||
wait_readable(p_r.read).unwrap();
|
||||
let mut buf = [0u8; 4];
|
||||
let n = raw_read(p_r.read, &mut buf);
|
||||
assert!(n > 0);
|
||||
c.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
let writer = spawn(move || {
|
||||
for _ in 0..4 {
|
||||
yield_now();
|
||||
smarm::sleep(Duration::from_millis(2));
|
||||
let _ = raw_write(p_w.write, b"z");
|
||||
}
|
||||
});
|
||||
|
||||
reader.join().unwrap();
|
||||
writer.join().unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 4);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sanity that wait_writable on an already-writable pipe returns promptly.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn wait_writable_on_empty_pipe_returns_quickly() {
|
||||
let p = Arc::new(Pipe::new());
|
||||
let p_w = p.clone();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
run(move || {
|
||||
wait_writable(p_w.write).unwrap();
|
||||
});
|
||||
let elapsed = start.elapsed();
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(200),
|
||||
"wait_writable should be fast on a writable fd, took {:?}",
|
||||
elapsed
|
||||
);
|
||||
}
|
||||
314
tests/mutex.rs
314
tests/mutex.rs
@@ -1,314 +0,0 @@
|
||||
//! `loom::Mutex<T>` tests. All run under the scheduler because `lock()`
|
||||
//! needs to be able to park.
|
||||
|
||||
use smarm::{run, spawn, yield_now, LockTimeout, Mutex};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Uncontended fast path
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn lock_free_mutex_succeeds() {
|
||||
let captured = Arc::new(AtomicU32::new(0));
|
||||
let c = captured.clone();
|
||||
run(move || {
|
||||
let m = Mutex::new(42u32);
|
||||
{
|
||||
let g = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
c.store(*g, Ordering::SeqCst);
|
||||
}
|
||||
// After drop we can lock again.
|
||||
let g2 = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
assert_eq!(*g2, 42);
|
||||
});
|
||||
assert_eq!(captured.load(Ordering::SeqCst), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_lock_returns_some_when_free_none_when_held() {
|
||||
let success_flag = Arc::new(AtomicU32::new(0));
|
||||
let s = success_flag.clone();
|
||||
run(move || {
|
||||
let m = Mutex::new(0u32);
|
||||
let g = m.try_lock().expect("free");
|
||||
// Holding the guard; a second try_lock on the same actor should fail.
|
||||
assert!(m.try_lock().is_none());
|
||||
drop(g);
|
||||
// Now free again.
|
||||
let g2 = m.try_lock().expect("free again");
|
||||
drop(g2);
|
||||
s.store(1, Ordering::SeqCst);
|
||||
});
|
||||
assert_eq!(success_flag.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_mutates_value_visible_through_next_lock() {
|
||||
let final_value = Arc::new(AtomicU32::new(0));
|
||||
let f = final_value.clone();
|
||||
run(move || {
|
||||
let m = Mutex::new(0u32);
|
||||
{
|
||||
let mut g = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
*g = 7;
|
||||
}
|
||||
let g2 = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
f.store(*g2, Ordering::SeqCst);
|
||||
});
|
||||
assert_eq!(final_value.load(Ordering::SeqCst), 7);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Contention: a second actor parks until the first releases.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn contended_lock_parks_until_holder_releases() {
|
||||
// Actor A locks, yields (still holding), then releases. Actor B tries
|
||||
// to lock in between — B should park, then succeed after A drops.
|
||||
let log: Arc<StdMutex<Vec<&'static str>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
let la = log.clone();
|
||||
let lb = log.clone();
|
||||
|
||||
run(move || {
|
||||
let m = Mutex::new(0u32);
|
||||
let m_a = m.clone();
|
||||
let m_b = m.clone();
|
||||
|
||||
let a = spawn(move || {
|
||||
let g = m_a.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
la.lock().unwrap().push("A_locked");
|
||||
// First yield: lets B run past its first yield_now.
|
||||
yield_now();
|
||||
// Second yield: lets B reach B_try and attempt lock() while we
|
||||
// still hold it, so B parks on the mutex.
|
||||
yield_now();
|
||||
la.lock().unwrap().push("A_dropping");
|
||||
drop(g);
|
||||
la.lock().unwrap().push("A_dropped");
|
||||
});
|
||||
let b = spawn(move || {
|
||||
// One yield: lets A run and acquire the lock first.
|
||||
yield_now();
|
||||
lb.lock().unwrap().push("B_try");
|
||||
let _g = m_b.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
lb.lock().unwrap().push("B_locked");
|
||||
});
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
let v = log.lock().unwrap();
|
||||
// A locks, B tries (parks), A drops, B gets the lock.
|
||||
let pos_a_locked = v.iter().position(|s| *s == "A_locked").unwrap();
|
||||
let pos_b_try = v.iter().position(|s| *s == "B_try").unwrap();
|
||||
let pos_a_dropped = v.iter().position(|s| *s == "A_dropped").unwrap();
|
||||
let pos_b_locked = v.iter().position(|s| *s == "B_locked").unwrap();
|
||||
|
||||
assert!(pos_a_locked < pos_b_try, "log: {:?}", *v);
|
||||
assert!(pos_b_try < pos_a_dropped, "B should attempt before A drops: {:?}", *v);
|
||||
assert!(pos_a_dropped < pos_b_locked, "B should lock only after A drops: {:?}", *v);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Timeout: B times out while A holds forever.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn lock_timeout_returns_err_when_holder_never_releases() {
|
||||
let saw_err = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let s = saw_err.clone();
|
||||
|
||||
run(move || {
|
||||
let m: Mutex<u32> = Mutex::new(0);
|
||||
let m_a = m.clone();
|
||||
let m_b = m.clone();
|
||||
|
||||
let a = spawn(move || {
|
||||
// Hold the lock for 100ms, blocking B's attempt with a 20ms timeout.
|
||||
let _g = m_a.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
smarm::sleep(Duration::from_millis(100));
|
||||
// _g drops here.
|
||||
});
|
||||
let b = spawn(move || {
|
||||
// Let A acquire first.
|
||||
yield_now();
|
||||
let t0 = Instant::now();
|
||||
let res = m_b.lock_timeout(Duration::from_millis(20));
|
||||
let elapsed = t0.elapsed();
|
||||
assert!(matches!(res, Err(LockTimeout)), "got {:?}", res);
|
||||
// Sanity: actually waited approximately the timeout.
|
||||
assert!(
|
||||
elapsed >= Duration::from_millis(15),
|
||||
"timed out too fast: {:?}",
|
||||
elapsed
|
||||
);
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(80),
|
||||
"timed out far too slow: {:?}",
|
||||
elapsed
|
||||
);
|
||||
s.store(true, Ordering::SeqCst);
|
||||
});
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
assert!(saw_err.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FIFO fairness: when many actors queue, they get the lock in arrival order.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn waiters_are_granted_the_lock_in_fifo_order() {
|
||||
let order: Arc<StdMutex<Vec<u32>>> = Arc::new(StdMutex::new(Vec::new()));
|
||||
|
||||
run({
|
||||
let order = order.clone();
|
||||
move || {
|
||||
let m: Mutex<()> = Mutex::new(());
|
||||
|
||||
// Holder: takes the lock, yields to let others queue up, then
|
||||
// releases. Each waiter records its arrival order on acquisition.
|
||||
let m_holder = m.clone();
|
||||
let holder = spawn(move || {
|
||||
let g = m_holder.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
// Let waiters pile up.
|
||||
for _ in 0..5 {
|
||||
yield_now();
|
||||
}
|
||||
drop(g);
|
||||
});
|
||||
|
||||
// Spawn 4 waiters in order 1, 2, 3, 4. Each yields once before
|
||||
// calling lock(), so we know the holder ran first.
|
||||
let mut handles = vec![holder];
|
||||
for id in 1u32..=4 {
|
||||
let m_w = m.clone();
|
||||
let o = order.clone();
|
||||
handles.push(spawn(move || {
|
||||
// Stagger the lock attempts so they arrive in order.
|
||||
for _ in 0..id {
|
||||
yield_now();
|
||||
}
|
||||
let _g = m_w.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
o.lock().unwrap().push(id);
|
||||
}));
|
||||
}
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let v = order.lock().unwrap().clone();
|
||||
assert_eq!(v, vec![1, 2, 3, 4], "waiters should acquire in arrival order");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Grant-vs-timeout race: holder drops just before timer would fire — waiter
|
||||
// should get the lock, not LockTimeout.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn grant_wins_when_holder_releases_before_timeout() {
|
||||
let got_lock = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let g = got_lock.clone();
|
||||
|
||||
run(move || {
|
||||
let m: Mutex<u32> = Mutex::new(0);
|
||||
let m_a = m.clone();
|
||||
let m_b = m.clone();
|
||||
|
||||
let a = spawn(move || {
|
||||
let _g = m_a.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
// Hold for 10ms, well under B's 100ms timeout.
|
||||
smarm::sleep(Duration::from_millis(10));
|
||||
});
|
||||
let b = spawn(move || {
|
||||
yield_now();
|
||||
let res = m_b.lock_timeout(Duration::from_millis(100));
|
||||
if res.is_ok() {
|
||||
g.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
assert!(got_lock.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Panic in critical section: next waiter still gets the lock (no poisoning).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn next_waiter_gets_lock_after_holder_panics() {
|
||||
let next_got_it = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let n = next_got_it.clone();
|
||||
|
||||
run(move || {
|
||||
let m: Mutex<u32> = Mutex::new(7);
|
||||
let m_a = m.clone();
|
||||
let m_b = m.clone();
|
||||
|
||||
let a = spawn(move || {
|
||||
let _g = m_a.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
yield_now();
|
||||
panic!("holder dies mid-critical-section");
|
||||
});
|
||||
let b = spawn(move || {
|
||||
yield_now();
|
||||
// A is dead but its guard's Drop ran during unwind. We get the lock.
|
||||
let g = m_b.lock_timeout(Duration::from_millis(100)).unwrap();
|
||||
assert_eq!(*g, 7);
|
||||
n.store(true, Ordering::SeqCst);
|
||||
});
|
||||
let _ = a.join(); // panic — expected
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
assert!(next_got_it.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Multiple short critical sections under contention all complete (no lost
|
||||
// wakeups, no deadlock). Counts up to N from M actors.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn many_actors_increment_shared_counter_via_mutex() {
|
||||
const ACTORS: u32 = 8;
|
||||
const PER_ACTOR: u32 = 50;
|
||||
|
||||
let final_value = Arc::new(AtomicU32::new(0));
|
||||
let fv = final_value.clone();
|
||||
|
||||
run(move || {
|
||||
let m: Mutex<u32> = Mutex::new(0);
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..ACTORS {
|
||||
let m_i = m.clone();
|
||||
handles.push(spawn(move || {
|
||||
for _ in 0..PER_ACTOR {
|
||||
let mut g = m_i.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
*g += 1;
|
||||
}
|
||||
}));
|
||||
}
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
let g = m.lock_timeout(Duration::from_millis(500)).unwrap();
|
||||
fv.store(*g, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
assert_eq!(final_value.load(Ordering::SeqCst), ACTORS * PER_ACTOR);
|
||||
}
|
||||
22
tests/pid.rs
22
tests/pid.rs
@@ -1,22 +0,0 @@
|
||||
use smarm::pid::Pid;
|
||||
|
||||
#[test]
|
||||
fn pid_equality() {
|
||||
assert_eq!(Pid::new(0, 0), Pid::new(0, 0));
|
||||
assert_ne!(Pid::new(0, 0), Pid::new(0, 1));
|
||||
assert_ne!(Pid::new(0, 0), Pid::new(1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pid_accessors() {
|
||||
let p = Pid::new(42, 7);
|
||||
assert_eq!(p.index(), 42);
|
||||
assert_eq!(p.generation(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pid_debug_is_useful() {
|
||||
let p = Pid::new(3, 5);
|
||||
let s = format!("{:?}", p);
|
||||
assert!(s.contains('3') && s.contains('5'), "got: {}", s);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
//! Tests for explicit preemption via `smarm::check!()`.
|
||||
|
||||
use smarm::{run, spawn};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn check_yields_when_timeslice_expired() {
|
||||
// A single actor that drives the timeslice clock to zero manually,
|
||||
// then calls check!() and expects to yield. The scheduler has nothing
|
||||
// else to run, so it just re-queues us. To prove we actually yielded,
|
||||
// observe the run counter on the slot... we don't have one. So
|
||||
// instead: spawn a second actor that increments a counter and joins
|
||||
// it; verify both actors made progress in interleaved order under
|
||||
// forced timeslice expiry.
|
||||
let order: Arc<std::sync::Mutex<Vec<u8>>> = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let o1 = order.clone();
|
||||
let o2 = order.clone();
|
||||
|
||||
run(move || {
|
||||
let a = spawn(move || {
|
||||
o1.lock().unwrap().push(b'A');
|
||||
// Force the timeslice to be considered expired.
|
||||
smarm::preempt::expire_timeslice_for_test();
|
||||
smarm::check!();
|
||||
o1.lock().unwrap().push(b'a');
|
||||
});
|
||||
let b = spawn(move || {
|
||||
o2.lock().unwrap().push(b'B');
|
||||
smarm::preempt::expire_timeslice_for_test();
|
||||
smarm::check!();
|
||||
o2.lock().unwrap().push(b'b');
|
||||
});
|
||||
a.join().unwrap();
|
||||
b.join().unwrap();
|
||||
});
|
||||
|
||||
// FIFO scheduling + forced preemption: A starts, expires, yields to B;
|
||||
// B starts, expires, yields to A; A finishes, B finishes.
|
||||
// Required: both uppercase letters appear before either lowercase.
|
||||
let v = order.lock().unwrap();
|
||||
let pos_big_a = v.iter().position(|&c| c == b'A').unwrap();
|
||||
let pos_big_b = v.iter().position(|&c| c == b'B').unwrap();
|
||||
let pos_lit_a = v.iter().position(|&c| c == b'a').unwrap();
|
||||
let pos_lit_b = v.iter().position(|&c| c == b'b').unwrap();
|
||||
assert!(pos_big_a < pos_lit_a, "A's tail ran before B's head: {:?}", *v);
|
||||
assert!(pos_big_b < pos_lit_b, "B's tail ran before A's head: {:?}", *v);
|
||||
assert!(pos_big_a.max(pos_big_b) < pos_lit_a.min(pos_lit_b),
|
||||
"preemption didn't interleave: {:?}", *v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_is_a_noop_when_timeslice_not_expired() {
|
||||
// After a fresh resume, check!() should be cheap and not yield. Run
|
||||
// a single actor that calls check!() many times; it should complete
|
||||
// promptly.
|
||||
let count = Arc::new(AtomicU64::new(0));
|
||||
let c = count.clone();
|
||||
run(move || {
|
||||
for _ in 0..1_000 {
|
||||
smarm::check!();
|
||||
c.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
});
|
||||
assert_eq!(count.load(Ordering::Relaxed), 1_000);
|
||||
}
|
||||
426
tests/runtime.rs
426
tests/runtime.rs
@@ -1,426 +0,0 @@
|
||||
//! Tests for the multi-scheduler runtime: Config, Runtime::run, and
|
||||
//! correctness under genuine parallelism.
|
||||
//!
|
||||
//! The single-threaded correctness properties (channel ordering, mutex
|
||||
//! fairness, timer accuracy, etc.) are already covered by the per-module
|
||||
//! tests. This file focuses on what changes when N > 1 scheduler threads
|
||||
//! are involved:
|
||||
//!
|
||||
//! - Config construction and validation
|
||||
//! - Runtime::run blocks until all actors finish
|
||||
//! - All existing cooperative behaviours hold under multi-threading
|
||||
//! - Actors genuinely run on different OS threads
|
||||
//! - No lost wakeups under concurrent park/unpark
|
||||
//! - No slot leaks under high spawn/join churn
|
||||
//! - Panic on one scheduler thread doesn't kill others
|
||||
|
||||
use smarm::{channel, runtime::{Config, Runtime}, spawn, yield_now, JoinHandle};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
|
||||
Arc, Barrier,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use std::collections::HashSet;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Build a runtime with exactly `n` scheduler threads.
|
||||
fn rt(n: usize) -> Runtime {
|
||||
smarm::runtime::init(Config::exact(n))
|
||||
}
|
||||
|
||||
/// Convenient single-threaded runtime (regression guard).
|
||||
fn rt1() -> Runtime { rt(1) }
|
||||
|
||||
/// Multi-threaded runtime using all available parallelism.
|
||||
fn rt_par() -> Runtime {
|
||||
smarm::runtime::init(Config::default())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn config_exact_overrides_bounds() {
|
||||
let c = Config::exact(3);
|
||||
assert_eq!(c.resolved_thread_count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_default_clamps_to_available_parallelism() {
|
||||
let c = Config::default();
|
||||
let n = c.resolved_thread_count();
|
||||
let avail = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1);
|
||||
// Default min is 1, default max is available_parallelism.
|
||||
assert!(n >= 1 && n <= avail);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_min_max_clamps() {
|
||||
// Force a range that excludes exact: min=2, max=4, available might be >4.
|
||||
let c = Config::new(2, 4, None);
|
||||
let n = c.resolved_thread_count();
|
||||
assert!(n >= 2 && n <= 4, "expected 2..=4, got {n}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_min_1_max_1_is_single_threaded() {
|
||||
let c = Config::new(1, 1, None);
|
||||
assert_eq!(c.resolved_thread_count(), 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runtime::run — basic lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn runtime_run_executes_closure() {
|
||||
let flag = Arc::new(AtomicBool::new(false));
|
||||
let f = flag.clone();
|
||||
rt(1).run(move || { f.store(true, Ordering::SeqCst); });
|
||||
assert!(flag.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_run_blocks_until_all_actors_done() {
|
||||
// Spawn a chain of actors; the counter should be exactly N when run returns.
|
||||
let counter = Arc::new(AtomicU64::new(0));
|
||||
let c = counter.clone();
|
||||
rt(2).run(move || {
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..20 {
|
||||
let cc = c.clone();
|
||||
handles.push(spawn(move || {
|
||||
cc.fetch_add(1, Ordering::SeqCst);
|
||||
}));
|
||||
}
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_can_be_used_multiple_times_sequentially() {
|
||||
// Each call to run() is independent.
|
||||
let r = rt(2);
|
||||
let a = Arc::new(AtomicU64::new(0));
|
||||
let b = Arc::new(AtomicU64::new(0));
|
||||
let ac = a.clone();
|
||||
let bc = b.clone();
|
||||
r.run(move || { ac.fetch_add(1, Ordering::SeqCst); });
|
||||
r.run(move || { bc.fetch_add(1, Ordering::SeqCst); });
|
||||
assert_eq!(a.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(b.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Single-threaded regression: exact(1) must behave identically to old run()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn exact_1_spawn_join_works() {
|
||||
let v = Arc::new(AtomicU64::new(0));
|
||||
let vc = v.clone();
|
||||
rt1().run(move || {
|
||||
let h = spawn(move || { vc.store(42, Ordering::SeqCst); });
|
||||
h.join().unwrap();
|
||||
});
|
||||
assert_eq!(v.load(Ordering::SeqCst), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_1_channel_recv_parks_and_wakes() {
|
||||
let v = Arc::new(AtomicU64::new(0));
|
||||
let vc = v.clone();
|
||||
rt1().run(move || {
|
||||
let (tx, rx) = channel::<u64>();
|
||||
let h = spawn(move || {
|
||||
let val = rx.recv().unwrap();
|
||||
vc.store(val, Ordering::SeqCst);
|
||||
});
|
||||
yield_now();
|
||||
tx.send(99).unwrap();
|
||||
h.join().unwrap();
|
||||
});
|
||||
assert_eq!(v.load(Ordering::SeqCst), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_1_panic_captured() {
|
||||
let saw_err = Arc::new(AtomicBool::new(false));
|
||||
let s = saw_err.clone();
|
||||
rt1().run(move || {
|
||||
let h = spawn(|| panic!("oops"));
|
||||
if h.join().is_err() { s.store(true, Ordering::SeqCst); }
|
||||
});
|
||||
assert!(saw_err.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Multi-threaded correctness
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn multi_thread_all_actors_complete() {
|
||||
let counter = Arc::new(AtomicU64::new(0));
|
||||
let c = counter.clone();
|
||||
rt_par().run(move || {
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..100 {
|
||||
let cc = c.clone();
|
||||
handles.push(spawn(move || {
|
||||
cc.fetch_add(1, Ordering::SeqCst);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
});
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_thread_channel_wakeup_across_threads() {
|
||||
// Receiver parks; sender runs (potentially on a different OS thread).
|
||||
// Verifies no lost wakeup.
|
||||
let received = Arc::new(AtomicU64::new(0));
|
||||
let rc = received.clone();
|
||||
rt_par().run(move || {
|
||||
let (tx, rx) = channel::<u64>();
|
||||
let h = spawn(move || {
|
||||
let v = rx.recv().unwrap();
|
||||
rc.store(v, Ordering::SeqCst);
|
||||
});
|
||||
// Let receiver park.
|
||||
yield_now();
|
||||
tx.send(7).unwrap();
|
||||
h.join().unwrap();
|
||||
});
|
||||
assert_eq!(received.load(Ordering::SeqCst), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_thread_many_channels_no_lost_wakeups() {
|
||||
// N pairs of (sender actor, receiver actor). Each pair exchanges one
|
||||
// message. All must complete — any lost wakeup causes a deadlock/timeout.
|
||||
const PAIRS: usize = 50;
|
||||
let count = Arc::new(AtomicU64::new(0));
|
||||
let c = count.clone();
|
||||
rt_par().run(move || {
|
||||
let mut handles: Vec<JoinHandle> = Vec::new();
|
||||
for _ in 0..PAIRS {
|
||||
let (tx, rx) = channel::<u64>();
|
||||
let cc = c.clone();
|
||||
handles.push(spawn(move || {
|
||||
let v = rx.recv().unwrap();
|
||||
cc.fetch_add(v, Ordering::SeqCst);
|
||||
}));
|
||||
handles.push(spawn(move || {
|
||||
tx.send(1).unwrap();
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
});
|
||||
assert_eq!(count.load(Ordering::SeqCst), PAIRS as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_thread_mutex_contention_no_deadlock() {
|
||||
use smarm::Mutex;
|
||||
const ACTORS: usize = 20;
|
||||
const PER: u64 = 100;
|
||||
let total = Arc::new(AtomicU64::new(0));
|
||||
let t = total.clone();
|
||||
rt_par().run(move || {
|
||||
let m: Mutex<u64> = Mutex::new(0);
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..ACTORS {
|
||||
let mc = m.clone();
|
||||
let tc = t.clone();
|
||||
handles.push(spawn(move || {
|
||||
for _ in 0..PER {
|
||||
let mut g = mc.lock_timeout(Duration::from_secs(5)).unwrap();
|
||||
*g += 1;
|
||||
tc.fetch_add(0, Ordering::SeqCst); // just a memory barrier
|
||||
}
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
let g = m.lock_timeout(Duration::from_secs(1)).unwrap();
|
||||
t.store(*g, Ordering::SeqCst);
|
||||
});
|
||||
assert_eq!(total.load(Ordering::SeqCst), ACTORS as u64 * PER);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_thread_join_across_threads() {
|
||||
// Parent joins a child that may run on a different scheduler thread.
|
||||
let v = Arc::new(AtomicU64::new(0));
|
||||
let vc = v.clone();
|
||||
rt_par().run(move || {
|
||||
let h = spawn(move || {
|
||||
// Do some work to make scheduling interesting.
|
||||
for _ in 0..10 { yield_now(); }
|
||||
vc.store(1, Ordering::SeqCst);
|
||||
});
|
||||
h.join().unwrap();
|
||||
});
|
||||
assert_eq!(v.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actors run on distinct OS threads
|
||||
//
|
||||
// We collect the OS thread IDs that actors execute on. With N schedulers
|
||||
// and enough actors, we expect to see more than one thread ID.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn actors_run_on_multiple_os_threads() {
|
||||
let thread_ids: Arc<smarm::Mutex<HashSet<u64>>> =
|
||||
Arc::new(smarm::Mutex::new(HashSet::new()));
|
||||
|
||||
rt_par().run({
|
||||
let ids = thread_ids.clone();
|
||||
move || {
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..64 {
|
||||
let idc = ids.clone();
|
||||
handles.push(spawn(move || {
|
||||
let tid = unsafe { libc::syscall(libc::SYS_gettid) as u64 };
|
||||
let mut g = idc.lock_timeout(Duration::from_secs(1)).unwrap();
|
||||
g.insert(tid);
|
||||
}));
|
||||
}
|
||||
for h in handles { h.join().unwrap(); }
|
||||
}
|
||||
});
|
||||
|
||||
let n = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
|
||||
let ids = thread_ids.lock_timeout(Duration::from_secs(1)).unwrap();
|
||||
// If we have >1 scheduler threads, we expect >1 OS thread IDs.
|
||||
// On a single-CPU machine this may be 1; we just assert ≥ 1.
|
||||
assert!(!ids.is_empty());
|
||||
if n > 1 {
|
||||
// Strongly expect parallelism — not a hard assert since scheduling
|
||||
// is non-deterministic, but 64 actors should spread.
|
||||
// We log rather than assert to avoid flakiness on loaded CI.
|
||||
if ids.len() == 1 {
|
||||
eprintln!("WARNING: 64 actors all ran on the same OS thread (flaky on loaded system)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scheduler stats (RFC 000 Layer 1 primitives)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn scheduler_stats_run_queue_len_is_observable() {
|
||||
// After spawning actors but before they run, the queue should be non-empty.
|
||||
// We can't observe this from inside run() without a snapshot API, but we
|
||||
// can verify the stats struct is accessible and returns sane values after
|
||||
// run() completes (queue len == 0 at quiescence).
|
||||
let r = rt_par();
|
||||
r.run(|| {
|
||||
for _ in 0..10 { spawn(|| {}); }
|
||||
// Don't join — let them drain naturally.
|
||||
});
|
||||
let stats = r.stats();
|
||||
assert_eq!(stats.total_run_queue_len(), 0, "queue should be empty after run()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduler_stats_thread_count_matches_config() {
|
||||
let r = rt(3);
|
||||
r.run(|| {});
|
||||
assert_eq!(r.stats().scheduler_count(), 3);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Panic isolation: a panicking actor doesn't kill the scheduler thread
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn panic_in_actor_does_not_kill_runtime() {
|
||||
let completed = Arc::new(AtomicU64::new(0));
|
||||
let c = completed.clone();
|
||||
rt_par().run(move || {
|
||||
// Spawn a panicker alongside well-behaved actors.
|
||||
let bad = spawn(|| panic!("deliberate"));
|
||||
let mut good_handles = Vec::new();
|
||||
for _ in 0..10 {
|
||||
let cc = c.clone();
|
||||
good_handles.push(spawn(move || {
|
||||
cc.fetch_add(1, Ordering::SeqCst);
|
||||
}));
|
||||
}
|
||||
let _ = bad.join(); // expect Err
|
||||
for h in good_handles { h.join().unwrap(); }
|
||||
});
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 10);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// No slot leaks: rapid spawn/join churn
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn no_slot_leak_under_churn() {
|
||||
// Spawn and join many short actors in a loop. If slots leak, the slot
|
||||
// table grows unboundedly. We can't directly measure it without an
|
||||
// introspection API, but the test at least checks correctness under
|
||||
// churn and will OOM if there's a severe leak.
|
||||
let counter = Arc::new(AtomicU64::new(0));
|
||||
let c = counter.clone();
|
||||
rt_par().run(move || {
|
||||
for _ in 0..500 {
|
||||
let cc = c.clone();
|
||||
spawn(move || { cc.fetch_add(1, Ordering::SeqCst); })
|
||||
.join()
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 500);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ping-pong: channel round-trips between two actors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn ping_pong_completes() {
|
||||
const ROUNDS: u64 = 1_000;
|
||||
let final_val = Arc::new(AtomicU64::new(0));
|
||||
let fv = final_val.clone();
|
||||
rt_par().run(move || {
|
||||
let (tx_a, rx_a) = channel::<u64>();
|
||||
let (tx_b, rx_b) = channel::<u64>();
|
||||
let h_a = spawn(move || {
|
||||
tx_a.send(0).unwrap();
|
||||
for _ in 0..ROUNDS {
|
||||
let v = rx_b.recv().unwrap();
|
||||
tx_a.send(v + 1).unwrap();
|
||||
}
|
||||
});
|
||||
let h_b = spawn(move || {
|
||||
for _ in 0..=ROUNDS {
|
||||
let v = rx_a.recv().unwrap();
|
||||
if v < ROUNDS {
|
||||
tx_b.send(v).unwrap();
|
||||
} else {
|
||||
fv.store(v, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
});
|
||||
h_a.join().unwrap();
|
||||
h_b.join().unwrap();
|
||||
});
|
||||
assert_eq!(final_val.load(Ordering::SeqCst), ROUNDS);
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
//! End-to-end scheduler tests: spawning, joining, panic delivery,
|
||||
//! yield_now, self_pid.
|
||||
|
||||
use smarm::{channel, run, self_pid, spawn, spawn_under, yield_now, Signal};
|
||||
use std::cell::Cell;
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Single root actor runs to completion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn root_actor_runs() {
|
||||
let captured = Arc::new(AtomicI64::new(0));
|
||||
let c = captured.clone();
|
||||
run(move || { c.store(99, Ordering::SeqCst); });
|
||||
assert_eq!(captured.load(Ordering::SeqCst), 99);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spawn child, join it
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn spawn_and_join_returns_exit() {
|
||||
let captured = Arc::new(AtomicI64::new(0));
|
||||
let c = captured.clone();
|
||||
run(move || {
|
||||
let h = spawn(move || { c.store(7, Ordering::SeqCst); });
|
||||
let res = h.join();
|
||||
assert!(res.is_ok(), "join returned {:?}", res);
|
||||
});
|
||||
assert_eq!(captured.load(Ordering::SeqCst), 7);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// yield_now lets a sibling run
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn yield_now_interleaves_actors() {
|
||||
let log: Arc<std::sync::Mutex<Vec<u8>>> = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let l1 = log.clone();
|
||||
let l2 = log.clone();
|
||||
run(move || {
|
||||
let h1 = spawn(move || {
|
||||
l1.lock().unwrap().push(1);
|
||||
yield_now();
|
||||
l1.lock().unwrap().push(3);
|
||||
});
|
||||
let h2 = spawn(move || {
|
||||
l2.lock().unwrap().push(2);
|
||||
yield_now();
|
||||
l2.lock().unwrap().push(4);
|
||||
});
|
||||
h1.join().unwrap();
|
||||
h2.join().unwrap();
|
||||
});
|
||||
// Both actors get their first step before either second step. Exact order
|
||||
// is FIFO: 1, 2, then 3, 4.
|
||||
assert_eq!(*log.lock().unwrap(), vec![1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// self_pid returns this actor's pid inside the actor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn self_pid_is_stable_within_an_actor() {
|
||||
let pid_cell: Arc<std::sync::Mutex<Option<smarm::Pid>>> =
|
||||
Arc::new(std::sync::Mutex::new(None));
|
||||
let p2 = pid_cell.clone();
|
||||
run(move || {
|
||||
let h = spawn(move || {
|
||||
let me = self_pid();
|
||||
yield_now();
|
||||
assert_eq!(me, self_pid(), "self_pid changed across yield");
|
||||
*p2.lock().unwrap() = Some(me);
|
||||
});
|
||||
h.join().unwrap();
|
||||
});
|
||||
assert!(pid_cell.lock().unwrap().is_some());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Panic is captured; join returns Err; supervisor receives Signal::Panic
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn panicking_child_returns_join_error() {
|
||||
let saw_err = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let s = saw_err.clone();
|
||||
run(move || {
|
||||
let h = spawn(|| panic!("kaboom"));
|
||||
if h.join().is_err() {
|
||||
s.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
|
||||
assert!(saw_err.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_receives_panic_signal() {
|
||||
let saw_panic_signal = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let s = saw_panic_signal.clone();
|
||||
|
||||
run(move || {
|
||||
// Build a supervisor actor with its own mailbox.
|
||||
let (sig_tx, sig_rx) = channel::<Signal>();
|
||||
let sup_handle = spawn(move || {
|
||||
// Wait for exactly one signal.
|
||||
let sig = sig_rx.recv().unwrap();
|
||||
if let Signal::Panic(_, _) = sig {
|
||||
s.store(true, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
// Tell the runtime: when I spawn the next child, route signals here.
|
||||
let sup_pid = sup_handle.pid();
|
||||
smarm::scheduler::register_supervisor_channel(sup_pid, sig_tx);
|
||||
|
||||
let child = spawn_under(sup_pid, || panic!("oops"));
|
||||
let _ = child.join();
|
||||
sup_handle.join().unwrap();
|
||||
});
|
||||
|
||||
assert!(saw_panic_signal.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Multiple children, all complete, parent gets back control
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn many_children_all_complete() {
|
||||
let counter = Arc::new(AtomicI64::new(0));
|
||||
let c = counter.clone();
|
||||
run(move || {
|
||||
let mut handles = Vec::new();
|
||||
for _ in 0..10 {
|
||||
let cc = c.clone();
|
||||
handles.push(spawn(move || {
|
||||
cc.fetch_add(1, Ordering::SeqCst);
|
||||
}));
|
||||
}
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
});
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 10);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repeated yield_now inside an actor with no other actors completes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn yield_alone_terminates() {
|
||||
thread_local! {
|
||||
static N: Cell<i32> = const { Cell::new(0) };
|
||||
}
|
||||
N.with(|c| c.set(0));
|
||||
run(|| {
|
||||
for _ in 0..5 {
|
||||
N.with(|c| c.set(c.get() + 1));
|
||||
yield_now();
|
||||
}
|
||||
});
|
||||
assert_eq!(N.with(|c| c.get()), 5);
|
||||
}
|
||||
123
tests/stack.rs
123
tests/stack.rs
@@ -1,123 +0,0 @@
|
||||
//! Stack allocator tests.
|
||||
//!
|
||||
//! Covers allocation, alignment, read/write across the usable region, and
|
||||
//! (via subprocess) that the guard page actually SIGSEGVs.
|
||||
|
||||
use smarm::stack::Stack;
|
||||
|
||||
#[test]
|
||||
fn top_is_16_byte_aligned() {
|
||||
let s = Stack::new(64 * 1024).unwrap();
|
||||
assert_eq!(s.top() as usize % 16, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_is_within_allocation() {
|
||||
let s = Stack::new(64 * 1024).unwrap();
|
||||
let top = s.top() as usize;
|
||||
let base = s.usable_base() as usize;
|
||||
assert!(top > base);
|
||||
assert!(top <= base + s.stack_size());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_and_read_top_of_stack() {
|
||||
let s = Stack::new(64 * 1024).unwrap();
|
||||
let sentinel: u64 = 0xDEAD_BEEF_CAFE_1234;
|
||||
unsafe {
|
||||
let ptr = s.top().sub(8) as *mut u64;
|
||||
ptr.write_volatile(sentinel);
|
||||
assert_eq!(ptr.read_volatile(), sentinel);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_and_read_bottom_of_usable_region() {
|
||||
let s = Stack::new(64 * 1024).unwrap();
|
||||
let sentinel: u64 = 0x0102_0304_0506_0708;
|
||||
unsafe {
|
||||
let ptr = s.usable_base() as *mut u64;
|
||||
ptr.write_volatile(sentinel);
|
||||
assert_eq!(ptr.read_volatile(), sentinel);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_stack_allocates() {
|
||||
assert!(Stack::new(4096).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_stack_allocates() {
|
||||
assert!(Stack::new(8 * 1024 * 1024).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_size_at_least_requested() {
|
||||
let s = Stack::new(64 * 1024).unwrap();
|
||||
assert!(s.stack_size() >= 64 * 1024);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Guard page SIGSEGV tests — subprocess-based.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
fn run_as_child_if_requested() {
|
||||
match env::var("SMARM_SUBTEST").as_deref() {
|
||||
Ok("guard_page_direct") => {
|
||||
let s = Stack::new(64 * 1024).unwrap();
|
||||
unsafe {
|
||||
let guard_ptr = s.usable_base().sub(1);
|
||||
guard_ptr.write_volatile(0xAB);
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
Ok("stack_overflow") => {
|
||||
let s = Stack::new(64 * 1024).unwrap();
|
||||
unsafe {
|
||||
let mut ptr = s.top().sub(1);
|
||||
let stop = s.usable_base().sub(1);
|
||||
while ptr >= stop {
|
||||
ptr.write_volatile(0xFF);
|
||||
ptr = ptr.sub(1);
|
||||
}
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_subtest(name: &str) -> std::process::ExitStatus {
|
||||
let exe = env::current_exe().unwrap();
|
||||
Command::new(exe)
|
||||
.env("SMARM_SUBTEST", name)
|
||||
.args(["--test-threads=1", "--quiet"])
|
||||
.status()
|
||||
.expect("failed to spawn subprocess")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_page_causes_sigsegv() {
|
||||
run_as_child_if_requested();
|
||||
let status = spawn_subtest("guard_page_direct");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_overflow_causes_sigsegv() {
|
||||
run_as_child_if_requested();
|
||||
let status = spawn_subtest("stack_overflow");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
assert_eq!(status.signal(), Some(11), "expected SIGSEGV, got: {:?}", status);
|
||||
}
|
||||
}
|
||||
209
tests/timer.rs
209
tests/timer.rs
@@ -1,209 +0,0 @@
|
||||
//! 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};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
struct RecordingTarget {
|
||||
calls: RefCell<Vec<(Pid, u64)>>,
|
||||
}
|
||||
impl TimerTarget for RecordingTarget {
|
||||
fn on_timeout(&self, pid: Pid, seq: u64) {
|
||||
self.calls.borrow_mut().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 = Rc::new(RecordingTarget { calls: RefCell::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