Files
smarm/tests/monitor.rs
T

224 lines
7.7 KiB
Rust

//! Monitor tests: `monitor(pid)` delivers exactly one `Down` describing how
//! the target terminated, and `demonitor(&m)` tears a registration back down.
//!
//! A monitor is unidirectional and one-shot: it never propagates failure to
//! the watcher (that's a link), it just drops a `Down` into the returned
//! channel. These tests pin the three reasons — `Exit`, `Panic`, `NoProc` —
//! the fan-out case where several monitors watch the same target, and that
//! `demonitor` removes exactly the one registration it names.
//!
//! Scheduling note: under the default single-thread runtime the parent runs
//! until it parks, so every `monitor()` call below registers while the target
//! is still `Runnable` (it hasn't been resumed yet). Registration and
//! `finalize_actor` both serialize on the shared mutex, so a target that is
//! alive at registration time always produces a real `Down`, never a missed
//! one.
use smarm::{demonitor, monitor, run, spawn, DownReason};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
#[test]
fn monitor_sees_normal_exit() {
let ok = Arc::new(AtomicBool::new(false));
let o = ok.clone();
run(move || {
let h = spawn(|| {});
let pid = h.pid();
let down = monitor(pid);
let d = down.rx.recv().expect("monitor channel closed before Down");
assert_eq!(d.pid, pid, "Down reported the wrong pid");
if matches!(d.reason, DownReason::Exit) {
o.store(true, Ordering::SeqCst);
}
let _ = h.join();
});
assert!(ok.load(Ordering::SeqCst), "expected DownReason::Exit");
}
#[test]
fn monitor_sees_panic() {
let ok = Arc::new(AtomicBool::new(false));
let o = ok.clone();
run(move || {
let h = spawn(|| panic!("boom"));
let pid = h.pid();
let down = monitor(pid);
let d = down.rx.recv().expect("monitor channel closed before Down");
assert_eq!(d.pid, pid);
if matches!(d.reason, DownReason::Panic) {
o.store(true, Ordering::SeqCst);
}
// Drain the panic outcome so the joiner path is exercised too; the
// payload goes here, not to the monitor.
let _ = h.join();
});
assert!(ok.load(Ordering::SeqCst), "expected DownReason::Panic");
}
#[test]
fn monitor_already_dead_target_is_noproc() {
let ok = Arc::new(AtomicBool::new(false));
let o = ok.clone();
run(move || {
let h = spawn(|| {});
let pid = h.pid();
// Consume the only handle on a finished child: the slot is reclaimed
// and its generation bumped, so `pid` is now stale.
h.join().unwrap();
let down = monitor(pid);
let d = down
.rx
.recv()
.expect("NoProc Down should be delivered immediately");
assert_eq!(d.pid, pid);
if matches!(d.reason, DownReason::NoProc) {
o.store(true, Ordering::SeqCst);
}
});
assert!(ok.load(Ordering::SeqCst), "expected DownReason::NoProc");
}
#[test]
fn multiple_monitors_all_notified() {
let count = Arc::new(AtomicUsize::new(0));
let c = count.clone();
run(move || {
let h = spawn(|| {});
let pid = h.pid();
let monitors = [monitor(pid), monitor(pid), monitor(pid)];
let _ = h.join();
for m in monitors {
if matches!(m.rx.recv().unwrap().reason, DownReason::Exit) {
c.fetch_add(1, Ordering::SeqCst);
}
}
});
assert_eq!(
count.load(Ordering::SeqCst),
3,
"every monitor should see the Down"
);
}
#[test]
fn demonitor_stops_delivery() {
// Demonitoring a live registration removes the slot's only sender for that
// channel; the channel closes, so a later recv() observes Err rather than
// a Down. demonitor reports the id it tore down.
run(|| {
let h = spawn(|| {});
let pid = h.pid();
let m = monitor(pid);
assert_eq!(
demonitor(&m),
Some(m.id),
"live registration should be removed"
);
assert!(
m.rx.recv().is_err(),
"no Down should arrive after demonitor"
);
let _ = h.join();
});
}
#[test]
fn demonitor_one_of_many() {
// Three monitors on one target; demonitor the middle. The other two still
// fire; the demonitored channel is closed with no Down. Distinct ids mean
// tearing one down never disturbs its siblings.
run(|| {
let h = spawn(|| {});
let pid = h.pid();
let ms = [monitor(pid), monitor(pid), monitor(pid)];
assert_eq!(demonitor(&ms[1]), Some(ms[1].id));
let _ = h.join();
assert!(matches!(ms[0].rx.recv().unwrap().reason, DownReason::Exit));
assert!(matches!(ms[2].rx.recv().unwrap().reason, DownReason::Exit));
assert!(
ms[1].rx.recv().is_err(),
"demonitored channel should be closed"
);
});
}
#[test]
fn demonitor_after_fire_is_none() {
// Once the Down has fired, the registration is already drained from the
// slot, so demonitor finds nothing to remove and reports None.
run(|| {
let h = spawn(|| {});
let pid = h.pid();
let m = monitor(pid);
let d = m.rx.recv().expect("Down before close");
assert!(matches!(d.reason, DownReason::Exit));
assert_eq!(
demonitor(&m),
None,
"already-fired monitor has nothing to remove"
);
let _ = h.join();
});
}
// ---------------------------------------------------------------------------
// spawn_monitor: registration precedes publish, so a child that dies before
// the parent gets another instruction in still reports its real reason.
// ---------------------------------------------------------------------------
#[test]
fn spawn_monitor_never_reports_noproc_multi_thread() {
// 4 schedulers, 200 instantly-dying children. With spawn+monitor this
// observes NoProc a few percent of the time (the child finishes on
// another scheduler before monitor() registers); with spawn_monitor it
// must be Exit every time.
let noproc = Arc::new(AtomicUsize::new(0));
let exit = Arc::new(AtomicUsize::new(0));
let (n, e) = (noproc.clone(), exit.clone());
smarm::init(smarm::Config::exact(4)).run(move || {
let mut hs = Vec::new();
for _ in 0..200 {
let (h, m) = smarm::spawn_monitor(|| {});
let d = m.rx.recv().expect("Down");
assert_eq!(d.pid, h.pid());
match d.reason {
DownReason::Exit => e.fetch_add(1, Ordering::Relaxed),
DownReason::NoProc => n.fetch_add(1, Ordering::Relaxed),
other => panic!("unexpected {other:?}"),
};
hs.push(h);
}
for h in hs {
h.join().unwrap();
}
});
assert_eq!(noproc.load(Ordering::Relaxed), 0);
assert_eq!(exit.load(Ordering::Relaxed), 200);
}
#[test]
fn spawn_monitor_sees_panic_and_demonitor_works() {
let ok = Arc::new(AtomicBool::new(false));
let o = ok.clone();
run(move || {
let (h, m) = smarm::spawn_monitor(|| panic!("boom"));
let d = m.rx.recv().expect("Down");
assert_eq!(d.pid, h.pid());
assert!(matches!(d.reason, DownReason::Panic));
let _ = h.join();
// demonitor before the child runs: no Down ever arrives.
let (h2, m2) = smarm::spawn_monitor(|| {});
demonitor(&m2);
h2.join().unwrap();
// Last sender gone with nothing sent: the channel is closed and empty.
assert!(
!matches!(m2.rx.try_recv(), Ok(Some(_))),
"demonitored spawn_monitor still delivered"
);
o.store(true, Ordering::SeqCst);
});
assert!(ok.load(Ordering::SeqCst));
}