tree() / tree_from() fold a Chunk-1 snapshot into a forest by grouping each actor under its parent pid — one O(n) pass, no new reads. tree_from is public so a held (or synthetic) snapshot can be folded without a second scan. - D8: actors parented at ROOT_PID are genuine roots; an actor whose recorded parent is absent from the snapshot is re-rooted under the sentinel and flagged orphaned, so the forest stays total. take()-on- place doubles as a guard against re-entering a node. - D9: the edge is parent/spawned-by, documented as not necessarily supervision.
261 lines
8.5 KiB
Rust
261 lines
8.5 KiB
Rust
//! RFC 016 Chunk 1 — the read primitive. These exercise exactly what the RFC
|
|
//! promised: tests that assert an actor's state, parentage, names, mailbox
|
|
//! depth, and lifecycle counts directly off `snapshot()` / `actor_info()`
|
|
//! instead of sleeping-and-hoping.
|
|
|
|
use smarm::{
|
|
actor_info, channel, monitor, register, run, self_pid, send, snapshot, spawn, tree, tree_from,
|
|
ActorInfo, ActorState, Name, Pid, RuntimeSnapshot, SNAPSHOT_FORMAT_VERSION,
|
|
};
|
|
|
|
const SVC: Name<u64> = Name::new("svc");
|
|
|
|
/// Bounded poll on the introspection result itself (not a wall-clock sleep):
|
|
/// yield until `pred` holds for the given pid, panicking if it never does.
|
|
fn spin_until(pid: Pid, mut pred: impl FnMut(&smarm::ActorInfo) -> bool) -> smarm::ActorInfo {
|
|
for _ in 0..100_000 {
|
|
if let Some(info) = actor_info(pid) {
|
|
if pred(&info) {
|
|
return info;
|
|
}
|
|
}
|
|
smarm::yield_now();
|
|
}
|
|
panic!("actor {pid:?} never reached the expected state");
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_lists_actors_with_parent_edge() {
|
|
run(|| {
|
|
let (ready_tx, ready_rx) = channel::<()>();
|
|
let (gate_tx, gate_rx) = channel::<()>();
|
|
let (_cmd_tx, cmd_rx) = channel::<u64>();
|
|
|
|
let h = spawn(move || {
|
|
register(Name::<u64>::new("w"), _cmd_tx).unwrap();
|
|
ready_tx.send(()).unwrap();
|
|
gate_rx.recv().unwrap(); // park here until released
|
|
drop(cmd_rx);
|
|
});
|
|
ready_rx.recv().unwrap();
|
|
|
|
let me = self_pid();
|
|
let snap = snapshot();
|
|
assert_eq!(snap.format_version, SNAPSHOT_FORMAT_VERSION);
|
|
|
|
let worker = snap
|
|
.actors
|
|
.iter()
|
|
.find(|a| a.pid == h.pid())
|
|
.expect("worker present in snapshot");
|
|
assert_eq!(worker.names, vec!["w"]);
|
|
// `spawn` records the spawning actor as the parent (D9).
|
|
assert_eq!(worker.supervisor, me);
|
|
assert!(!worker.trap_exit);
|
|
assert_eq!((worker.monitors, worker.links, worker.joiners), (0, 0, 0));
|
|
|
|
// The root itself is on-CPU (it's running this code) and rooted under
|
|
// the forest sentinel.
|
|
let root = snap.actors.iter().find(|a| a.pid == me).expect("root present");
|
|
assert_eq!(root.state, ActorState::Running);
|
|
assert_eq!(root.supervisor, smarm::Pid::new(u32::MAX, u32::MAX));
|
|
|
|
gate_tx.send(()).unwrap();
|
|
h.join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn parked_state_is_observable() {
|
|
run(|| {
|
|
let (ready_tx, ready_rx) = channel::<()>();
|
|
let (gate_tx, gate_rx) = channel::<()>();
|
|
let h = spawn(move || {
|
|
ready_tx.send(()).unwrap();
|
|
gate_rx.recv().unwrap();
|
|
});
|
|
ready_rx.recv().unwrap();
|
|
// The worker has nothing to do but block on the empty gate channel, so
|
|
// it must reach Parked.
|
|
let info = spin_until(h.pid(), |a| a.state == ActorState::Parked);
|
|
assert_eq!(info.state, ActorState::Parked);
|
|
gate_tx.send(()).unwrap();
|
|
h.join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn mailbox_depth_counts_queued_messages() {
|
|
run(|| {
|
|
let (ready_tx, ready_rx) = channel::<()>();
|
|
let (gate_tx, gate_rx) = channel::<()>();
|
|
let (cmd_tx, _cmd_rx) = channel::<u64>();
|
|
|
|
let h = spawn(move || {
|
|
// Publish the command inbox, then block on an unrelated gate so the
|
|
// queued commands are never drained while we observe them.
|
|
register(SVC, cmd_tx).unwrap();
|
|
ready_tx.send(()).unwrap();
|
|
gate_rx.recv().unwrap();
|
|
drop(_cmd_rx);
|
|
});
|
|
ready_rx.recv().unwrap();
|
|
|
|
for i in 0..3 {
|
|
send(SVC, i).unwrap();
|
|
}
|
|
|
|
// Depth is a property of the queue, set synchronously by `send`, so it
|
|
// reads 3 regardless of the worker's scheduling state.
|
|
let via_snapshot = snapshot()
|
|
.actors
|
|
.into_iter()
|
|
.find(|a| a.pid == h.pid())
|
|
.expect("worker present");
|
|
assert_eq!(via_snapshot.mailbox_depth, 3);
|
|
assert_eq!(actor_info(h.pid()).unwrap().mailbox_depth, 3);
|
|
|
|
gate_tx.send(()).unwrap();
|
|
h.join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn monitor_count_is_visible() {
|
|
run(|| {
|
|
let (ready_tx, ready_rx) = channel::<()>();
|
|
let (gate_tx, gate_rx) = channel::<()>();
|
|
let h = spawn(move || {
|
|
ready_tx.send(()).unwrap();
|
|
gate_rx.recv().unwrap();
|
|
});
|
|
ready_rx.recv().unwrap();
|
|
|
|
let _m = monitor(h.pid());
|
|
let info = actor_info(h.pid()).expect("worker present");
|
|
assert_eq!(info.monitors, 1);
|
|
|
|
gate_tx.send(()).unwrap();
|
|
h.join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn stale_and_forged_pids_return_none() {
|
|
run(|| {
|
|
let (ready_tx, ready_rx) = channel::<()>();
|
|
let (gate_tx, gate_rx) = channel::<()>();
|
|
let h = spawn(move || {
|
|
ready_tx.send(()).unwrap();
|
|
gate_rx.recv().unwrap();
|
|
});
|
|
ready_rx.recv().unwrap();
|
|
let live = h.pid();
|
|
|
|
// Same slot index, wrong generation → stale, no such incarnation.
|
|
let stale = Pid::new(live.index(), live.generation().wrapping_add(7));
|
|
assert!(actor_info(stale).is_none());
|
|
|
|
// Out-of-range index → not in the slab at all.
|
|
let forged = Pid::new(u32::MAX - 1, 0);
|
|
assert!(actor_info(forged).is_none());
|
|
|
|
gate_tx.send(()).unwrap();
|
|
h.join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn done_actor_is_a_tombstone() {
|
|
run(|| {
|
|
// Hold the join handle so the slot is NOT reclaimed when the actor
|
|
// exits: outstanding_handles stays > 0, leaving a Done tombstone to
|
|
// observe.
|
|
let h = spawn(|| {});
|
|
let pid = h.pid();
|
|
let info = spin_until(pid, |a| a.state == ActorState::Done);
|
|
assert_eq!(info.state, ActorState::Done);
|
|
// The Actor record is gone at finalize, so a tombstone reports root-less
|
|
// with empty lifecycle counts.
|
|
assert_eq!(info.supervisor, Pid::new(u32::MAX, u32::MAX));
|
|
assert_eq!((info.monitors, info.links, info.joiners), (0, 0, 0));
|
|
h.join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn tree_places_child_under_its_spawner() {
|
|
run(|| {
|
|
let (ready_tx, ready_rx) = channel::<()>();
|
|
let (gate_tx, gate_rx) = channel::<()>();
|
|
let h = spawn(move || {
|
|
ready_tx.send(()).unwrap();
|
|
gate_rx.recv().unwrap();
|
|
});
|
|
ready_rx.recv().unwrap();
|
|
|
|
let me = self_pid();
|
|
let t = tree();
|
|
assert_eq!(t.format_version, SNAPSHOT_FORMAT_VERSION);
|
|
|
|
// The root is parented at the forest sentinel, so it's a genuine root,
|
|
// and the worker it spawned hangs beneath it.
|
|
let root = t.roots.iter().find(|n| n.info.pid == me).expect("root in forest");
|
|
assert!(!root.orphaned);
|
|
assert!(
|
|
root.children.iter().any(|c| c.info.pid == h.pid()),
|
|
"spawned worker should be a child of its spawner"
|
|
);
|
|
|
|
gate_tx.send(()).unwrap();
|
|
h.join().unwrap();
|
|
});
|
|
}
|
|
|
|
/// D8 re-rooting and nesting, exercised on a synthetic snapshot via the public
|
|
/// `tree_from` — the live lifecycle race (parent reclaimed while child lives)
|
|
/// is exactly what's awkward to stage deterministically, which is why the fold
|
|
/// is testable in isolation.
|
|
#[test]
|
|
fn tree_from_nests_children_and_reroots_orphans() {
|
|
let root_sentinel = Pid::new(u32::MAX, u32::MAX);
|
|
let root_pid = Pid::new(0, 1);
|
|
let child = Pid::new(1, 1);
|
|
let orphan = Pid::new(2, 1);
|
|
let absent_parent = Pid::new(99, 1);
|
|
|
|
let mk = |pid: Pid, supervisor: Pid| ActorInfo {
|
|
pid,
|
|
names: Vec::new(),
|
|
state: ActorState::Running,
|
|
supervisor,
|
|
trap_exit: false,
|
|
monitors: 0,
|
|
links: 0,
|
|
joiners: 0,
|
|
mailbox_depth: 0,
|
|
};
|
|
|
|
let snap = RuntimeSnapshot {
|
|
format_version: SNAPSHOT_FORMAT_VERSION,
|
|
actors: vec![
|
|
mk(root_pid, root_sentinel),
|
|
mk(child, root_pid),
|
|
mk(orphan, absent_parent),
|
|
],
|
|
};
|
|
|
|
let t = tree_from(snap);
|
|
assert_eq!(t.roots.len(), 2);
|
|
|
|
let root = t.roots.iter().find(|n| n.info.pid == root_pid).expect("root present");
|
|
assert!(!root.orphaned);
|
|
assert_eq!(root.children.len(), 1);
|
|
assert_eq!(root.children[0].info.pid, child);
|
|
assert!(!root.children[0].orphaned);
|
|
|
|
let o = t.roots.iter().find(|n| n.info.pid == orphan).expect("orphan re-rooted");
|
|
assert!(o.orphaned, "an actor whose parent is absent must be flagged orphaned");
|
|
assert!(o.children.is_empty());
|
|
}
|