use crate::common::{BOUND, PERPETUAL, SHORT_DRAIN, join_bounded, wait_registry_at_most};
use crate::scope_builders::{probed_runtime, scope_runtime};
use camber::{RuntimeError, runtime, schedule};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
const USER_PANIC: &str = "user-owned child panic";
const INTERNAL_PANIC: &str = "internally-owned child panic";
const CLOSURE_VALUE: u32 = 11;
const SIBLING_VALUE: u32 = 23;
fn panic_from_a_schedule_child() {
let (entered_tx, mut entered_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
let handle = schedule::every_async(PERPETUAL, move || {
let entered = entered_tx.clone();
async move {
entered
.send(())
.expect("the panic rendezvous receiver was dropped");
panic!("{}", INTERNAL_PANIC);
}
})
.unwrap();
handle.trigger();
runtime::block_on(tokio::time::timeout(BOUND, entered_rx.recv()))
.expect("the panicking schedule callback never entered")
.expect("the panic rendezvous sender was dropped");
}
#[test]
fn user_owned_panic_stays_on_its_handle_and_does_not_displace_run_result() {
let sibling_ran = Arc::new(AtomicBool::new(false));
let child_sibling = Arc::clone(&sibling_ran);
let outcome = scope_runtime(SHORT_DRAIN).run(move || {
let panicker = camber::spawn_async(async { panic!("{}", USER_PANIC) });
let sibling = camber::spawn_async(async move {
child_sibling.store(true, Ordering::SeqCst);
SIBLING_VALUE
});
let panicked = runtime::block_on(join_bounded(panicker, BOUND));
let survived = runtime::block_on(join_bounded(sibling, BOUND));
(panicked, survived, CLOSURE_VALUE)
});
match outcome {
Err(error) => panic!("a user-owned panic displaced the runtime result: {error}"),
Ok((panicked, survived, value)) => {
assert!(
matches!(&panicked, Err(RuntimeError::TaskPanicked(message)) if &**message == USER_PANIC),
"the panic was not delivered on its own handle: {panicked:?}"
);
assert!(
matches!(survived, Ok(SIBLING_VALUE)),
"the panicking child cancelled its sibling: {survived:?}"
);
assert_eq!(value, CLOSURE_VALUE);
}
}
assert!(
sibling_ran.load(Ordering::SeqCst),
"the sibling never ran alongside the panicking child"
);
}
#[test]
fn internal_panic_displaces_runtime_result_without_inspecting_closure_value() {
let sibling_ran = Arc::new(AtomicBool::new(false));
let child_sibling = Arc::clone(&sibling_ran);
let outcome = scope_runtime(SHORT_DRAIN).run(move || {
let sibling = camber::spawn_async(async move {
child_sibling.store(true, Ordering::SeqCst);
});
panic_from_a_schedule_child();
runtime::block_on(join_bounded(sibling, BOUND)).unwrap();
CLOSURE_VALUE
});
assert!(
matches!(&outcome, Err(RuntimeError::TaskPanicked(message)) if &**message == INTERNAL_PANIC),
"the internal panic did not displace the closure's value: {outcome:?}"
);
assert!(
sibling_ran.load(Ordering::SeqCst),
"the internal panic cancelled a sibling"
);
}
#[test]
fn internal_panic_outranks_drain_timeout_when_both_occur() {
let (controller, builder) = probed_runtime(SHORT_DRAIN);
let outcome = builder.run(|| {
camber::spawn_async(async { std::future::pending::<()>().await });
let before = controller.scope_registry_len().unwrap();
panic_from_a_schedule_child();
assert!(
wait_registry_at_most(&controller, before, BOUND),
"the panicking schedule child never left the root scope, so the \
drain timeout would have been read against an unrecorded panic"
);
});
assert!(
matches!(&outcome, Err(RuntimeError::TaskPanicked(message)) if &**message == INTERNAL_PANIC),
"the drain timeout outranked the internal panic: {outcome:?}"
);
}