fix(runtime): a root actor panic escapes run()

The trampoline caught the root's panic, recorded it as Outcome::Panic on
the slot, and run() dropped the initial handle without reading it — every
assert inside run(), the standard test-suite pattern, was silently
vacuous (found live: a failing-first test passed). run() now reads the
root outcome before the handle drop and resume_unwinds the payload after
full teardown, so a caller's catch_unwind leaves the Runtime reusable;
Exit and Stopped return normally. The payload message is printed before
re-raising (the throw-site hook output was suppressed in-actor).

Correct the two tests this unmasked, both born failing and never run:
the select loser-arm test kept a closed arm in the set (the documented
closed-arm rule: a closed arm reports ready forever — observe the
disconnect and drop it); the send_after-to-dead test expected Ok(None)
from a closed+empty channel (documented: Err(RecvError), which proves
nothing-delivered even more strongly).
This commit is contained in:
Claude (sandbox)
2026-07-18 21:52:50 +00:00
parent 527f045e17
commit 7eae56a296
4 changed files with 85 additions and 3 deletions
+32
View File
@@ -485,3 +485,35 @@ fn multi_thread_timer_only_no_pipe_contention() {
SLEEP_MS * 2,
);
}
// ---------------------------------------------------------------------------
// Root panic propagation
/// A panic in the root actor escapes `run()` to the caller. Anything else
/// makes every assert inside `run` silently vacuous — found live when a
/// failing-first test passed: the tripped assert was caught by the
/// trampoline, recorded as `Outcome::Panic` on the root slot, and dropped
/// unread with the initial handle.
#[test]
#[should_panic(expected = "root actor panic escapes")]
fn root_panic_escapes_run() {
rt1().run(|| {
panic!("root actor panic escapes");
});
}
/// Teardown completes before the root panic propagates: a caller that
/// catches it can immediately `run()` again on the same `Runtime` (the
/// documented sequential-reuse contract).
#[test]
fn runtime_reusable_after_root_panic() {
let r = rt1();
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
r.run(|| panic!("boom"));
}));
assert!(caught.is_err(), "root panic must escape run()");
let ran = Arc::new(AtomicBool::new(false));
let ran_t = ran.clone();
r.run(move || ran_t.store(true, Ordering::Relaxed));
assert!(ran.load(Ordering::Relaxed), "runtime unusable after root panic");
}