164 lines
5.5 KiB
Rust
164 lines
5.5 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();
|
|
});
|
|
}
|