swactor/tests/runtime_registry.rs
Claude 33df021afd refactor: split runtime_api.rs (4,622 lines) into 7 focused test files
Split monolithic test file into domain-specific test modules:
- common/mod.rs: shared messages, actors, helpers
- runtime_lifecycle.rs (57 tests): spawn, FIFO, threading, panic safety
- runtime_mechanics.rs (10): placement, backpressure, cleanup, recovery
- runtime_hooks.rs (12): on_start/on_stop, graceful stop
- runtime_timers.rs (6): one-shot and interval timers
- runtime_registry.rs (32): naming, monitoring, groups, ask pattern
- runtime_supervision.rs (20): supervisor + router
- runtime_hasher.rs (3): identity hasher correctness

All 140 tests pass.

Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
2026-02-13 14:48:35 +00:00

758 lines
25 KiB
Rust

mod common;
use common::*;
// ── Named Actor Registry ────────────────────────────────────────────────────
/// Given a named actor is spawned,
/// when I look it up by name,
/// then I get the same address that spawn returned.
#[test]
fn named_actor_lookup_returns_spawn_address() {
let rt = std_runtime(RuntimeConfig::default());
let addr = rt.spawn_named("greeter", PingPongActor).unwrap();
assert_eq!(rt.where_is("greeter"), Some(addr));
}
/// Given a named actor exists,
/// when I send a message to the looked-up address,
/// then the actor receives and processes it.
#[test]
fn named_actor_receives_messages_via_lookup() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let addr = rt.spawn_named("ponger", PingPongActor).unwrap();
assert_eq!(rt.where_is("ponger"), Some(addr));
rt.send_to(addr, Ping { reply_to: *inbox.addr() }).unwrap();
rt.tick();
assert!(inbox.try_recv().is_some(), "named actor should process message");
}
/// Given a name is already registered,
/// when I try to spawn another actor with the same name,
/// then I get an error and the original binding is preserved.
#[test]
fn duplicate_name_returns_error() {
let rt = std_runtime(RuntimeConfig::default());
let first_addr = rt.spawn_named("singleton", PingPongActor).unwrap();
let result = rt.spawn_named("singleton", PingPongActor);
assert!(result.is_err(), "duplicate name should fail");
assert_eq!(rt.where_is("singleton"), Some(first_addr), "original binding preserved");
}
/// Given no actors are registered,
/// when I look up a nonexistent name,
/// then I get None.
#[test]
fn where_is_returns_none_for_unknown_name() {
let rt = std_runtime(RuntimeConfig::default());
assert_eq!(rt.where_is("ghost"), None);
}
/// Given a named actor is stopped,
/// when the next tick runs cleanup,
/// then the name is automatically unregistered.
#[test]
fn name_auto_unregistered_on_actor_death() {
let rt = std_runtime(RuntimeConfig::default());
let addr = rt.spawn_named("ephemeral", PingPongActor).unwrap();
rt.tick(); // on_start
rt.stop_actor(addr).unwrap();
rt.tick(); // process StopSignal + cleanup
assert_eq!(rt.where_is("ephemeral"), None, "name should be freed after stop");
}
/// Given a named actor died and its name was freed,
/// when I spawn a new actor with the same name,
/// then registration succeeds with a new address.
#[test]
fn name_can_be_reused_after_actor_death() {
let rt = std_runtime(RuntimeConfig::default());
let first = rt.spawn_named("worker", PingPongActor).unwrap();
rt.tick();
rt.stop_actor(first).unwrap();
rt.tick(); // cleanup frees the name
let second = rt.spawn_named("worker", PingPongActor).unwrap();
assert_ne!(first, second, "new actor should have a different address");
assert_eq!(rt.where_is("worker"), Some(second));
}
/// Given a named actor panics (and is not restartable),
/// when the next tick runs cleanup,
/// then the name is freed.
#[test]
fn name_auto_unregistered_on_panic() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let _addr = rt.spawn_named("fragile", PanicActor).unwrap();
rt.tick(); // on_start
rt.send_to(_addr, PanicMsg).unwrap();
rt.tick(); // panic -> poison -> cleanup
assert_eq!(rt.where_is("fragile"), None, "name freed after panic");
// Can reuse the name
let _new = rt.spawn_named("fragile", PingPongActor).unwrap();
assert!(rt.where_is("fragile").is_some());
drop(inbox);
}
/// Given multiple named actors are registered,
/// when I call registered_names(),
/// then all names are returned.
#[test]
fn registered_names_lists_all() {
let rt = std_runtime(RuntimeConfig::default());
rt.spawn_named("alpha", PingPongActor).unwrap();
rt.spawn_named("beta", PingPongActor).unwrap();
rt.spawn_named("gamma", PingPongActor).unwrap();
let mut names = rt.registered_names();
names.sort();
assert_eq!(names, vec!["alpha", "beta", "gamma"]);
}
/// Given a named actor exists,
/// when I manually unregister the name,
/// then the name is freed but the actor continues running.
#[test]
fn manual_unregister_frees_name_but_actor_lives() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let addr = rt.spawn_named("temp-name", PingPongActor).unwrap();
rt.tick(); // on_start
let removed = rt.unregister("temp-name");
assert_eq!(removed, Some(addr));
assert_eq!(rt.where_is("temp-name"), None, "name freed");
// Actor still alive and can receive messages
rt.send_to(addr, Ping { reply_to: *inbox.addr() }).unwrap();
rt.tick();
assert!(inbox.try_recv().is_some(), "actor still processes messages");
}
/// An actor that 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();
}
}
}
/// Given a named actor exists,
/// when another actor calls ctx.where_is() from inside a handler,
/// then it resolves the correct address.
#[test]
fn ctx_where_is_resolves_inside_handler() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<MyAddr>().unwrap();
let target = rt.spawn_named("target", PingPongActor).unwrap();
let looker = rt.spawn(NameLookupActor {
target_name: "target",
reply_to: *inbox.addr(),
}).unwrap();
rt.tick(); // on_start
rt.send_to(looker, Ping { reply_to: ActorAddress::default() }).unwrap();
rt.tick(); // handle -> where_is -> send
rt.tick(); // deliver reply
let result = inbox.try_recv();
assert_eq!(result, Some(MyAddr(target)), "ctx.where_is found the named actor");
}
/// An actor that spawns a named child using ctx.spawn_named().
struct NamedSpawnerActor {
reply_to: ActorAddress,
}
impl ActorInterface for NamedSpawnerActor {
type Incoming = Ping;
type Response = ();
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
match ctx.spawn_named("child", PingPongActor) {
Ok(addr) => { ctx.send(self.reply_to, MyAddr(addr)).unwrap(); }
Err(_) => {}
}
}
}
/// Given an actor calls ctx.spawn_named("child", ...),
/// when the child is spawned,
/// then where_is("child") returns the correct address.
#[test]
fn ctx_spawn_named_registers_from_handler() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<MyAddr>().unwrap();
let spawner = rt.spawn(NamedSpawnerActor {
reply_to: *inbox.addr(),
}).unwrap();
rt.tick(); // on_start
rt.send_to(spawner, Ping { reply_to: ActorAddress::default() }).unwrap();
rt.tick(); // handle -> spawn_named
rt.tick(); // deliver reply
let child_addr = inbox.try_recv().expect("should receive child address");
assert_eq!(rt.where_is("child"), Some(child_addr.0), "name registered from handler");
}
// ── Actor Monitoring / Death Watch ──────────────────────────────────────────
/// An actor that monitors a target and forwards Down notifications to a reply address.
struct WatcherActor {
watch_target: ActorAddress,
reply_to: ActorAddress,
mref: Option<MonitorRef>,
}
impl ActorInterface for WatcherActor {
type Incoming = Down;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
self.mref = Some(ctx.monitor(self.watch_target));
}
fn handle(&mut self, ctx: &Ctx, msg: Down) {
// Forward the Down notification to the test inbox
ctx.send(self.reply_to, msg).unwrap();
}
}
/// Given actor A monitors actor B,
/// when B is gracefully stopped,
/// then A receives a Down { reason: Normal } message.
#[test]
fn monitor_notifies_on_graceful_stop() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Down>().unwrap();
let target = rt.spawn(PingPongActor).unwrap();
let _watcher = rt.spawn(WatcherActor {
watch_target: target,
reply_to: *inbox.addr(),
mref: None,
}).unwrap();
rt.tick(); // on_start -> watcher sets up monitor
rt.stop_actor(target).unwrap();
rt.tick(); // target receives StopSignal -> cleanup_dead emits Down
rt.tick(); // watcher receives Down -> forwards to inbox
let down = inbox.try_recv().expect("should receive Down notification");
assert_eq!(down.addr, target);
assert_eq!(down.reason, StopReason::Normal);
}
/// Given actor A monitors actor B,
/// when B panics,
/// then A receives a Down { reason: Panicked } message.
#[test]
fn monitor_notifies_on_panic() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Down>().unwrap();
let target = rt.spawn(PanicActor).unwrap();
let _watcher = rt.spawn(WatcherActor {
watch_target: target,
reply_to: *inbox.addr(),
mref: None,
}).unwrap();
rt.tick(); // on_start
rt.send_to(target, PanicMsg).unwrap();
rt.tick(); // target panics -> cleanup_dead emits Down
rt.tick(); // watcher receives Down -> forwards to inbox
let down = inbox.try_recv().expect("should receive Down on panic");
assert_eq!(down.addr, target);
assert_eq!(down.reason, StopReason::Panicked);
}
/// Given two actors both monitor the same target,
/// when the target dies,
/// then both watchers receive independent Down notifications.
#[test]
fn multiple_watchers_all_notified() {
let rt = std_runtime(RuntimeConfig::default());
let inbox1 = rt.new_inbox::<Down>().unwrap();
let inbox2 = rt.new_inbox::<Down>().unwrap();
let target = rt.spawn(PingPongActor).unwrap();
rt.spawn(WatcherActor {
watch_target: target,
reply_to: *inbox1.addr(),
mref: None,
}).unwrap();
rt.spawn(WatcherActor {
watch_target: target,
reply_to: *inbox2.addr(),
mref: None,
}).unwrap();
rt.tick(); // on_start for all
rt.stop_actor(target).unwrap();
rt.tick(); // cleanup -> Down emitted to both watchers
rt.tick(); // watchers forward Down to inboxes
assert!(inbox1.try_recv().is_some(), "watcher 1 should receive Down");
assert!(inbox2.try_recv().is_some(), "watcher 2 should receive Down");
}
/// An actor that demonitors in response to a Ping message.
struct DemonitorActor {
watch_target: ActorAddress,
mref: Option<MonitorRef>,
}
impl ActorInterface for DemonitorActor {
type Incoming = Ping;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
self.mref = Some(ctx.monitor(self.watch_target));
}
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
// Cancel the monitor
if let Some(mref) = self.mref.take() {
ctx.demonitor(mref);
}
}
}
/// Given actor A monitors actor B then demonitors,
/// when B dies,
/// then A does NOT receive a Down notification.
#[test]
fn demonitor_cancels_notification() {
let rt = std_runtime(RuntimeConfig::default());
let down_inbox = rt.new_inbox::<Down>().unwrap();
let target = rt.spawn(PingPongActor).unwrap();
let watcher = rt.spawn(DemonitorActor {
watch_target: target,
mref: None,
}).unwrap();
rt.tick(); // on_start -> monitor set up
// Trigger demonitor
rt.send_to(watcher, Ping { reply_to: ActorAddress::default() }).unwrap();
rt.tick(); // handle -> demonitor
// Now kill the target
rt.stop_actor(target).unwrap();
rt.tick(); // cleanup -- no Down should be emitted
rt.tick(); // extra tick to be sure
assert!(down_inbox.try_recv().is_none(), "demonitored -- should NOT receive Down");
}
/// Given actor A monitors B, and A dies before B,
/// when B dies,
/// then no Down is delivered (dead watcher cleaned up).
#[test]
fn dead_watcher_does_not_receive_down() {
let rt = std_runtime(RuntimeConfig::default());
let target = rt.spawn(PingPongActor).unwrap();
let watcher = rt.spawn(WatcherActor {
watch_target: target,
reply_to: ActorAddress::default(), // won't matter, watcher dies first
mref: None,
}).unwrap();
rt.tick(); // on_start -> monitor set up
rt.stop_actor(watcher).unwrap();
rt.tick(); // watcher dies -> its monitors are cleaned up
// Now kill the target -- the dead watcher's subscription should be gone
rt.stop_actor(target).unwrap();
rt.tick(); // cleanup -- should not panic or try to deliver to dead watcher
// If we get here without panic, the test passes
}
/// Given an external inbox monitors via the runtime,
/// when the target dies,
/// then the inbox receives a Down message.
#[test]
fn down_delivered_to_external_inbox() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Down>().unwrap();
let target = rt.spawn(PingPongActor).unwrap();
let _watcher = rt.spawn(WatcherActor {
watch_target: target,
reply_to: *inbox.addr(),
mref: None,
}).unwrap();
rt.tick(); // on_start
rt.stop_actor(target).unwrap();
rt.tick(); // cleanup -> Down to watcher
rt.tick(); // watcher forwards to inbox
let down = inbox.try_recv().expect("inbox should receive forwarded Down");
assert_eq!(down.addr, target);
assert_eq!(down.reason, StopReason::Normal);
}
/// Given actor A monitors B with two independent monitors,
/// when B dies,
/// then A receives two Down messages (one per monitor).
#[test]
fn stacked_monitors_produce_multiple_notifications() {
/// An actor that creates two monitors on the same target.
struct DoubleWatcherActor {
target: ActorAddress,
reply_to: ActorAddress,
}
impl ActorInterface for DoubleWatcherActor {
type Incoming = Down;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.monitor(self.target);
ctx.monitor(self.target);
}
fn handle(&mut self, ctx: &Ctx, msg: Down) {
ctx.send(self.reply_to, msg).unwrap();
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Down>().unwrap();
let target = rt.spawn(PingPongActor).unwrap();
rt.spawn(DoubleWatcherActor {
target,
reply_to: *inbox.addr(),
}).unwrap();
rt.tick(); // on_start -> 2 monitors
rt.stop_actor(target).unwrap();
rt.tick(); // cleanup -> 2 Down messages to watcher
rt.tick(); // watcher forwards both to inbox
assert!(inbox.try_recv().is_some(), "first Down");
assert!(inbox.try_recv().is_some(), "second Down");
assert!(inbox.try_recv().is_none(), "no more");
}
// ── Actor Groups / Pub-Sub ──────────────────────────────────────────────────
/// Given actors join a group,
/// when I query group_members,
/// then all joined actors are listed.
#[test]
fn group_members_returns_joined_actors() {
let rt = std_runtime(RuntimeConfig::default());
let a = rt.spawn(PingPongActor).unwrap();
let b = rt.spawn(PingPongActor).unwrap();
rt.join_group(a, "workers");
rt.join_group(b, "workers");
let mut members = rt.group_members("workers");
members.sort_by_key(|addr| addr.0);
let mut expected = vec![a, b];
expected.sort_by_key(|addr| addr.0);
assert_eq!(members, expected);
}
/// Given no actors have joined a group,
/// when I query group_members,
/// then the result is empty.
#[test]
fn empty_group_returns_no_members() {
let rt = std_runtime(RuntimeConfig::default());
assert!(rt.group_members("nonexistent").is_empty());
}
/// Given actors in a group,
/// when a message is published to the group,
/// then all members receive the message.
#[test]
fn publish_broadcasts_to_all_members() {
let rt = std_runtime(RuntimeConfig::default());
let inbox1 = rt.new_inbox::<Pong>().unwrap();
let inbox2 = rt.new_inbox::<Pong>().unwrap();
let a = rt.spawn(PingPongActor).unwrap();
let b = rt.spawn(PingPongActor).unwrap();
rt.join_group(a, "pongers");
rt.join_group(b, "pongers");
rt.tick(); // on_start
// Publish a Ping with inbox1's addr as reply_to.
let count = rt.publish_to("pongers", Ping { reply_to: *inbox1.addr() });
assert_eq!(count, 2, "two members, two messages sent");
rt.tick(); // actors handle Ping -> send Pong to inbox1
// Both actors send to inbox1
assert!(inbox1.try_recv().is_some(), "first Pong");
assert!(inbox1.try_recv().is_some(), "second Pong");
assert!(inbox1.try_recv().is_none(), "no more");
drop(inbox2);
}
/// Given an actor leaves a group,
/// when a message is published,
/// then the leaver does not receive it.
#[test]
fn leave_group_stops_receiving_publishes() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let a = rt.spawn(PingPongActor).unwrap();
let b = rt.spawn(PingPongActor).unwrap();
rt.join_group(a, "pool");
rt.join_group(b, "pool");
rt.leave_group(b, "pool");
rt.tick(); // on_start
let count = rt.publish_to("pool", Ping { reply_to: *inbox.addr() });
assert_eq!(count, 1, "only one member after leave");
rt.tick();
assert!(inbox.try_recv().is_some(), "one Pong from remaining member");
assert!(inbox.try_recv().is_none(), "no second Pong");
}
/// Given a group member dies,
/// when a message is published,
/// then the dead member is not included.
#[test]
fn dead_actor_auto_removed_from_group() {
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let a = rt.spawn(PingPongActor).unwrap();
let b = rt.spawn(PingPongActor).unwrap();
rt.join_group(a, "team");
rt.join_group(b, "team");
rt.tick(); // on_start
rt.stop_actor(b).unwrap();
rt.tick(); // b dies, cleaned up from group
let count = rt.publish_to("team", Ping { reply_to: *inbox.addr() });
assert_eq!(count, 1, "dead actor removed from group");
rt.tick();
assert!(inbox.try_recv().is_some());
assert!(inbox.try_recv().is_none());
}
/// Given an actor is in multiple groups,
/// when the actor dies,
/// then it is removed from all groups.
#[test]
fn actor_removed_from_all_groups_on_death() {
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(); // cleanup removes from all groups
assert!(rt.group_members("alpha").is_empty());
assert!(rt.group_members("beta").is_empty());
assert!(rt.group_members("gamma").is_empty());
}
/// Given a group becomes empty after its last member leaves,
/// then the group name disappears from the active groups list.
#[test]
fn 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 should be removed");
}
/// Given actors join groups from handlers using ctx.join_group(),
/// when group_members is queried,
/// then the joining actors are listed.
#[test]
fn ctx_join_group_from_handler() {
struct GroupJoinerActor;
impl ActorInterface for GroupJoinerActor {
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 a = rt.spawn(GroupJoinerActor).unwrap();
let b = rt.spawn(GroupJoinerActor).unwrap();
rt.tick(); // on_start -> both join "auto-joined"
let members = rt.group_members("auto-joined");
assert_eq!(members.len(), 2);
assert!(members.contains(&a));
assert!(members.contains(&b));
}
/// Given an actor uses ctx.publish() from inside a handler,
/// when the published message is processed,
/// then all group members receive it.
#[test]
fn ctx_publish_broadcasts_from_handler() {
#[derive(Clone)]
struct BroadcastCmd {
reply_to: ActorAddress,
}
struct BroadcasterActor;
impl ActorInterface for BroadcasterActor {
type Incoming = BroadcastCmd;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.join_group("broadcast-test");
}
fn handle(&mut self, ctx: &Ctx, msg: BroadcastCmd) {
ctx.publish("broadcast-test", Ping { reply_to: msg.reply_to });
}
}
let rt = std_runtime(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
// Spawn 3 PingPongActors and one Broadcaster, all in the same group
let _p1 = rt.spawn(PingPongActor).unwrap();
let _p2 = rt.spawn(PingPongActor).unwrap();
rt.join_group(_p1, "broadcast-test");
rt.join_group(_p2, "broadcast-test");
let broadcaster = rt.spawn(BroadcasterActor).unwrap();
rt.tick(); // on_start (broadcaster joins group too)
// Send BroadcastCmd to broadcaster
rt.send_to(broadcaster, BroadcastCmd { reply_to: *inbox.addr() }).unwrap();
rt.tick(); // broadcaster handles -> publish Ping to all 3 members (including self)
rt.tick(); // PingPong actors handle Ping -> send Pong to inbox
// At least 2 Pongs from the PingPongActors
let mut pong_count = 0;
while inbox.try_recv().is_some() {
pong_count += 1;
}
assert!(pong_count >= 2, "at least 2 PingPong members should reply, got {pong_count}");
}
// ── Ask Pattern ─────────────────────────────────────────────────────────────
/// Given a PingPong actor,
/// when I ask with recv_ticking,
/// then I get the Pong response.
#[test]
fn ask_recv_ticking_returns_response() {
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(PingPongActor).unwrap();
rt.tick(); // on_start
let pong: Pong = rt.ask(actor, |reply_to| Ping { reply_to })
.unwrap()
.recv_ticking(&rt, 10)
.unwrap();
assert_eq!(pong, Pong);
}
/// Given a CounterActor,
/// when I ask multiple times,
/// then each response reflects the updated state.
#[test]
fn ask_multiple_times_tracks_state() {
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(CounterActor { count: 0 }).unwrap();
rt.tick(); // on_start
let c1: Count = rt.ask(actor, |reply_to| Increment { reply_to })
.unwrap().recv_ticking(&rt, 10).unwrap();
let c2: Count = rt.ask(actor, |reply_to| Increment { reply_to })
.unwrap().recv_ticking(&rt, 10).unwrap();
let c3: Count = rt.ask(actor, |reply_to| Increment { reply_to })
.unwrap().recv_ticking(&rt, 10).unwrap();
assert_eq!(c1, Count(1));
assert_eq!(c2, Count(2));
assert_eq!(c3, Count(3));
}
/// Given a dead actor,
/// when I ask and tick,
/// then recv_ticking returns a timeout error.
#[test]
fn ask_timeout_when_no_response() {
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(PingPongActor).unwrap();
rt.tick();
rt.stop_actor(actor).unwrap();
rt.tick(); // actor dies
// Ask the dead actor -- message is undeliverable, no response
let result = rt.ask::<Ping, Pong>(actor, |reply_to| Ping { reply_to });
// send_to may succeed or fail
if let Ok(ask) = result {
let err = ask.recv_ticking(&rt, 5);
assert!(err.is_err(), "should timeout with no response");
}
}
/// Given an ask handle,
/// when I use try_recv before ticking,
/// then it returns None (response hasn't arrived yet).
#[test]
fn ask_try_recv_returns_none_before_tick() {
let rt = std_runtime(RuntimeConfig::default());
let actor = rt.spawn(PingPongActor).unwrap();
rt.tick(); // on_start
let ask = rt.ask::<Ping, Pong>(actor, |reply_to| Ping { reply_to }).unwrap();
assert!(ask.try_recv().is_none(), "no response before ticking");
rt.tick(); // process message
assert_eq!(ask.try_recv(), Some(Pong));
}
/// Given an ask, the reply_addr() returns the inbox address for manual use.
#[test]
fn ask_reply_addr_is_accessible() {
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();
let addr = *ask.reply_addr();
// The address should be valid (non-zero)
assert_ne!(addr, ActorAddress::default());
}