v0.1: green-thread actors, supervision, channels, benchmark
Hand-rolled context switching on mmap'd stacks with guard pages, allocator-driven RDTSC preemption, unbounded MPSC channels, supervision via per-slot Signal mailboxes, root supervisor as sentinel PID. Lib + tests + benches clean check/clippy. All 29 tests pass. Bench: smarm 3.4% over serial baseline, within 160us of tokio current-thread on prime-counting fan-out.
This commit is contained in:
110
tests/channel.rs
Normal file
110
tests/channel.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
//! 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
Normal file
137
tests/context.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
//! 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);
|
||||
}
|
||||
22
tests/pid.rs
Normal file
22
tests/pid.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
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);
|
||||
}
|
||||
171
tests/scheduler.rs
Normal file
171
tests/scheduler.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
//! 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
Normal file
123
tests/stack.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user