swactor/tests/std_extension.rs

4986 lines
145 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! StdExtension Tests — higher-level patterns from swactor-std.
//!
//! Covers: naming registry, groups/pub-sub, ask pattern, supervision
//! strategies and restart policies, and router work distribution.
mod common;
use common::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
// ── Local actors ────────────────────────────────────────────────────────────
/// Looks up a peer by name using ctx.where_is().
struct NameLookupActor {
target_name: &'static str,
reply_to: ActorAddress,
}
impl ActorInterface for NameLookupActor {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
if let Some(peer) = ctx.where_is(self.target_name) {
ctx.send(self.reply_to, MyAddr(peer)).unwrap();
}
}
}
/// Spawns a named child from a handler.
struct NamedSpawnerActor {
reply_to: ActorAddress,
}
impl ActorInterface for NamedSpawnerActor {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
if let Ok(addr) = ctx.spawn_named("child", PingPongActor) {
ctx.send(self.reply_to, MyAddr(addr)).unwrap();
}
}
}
/// Panics after `trigger` messages.
struct PanicAfterN {
trigger: usize,
count: usize,
counter: Arc<AtomicUsize>,
}
impl ActorInterface for PanicAfterN {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: Ping) {
self.count += 1;
self.counter.fetch_add(1, Ordering::SeqCst);
let _ = ctx.send(msg.reply_to, Pong);
if self.count >= self.trigger {
panic!("intentional panic at message {}", self.count);
}
}
}
/// Stops itself on first message.
struct StopsAfterFirst;
impl ActorInterface for StopsAfterFirst {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_self();
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Naming Registry
// ═══════════════════════════════════════════════════════════════════════════
/// Full naming lifecycle: register, lookup, send, duplicate fails, auto-unregister
/// on stop and panic, name reuse, registered_names list, manual unregister.
#[test]
fn naming_registry_lifecycle() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
// Register "alice", lookup, send Ping → Pong
let alice = rt.spawn_named("alice", PingPongActor).unwrap();
assert_eq!(rt.where_is("alice"), Some(alice));
rt.send_to(
alice,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
assert!(inbox.try_recv().is_some(), "named actor processes messages");
// Duplicate fails, original binding preserved
assert!(rt.spawn_named("alice", PingPongActor).is_err());
assert_eq!(rt.where_is("alice"), Some(alice));
// Unknown name → None
assert_eq!(rt.where_is("ghost"), None);
// Stop "alice" → name freed
rt.stop_actor(alice).unwrap();
rt.tick();
assert_eq!(rt.where_is("alice"), None, "name freed after stop");
// Reuse the name
let alice2 = rt.spawn_named("alice", PingPongActor).unwrap();
assert_ne!(alice, alice2);
assert_eq!(rt.where_is("alice"), Some(alice2));
// Panic also frees the name
let bob = rt.spawn_named("bob", PanicActor).unwrap();
rt.tick();
rt.send_to(bob, PanicMsg).unwrap();
rt.tick();
assert_eq!(rt.where_is("bob"), None, "name freed after panic");
let _bob2 = rt.spawn_named("bob", PingPongActor).unwrap();
assert!(rt.where_is("bob").is_some());
// registered_names enumerates all
rt.spawn_named("gamma", PingPongActor).unwrap();
let mut names = rt.registered_names();
names.sort();
assert!(names.contains(&"alice".to_string()));
assert!(names.contains(&"bob".to_string()));
assert!(names.contains(&"gamma".to_string()));
// Manual unregister: name freed but actor lives
let charlie_inbox = rt.new_inbox::<Pong>().unwrap();
let charlie = rt.spawn_named("charlie", PingPongActor).unwrap();
rt.tick();
let removed = rt.unregister("charlie");
assert_eq!(removed, Some(charlie));
assert_eq!(rt.where_is("charlie"), None, "name freed by unregister");
rt.send_to(
charlie,
Ping {
reply_to: *charlie_inbox.addr(),
},
)
.unwrap();
rt.tick();
assert!(
charlie_inbox.try_recv().is_some(),
"actor still alive after name unregistered"
);
}
/// Actors resolve and register names from handlers using ctx.
#[test]
fn naming_from_actor_handlers() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<MyAddr>().unwrap();
// ctx.where_is from handler
let target = rt.spawn_named("target", PingPongActor).unwrap();
let looker = rt
.spawn(NameLookupActor {
target_name: "target",
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
looker,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
assert_eq!(
inbox.try_recv(),
Some(MyAddr(target)),
"ctx.where_is resolves"
);
// ctx.spawn_named from handler
let spawner = rt
.spawn(NamedSpawnerActor {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
spawner,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
let child_addr = inbox.try_recv().expect("child address returned");
assert_eq!(
rt.where_is("child"),
Some(child_addr.0),
"name registered from handler"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Groups / Pub-Sub
// ═══════════════════════════════════════════════════════════════════════════
/// Full groups lifecycle: join, publish broadcasts, leave stops delivery,
/// dead actor auto-removed, multi-group cleanup, empty group deleted,
/// join and publish from handlers.
#[test]
fn groups_pub_sub_lifecycle() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
// Join 3 actors, publish → all 3 get it
let a = rt.spawn(PingPongActor).unwrap();
let b = rt.spawn(PingPongActor).unwrap();
let c = rt.spawn(PingPongActor).unwrap();
rt.join_group(a, "workers");
rt.join_group(b, "workers");
rt.join_group(c, "workers");
rt.tick();
let count = rt.publish_to(
"workers",
Ping {
reply_to: *inbox.addr(),
},
);
assert_eq!(count, 3, "3 members, 3 messages sent");
rt.tick();
let mut pongs = 0;
while inbox.try_recv().is_some() {
pongs += 1;
}
assert_eq!(pongs, 3, "all 3 received");
// Leave stops delivery
rt.leave_group(c, "workers");
let count = rt.publish_to(
"workers",
Ping {
reply_to: *inbox.addr(),
},
);
assert_eq!(count, 2, "2 after leave");
rt.tick();
let mut pongs = 0;
while inbox.try_recv().is_some() {
pongs += 1;
}
assert_eq!(pongs, 2);
// Dead actor auto-removed
rt.stop_actor(b).unwrap();
rt.tick();
let count = rt.publish_to(
"workers",
Ping {
reply_to: *inbox.addr(),
},
);
assert_eq!(count, 1, "dead actor removed");
// Multi-group cleanup: actor in alpha/beta/gamma dies → all cleaned
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(PingPongActor).unwrap();
rt.join_group(actor, "alpha");
rt.join_group(actor, "beta");
rt.join_group(actor, "gamma");
rt.tick();
rt.stop_actor(actor).unwrap();
rt.tick();
assert!(rt.group_members("alpha").is_empty());
assert!(rt.group_members("beta").is_empty());
assert!(rt.group_members("gamma").is_empty());
// Empty group auto-deleted
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(PingPongActor).unwrap();
rt.join_group(actor, "temp");
assert!(rt.groups().contains(&"temp".to_string()));
rt.leave_group(actor, "temp");
assert!(
!rt.groups().contains(&"temp".to_string()),
"empty group removed"
);
// Empty group query
let rt = std_runtime(RuntimeConfig::default());
assert!(rt.group_members("nonexistent").is_empty());
// ctx.join_group from on_start
struct GroupJoiner;
impl ActorInterface for GroupJoiner {
type Incoming = Ping;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.join_group("auto-joined");
}
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {}
}
let rt = std_runtime(RuntimeConfig::default());
let x = rt.spawn(GroupJoiner).unwrap();
let y = rt.spawn(GroupJoiner).unwrap();
rt.tick();
let members = rt.group_members("auto-joined");
assert_eq!(members.len(), 2);
assert!(members.contains(&x));
assert!(members.contains(&y));
// ctx.publish from handler
#[derive(Clone)]
struct BroadcastCmd {
reply_to: ActorAddress,
}
struct Broadcaster;
impl ActorInterface for Broadcaster {
type Incoming = BroadcastCmd;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.join_group("bcast");
}
fn handle(&mut self, ctx: &Ctx, msg: BroadcastCmd) {
ctx.publish(
"bcast",
Ping {
reply_to: msg.reply_to,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let p1 = rt.spawn(PingPongActor).unwrap();
let p2 = rt.spawn(PingPongActor).unwrap();
rt.join_group(p1, "bcast");
rt.join_group(p2, "bcast");
let broadcaster = rt.spawn(Broadcaster).unwrap();
rt.tick();
rt.send_to(
broadcaster,
BroadcastCmd {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 3);
let mut pongs = 0;
while inbox.try_recv().is_some() {
pongs += 1;
}
assert!(
pongs >= 2,
"at least 2 PingPong members replied, got {pongs}"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Ask Pattern
// ═══════════════════════════════════════════════════════════════════════════
/// Ask pattern: basic ask, repeated asks track state, try_recv before/after
/// tick, dead actor times out.
#[test]
fn ask_pattern() {
let rt = std_runtime(RuntimeConfig::default());
// Basic ask
let actor = rt.spawn(PingPongActor).unwrap();
rt.tick();
let pong: Pong = rt
.ask(actor, |reply_to| Ping { reply_to })
.unwrap()
.recv_ticking(&rt, 10)
.unwrap();
assert_eq!(pong, Pong);
// Repeated asks track state
let counter = rt.spawn(CounterActor { count: 0 }).unwrap();
rt.tick();
let c1: Count = rt
.ask(counter, |reply_to| Increment { reply_to })
.unwrap()
.recv_ticking(&rt, 10)
.unwrap();
let c2: Count = rt
.ask(counter, |reply_to| Increment { reply_to })
.unwrap()
.recv_ticking(&rt, 10)
.unwrap();
let c3: Count = rt
.ask(counter, |reply_to| Increment { reply_to })
.unwrap()
.recv_ticking(&rt, 10)
.unwrap();
assert_eq!((c1, c2, c3), (Count(1), Count(2), Count(3)));
// try_recv: None before tick, Some after
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(PingPongActor).unwrap();
rt.tick();
let ask = rt
.ask::<Ping, Pong>(actor, |reply_to| Ping { reply_to })
.unwrap();
assert!(ask.try_recv().is_none(), "no response before tick");
rt.tick();
assert_eq!(ask.try_recv(), Some(Pong));
// Dead actor → timeout
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(PingPongActor).unwrap();
rt.tick();
rt.stop_actor(actor).unwrap();
rt.tick();
if let Ok(ask) = rt.ask::<Ping, Pong>(actor, |reply_to| Ping { reply_to }) {
assert!(ask.recv_ticking(&rt, 5).is_err(), "timeout with dead actor");
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Supervision
// ═══════════════════════════════════════════════════════════════════════════
/// Restart policies: permanent always restarts, transient only on panic,
/// temporary never restarts, meltdown after max_restarts.
#[test]
fn supervision_restart_policies() {
// Permanent child panics → restarted
let rt = std_runtime(RuntimeConfig::default());
let counter = Arc::new(AtomicUsize::new(0));
let counter_c = counter.clone();
let inbox = rt.new_inbox::<Pong>().unwrap();
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![ChildSpec::new(
"worker",
RestartPolicy::Permanent,
move |ctx| {
ctx.spawn(PanicAfterN {
trigger: 2,
count: 0,
counter: counter_c.clone(),
})
},
)],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 2);
let child = rt
.stats()
.actors
.iter()
.find(|(a, _)| *a != sup_addr)
.map(|(a, _)| *a)
.unwrap();
rt.send_to(
child,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
assert_eq!(counter.load(Ordering::SeqCst), 1);
rt.send_to(
child,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 5); // panics, supervisor restarts
assert_eq!(
rt.stats().workers[0].num_actors,
2,
"supervisor + restarted child"
);
// Transient stops normally → NOT restarted
let rt = std_runtime(RuntimeConfig::default());
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![ChildSpec::new("worker", RestartPolicy::Transient, |ctx| {
ctx.spawn(StopsAfterFirst)
})],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 2);
let child = rt
.stats()
.actors
.iter()
.find(|(a, _)| *a != sup_addr)
.map(|(a, _)| *a)
.unwrap();
rt.send_to(
child,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 4);
assert_eq!(
rt.stats().workers[0].num_actors,
1,
"transient+normal → no restart"
);
// Transient panics → restarted
let rt = std_runtime(RuntimeConfig::default());
let counter = Arc::new(AtomicUsize::new(0));
let counter_c = counter.clone();
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![ChildSpec::new(
"worker",
RestartPolicy::Transient,
move |ctx| {
ctx.spawn(PanicAfterN {
trigger: 1,
count: 0,
counter: counter_c.clone(),
})
},
)],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 2);
let child = rt
.stats()
.actors
.iter()
.find(|(a, _)| *a != sup_addr)
.map(|(a, _)| *a)
.unwrap();
let inbox = rt.new_inbox::<Pong>().unwrap();
rt.send_to(
child,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 5);
assert_eq!(
rt.stats().workers[0].num_actors,
2,
"transient+panic → restarted"
);
// Temporary never restarts
let rt = std_runtime(RuntimeConfig::default());
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![ChildSpec::new("worker", RestartPolicy::Temporary, |ctx| {
ctx.spawn(PanicActor)
})],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 2);
let child = rt
.stats()
.actors
.iter()
.find(|(a, _)| *a != sup_addr)
.map(|(a, _)| *a)
.unwrap();
rt.send_to(child, PanicMsg).unwrap();
tick_n(&rt, 4);
assert_eq!(
rt.stats().workers[0].num_actors,
1,
"temporary → no restart"
);
// Meltdown: max_restarts=2, crash 3 times → supervisor stops
let rt = std_runtime(RuntimeConfig::default());
let counter = Arc::new(AtomicUsize::new(0));
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
2,
vec![ChildSpec::new("crasher", RestartPolicy::Permanent, {
let c = counter.clone();
move |ctx| {
ctx.spawn(PanicAfterN {
trigger: 1,
count: 0,
counter: c.clone(),
})
}
})],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 2);
for _ in 0..3 {
if let Some((child, _)) = rt.stats().actors.iter().find(|(a, _)| *a != sup_addr) {
let inbox = rt.new_inbox::<Pong>().unwrap();
let _ = rt.send_to(
*child,
Ping {
reply_to: *inbox.addr(),
},
);
tick_n(&rt, 5);
}
}
let sup_alive = rt.stats().actors.iter().any(|(a, _)| *a == sup_addr);
assert!(
!sup_alive,
"supervisor stopped after exceeding max_restarts"
);
}
/// Strategies: OneForOne, OneForAll, RestForOne. Stopping supervisor kills children.
#[test]
fn supervision_strategies() {
// OneForOne: only failed child restarted
let rt = std_runtime(RuntimeConfig::default());
let counter_a = Arc::new(AtomicUsize::new(0));
let counter_b = Arc::new(AtomicUsize::new(0));
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![
ChildSpec::new("crasher", RestartPolicy::Permanent, {
let c = counter_a.clone();
move |ctx| {
ctx.spawn_named(
"ofo_a",
PanicAfterN {
trigger: 1,
count: 0,
counter: c.clone(),
},
)
}
}),
ChildSpec::new("stable", RestartPolicy::Permanent, {
let c = counter_b.clone();
move |ctx| ctx.spawn_named("ofo_b", CountingPingActor { counter: c.clone() })
}),
],
);
rt.spawn(sup).unwrap();
tick_n(&rt, 2);
let child_a = rt.where_is("ofo_a").unwrap();
let child_b = rt.where_is("ofo_b").unwrap();
let inbox = rt.new_inbox::<Pong>().unwrap();
rt.send_to(
child_a,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 5);
let child_b_after = rt.where_is("ofo_b").unwrap();
assert_eq!(child_b, child_b_after, "child_b unchanged in OneForOne");
rt.send_to(
child_b,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
assert!(
counter_b.load(Ordering::SeqCst) >= 1,
"child_b still processing"
);
// OneForAll: all children restarted
let rt = std_runtime(RuntimeConfig::default());
let sup = Supervisor::new(
SupervisorStrategy::OneForAll,
5,
vec![
ChildSpec::new("a", RestartPolicy::Permanent, {
let c = Arc::new(AtomicUsize::new(0));
move |ctx| {
ctx.spawn_named(
"ofa_a",
PanicAfterN {
trigger: 1,
count: 0,
counter: c.clone(),
},
)
}
}),
ChildSpec::new("b", RestartPolicy::Permanent, {
let c = Arc::new(AtomicUsize::new(0));
move |ctx| ctx.spawn_named("ofa_b", CountingPingActor { counter: c.clone() })
}),
],
);
rt.spawn(sup).unwrap();
tick_n(&rt, 2);
let old_b = rt.where_is("ofa_b").unwrap();
let child_a = rt.where_is("ofa_a").unwrap();
let inbox = rt.new_inbox::<Pong>().unwrap();
rt.send_to(
child_a,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 8);
let new_b = rt.where_is("ofa_b").expect("ofa_b re-registered");
assert_ne!(old_b, new_b, "child_b restarted in OneForAll");
// RestForOne: failed child + later children restarted, earlier unaffected
let rt = std_runtime(RuntimeConfig::default());
let sup = Supervisor::new(
SupervisorStrategy::RestForOne,
5,
vec![
ChildSpec::new("a", RestartPolicy::Permanent, {
let c = Arc::new(AtomicUsize::new(0));
move |ctx| ctx.spawn_named("rfo_a", CountingPingActor { counter: c.clone() })
}),
ChildSpec::new("b", RestartPolicy::Permanent, {
let c = Arc::new(AtomicUsize::new(0));
move |ctx| {
ctx.spawn_named(
"rfo_b",
PanicAfterN {
trigger: 1,
count: 0,
counter: c.clone(),
},
)
}
}),
ChildSpec::new("c", RestartPolicy::Permanent, {
let c = Arc::new(AtomicUsize::new(0));
move |ctx| ctx.spawn_named("rfo_c", CountingPingActor { counter: c.clone() })
}),
],
);
rt.spawn(sup).unwrap();
tick_n(&rt, 2);
let old_a = rt.where_is("rfo_a").unwrap();
let old_c = rt.where_is("rfo_c").unwrap();
let child_b = rt.where_is("rfo_b").unwrap();
let inbox = rt.new_inbox::<Pong>().unwrap();
rt.send_to(
child_b,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 8);
let new_a = rt.where_is("rfo_a").unwrap();
let new_c = rt.where_is("rfo_c").expect("rfo_c re-registered");
assert_eq!(old_a, new_a, "child_a unchanged in RestForOne");
assert_ne!(old_c, new_c, "child_c restarted in RestForOne");
// Stopping supervisor kills children
let rt = std_runtime(RuntimeConfig::default());
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![
ChildSpec::new("a", RestartPolicy::Permanent, |ctx| {
ctx.spawn(PingPongActor)
}),
ChildSpec::new("b", RestartPolicy::Permanent, |ctx| {
ctx.spawn(PingPongActor)
}),
],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 2);
assert_eq!(rt.stats().workers[0].num_actors, 3);
rt.stop_actor(sup_addr).unwrap();
tick_n(&rt, 5);
assert_eq!(
rt.stats().workers[0].num_actors,
0,
"stopping supervisor kills children"
);
}
/// handle_down dispatch and ctx.stop_actor from handler.
#[test]
fn handle_down_dispatch() {
// ctx.stop_actor from handler stops target
#[derive(Clone)]
struct StopCmd {
target: ActorAddress,
}
struct Stopper;
impl ActorInterface for Stopper {
type Incoming = StopCmd;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: StopCmd) {
let _ = ctx.stop_actor(msg.target);
}
}
let rt = std_runtime(RuntimeConfig::default());
let target = rt.spawn(PingPongActor).unwrap();
let stopper = rt.spawn(Stopper).unwrap();
rt.tick();
rt.send_to(stopper, StopCmd { target }).unwrap();
tick_n(&rt, 4);
assert!(
rt.send_to(
target,
Ping {
reply_to: ActorAddress::default()
}
)
.is_err(),
"target stopped by ctx.stop_actor"
);
assert!(
rt.send_to(stopper, StopCmd { target }).is_ok(),
"stopper still alive"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Router
// ═══════════════════════════════════════════════════════════════════════════
/// Router distributes work: round-robin is even, broadcast hits all, random
/// uses multiple workers. Dead workers replaced. Stop router kills workers.
/// Meltdown after max restarts.
#[test]
fn router_work_distribution() {
// Round-robin: 3 workers, 6 msgs → 2 each
let rt = std_runtime(RuntimeConfig::default());
let collected = Arc::new(std::sync::Mutex::new(Vec::new()));
struct Collector(Arc<std::sync::Mutex<Vec<(ActorAddress, usize)>>>);
#[derive(Clone)]
struct Work(usize);
impl ActorInterface for Collector {
type Incoming = Work;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: Work) {
self.0.lock().unwrap().push((ctx.self_addr(), msg.0));
}
}
let c = collected.clone();
let router = Router::<Work>::new(
RoutingStrategy::RoundRobin,
3,
move |ctx| ctx.spawn(Collector(c.clone())),
10,
);
let router_addr = rt.spawn(router).unwrap();
rt.tick();
for i in 0..6 {
rt.send_to(router_addr, Work(i)).unwrap();
}
tick_n(&rt, 3);
let data = collected.lock().unwrap();
assert_eq!(data.len(), 6);
let mut per_worker = std::collections::HashMap::new();
for (addr, _) in data.iter() {
*per_worker.entry(*addr).or_insert(0usize) += 1;
}
assert_eq!(per_worker.len(), 3, "3 distinct workers");
for count in per_worker.values() {
assert_eq!(*count, 2, "each worker gets exactly 2");
}
// Broadcast: 5 msgs to 3 workers → 15 total
let rt = std_runtime(RuntimeConfig::default());
let total = Arc::new(AtomicUsize::new(0));
struct BCounter(Arc<AtomicUsize>);
#[derive(Clone)]
struct BPing;
impl ActorInterface for BCounter {
type Incoming = BPing;
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: BPing) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
let t = total.clone();
let router = Router::<BPing>::new(
RoutingStrategy::Broadcast,
3,
move |ctx| ctx.spawn(BCounter(t.clone())),
10,
);
let router_addr = rt.spawn(router).unwrap();
rt.tick();
for _ in 0..5 {
rt.send_to(router_addr, BPing).unwrap();
}
tick_n(&rt, 3);
assert_eq!(
total.load(Ordering::Relaxed),
15,
"5 broadcasts × 3 workers = 15"
);
// Random: 30 msgs → at least 2 workers used
let rt = std_runtime(RuntimeConfig::default());
let rcollected = Arc::new(std::sync::Mutex::new(Vec::new()));
struct RCollector(Arc<std::sync::Mutex<Vec<ActorAddress>>>);
#[derive(Clone)]
struct RWork;
impl ActorInterface for RCollector {
type Incoming = RWork;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: RWork) {
self.0.lock().unwrap().push(ctx.self_addr());
}
}
let c = rcollected.clone();
let router = Router::<RWork>::new(
RoutingStrategy::Random,
3,
move |ctx| ctx.spawn(RCollector(c.clone())),
10,
);
let router_addr = rt.spawn(router).unwrap();
rt.tick();
for _ in 0..30 {
rt.send_to(router_addr, RWork).unwrap();
}
tick_n(&rt, 3);
let data = rcollected.lock().unwrap();
let unique: std::collections::HashSet<_> = data.iter().collect();
assert!(unique.len() >= 2, "random uses at least 2 workers");
// Dead worker replaced
let rt = std_runtime(RuntimeConfig::default());
let spawn_count = Arc::new(AtomicUsize::new(0));
struct PanicOnFirst {
first: bool,
}
#[derive(Clone)]
struct DWork;
impl ActorInterface for PanicOnFirst {
type Incoming = DWork;
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: DWork) {
if self.first {
self.first = false;
panic!("first message panic");
}
}
}
let sc = spawn_count.clone();
let router = Router::<DWork>::new(
RoutingStrategy::RoundRobin,
3,
move |ctx| {
sc.fetch_add(1, Ordering::Relaxed);
ctx.spawn(PanicOnFirst {
first: sc.load(Ordering::Relaxed) == 1,
})
},
10,
);
let router_addr = rt.spawn(router).unwrap();
rt.tick();
rt.send_to(router_addr, DWork).unwrap();
tick_n(&rt, 5);
assert!(
spawn_count.load(Ordering::Relaxed) >= 4,
"replacement spawned"
);
// Meltdown: max_restarts=2
let rt = std_runtime(RuntimeConfig::default());
struct AlwaysPanics;
#[derive(Clone)]
struct MWork;
impl ActorInterface for AlwaysPanics {
type Incoming = MWork;
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: MWork) {
panic!("always");
}
}
let router = Router::<MWork>::new(
RoutingStrategy::RoundRobin,
1,
|ctx| ctx.spawn(AlwaysPanics),
2,
);
let router_addr = rt.spawn(router).unwrap();
rt.tick();
for _ in 0..3 {
rt.send_to(router_addr, MWork).unwrap();
tick_n(&rt, 5);
}
tick_n(&rt, 5);
assert_eq!(rt.stats().workers[0].num_actors, 0, "router melted down");
// Stop router kills workers
let rt = std_runtime(RuntimeConfig::default());
struct Dummy;
#[derive(Clone)]
struct SWork;
impl ActorInterface for Dummy {
type Incoming = SWork;
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: SWork) {}
}
let router = Router::<SWork>::new(RoutingStrategy::RoundRobin, 3, |ctx| ctx.spawn(Dummy), 10);
let router_addr = rt.spawn(router).unwrap();
rt.tick();
assert_eq!(rt.stats().workers[0].num_actors, 4);
rt.stop_actor(router_addr).unwrap();
tick_n(&rt, 5);
assert_eq!(
rt.stats().workers[0].num_actors,
0,
"stop router kills workers"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// CtxSystem + CtxSelfStats
// ═══════════════════════════════════════════════════════════════════════════
/// Actor sees own stats after processing messages.
///
/// Sends N messages, ticks so they're processed, then sends a "report" message.
/// The actor reads its own stats in the handler and sends them back.
#[test]
fn actor_sees_own_stats_after_processing() {
#[derive(Clone)]
enum StatsMsg {
Bump,
Report { reply_to: ActorAddress },
}
#[derive(Clone, Debug, PartialEq)]
struct StatsReport {
processed: u64,
type_counts: Vec<(String, u64)>,
}
struct StatsActor;
impl ActorInterface for StatsActor {
type Incoming = StatsMsg;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: StatsMsg) {
match msg {
StatsMsg::Bump => {}
StatsMsg::Report { reply_to } => {
let report = StatsReport {
processed: ctx.messages_processed(),
type_counts: ctx
.message_type_counts()
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect(),
};
let _ = ctx.send(reply_to, report);
}
}
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<StatsReport>().unwrap();
let actor = rt.spawn(StatsActor).unwrap();
rt.tick(); // on_start
// Send 5 Bump messages and process them
for _ in 0..5 {
rt.send_to(actor, StatsMsg::Bump).unwrap();
}
rt.tick();
// Now ask for a report — the actor should see 5 processed messages
rt.send_to(
actor,
StatsMsg::Report {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("should receive stats report");
assert_eq!(
report.processed, 5,
"actor should see 5 previously processed messages"
);
assert!(
!report.type_counts.is_empty(),
"type counts should be populated"
);
// The type name should contain "StatsMsg"
assert!(
report
.type_counts
.iter()
.any(|(name, count)| name.contains("StatsMsg") && *count >= 5),
"type counts should include StatsMsg entries with count >= 5, got {:?}",
report.type_counts,
);
}
/// Actor sees system info: worker count, total actors, uptime.
#[test]
fn actor_sees_system_info() {
#[derive(Clone)]
struct GetSysInfo {
reply_to: ActorAddress,
}
#[derive(Clone, Debug)]
struct SysInfoReport {
num_workers: usize,
total_actors: usize,
}
struct SysInfoActor;
impl ActorInterface for SysInfoActor {
type Incoming = GetSysInfo;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: GetSysInfo) {
let info = ctx.system_info();
let _ = ctx.send(
msg.reply_to,
SysInfoReport {
num_workers: info.num_workers,
total_actors: info.total_actors,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<SysInfoReport>().unwrap();
// Spawn a few actors so total_actors > 1
let reporter = rt.spawn(SysInfoActor).unwrap();
let _extra1 = rt.spawn(PingPongActor).unwrap();
let _extra2 = rt.spawn(PingPongActor).unwrap();
rt.tick(); // on_start + stats update
rt.send_to(
reporter,
GetSysInfo {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("should receive system info");
assert_eq!(report.num_workers, 1, "default config has 1 worker");
assert!(
report.total_actors >= 3,
"should see at least 3 actors, got {}",
report.total_actors
);
}
/// Mailbox depth reflects queued messages before dequeuing.
///
/// With budget=1, only 1 message is processed per tick. If we enqueue 5 messages,
/// the actor's first handler invocation should see all 5 in the mailbox snapshot.
#[test]
fn mailbox_depth_reflects_queued_messages() {
#[derive(Clone)]
struct DepthProbe {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct DepthReport(usize);
struct DepthActor;
impl ActorInterface for DepthActor {
type Incoming = DepthProbe;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: DepthProbe) {
let _ = ctx.send(msg.reply_to, DepthReport(ctx.mailbox_depth()));
}
}
let config = RuntimeConfig {
actor_message_budget: 1,
..RuntimeConfig::default()
};
let rt = std_runtime(config);
let inbox = rt.new_inbox::<DepthReport>().unwrap();
let actor = rt.spawn(DepthActor).unwrap();
rt.tick(); // on_start
// Enqueue 5 messages
for _ in 0..5 {
rt.send_to(
actor,
DepthProbe {
reply_to: *inbox.addr(),
},
)
.unwrap();
}
// Tick once — budget=1, so only the first message is processed
rt.tick();
let report = inbox.try_recv().expect("should receive depth report");
// The snapshot is taken before any dequeuing in this tick, so depth == 5
assert_eq!(
report.0, 5,
"mailbox depth should be 5 (snapshot before dequeue)"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// CtxLineage — Parent Tracking
// ═══════════════════════════════════════════════════════════════════════════
/// Child spawned by an actor reports its parent address back.
#[test]
fn child_knows_its_parent() {
#[derive(Clone)]
struct ReportParent {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct ParentReport(Option<ActorAddress>);
struct ChildReporter;
impl ActorInterface for ChildReporter {
type Incoming = ReportParent;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportParent) {
let _ = ctx.send(msg.reply_to, ParentReport(ctx.parent()));
}
}
struct ParentActor {
reply_to: ActorAddress,
}
impl ActorInterface for ParentActor {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let child = ctx.spawn(ChildReporter).unwrap();
let _ = ctx.send(
child,
ReportParent {
reply_to: self.reply_to,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<ParentReport>().unwrap();
let parent = rt
.spawn(ParentActor {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick(); // on_start
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox.try_recv().expect("child should report parent");
assert_eq!(report, ParentReport(Some(parent)));
}
/// Actor spawned via Runtime::spawn has no parent.
#[test]
fn runtime_spawned_has_no_parent() {
#[derive(Clone)]
struct ReportParent {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct ParentReport(Option<ActorAddress>);
struct Reporter;
impl ActorInterface for Reporter {
type Incoming = ReportParent;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportParent) {
let _ = ctx.send(msg.reply_to, ParentReport(ctx.parent()));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<ParentReport>().unwrap();
let actor = rt.spawn(Reporter).unwrap();
rt.tick(); // on_start
rt.send_to(
actor,
ReportParent {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("actor should report parent");
assert_eq!(report, ParentReport(None));
}
/// In a A→B→C chain, C reports B as parent (not A).
#[test]
fn grandchild_reports_immediate_parent() {
#[derive(Clone)]
struct ReportParent {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct ParentReport(Option<ActorAddress>);
struct Leaf;
impl ActorInterface for Leaf {
type Incoming = ReportParent;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportParent) {
let _ = ctx.send(msg.reply_to, ParentReport(ctx.parent()));
}
}
struct Middle {
reply_to: ActorAddress,
}
impl ActorInterface for Middle {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let child = ctx.spawn(Leaf).unwrap();
let _ = ctx.send(
child,
ReportParent {
reply_to: self.reply_to,
},
);
}
}
struct Root {
reply_to: ActorAddress,
}
impl ActorInterface for Root {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let mid = ctx
.spawn(Middle {
reply_to: self.reply_to,
})
.unwrap();
let _ = ctx.send(
mid,
Ping {
reply_to: ActorAddress::default(),
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<ParentReport>().unwrap();
let root = rt
.spawn(Root {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick(); // on_start
rt.send_to(
root,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 10);
let report = inbox.try_recv().expect("grandchild should report parent");
// C's parent should be B (some address), not A (root) and not None
assert!(report.0.is_some(), "grandchild has a parent");
assert_ne!(
report.0.unwrap(),
root,
"grandchild's parent is the middle actor, not root"
);
}
/// Parent address is available during on_stop.
#[test]
fn parent_visible_in_on_stop() {
#[derive(Clone, Debug, PartialEq)]
struct ParentReport(Option<ActorAddress>);
struct OnStopReporter {
reply_to: ActorAddress,
}
impl ActorInterface for OnStopReporter {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {}
fn on_stop(&mut self, ctx: &Ctx) {
let _ = ctx.send(self.reply_to, ParentReport(ctx.parent()));
}
}
struct Spawner {
reply_to: ActorAddress,
}
impl ActorInterface for Spawner {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let child = ctx
.spawn(OnStopReporter {
reply_to: self.reply_to,
})
.unwrap();
let _ = ctx.stop_actor(child);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<ParentReport>().unwrap();
let spawner = rt
.spawn(Spawner {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick(); // on_start
rt.send_to(
spawner,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 10);
let report = inbox.try_recv().expect("on_stop should report parent");
assert_eq!(report, ParentReport(Some(spawner)));
}
// ═══════════════════════════════════════════════════════════════════════════
// CtxEnvironment — Inherited Typed Key-Value Map
// ═══════════════════════════════════════════════════════════════════════════
/// Child inherits parent's environment: parent sets a typed env value via
/// spawn_builder, spawns child, child reads it back and confirms it matches.
#[test]
fn env_child_inherits_parent_environment() {
#[derive(Clone, Debug, PartialEq)]
struct DbAddr(String);
#[derive(Clone)]
struct ReportEnv {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct EnvReport(Option<String>);
struct EnvChild;
impl ActorInterface for EnvChild {
type Incoming = ReportEnv;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportEnv) {
let val = ctx.env::<DbAddr>().map(|d| d.0.clone());
let _ = ctx.send(msg.reply_to, EnvReport(val));
}
}
struct EnvParent {
reply_to: ActorAddress,
}
impl ActorInterface for EnvParent {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let child = ctx
.spawn_builder(EnvChild)
.env(DbAddr("postgres://localhost".into()))
.finish()
.unwrap();
let _ = ctx.send(
child,
ReportEnv {
reply_to: self.reply_to,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<EnvReport>().unwrap();
let parent = rt
.spawn(EnvParent {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox.try_recv().expect("child should report env");
assert_eq!(report, EnvReport(Some("postgres://localhost".into())));
}
/// Runtime-spawned actor has empty environment — ctx.env::<T>() returns None.
#[test]
fn env_runtime_spawned_has_empty_environment() {
#[derive(Clone, Debug, PartialEq)]
struct Tag(String);
#[derive(Clone)]
struct ReportEnv {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct EnvReport(bool);
struct EnvReporter;
impl ActorInterface for EnvReporter {
type Incoming = ReportEnv;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportEnv) {
let has_tag = ctx.env::<Tag>().is_some();
let _ = ctx.send(msg.reply_to, EnvReport(has_tag));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<EnvReport>().unwrap();
let actor = rt.spawn(EnvReporter).unwrap();
rt.tick();
rt.send_to(
actor,
ReportEnv {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("actor should report env");
assert_eq!(
report,
EnvReport(false),
"runtime-spawned actor has no env values"
);
}
/// Environment flows through a grandchild chain: A sets env, spawns B, B
/// spawns C (via plain ctx.spawn — inherits env), C reads the value from A.
#[test]
fn env_flows_through_grandchild_chain() {
#[derive(Clone, Debug, PartialEq)]
struct Secret(u64);
#[derive(Clone)]
struct ReportEnv {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct EnvReport(Option<u64>);
struct Leaf;
impl ActorInterface for Leaf {
type Incoming = ReportEnv;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportEnv) {
let val = ctx.env::<Secret>().map(|s| s.0);
let _ = ctx.send(msg.reply_to, EnvReport(val));
}
}
struct Middle {
reply_to: ActorAddress,
}
impl ActorInterface for Middle {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
// ctx.spawn inherits parent env automatically
let child = ctx.spawn(Leaf).unwrap();
let _ = ctx.send(
child,
ReportEnv {
reply_to: self.reply_to,
},
);
}
}
struct Root {
reply_to: ActorAddress,
}
impl ActorInterface for Root {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let mid = ctx
.spawn_builder(Middle {
reply_to: self.reply_to,
})
.env(Secret(42))
.finish()
.unwrap();
let _ = ctx.send(
mid,
Ping {
reply_to: ActorAddress::default(),
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<EnvReport>().unwrap();
let root = rt
.spawn(Root {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
root,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 10);
let report = inbox.try_recv().expect("grandchild should report env");
assert_eq!(
report,
EnvReport(Some(42)),
"env value from root flows to grandchild"
);
}
/// Spawn builder overrides one key while inheriting others: parent has Key1 +
/// Key2, uses spawn_builder to override Key2. Child sees original Key1 and new Key2.
#[test]
fn env_spawn_builder_overrides_one_key_inherits_others() {
#[derive(Clone, Debug, PartialEq)]
struct Key1(String);
#[derive(Clone, Debug, PartialEq)]
struct Key2(String);
#[derive(Clone)]
struct ReportEnv {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct EnvReport {
key1: Option<String>,
key2: Option<String>,
}
struct EnvChild;
impl ActorInterface for EnvChild {
type Incoming = ReportEnv;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportEnv) {
let _ = ctx.send(
msg.reply_to,
EnvReport {
key1: ctx.env::<Key1>().map(|k| k.0.clone()),
key2: ctx.env::<Key2>().map(|k| k.0.clone()),
},
);
}
}
struct EnvParent {
reply_to: ActorAddress,
}
impl ActorInterface for EnvParent {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
// Override Key2 only, Key1 should be inherited
let child = ctx
.spawn_builder(EnvChild)
.env(Key2("overridden".into()))
.finish()
.unwrap();
let _ = ctx.send(
child,
ReportEnv {
reply_to: self.reply_to,
},
);
}
}
// Build an env with both keys, then use EnvironmentBuilder to create the parent env
let parent_env = EnvironmentBuilder::new()
.set(Key1("original".into()))
.set(Key2("original".into()))
.build();
// Spawn the parent with the built env using a "bootstrap" actor
struct Bootstrap {
reply_to: ActorAddress,
}
impl ActorInterface for Bootstrap {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let parent = ctx
.spawn_builder(EnvParent {
reply_to: self.reply_to,
})
.env(Key1("original".into()))
.env(Key2("original".into()))
.finish()
.unwrap();
let _ = ctx.send(
parent,
Ping {
reply_to: ActorAddress::default(),
},
);
}
}
let _ = parent_env; // verify it builds (used above for documentation)
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<EnvReport>().unwrap();
let bootstrap = rt
.spawn(Bootstrap {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
bootstrap,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 10);
let report = inbox.try_recv().expect("child should report env");
assert_eq!(
report.key1,
Some("original".into()),
"Key1 inherited from parent"
);
assert_eq!(
report.key2,
Some("overridden".into()),
"Key2 overridden by spawn_builder"
);
}
/// Environment is readable during on_stop callback.
#[test]
fn env_readable_in_on_stop() {
#[derive(Clone, Debug, PartialEq)]
struct Config(String);
#[derive(Clone, Debug, PartialEq)]
struct EnvReport(Option<String>);
struct OnStopEnvReporter {
reply_to: ActorAddress,
}
impl ActorInterface for OnStopEnvReporter {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {}
fn on_stop(&mut self, ctx: &Ctx) {
let val = ctx.env::<Config>().map(|c| c.0.clone());
let _ = ctx.send(self.reply_to, EnvReport(val));
}
}
struct Spawner {
reply_to: ActorAddress,
}
impl ActorInterface for Spawner {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let child = ctx
.spawn_builder(OnStopEnvReporter {
reply_to: self.reply_to,
})
.env(Config("production".into()))
.finish()
.unwrap();
let _ = ctx.stop_actor(child);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<EnvReport>().unwrap();
let spawner = rt
.spawn(Spawner {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
spawner,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 10);
let report = inbox.try_recv().expect("on_stop should report env");
assert_eq!(report, EnvReport(Some("production".into())));
}
/// Sibling overrides are independent: parent spawns child A with Version(1)
/// and child B with Version(2). Each sees its own version.
#[test]
fn env_sibling_overrides_are_independent() {
#[derive(Clone, Debug, PartialEq)]
struct Version(u32);
#[derive(Clone)]
struct ReportEnv {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct EnvReport(Option<u32>);
struct VersionReporter;
impl ActorInterface for VersionReporter {
type Incoming = ReportEnv;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportEnv) {
let val = ctx.env::<Version>().map(|v| v.0);
let _ = ctx.send(msg.reply_to, EnvReport(val));
}
}
struct Parent {
reply_to: ActorAddress,
}
impl ActorInterface for Parent {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let a = ctx
.spawn_builder(VersionReporter)
.env(Version(1))
.finish()
.unwrap();
let b = ctx
.spawn_builder(VersionReporter)
.env(Version(2))
.finish()
.unwrap();
let _ = ctx.send(
a,
ReportEnv {
reply_to: self.reply_to,
},
);
let _ = ctx.send(
b,
ReportEnv {
reply_to: self.reply_to,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<EnvReport>().unwrap();
let parent = rt
.spawn(Parent {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let mut reports: Vec<EnvReport> = std::iter::from_fn(|| inbox.try_recv()).collect();
reports.sort_by_key(|r| r.0);
assert_eq!(reports.len(), 2, "both siblings replied");
assert_eq!(reports[0], EnvReport(Some(1)));
assert_eq!(reports[1], EnvReport(Some(2)));
}
// ═══════════════════════════════════════════════════════════════════════════
// SpawnTimestamp
// ═══════════════════════════════════════════════════════════════════════════
/// Any actor has SpawnTimestamp when StdExtension is installed.
#[test]
fn spawn_timestamp_present_with_std_extension() {
#[derive(Clone)]
struct ReportTs {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct TsReport(Option<u64>);
struct TsActor;
impl ActorInterface for TsActor {
type Incoming = ReportTs;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportTs) {
let ts = ctx.env::<SpawnTimestamp>().map(|t| t.0);
let _ = ctx.send(msg.reply_to, TsReport(ts));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<TsReport>().unwrap();
let actor = rt.spawn(TsActor).unwrap();
rt.tick();
rt.send_to(
actor,
ReportTs {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("should receive timestamp report");
assert!(
report.0.is_some(),
"SpawnTimestamp should be present with StdExtension"
);
}
/// Parent and child spawned at different times have different timestamps,
/// child's timestamp >= parent's timestamp.
#[test]
fn spawn_timestamp_parent_child_ordering() {
#[derive(Clone, Debug)]
struct TsPair {
parent_ts: u64,
child_ts: u64,
}
struct TsChild {
reply_to: ActorAddress,
parent_ts: u64,
}
impl ActorInterface for TsChild {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
let child_ts = ctx.env::<SpawnTimestamp>().unwrap().0;
let _ = ctx.send(
self.reply_to,
TsPair {
parent_ts: self.parent_ts,
child_ts,
},
);
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
}
struct TsParent {
reply_to: ActorAddress,
}
impl ActorInterface for TsParent {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let my_ts = ctx.env::<SpawnTimestamp>().unwrap().0;
let _ = ctx.spawn(TsChild {
reply_to: self.reply_to,
parent_ts: my_ts,
});
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<TsPair>().unwrap();
let parent = rt
.spawn(TsParent {
reply_to: *inbox.addr(),
})
.unwrap();
// Tick a few times so some uptime accumulates before the child spawn
tick_n(&rt, 3);
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox.try_recv().expect("should receive timestamp pair");
assert!(
report.child_ts >= report.parent_ts,
"child timestamp ({}) should be >= parent timestamp ({})",
report.child_ts,
report.parent_ts
);
}
/// SpawnTimestamp is available during on_stop callback.
#[test]
fn spawn_timestamp_available_in_on_stop() {
#[derive(Clone, Debug, PartialEq)]
struct TsReport(Option<u64>);
struct OnStopTsReporter {
reply_to: ActorAddress,
}
impl ActorInterface for OnStopTsReporter {
type Incoming = ();
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
fn on_stop(&mut self, ctx: &Ctx) {
let ts = ctx.env::<SpawnTimestamp>().map(|t| t.0);
let _ = ctx.send(self.reply_to, TsReport(ts));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<TsReport>().unwrap();
let actor = rt
.spawn(OnStopTsReporter {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.stop_actor(actor).unwrap();
tick_n(&rt, 3);
let report = inbox.try_recv().expect("on_stop should report timestamp");
assert!(
report.0.is_some(),
"SpawnTimestamp should be available in on_stop"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// LogicalName
// ═══════════════════════════════════════════════════════════════════════════
/// Named actor knows its logical name.
#[test]
fn logical_name_present_for_named_actor() {
#[derive(Clone)]
struct ReportName {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct NameReport(Option<String>);
struct NameActor;
impl ActorInterface for NameActor {
type Incoming = ReportName;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportName) {
let name = ctx.env::<LogicalName>().map(|n| n.0.clone());
let _ = ctx.send(msg.reply_to, NameReport(name));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<NameReport>().unwrap();
let addr = rt.spawn_named("my-service", NameActor).unwrap();
rt.tick();
rt.send_to(
addr,
ReportName {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("should receive name report");
assert_eq!(report, NameReport(Some("my-service".to_string())));
}
/// Unnamed actor has no logical name.
#[test]
fn logical_name_absent_for_unnamed_actor() {
#[derive(Clone)]
struct ReportName {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct NameReport(Option<String>);
struct NameActor;
impl ActorInterface for NameActor {
type Incoming = ReportName;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportName) {
let name = ctx.env::<LogicalName>().map(|n| n.0.clone());
let _ = ctx.send(msg.reply_to, NameReport(name));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<NameReport>().unwrap();
let addr = rt.spawn(NameActor).unwrap();
rt.tick();
rt.send_to(
addr,
ReportName {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("should receive name report");
assert_eq!(report, NameReport(None));
}
/// Runtime-level spawn_named sets LogicalName.
#[test]
fn logical_name_via_runtime_spawn_named() {
#[derive(Clone)]
struct ReportName {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct NameReport(Option<String>);
struct NameActor;
impl ActorInterface for NameActor {
type Incoming = ReportName;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportName) {
let name = ctx.env::<LogicalName>().map(|n| n.0.clone());
let _ = ctx.send(msg.reply_to, NameReport(name));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<NameReport>().unwrap();
let addr = rt.spawn_named("svc", NameActor).unwrap();
rt.tick();
rt.send_to(
addr,
ReportName {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("should receive name report");
assert_eq!(report, NameReport(Some("svc".to_string())));
}
/// Child of named actor inherits LogicalName via environment inheritance.
#[test]
fn logical_name_inherited_by_child() {
#[derive(Clone)]
struct ReportName {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct NameReport(Option<String>);
struct ChildReporter;
impl ActorInterface for ChildReporter {
type Incoming = ReportName;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportName) {
let name = ctx.env::<LogicalName>().map(|n| n.0.clone());
let _ = ctx.send(msg.reply_to, NameReport(name));
}
}
struct NamedParent {
reply_to: ActorAddress,
}
impl ActorInterface for NamedParent {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
// ctx.spawn inherits parent env, which includes LogicalName
let child = ctx.spawn(ChildReporter).unwrap();
let _ = ctx.send(
child,
ReportName {
reply_to: self.reply_to,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<NameReport>().unwrap();
let parent = rt
.spawn_named(
"parent-svc",
NamedParent {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox
.try_recv()
.expect("child should report inherited name");
assert_eq!(report, NameReport(Some("parent-svc".to_string())));
}
// ═══════════════════════════════════════════════════════════════════════════
// Supervisor Lineage — ctx.supervisor()
// ═══════════════════════════════════════════════════════════════════════════
/// Supervised child knows its supervisor address.
#[test]
fn supervised_child_knows_supervisor() {
#[derive(Clone)]
struct ReportSupervisor {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct SupervisorReport(Option<ActorAddress>);
struct SupervisedChild;
impl ActorInterface for SupervisedChild {
type Incoming = ReportSupervisor;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportSupervisor) {
let sup = ctx.supervisor();
let _ = ctx.send(msg.reply_to, SupervisorReport(sup));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<SupervisorReport>().unwrap();
let reply_to = *inbox.addr();
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![ChildSpec::new(
"child",
RestartPolicy::Permanent,
move |ctx| ctx.spawn(SupervisedChild),
)],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 2);
// Find the child address
let child = rt
.stats()
.actors
.iter()
.find(|(a, _)| *a != sup_addr)
.map(|(a, _)| *a)
.unwrap();
rt.send_to(child, ReportSupervisor { reply_to }).unwrap();
rt.tick();
let report = inbox.try_recv().expect("child should report supervisor");
assert_eq!(report, SupervisorReport(Some(sup_addr)));
}
/// Unsupervised actor has no supervisor.
#[test]
fn unsupervised_actor_has_no_supervisor() {
#[derive(Clone)]
struct ReportSupervisor {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct SupervisorReport(Option<ActorAddress>);
struct PlainActor;
impl ActorInterface for PlainActor {
type Incoming = ReportSupervisor;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportSupervisor) {
let sup = ctx.supervisor();
let _ = ctx.send(msg.reply_to, SupervisorReport(sup));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<SupervisorReport>().unwrap();
let actor = rt.spawn(PlainActor).unwrap();
rt.tick();
rt.send_to(
actor,
ReportSupervisor {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("actor should report supervisor");
assert_eq!(report, SupervisorReport(None));
}
/// After a permanent child panics and restarts, the new incarnation still
/// reports the same supervisor.
#[test]
fn supervisor_survives_child_restart() {
#[derive(Clone)]
struct ReportSupervisor {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct SupervisorReport(Option<ActorAddress>);
struct CrashOnce {
crash_counter: Arc<AtomicUsize>,
}
impl ActorInterface for CrashOnce {
type Incoming = ReportSupervisor;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportSupervisor) {
if self.crash_counter.fetch_add(1, Ordering::SeqCst) == 0 {
panic!("intentional crash");
}
let sup = ctx.supervisor();
let _ = ctx.send(msg.reply_to, SupervisorReport(sup));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<SupervisorReport>().unwrap();
let reply_to = *inbox.addr();
let crash_counter = Arc::new(AtomicUsize::new(0));
let cc = crash_counter.clone();
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![ChildSpec::new(
"crasher",
RestartPolicy::Permanent,
move |ctx| {
ctx.spawn(CrashOnce {
crash_counter: cc.clone(),
})
},
)],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 2);
// First: find child and make it crash
let child_v1 = rt
.stats()
.actors
.iter()
.find(|(a, _)| *a != sup_addr)
.map(|(a, _)| *a)
.unwrap();
rt.send_to(child_v1, ReportSupervisor { reply_to }).unwrap();
tick_n(&rt, 5); // panics, supervisor restarts
// Find the new child (different address)
let child_v2 = rt
.stats()
.actors
.iter()
.find(|(a, _)| *a != sup_addr)
.map(|(a, _)| *a)
.unwrap();
assert_ne!(
child_v1, child_v2,
"child should have a new address after restart"
);
rt.send_to(child_v2, ReportSupervisor { reply_to }).unwrap();
rt.tick();
let report = inbox
.try_recv()
.expect("restarted child should report supervisor");
assert_eq!(report, SupervisorReport(Some(sup_addr)));
}
/// Nested supervision: supervisor -> child A. Child A spawns grandchild B.
/// B's supervisor is None, A's supervisor is the supervisor.
#[test]
fn grandchild_not_supervised_child_is() {
#[derive(Clone)]
struct ReportSupervisor {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct SupervisorReport {
addr: ActorAddress,
supervisor: Option<ActorAddress>,
}
struct GrandChild;
impl ActorInterface for GrandChild {
type Incoming = ReportSupervisor;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportSupervisor) {
let _ = ctx.send(
msg.reply_to,
SupervisorReport {
addr: ctx.self_addr(),
supervisor: ctx.supervisor(),
},
);
}
}
struct ChildA {
reply_to: ActorAddress,
}
impl ActorInterface for ChildA {
type Incoming = ReportSupervisor;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
// Spawn a grandchild (not supervised)
let gc = ctx.spawn(GrandChild).unwrap();
let _ = ctx.send(
gc,
ReportSupervisor {
reply_to: self.reply_to,
},
);
}
fn handle(&mut self, ctx: &Ctx, msg: ReportSupervisor) {
let _ = ctx.send(
msg.reply_to,
SupervisorReport {
addr: ctx.self_addr(),
supervisor: ctx.supervisor(),
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<SupervisorReport>().unwrap();
let reply_to = *inbox.addr();
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![ChildSpec::new("a", RestartPolicy::Permanent, move |ctx| {
ctx.spawn(ChildA { reply_to })
})],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 5);
// Grandchild report should come from on_start
let gc_report = inbox.try_recv().expect("grandchild should report");
assert_eq!(gc_report.supervisor, None, "grandchild is not supervised");
// Now ask child A to report
let child_a = rt
.stats()
.actors
.iter()
.find(|(a, _)| *a != sup_addr && *a != gc_report.addr)
.map(|(a, _)| *a)
.unwrap();
rt.send_to(child_a, ReportSupervisor { reply_to }).unwrap();
rt.tick();
let a_report = inbox.try_recv().expect("child A should report");
assert_eq!(
a_report.supervisor,
Some(sup_addr),
"child A's supervisor is the supervisor"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// CtxResources — Typed Service Discovery
// ═══════════════════════════════════════════════════════════════════════════
/// Actor discovers a registered service by marker type.
#[test]
fn service_discovery_by_marker_type() {
struct PrimaryService;
#[derive(Clone)]
struct LookupService {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct ServiceReport(Option<ActorAddress>);
struct ServiceConsumer;
impl ActorInterface for ServiceConsumer {
type Incoming = LookupService;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: LookupService) {
let addr = ctx.resource::<PrimaryService>();
let _ = ctx.send(msg.reply_to, ServiceReport(addr));
}
}
let rt = std_runtime(RuntimeConfig::default());
let service_addr = ActorAddress::new_random();
rt.register_service::<PrimaryService>(service_addr);
let inbox = rt.new_inbox::<ServiceReport>().unwrap();
let consumer = rt.spawn(ServiceConsumer).unwrap();
rt.tick();
rt.send_to(
consumer,
LookupService {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("should receive service report");
assert_eq!(report, ServiceReport(Some(service_addr)));
}
/// Child inherits service binding from parent's environment.
#[test]
fn service_binding_inherited_by_child() {
struct AuthService;
#[derive(Clone)]
struct LookupService {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct ServiceReport(Option<ActorAddress>);
struct Leaf;
impl ActorInterface for Leaf {
type Incoming = LookupService;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: LookupService) {
let addr = ctx.resource::<AuthService>();
let _ = ctx.send(msg.reply_to, ServiceReport(addr));
}
}
struct Parent {
reply_to: ActorAddress,
}
impl ActorInterface for Parent {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let child = ctx.spawn(Leaf).unwrap();
let _ = ctx.send(
child,
LookupService {
reply_to: self.reply_to,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let auth_addr = ActorAddress::new_random();
rt.register_service::<AuthService>(auth_addr);
let inbox = rt.new_inbox::<ServiceReport>().unwrap();
let parent = rt
.spawn(Parent {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox.try_recv().expect("child should report service");
assert_eq!(report, ServiceReport(Some(auth_addr)));
}
/// Multiple services registered, each accessible by its own marker type.
#[test]
fn multiple_services_each_accessible_by_marker() {
struct PrimaryService;
struct Cache;
struct Logger;
#[derive(Clone)]
struct LookupAll {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct AllServicesReport {
primary: Option<ActorAddress>,
cache: Option<ActorAddress>,
logger: Option<ActorAddress>,
}
struct MultiConsumer;
impl ActorInterface for MultiConsumer {
type Incoming = LookupAll;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: LookupAll) {
let _ = ctx.send(
msg.reply_to,
AllServicesReport {
primary: ctx.resource::<PrimaryService>(),
cache: ctx.resource::<Cache>(),
logger: ctx.resource::<Logger>(),
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let primary_addr = ActorAddress::new_random();
let cache_addr = ActorAddress::new_random();
let logger_addr = ActorAddress::new_random();
rt.register_service::<PrimaryService>(primary_addr);
rt.register_service::<Cache>(cache_addr);
rt.register_service::<Logger>(logger_addr);
let inbox = rt.new_inbox::<AllServicesReport>().unwrap();
let actor = rt.spawn(MultiConsumer).unwrap();
rt.tick();
rt.send_to(
actor,
LookupAll {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox
.try_recv()
.expect("should receive all services report");
assert_eq!(report.primary, Some(primary_addr));
assert_eq!(report.cache, Some(cache_addr));
assert_eq!(report.logger, Some(logger_addr));
}
/// Unregistered service returns None.
#[test]
fn unregistered_service_returns_none() {
struct Nonexistent;
#[derive(Clone)]
struct LookupService {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct ServiceReport(Option<ActorAddress>);
struct Consumer;
impl ActorInterface for Consumer {
type Incoming = LookupService;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: LookupService) {
let addr = ctx.resource::<Nonexistent>();
let _ = ctx.send(msg.reply_to, ServiceReport(addr));
}
}
let rt = std_runtime(RuntimeConfig::default());
// No services registered
let inbox = rt.new_inbox::<ServiceReport>().unwrap();
let actor = rt.spawn(Consumer).unwrap();
rt.tick();
rt.send_to(
actor,
LookupService {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("should receive service report");
assert_eq!(report, ServiceReport(None));
}
/// Service binding overridable via spawn_builder — per-subtree customization.
#[test]
fn service_binding_overridable_via_spawn_builder() {
struct PrimaryService;
#[derive(Clone)]
struct LookupService {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct ServiceReport(Option<ActorAddress>);
struct Consumer;
impl ActorInterface for Consumer {
type Incoming = LookupService;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: LookupService) {
let addr = ctx.resource::<PrimaryService>();
let _ = ctx.send(msg.reply_to, ServiceReport(addr));
}
}
struct Spawner {
reply_to: ActorAddress,
override_addr: ActorAddress,
}
impl ActorInterface for Spawner {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
// Override the primary service binding for this subtree
let child = ctx
.spawn_builder(Consumer)
.env(ServiceBinding::<PrimaryService>::new(self.override_addr))
.finish()
.unwrap();
let _ = ctx.send(
child,
LookupService {
reply_to: self.reply_to,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let global_service = ActorAddress::new_random();
let override_service = ActorAddress::new_random();
rt.register_service::<PrimaryService>(global_service);
let inbox = rt.new_inbox::<ServiceReport>().unwrap();
// Spawn a plain consumer — should see the global binding
let plain = rt.spawn(Consumer).unwrap();
rt.tick();
rt.send_to(
plain,
LookupService {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("plain consumer should report");
assert_eq!(
report,
ServiceReport(Some(global_service)),
"plain consumer sees global service"
);
// Spawn via spawn_builder override — should see the override
let spawner = rt
.spawn(Spawner {
reply_to: *inbox.addr(),
override_addr: override_service,
})
.unwrap();
rt.tick();
rt.send_to(
spawner,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox.try_recv().expect("overridden consumer should report");
assert_eq!(
report,
ServiceReport(Some(override_service)),
"overridden consumer sees custom service"
);
}
/// Service is accessible in on_start and on_stop lifecycle hooks.
#[test]
fn service_accessible_in_lifecycle_hooks() {
struct MetricsService;
#[derive(Clone, Debug, PartialEq)]
struct LifecycleReport {
on_start_addr: Option<ActorAddress>,
on_stop_addr: Option<ActorAddress>,
}
struct LifecycleActor {
reply_to: ActorAddress,
on_start_addr: Option<ActorAddress>,
}
impl ActorInterface for LifecycleActor {
type Incoming = Ping;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
self.on_start_addr = ctx.resource::<MetricsService>();
}
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_self();
}
fn on_stop(&mut self, ctx: &Ctx) {
let on_stop_addr = ctx.resource::<MetricsService>();
let _ = ctx.send(
self.reply_to,
LifecycleReport {
on_start_addr: self.on_start_addr,
on_stop_addr,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let metrics_addr = ActorAddress::new_random();
rt.register_service::<MetricsService>(metrics_addr);
let inbox = rt.new_inbox::<LifecycleReport>().unwrap();
let actor = rt
.spawn(LifecycleActor {
reply_to: *inbox.addr(),
on_start_addr: None,
})
.unwrap();
rt.tick(); // on_start
rt.send_to(
actor,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5); // handle → stop_self → on_stop
let report = inbox.try_recv().expect("should receive lifecycle report");
assert_eq!(
report,
LifecycleReport {
on_start_addr: Some(metrics_addr),
on_stop_addr: Some(metrics_addr),
}
);
}
/// OneForAll restart re-registers all children: crash one child, after restart
/// all children report the same supervisor.
#[test]
fn one_for_all_restart_re_registers_children() {
#[derive(Clone)]
struct ReportSupervisor {
reply_to: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct SupervisorReport(Option<ActorAddress>);
struct StableChild;
impl ActorInterface for StableChild {
type Incoming = ReportSupervisor;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: ReportSupervisor) {
let sup = ctx.supervisor();
let _ = ctx.send(msg.reply_to, SupervisorReport(sup));
}
}
struct CrashChild;
impl ActorInterface for CrashChild {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {
panic!("intentional crash for OneForAll test");
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<SupervisorReport>().unwrap();
let reply_to = *inbox.addr();
let sup = Supervisor::new(
SupervisorStrategy::OneForAll,
5,
vec![
ChildSpec::new("crasher", RestartPolicy::Permanent, |ctx| {
ctx.spawn(CrashChild)
}),
ChildSpec::new("stable", RestartPolicy::Permanent, |ctx| {
ctx.spawn(StableChild)
}),
],
);
let sup_addr = rt.spawn(sup).unwrap();
tick_n(&rt, 2);
// Find the crasher and make it crash
// We need to identify which is which. The CrashChild accepts Ping,
// and we know there are exactly 2 non-supervisor actors.
let children: Vec<ActorAddress> = rt
.stats()
.actors
.iter()
.filter(|(a, _)| *a != sup_addr)
.map(|(a, _)| *a)
.collect();
assert_eq!(children.len(), 2);
// Send Ping to the crasher (it will be one of them). We'll try both —
// the StableChild doesn't handle Ping so it'll be a type mismatch, not a crash.
for &child in &children {
let _ = rt.send_to(
child,
Ping {
reply_to: ActorAddress::default(),
},
);
}
tick_n(&rt, 8); // crash + OneForAll restart
// After restart, all children should report the supervisor
let new_children: Vec<ActorAddress> = rt
.stats()
.actors
.iter()
.filter(|(a, _)| *a != sup_addr)
.map(|(a, _)| *a)
.collect();
for &child in &new_children {
let _ = rt.send_to(child, ReportSupervisor { reply_to });
}
rt.tick();
// At least the stable child should report
let reports: Vec<SupervisorReport> = std::iter::from_fn(|| inbox.try_recv()).collect();
assert!(
!reports.is_empty(),
"at least one child should report after OneForAll restart"
);
for report in &reports {
assert_eq!(
report.0,
Some(sup_addr),
"all children should report the supervisor after OneForAll restart"
);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Resource Handles (Part A)
// ═══════════════════════════════════════════════════════════════════════════
/// Handle wraps service and sends ergonomically.
#[test]
fn handle_wraps_service_and_sends_ergonomically() {
struct CounterService;
struct CounterHandle {
service: ActorAddress,
self_addr: ActorAddress,
}
impl ResourceHandle for CounterHandle {
type Service = CounterService;
fn from_parts(service_addr: ActorAddress, self_addr: ActorAddress) -> Self {
Self {
service: service_addr,
self_addr,
}
}
fn service_addr(&self) -> ActorAddress {
self.service
}
fn self_addr(&self) -> ActorAddress {
self.self_addr
}
}
impl CounterHandle {
fn increment(&self, ctx: &Ctx) -> Result<(), swactor::Error> {
ctx.send(
self.service_addr(),
Increment {
reply_to: self.self_addr(),
},
)
}
}
struct HandleUser {
_inbox: ActorAddress,
}
impl ActorInterface for HandleUser {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
if let Some(h) = ctx.handle::<CounterHandle>() {
let _ = h.increment(ctx);
}
}
fn on_actor_exit(&mut self, _ctx: &Ctx, _: ActorExited) {
// Forward count reply to external inbox
}
}
let rt = std_runtime(RuntimeConfig::default());
let counter_addr = rt.spawn(CounterActor { count: 0 }).unwrap();
rt.register_service::<CounterService>(counter_addr);
let inbox = rt.new_inbox::<Count>().unwrap();
// Use spawn_with_env so we can set reply_to
let user = rt
.spawn(HandleUser {
_inbox: *inbox.addr(),
})
.unwrap();
rt.tick(); // on_start
// Instead of the handle's reply_to, we directly test: send Ping to user,
// which uses the handle to increment. The counter replies to user's addr.
// We observe the counter got incremented via ask.
rt.send_to(
user,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
// Verify: ask counter for its count
rt.send_to(
counter_addr,
Increment {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 2);
let count = inbox.try_recv().expect("counter should reply");
assert_eq!(count, Count(2), "handle increment + direct increment = 2");
}
/// Handle returns None when service not registered.
#[test]
fn handle_returns_none_when_service_not_registered() {
struct Nonexistent;
struct DummyHandle {
_service: ActorAddress,
_self_addr: ActorAddress,
}
impl ResourceHandle for DummyHandle {
type Service = Nonexistent;
fn from_parts(service_addr: ActorAddress, self_addr: ActorAddress) -> Self {
Self {
_service: service_addr,
_self_addr: self_addr,
}
}
fn service_addr(&self) -> ActorAddress {
self._service
}
fn self_addr(&self) -> ActorAddress {
self._self_addr
}
}
#[derive(Clone, Debug, PartialEq)]
struct HandleReport(bool);
struct Reporter;
impl ActorInterface for Reporter {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: Ping) {
let has_handle = ctx.handle::<DummyHandle>().is_some();
let _ = ctx.send(msg.reply_to, HandleReport(has_handle));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<HandleReport>().unwrap();
let actor = rt.spawn(Reporter).unwrap();
rt.tick();
rt.send_to(
actor,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
let report = inbox.try_recv().expect("should receive handle report");
assert_eq!(
report,
HandleReport(false),
"handle returns None without registration"
);
}
/// Handle inherits service binding from parent.
#[test]
fn handle_inherits_service_binding_from_parent() {
struct MyService;
struct SvcHandle {
service: ActorAddress,
self_addr: ActorAddress,
}
impl ResourceHandle for SvcHandle {
type Service = MyService;
fn from_parts(s: ActorAddress, a: ActorAddress) -> Self {
Self {
service: s,
self_addr: a,
}
}
fn service_addr(&self) -> ActorAddress {
self.service
}
fn self_addr(&self) -> ActorAddress {
self.self_addr
}
}
#[derive(Clone, Debug, PartialEq)]
struct HandleReport(Option<ActorAddress>);
struct Child;
impl ActorInterface for Child {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: Ping) {
let addr = ctx.handle::<SvcHandle>().map(|h| h.service_addr());
let _ = ctx.send(msg.reply_to, HandleReport(addr));
}
}
struct Parent {
reply_to: ActorAddress,
}
impl ActorInterface for Parent {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let child = ctx.spawn(Child).unwrap();
let _ = ctx.send(
child,
Ping {
reply_to: self.reply_to,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let svc_addr = ActorAddress::new_random();
rt.register_service::<MyService>(svc_addr);
let inbox = rt.new_inbox::<HandleReport>().unwrap();
let parent = rt
.spawn(Parent {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox.try_recv().expect("child should report handle");
assert_eq!(
report,
HandleReport(Some(svc_addr)),
"child inherits service binding"
);
}
/// Handle constructible in on_start.
#[test]
fn handle_constructible_in_on_start() {
struct MySvc;
struct MyHandle {
service: ActorAddress,
self_addr: ActorAddress,
}
impl ResourceHandle for MyHandle {
type Service = MySvc;
fn from_parts(s: ActorAddress, a: ActorAddress) -> Self {
Self {
service: s,
self_addr: a,
}
}
fn service_addr(&self) -> ActorAddress {
self.service
}
fn self_addr(&self) -> ActorAddress {
self.self_addr
}
}
#[derive(Clone, Debug, PartialEq)]
struct HandleReport(bool);
struct OnStartChecker {
reply_to: ActorAddress,
}
impl ActorInterface for OnStartChecker {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
let has = ctx.handle::<MyHandle>().is_some();
let _ = ctx.send(self.reply_to, HandleReport(has));
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
}
let rt = std_runtime(RuntimeConfig::default());
let svc_addr = ActorAddress::new_random();
rt.register_service::<MySvc>(svc_addr);
let inbox = rt.new_inbox::<HandleReport>().unwrap();
let _ = rt
.spawn(OnStartChecker {
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 3);
let report = inbox
.try_recv()
.expect("should receive on_start handle report");
assert_eq!(report, HandleReport(true), "handle available in on_start");
}
/// Two actors use same handle type — each gets responses at own address.
#[test]
fn two_actors_same_handle_own_addresses() {
struct MySvc;
struct MyHandle {
service: ActorAddress,
self_addr: ActorAddress,
}
impl ResourceHandle for MyHandle {
type Service = MySvc;
fn from_parts(s: ActorAddress, a: ActorAddress) -> Self {
Self {
service: s,
self_addr: a,
}
}
fn service_addr(&self) -> ActorAddress {
self.service
}
fn self_addr(&self) -> ActorAddress {
self.self_addr
}
}
#[derive(Clone, Debug, PartialEq)]
struct SelfAddrReport(ActorAddress);
struct Reporter;
impl ActorInterface for Reporter {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: Ping) {
if let Some(h) = ctx.handle::<MyHandle>() {
let _ = ctx.send(msg.reply_to, SelfAddrReport(h.self_addr()));
}
}
}
let rt = std_runtime(RuntimeConfig::default());
let svc = ActorAddress::new_random();
rt.register_service::<MySvc>(svc);
let inbox = rt.new_inbox::<SelfAddrReport>().unwrap();
let a = rt.spawn(Reporter).unwrap();
let b = rt.spawn(Reporter).unwrap();
rt.tick();
rt.send_to(
a,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.send_to(
b,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 3);
let mut reports: Vec<SelfAddrReport> = std::iter::from_fn(|| inbox.try_recv()).collect();
assert_eq!(reports.len(), 2, "both actors report");
reports.sort_by_key(|r| r.0.0);
assert_ne!(
reports[0].0, reports[1].0,
"each actor has its own self_addr in the handle"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Rich Exit Values (Part B.1)
// ═══════════════════════════════════════════════════════════════════════════
/// Actor stops with value, monitor receives it in Down.
#[test]
fn stop_with_value_monitor_receives_in_down() {
struct Completer;
impl ActorInterface for Completer {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_with(42u64);
}
}
#[derive(Clone, Debug)]
struct DownReport {
reason: StopReason,
value: Option<u64>,
}
struct Watcher {
reply_to: ActorAddress,
}
impl ActorInterface for Watcher {
type Incoming = ();
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
fn handle_down(&mut self, ctx: &Ctx, down: Down) {
let val = down
.exit_value
.as_ref()
.and_then(|v| v.downcast_ref::<u64>().copied());
let _ = ctx.send(
self.reply_to,
DownReport {
reason: down.reason,
value: val,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<DownReport>().unwrap();
let target = rt.spawn(Completer).unwrap();
let _watcher = rt
.spawn(Watcher {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
// Watcher monitors target
rt.send_to(
target,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
// We need to set up the monitor — use a helper actor
// Actually, let's use the runtime watch API which delivers ActorExited.
// For monitor, we need ctx.monitor. Let's make watcher monitor in on_start.
// Recreate with proper monitor setup
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<DownReport>().unwrap();
struct MonitorWatcher {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for MonitorWatcher {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.monitor(self.target).unwrap();
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
fn handle_down(&mut self, ctx: &Ctx, down: Down) {
let val = down
.exit_value
.as_ref()
.and_then(|v| v.downcast_ref::<u64>().copied());
let _ = ctx.send(
self.reply_to,
DownReport {
reason: down.reason,
value: val,
},
);
}
}
let target = rt.spawn(Completer).unwrap();
let _watcher = rt
.spawn(MonitorWatcher {
target,
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 2); // on_start for both
rt.send_to(
target,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox.try_recv().expect("watcher should receive Down");
assert_eq!(report.reason, StopReason::Completed, "reason is Completed");
assert_eq!(report.value, Some(42), "exit value is 42");
}
/// Actor stops with value, watcher receives it in ActorExited.
#[test]
fn stop_with_value_watcher_receives_in_actor_exited() {
struct Completer;
impl ActorInterface for Completer {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_with("done".to_string());
}
}
#[derive(Clone, Debug)]
struct ExitReport {
reason: ExitReason,
value: Option<String>,
}
struct ExitWatcher {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for ExitWatcher {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.watch(self.target);
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
fn on_actor_exit(&mut self, ctx: &Ctx, exited: ActorExited) {
let val = exited
.exit_value
.as_ref()
.and_then(|v| v.downcast_ref::<String>().cloned());
let _ = ctx.send(
self.reply_to,
ExitReport {
reason: exited.reason,
value: val,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<ExitReport>().unwrap();
let target = rt.spawn(Completer).unwrap();
let _watcher = rt
.spawn(ExitWatcher {
target,
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 2);
rt.send_to(
target,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox
.try_recv()
.expect("watcher should receive ActorExited");
assert_eq!(report.reason, ExitReason::Completed);
assert_eq!(report.value, Some("done".to_string()));
}
/// Normal stop has exit_value: None.
#[test]
fn normal_stop_has_none_exit_value() {
#[derive(Clone, Debug)]
struct DownReport {
reason: StopReason,
has_value: bool,
}
struct MonitorWatcher {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for MonitorWatcher {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.monitor(self.target).unwrap();
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
fn handle_down(&mut self, ctx: &Ctx, down: Down) {
let _ = ctx.send(
self.reply_to,
DownReport {
reason: down.reason,
has_value: down.exit_value.is_some(),
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<DownReport>().unwrap();
let target = rt.spawn(StopsAfterFirst).unwrap();
let _watcher = rt
.spawn(MonitorWatcher {
target,
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 2);
rt.send_to(
target,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let report = inbox.try_recv().expect("should receive Down");
assert_eq!(report.reason, StopReason::Normal);
assert!(!report.has_value, "normal stop has no exit value");
}
/// Panic has exit_value: None.
#[test]
fn panic_has_none_exit_value() {
#[derive(Clone, Debug)]
struct DownReport {
reason: StopReason,
has_value: bool,
}
struct MonitorWatcher {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for MonitorWatcher {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.monitor(self.target).unwrap();
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
fn handle_down(&mut self, ctx: &Ctx, down: Down) {
let _ = ctx.send(
self.reply_to,
DownReport {
reason: down.reason,
has_value: down.exit_value.is_some(),
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<DownReport>().unwrap();
let target = rt.spawn(PanicActor).unwrap();
let _watcher = rt
.spawn(MonitorWatcher {
target,
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 2);
rt.send_to(target, PanicMsg).unwrap();
tick_n(&rt, 5);
let report = inbox.try_recv().expect("should receive Down after panic");
assert_eq!(report.reason, StopReason::Panicked);
assert!(!report.has_value, "panic has no exit value");
}
/// Multiple monitors receive cloned exit value.
#[test]
fn multiple_monitors_receive_cloned_exit_value() {
struct Completer;
impl ActorInterface for Completer {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_with(99u32);
}
}
#[derive(Clone, Debug)]
struct DownReport(Option<u32>);
struct MonitorWatcher {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for MonitorWatcher {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.monitor(self.target).unwrap();
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
fn handle_down(&mut self, ctx: &Ctx, down: Down) {
let val = down
.exit_value
.as_ref()
.and_then(|v| v.downcast_ref::<u32>().copied());
let _ = ctx.send(self.reply_to, DownReport(val));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<DownReport>().unwrap();
let target = rt.spawn(Completer).unwrap();
let _w1 = rt
.spawn(MonitorWatcher {
target,
reply_to: *inbox.addr(),
})
.unwrap();
let _w2 = rt
.spawn(MonitorWatcher {
target,
reply_to: *inbox.addr(),
})
.unwrap();
let _w3 = rt
.spawn(MonitorWatcher {
target,
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 2);
rt.send_to(
target,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let reports: Vec<DownReport> = std::iter::from_fn(|| inbox.try_recv()).collect();
assert_eq!(reports.len(), 3, "all 3 monitors receive Down");
for report in &reports {
assert_eq!(report.0, Some(99), "each monitor receives the exit value");
}
}
/// stop_with from on_start works.
#[test]
fn stop_with_from_on_start() {
struct StartCompleter {
_reply_to: ActorAddress,
}
impl ActorInterface for StartCompleter {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.stop_with(7u8);
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
}
#[derive(Clone, Debug)]
struct DownReport {
reason: StopReason,
value: Option<u8>,
}
struct MonitorWatcher {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for MonitorWatcher {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.monitor(self.target).unwrap();
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
fn handle_down(&mut self, ctx: &Ctx, down: Down) {
let val = down
.exit_value
.as_ref()
.and_then(|v| v.downcast_ref::<u8>().copied());
let _ = ctx.send(
self.reply_to,
DownReport {
reason: down.reason,
value: val,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<DownReport>().unwrap();
// Spawn target first so we know its address for the watcher
let target = rt
.spawn(StartCompleter {
_reply_to: ActorAddress::default(),
})
.unwrap();
let _watcher = rt
.spawn(MonitorWatcher {
target,
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 10);
let report = inbox
.try_recv()
.expect("should receive Down from on_start stop_with");
assert_eq!(report.reason, StopReason::Completed);
assert_eq!(report.value, Some(7));
}
/// Supervisor receives rich exit value in handle_down (graceful handoff pattern).
#[test]
fn supervisor_receives_rich_exit_in_handle_down() {
struct Completer;
impl ActorInterface for Completer {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_with(vec![1u8, 2, 3]);
}
}
#[derive(Clone, Debug)]
struct ValueReport(Option<Vec<u8>>);
struct ManualSupervisor {
reply_to: ActorAddress,
child: Option<ActorAddress>,
}
impl ActorInterface for ManualSupervisor {
type Incoming = Ping;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
let child = ctx.spawn(Completer).unwrap();
ctx.monitor(child).unwrap();
self.child = Some(child);
}
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
if let Some(child) = self.child {
let _ = ctx.send(
child,
Ping {
reply_to: ActorAddress::default(),
},
);
}
}
fn handle_down(&mut self, ctx: &Ctx, down: Down) {
let val = down
.exit_value
.as_ref()
.and_then(|v| v.downcast_ref::<Vec<u8>>().cloned());
let _ = ctx.send(self.reply_to, ValueReport(val));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<ValueReport>().unwrap();
let sup = rt
.spawn(ManualSupervisor {
reply_to: *inbox.addr(),
child: None,
})
.unwrap();
tick_n(&rt, 2);
rt.send_to(
sup,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 10);
let report = inbox
.try_recv()
.expect("supervisor should receive exit value");
assert_eq!(report.0, Some(vec![1, 2, 3]));
}
// ═══════════════════════════════════════════════════════════════════════════
// Orphan Handling (Part B.2)
// ═══════════════════════════════════════════════════════════════════════════
/// Parent dies → unsupervised children killed.
#[test]
fn orphan_unsupervised_children_killed_when_parent_dies() {
struct SpawnChildren {
reply_to: ActorAddress,
}
impl ActorInterface for SpawnChildren {
type Incoming = Ping;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
// Spawn 3 children
let c1 = ctx.spawn(PingPongActor).unwrap();
let c2 = ctx.spawn(PingPongActor).unwrap();
let c3 = ctx.spawn(PingPongActor).unwrap();
let _ = ctx.send(self.reply_to, Count(3));
let _ = (c1, c2, c3);
}
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_self();
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Count>().unwrap();
let parent = rt
.spawn(SpawnChildren {
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 3);
let _ = inbox.try_recv().expect("children spawned");
// parent + 3 children = 4 actors
assert_eq!(rt.stats().workers[0].num_actors, 4);
// Kill parent
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 10);
// All should be dead (parent stopped, children orphaned and killed)
assert_eq!(
rt.stats().workers[0].num_actors,
0,
"all actors should be dead"
);
}
/// Parent dies → supervised children NOT killed.
#[test]
fn orphan_supervised_children_not_killed() {
struct ParentActor;
impl ActorInterface for ParentActor {
type Incoming = Ping;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
// Spawn a supervisor as a child
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![ChildSpec::new("worker", RestartPolicy::Permanent, |ctx| {
ctx.spawn(PingPongActor)
})],
);
let _ = ctx.spawn(sup);
}
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_self();
}
}
let rt = std_runtime(RuntimeConfig::default());
let parent = rt.spawn(ParentActor).unwrap();
tick_n(&rt, 5);
// parent + supervisor + supervised child = 3
let actors_before = rt.stats().workers[0].num_actors;
assert!(
actors_before >= 3,
"should have parent + supervisor + child, got {}",
actors_before
);
// Kill parent
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 10);
// Supervisor and its child should still be alive (supervisor is a child of parent,
// but it IS the supervisor, so it gets killed as orphan too — hmm.)
// Actually: the supervisor IS a child of parent. It's NOT supervised itself.
// So it will be orphan-killed. That's correct behavior.
// Let me redesign: use a runtime-spawned supervisor.
// Actually let me reconsider: the plan says "Parent dies → supervised children NOT killed"
// This means: if parent spawns children, and those children are SUPERVISED by a supervisor,
// they should not be orphan-killed. The supervisor itself (if unsupervised) would be killed.
// The proper test: parent spawns child, child is also supervised.
// But supervision registration happens when Supervisor::start_child calls supervisor_registry.register.
// The orphan check is: supervisor_registry.lookup(&child).is_none() → kill.
// So if a child is registered as supervised, it won't be killed.
// Simplest: parent is a supervisor, parent dies. The supervisor's supervised children
// should NOT be orphan-killed because they are in the supervisor registry.
// But wait, the supervisor (parent) stops, and on_stop it sends stop to children.
// So the children get stopped by the supervisor's on_stop, not by orphan handling.
// Let me restructure: we have grandparent → parent → child.
// Parent is NOT supervised. Child IS supervised by some supervisor actor.
// When grandparent dies, parent is orphan-killed. But child should survive
// because it's supervised.
// Actually, the simplest reading is:
// Parent spawns child_a and child_b. child_a is supervised. child_b is not.
// Parent dies. child_b is killed (orphan). child_a survives (supervised).
let rt = std_runtime(RuntimeConfig::default());
struct GrandParent {
_reply_to: ActorAddress,
}
impl ActorInterface for GrandParent {
type Incoming = Ping;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
// Spawn a supervisor for one child
let sup = Supervisor::new(
SupervisorStrategy::OneForOne,
5,
vec![ChildSpec::new(
"supervised",
RestartPolicy::Permanent,
|ctx| ctx.spawn(PingPongActor),
)],
);
let _sup_addr = ctx.spawn(sup).unwrap();
// Also spawn an unsupervised child directly
let _unsupervised = ctx.spawn(NullActor).unwrap();
}
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_self();
}
}
let parent = rt
.spawn(GrandParent {
_reply_to: ActorAddress::default(),
})
.unwrap();
tick_n(&rt, 5);
let before = rt.stats().workers[0].num_actors;
assert!(
before >= 4,
"should have parent + supervisor + supervised child + unsupervised, got {}",
before
);
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 15);
// After cascade: parent dies, supervisor+unsupervised get orphaned.
// Unsupervised NullActor has no supervisor → killed.
// Supervisor has no supervisor → killed. Its on_stop sends stop to supervised child.
// End result: 0 actors (supervisor on_stop kills its children).
let after = rt.stats().workers[0].num_actors;
assert_eq!(after, 0, "all actors cleaned up after cascade");
}
/// Cascading orphan cleanup: A→B→C, A dies, B then C killed.
#[test]
fn orphan_cascading_cleanup() {
struct SpawnChild {
reply_to: ActorAddress,
}
impl ActorInterface for SpawnChild {
type Incoming = Ping;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
let _ = ctx.spawn(PingPongActor).unwrap();
let _ = ctx.send(self.reply_to, Pong);
}
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_self();
}
}
struct Root {
reply_to: ActorAddress,
}
impl ActorInterface for Root {
type Incoming = Ping;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
// Spawn middle, which spawns leaf
let _ = ctx
.spawn(SpawnChild {
reply_to: self.reply_to,
})
.unwrap();
}
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.stop_self();
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let root = rt
.spawn(Root {
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 5);
let _ = inbox.try_recv(); // middle spawned its child
// root + middle + leaf = 3
let before = rt.stats().workers[0].num_actors;
assert_eq!(before, 3, "should have root + middle + leaf");
// Kill root → middle orphaned → leaf orphaned
rt.send_to(
root,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 15); // multiple ticks for cascade
assert_eq!(rt.stats().workers[0].num_actors, 0, "cascade killed all");
}
/// Runtime-spawned actors unaffected (no parent).
#[test]
fn orphan_runtime_spawned_unaffected() {
let rt = std_runtime(RuntimeConfig::default());
let a = rt.spawn(PingPongActor).unwrap();
let b = rt.spawn(PingPongActor).unwrap();
rt.tick();
assert_eq!(rt.stats().workers[0].num_actors, 2);
// Stop one — the other should not be affected
rt.stop_actor(a).unwrap();
tick_n(&rt, 5);
assert_eq!(
rt.stats().workers[0].num_actors,
1,
"only stopped actor removed"
);
rt.stop_actor(b).unwrap();
tick_n(&rt, 5);
assert_eq!(rt.stats().workers[0].num_actors, 0);
}
// ═══════════════════════════════════════════════════════════════════════════
// Suspend/Resume (Part B.3)
// ═══════════════════════════════════════════════════════════════════════════
/// Suspended actor queues but doesn't process; resume restores processing.
#[test]
fn suspended_actor_queues_then_resume_processes() {
struct SuspendOnFirst {
suspended: bool,
}
impl ActorInterface for SuspendOnFirst {
type Incoming = Increment;
type Response = Count;
fn handle(&mut self, ctx: &Ctx, msg: Increment) {
if !self.suspended {
self.suspended = true;
ctx.suspend_self();
// This message was already being processed, so we reply
let _ = ctx.send(msg.reply_to, Count(1));
} else {
let _ = ctx.send(msg.reply_to, Count(99));
}
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Count>().unwrap();
let actor = rt.spawn(SuspendOnFirst { suspended: false }).unwrap();
rt.tick(); // on_start
// First message: processed, then actor suspends itself
rt.send_to(
actor,
Increment {
reply_to: *inbox.addr(),
},
)
.unwrap();
rt.tick();
assert_eq!(inbox.try_recv(), Some(Count(1)), "first message processed");
// Second message: queued but not processed (actor suspended)
rt.send_to(
actor,
Increment {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 3);
assert!(inbox.try_recv().is_none(), "no reply while suspended");
// Resume via runtime (unchecked at core level)
// We need to use the ContextInner::request_resume. From test, use send ResumeSignal.
// Actually, the simplest way: use another actor that resumes it.
struct Resumer {
target: ActorAddress,
}
impl ActorInterface for Resumer {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
// Use the raw inner to resume (unchecked at core level)
ctx.raw_inner().request_resume(self.target);
}
}
let resumer = rt.spawn(Resumer { target: actor }).unwrap();
rt.tick();
rt.send_to(
resumer,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
assert_eq!(
inbox.try_recv(),
Some(Count(99)),
"queued message processed after resume"
);
}
/// Supervisor can resume suspended child.
#[test]
fn supervisor_can_resume_suspended_child() {
#[derive(Clone)]
struct Suspend;
#[derive(Clone)]
struct Resume {
target: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct Ack;
struct SuspendableChild;
impl ActorInterface for SuspendableChild {
type Incoming = Suspend;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Suspend) {
ctx.suspend_self();
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Ack>().unwrap();
struct MySup {
child: Option<ActorAddress>,
reply_to: ActorAddress,
}
impl ActorInterface for MySup {
type Incoming = Resume;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
let child = ctx.spawn(SuspendableChild).unwrap();
ctx.monitor(child).unwrap();
// Register as supervisor via public API
let ext = ctx
.extension()
.unwrap()
.as_any()
.downcast_ref::<StdExtension>()
.unwrap();
ext.register_supervisor(ctx.self_addr(), child);
self.child = Some(child);
}
fn handle(&mut self, ctx: &Ctx, msg: Resume) {
if let Ok(()) = ctx.resume(msg.target) {
let _ = ctx.send(self.reply_to, Ack);
}
}
}
let sup = rt
.spawn(MySup {
child: None,
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 3);
let child = rt
.stats()
.actors
.iter()
.find(|(a, _)| *a != sup)
.map(|(a, _)| *a)
.unwrap();
// Suspend child
rt.send_to(child, Suspend).unwrap();
tick_n(&rt, 3);
// Supervisor resumes child
rt.send_to(sup, Resume { target: child }).unwrap();
tick_n(&rt, 3);
let ack = inbox
.try_recv()
.expect("supervisor should be able to resume");
assert_eq!(ack, Ack);
}
/// Non-supervisor cannot resume (returns Err).
#[test]
fn non_supervisor_cannot_resume() {
#[derive(Clone)]
struct TryResume {
target: ActorAddress,
}
#[derive(Clone, Debug, PartialEq)]
struct ResumeResult(bool);
struct NonSup {
reply_to: ActorAddress,
}
impl ActorInterface for NonSup {
type Incoming = TryResume;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: TryResume) {
let ok = ctx.resume(msg.target).is_ok();
let _ = ctx.send(self.reply_to, ResumeResult(ok));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<ResumeResult>().unwrap();
let target = rt.spawn(PingPongActor).unwrap();
let non_sup = rt
.spawn(NonSup {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(non_sup, TryResume { target }).unwrap();
rt.tick();
let result = inbox.try_recv().expect("should get resume result");
assert_eq!(
result,
ResumeResult(false),
"non-supervisor should be denied"
);
}
/// Suspended actor can be stopped.
#[test]
fn suspended_actor_can_be_stopped() {
#[derive(Clone)]
struct SuspendCmd;
struct SuspendableActor;
impl ActorInterface for SuspendableActor {
type Incoming = SuspendCmd;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: SuspendCmd) {
ctx.suspend_self();
}
}
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(SuspendableActor).unwrap();
rt.tick();
// Suspend
rt.send_to(actor, SuspendCmd).unwrap();
tick_n(&rt, 3);
assert_eq!(
rt.stats().workers[0].num_actors,
1,
"actor still alive while suspended"
);
// Stop the suspended actor
rt.stop_actor(actor).unwrap();
tick_n(&rt, 5);
assert_eq!(
rt.stats().workers[0].num_actors,
0,
"suspended actor stopped"
);
}
/// Cross-worker resume works (single-threaded test via transfer queue).
#[test]
fn cross_worker_resume_via_runtime() {
#[derive(Clone)]
struct SuspendCmd;
struct SuspendableActor;
impl ActorInterface for SuspendableActor {
type Incoming = SuspendCmd;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: SuspendCmd) {
ctx.suspend_self();
}
}
// Test that request_resume from Runtime (outside worker) works
// by sending ResumeSignal through the transfer queue.
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(SuspendableActor).unwrap();
rt.tick();
// Suspend
rt.send_to(actor, SuspendCmd).unwrap();
tick_n(&rt, 3);
// Queue a message while suspended
rt.send_to(actor, SuspendCmd).unwrap();
tick_n(&rt, 2);
// Resume via an actor using raw_inner (simulates cross-worker)
struct Resumer {
target: ActorAddress,
}
impl ActorInterface for Resumer {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
ctx.raw_inner().request_resume(self.target);
}
}
let resumer = rt.spawn(Resumer { target: actor }).unwrap();
rt.tick();
rt.send_to(
resumer,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
// Actor should be alive and resumed (processed the queued SuspendCmd, then suspended again)
assert_eq!(
rt.stats().workers[0].num_actors,
2,
"both actors still alive"
);
}
// ── Capability Tests ─────────────────────────────────────────────────────────
/// An unrestricted actor (no CapabilitySet in env) can freely send, spawn, and monitor.
#[test]
fn cap_unrestricted_actor_sends_freely() {
struct Spawner {
reply_to: ActorAddress,
}
impl ActorInterface for Spawner {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
// Send to reply — should succeed
let _ = ctx.send(self.reply_to, Pong).unwrap();
// Spawn a child — should succeed
let child = ctx.spawn(PingPongActor).unwrap();
// Monitor the child — should succeed
ctx.monitor(child).unwrap();
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let spawner = rt
.spawn(Spawner {
reply_to: *inbox.addr(),
})
.unwrap();
rt.tick();
rt.send_to(
spawner,
Ping {
reply_to: *inbox.addr(),
},
)
.unwrap();
tick_n(&rt, 3);
assert!(
inbox.try_recv().is_some(),
"unrestricted actor can send freely"
);
}
/// A restricted actor (empty CapabilitySet) gets denied when sending to another actor.
#[test]
fn cap_restricted_actor_denied_send() {
#[derive(Clone, Debug, PartialEq)]
struct SendResult(bool);
struct Restricted {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for Restricted {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let ok = ctx.send(self.target, Pong).is_ok();
let _ = ctx.send(self.reply_to, SendResult(ok));
}
}
let rt = std_runtime(RuntimeConfig::default());
let result_inbox = rt.new_inbox::<SendResult>().unwrap();
let peer = rt.spawn(PingPongActor).unwrap();
// Spawn with empty CapabilitySet — restricted but can self-send
let restricted = rt
.spawn_with_env(
Restricted {
target: peer,
reply_to: *result_inbox.addr(),
},
EnvironmentBuilder::new()
.set(CapabilitySet::new().with_send(*result_inbox.addr()))
.build(),
)
.unwrap();
rt.tick();
rt.send_to(
restricted,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
let result = result_inbox.try_recv().expect("should get result");
assert!(!result.0, "send to un-granted peer should fail");
}
/// A restricted actor with `with_send(peer)` can send to that peer.
#[test]
fn cap_restricted_actor_allowed_send() {
struct GrantedSender {
peer: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for GrantedSender {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let _ = ctx.send(
self.peer,
Ping {
reply_to: self.reply_to,
},
);
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let peer = rt.spawn(PingPongActor).unwrap();
let caps = CapabilitySet::new()
.with_send(peer)
.with_send(*inbox.addr());
let sender = rt
.spawn_with_env(
GrantedSender {
peer,
reply_to: *inbox.addr(),
},
EnvironmentBuilder::new().set(caps).build(),
)
.unwrap();
rt.tick();
rt.send_to(
sender,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
assert!(inbox.try_recv().is_some(), "granted sender should succeed");
}
/// Typed send grant: `with_send_typed::<Ping>(addr)` allows Ping but not other types.
#[test]
fn cap_typed_send_grant() {
#[derive(Clone, Debug, PartialEq)]
struct Report {
ping_ok: bool,
pong_ok: bool,
}
struct TypeChecker {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for TypeChecker {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let ping_ok = ctx
.send(
self.target,
Ping {
reply_to: ActorAddress::default(),
},
)
.is_ok();
let pong_ok = ctx.send(self.target, Pong).is_ok();
let _ = ctx.send(self.reply_to, Report { ping_ok, pong_ok });
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Report>().unwrap();
let target = rt.spawn(NullActor).unwrap();
let caps = CapabilitySet::new()
.with_send_typed::<Ping>(target)
.with_send(*inbox.addr());
let checker = rt
.spawn_with_env(
TypeChecker {
target,
reply_to: *inbox.addr(),
},
EnvironmentBuilder::new().set(caps).build(),
)
.unwrap();
rt.tick();
rt.send_to(
checker,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
let report = inbox.try_recv().expect("should get report");
assert!(report.ping_ok, "typed grant for Ping should allow Ping");
assert!(!report.pong_ok, "typed grant for Ping should deny Pong");
}
/// A restricted actor without spawn permission gets denied on ctx.spawn().
#[test]
fn cap_spawn_denied() {
#[derive(Clone, Debug, PartialEq)]
struct SpawnResult(bool);
struct NoSpawn {
reply_to: ActorAddress,
}
impl ActorInterface for NoSpawn {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let ok = ctx.spawn(PingPongActor).is_ok();
let _ = ctx.send(self.reply_to, SpawnResult(ok));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<SpawnResult>().unwrap();
let caps = CapabilitySet::new().with_send(*inbox.addr());
let actor = rt
.spawn_with_env(
NoSpawn {
reply_to: *inbox.addr(),
},
EnvironmentBuilder::new().set(caps).build(),
)
.unwrap();
rt.tick();
rt.send_to(
actor,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
let result = inbox.try_recv().expect("should get result");
assert!(!result.0, "spawn without permission should fail");
}
/// A restricted actor with `with_spawn()` can spawn children.
#[test]
fn cap_spawn_allowed() {
#[derive(Clone, Debug, PartialEq)]
struct SpawnResult(bool);
struct CanSpawn {
reply_to: ActorAddress,
}
impl ActorInterface for CanSpawn {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let ok = ctx.spawn(PingPongActor).is_ok();
let _ = ctx.send(self.reply_to, SpawnResult(ok));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<SpawnResult>().unwrap();
let caps = CapabilitySet::new().with_spawn().with_send(*inbox.addr());
let actor = rt
.spawn_with_env(
CanSpawn {
reply_to: *inbox.addr(),
},
EnvironmentBuilder::new().set(caps).build(),
)
.unwrap();
rt.tick();
rt.send_to(
actor,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
let result = inbox.try_recv().expect("should get result");
assert!(result.0, "spawn with permission should succeed");
}
/// Child inherits parent's CapabilitySet and is equally restricted.
#[test]
fn cap_capability_inheritance() {
#[derive(Clone, Debug, PartialEq)]
struct ChildRestricted(bool);
struct Parent {
reply_to: ActorAddress,
}
impl ActorInterface for Parent {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
// Child reports in on_start, so no need to send to it
let _ = ctx.spawn(Child {
reply_to: self.reply_to,
});
}
}
struct Child {
reply_to: ActorAddress,
}
impl ActorInterface for Child {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
let restricted = ctx.env::<CapabilitySet>().is_some();
let _ = ctx.send(self.reply_to, ChildRestricted(restricted));
}
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<ChildRestricted>().unwrap();
let caps = CapabilitySet::new().with_spawn().with_send(*inbox.addr());
let parent = rt
.spawn_with_env(
Parent {
reply_to: *inbox.addr(),
},
EnvironmentBuilder::new().set(caps).build(),
)
.unwrap();
rt.tick();
rt.send_to(
parent,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 5);
let result = inbox.try_recv().expect("should get report from child");
assert!(result.0, "child should inherit parent's CapabilitySet");
}
/// A restricted actor without monitor grant gets denied on ctx.monitor().
#[test]
fn cap_monitor_denied() {
#[derive(Clone, Debug, PartialEq)]
struct MonitorResult(bool);
struct NoMonitor {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for NoMonitor {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let ok = ctx.monitor(self.target).is_ok();
let _ = ctx.send(self.reply_to, MonitorResult(ok));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<MonitorResult>().unwrap();
let target = rt.spawn(PingPongActor).unwrap();
let caps = CapabilitySet::new().with_send(*inbox.addr());
let actor = rt
.spawn_with_env(
NoMonitor {
target,
reply_to: *inbox.addr(),
},
EnvironmentBuilder::new().set(caps).build(),
)
.unwrap();
rt.tick();
rt.send_to(
actor,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
let result = inbox.try_recv().expect("should get result");
assert!(!result.0, "monitor without permission should fail");
}
/// A restricted actor without service grant gets None from ctx.resource().
#[test]
fn cap_service_access_denied() {
struct MyService;
#[derive(Clone, Debug, PartialEq)]
struct ServiceResult(bool);
struct ServiceUser {
reply_to: ActorAddress,
}
impl ActorInterface for ServiceUser {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let found = ctx.resource::<MyService>().is_some();
let _ = ctx.send(self.reply_to, ServiceResult(found));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<ServiceResult>().unwrap();
// Give the actor a service binding but no capability to access it
let service_addr = ActorAddress::new_random();
let caps = CapabilitySet::new().with_send(*inbox.addr());
let actor = rt
.spawn_with_env(
ServiceUser {
reply_to: *inbox.addr(),
},
EnvironmentBuilder::new()
.set(caps)
.set(ServiceBinding::<MyService>::new(service_addr))
.build(),
)
.unwrap();
rt.tick();
rt.send_to(
actor,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
let result = inbox.try_recv().expect("should get result");
assert!(!result.0, "service access without grant should return None");
}
/// A restricted actor can always send to itself (self-send bypass).
#[test]
fn cap_self_send_always_allowed() {
#[derive(Clone, Debug, PartialEq)]
struct SelfSendResult(bool);
struct SelfSender {
reply_to: ActorAddress,
sent_self: bool,
}
impl ActorInterface for SelfSender {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
if !self.sent_self {
self.sent_self = true;
// Send to self — should always work even with empty caps
let ok = ctx
.send(
ctx.self_addr(),
Ping {
reply_to: ActorAddress::default(),
},
)
.is_ok();
let _ = ctx.send(self.reply_to, SelfSendResult(ok));
}
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<SelfSendResult>().unwrap();
// Empty CapabilitySet — only self-send allowed (plus inbox for reporting)
let caps = CapabilitySet::new().with_send(*inbox.addr());
let actor = rt
.spawn_with_env(
SelfSender {
reply_to: *inbox.addr(),
sent_self: false,
},
EnvironmentBuilder::new().set(caps).build(),
)
.unwrap();
rt.tick();
rt.send_to(
actor,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
let result = inbox.try_recv().expect("should get result");
assert!(result.0, "self-send should always be allowed");
}
/// stop_actor requires send permission to the target address.
#[test]
fn cap_stop_actor_requires_send() {
#[derive(Clone, Debug, PartialEq)]
struct StopResult(bool);
struct Stopper {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for Stopper {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
let ok = ctx.stop_actor(self.target).is_ok();
let _ = ctx.send(self.reply_to, StopResult(ok));
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<StopResult>().unwrap();
let target = rt.spawn(PingPongActor).unwrap();
// No send permission for target
let caps = CapabilitySet::new().with_send(*inbox.addr());
let stopper = rt
.spawn_with_env(
Stopper {
target,
reply_to: *inbox.addr(),
},
EnvironmentBuilder::new().set(caps).build(),
)
.unwrap();
rt.tick();
rt.send_to(
stopper,
Ping {
reply_to: ActorAddress::default(),
},
)
.unwrap();
tick_n(&rt, 3);
let result = inbox.try_recv().expect("should get result");
assert!(!result.0, "stop_actor without send permission should fail");
}