//! Process-group tests that run under the scheduler: `join` installs a real //! monitor on a live actor, and a real death drives eviction (the reaper //! actor sweeps it; the read path hides it in the meantime). //! (Pure structural invariants live in the `pg` unit tests.) use smarm::{channel, members, pick, run, spawn}; use smarm::{join, leave}; #[test] fn join_then_members_lists_a_live_member() { run(|| { let (tx, rx) = channel::<()>(); let h = spawn(move || { rx.recv().unwrap(); }); let pid = h.pid(); assert!(join("workers", pid), "first join is new"); assert!(!join("workers", pid), "second join is idempotent"); assert_eq!(members("workers"), vec![pid]); assert_eq!(pick("workers"), Some(pid)); tx.send(()).unwrap(); h.join().unwrap(); }); } #[test] fn a_dead_actor_vanishes_from_every_group_it_joined() { run(|| { let (tx, rx) = channel::<()>(); let h = spawn(move || { rx.recv().unwrap(); }); let pid = h.pid(); join("g1", pid); join("g2", pid); assert_eq!(members("g1"), vec![pid]); assert_eq!(members("g2"), vec![pid]); // Release the actor. finalize_actor queues the Down to the reaper and // marks the slot dead before unparking joiners, so by the time join() // returns every read hides the pid whether or not the reaper has run. tx.send(()).unwrap(); h.join().unwrap(); // Gone from every group it joined, not just one. assert!(members("g1").is_empty(), "gone from g1"); assert!(members("g2").is_empty(), "and from g2"); assert_eq!(pick("g1"), None); }); } #[test] fn pick_returns_none_when_the_only_member_is_dead() { run(|| { let (tx, rx) = channel::<()>(); let h = spawn(move || { rx.recv().unwrap(); }); let pid = h.pid(); join("pool", pid); tx.send(()).unwrap(); h.join().unwrap(); assert_eq!(pick("pool"), None); assert!(members("pool").is_empty()); }); } #[test] fn live_members_survive_a_peers_death() { run(|| { let (tx_a, rx_a) = channel::<()>(); let (tx_b, rx_b) = channel::<()>(); let a = spawn(move || { rx_a.recv().unwrap(); }); let b = spawn(move || { rx_b.recv().unwrap(); }); join("svc", a.pid()); join("svc", b.pid()); // Kill a; b is still parked on its channel. tx_a.send(()).unwrap(); a.join().unwrap(); assert_eq!(members("svc"), vec![b.pid()], "only the dead peer is gone"); assert_eq!(pick("svc"), Some(b.pid())); tx_b.send(()).unwrap(); b.join().unwrap(); }); } #[test] fn leave_drops_a_membership_without_affecting_others() { run(|| { let (tx_a, rx_a) = channel::<()>(); let (tx_b, rx_b) = channel::<()>(); let a = spawn(move || { rx_a.recv().unwrap(); }); let b = spawn(move || { rx_b.recv().unwrap(); }); join("g", a.pid()); join("g", b.pid()); assert!(leave("g", a.pid()), "a was a member"); assert!(!leave("g", a.pid()), "leaving twice finds nothing"); assert_eq!(members("g"), vec![b.pid()]); tx_a.send(()).unwrap(); tx_b.send(()).unwrap(); a.join().unwrap(); b.join().unwrap(); }); } #[test] fn joining_an_already_dead_pid_never_shows_in_a_read() { run(|| { let h = spawn(|| {}); let pid = h.pid(); h.join().unwrap(); // actor is finalized before we join it to anything // join() on a gone pid queues a NoProc Down to the reaper immediately; // reads never show it either way (slot-liveness backstop). join("late", pid); assert!( members("late").is_empty(), "dead-at-join member never reads as live" ); assert_eq!(pick("late"), None); }); }