swactor/tests/std_extension.rs

4987 lines
145 KiB
Rust
Raw Normal View History

//! 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");
}