use crate::common::{
ArmedWatch, BOUND, RecordingResource, SHORT_DRAIN, WedgedHandle, assert_forced_abort,
block_on_detached, ignore_hook, join_bounded, observe_armed_sequence, observe_armed_window,
registry_len,
};
use crate::scope_builders::{probed_runtime, scope_runtime};
use camber::RuntimeError;
use camber::runtime_test_support::{RuntimeCheckpoint, RuntimeController, wait_scope_closing};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
const SLACK: Duration = Duration::from_millis(1500);
const BLOCKING_PARK: Duration = Duration::from_secs(4);
const AWAITING_ONE: RuntimeCheckpoint = RuntimeCheckpoint::ScopeWaitObserved(1);
const DRAINED: RuntimeCheckpoint = RuntimeCheckpoint::ScopeWaitObserved(0);
fn read_scope(controller: &RuntimeController) -> (usize, usize) {
(
controller.scope_joined_count().unwrap_or(usize::MAX),
registry_len(controller),
)
}
fn observe_drain_end<F, T>(
shutdown_timeout: Duration,
body: F,
) -> (Result<T, RuntimeError>, Option<(usize, usize)>)
where
F: FnOnce() -> T,
{
observe_armed_window(
|builder| builder.shutdown_timeout(shutdown_timeout),
DRAINED,
|_| body(),
read_scope,
)
}
#[test]
fn cooperative_child_is_awaited_within_bounded_drain() {
const VALUE: u32 = 7;
let completed = Arc::new(AtomicBool::new(false));
let (release_tx, release_rx) = tokio::sync::oneshot::channel::<()>();
let child_completed = Arc::clone(&completed);
let observer_completed = Arc::clone(&completed);
let (value, (awaited, released, completed_at_zero)) = observe_armed_sequence(
|builder| builder.shutdown_timeout(BOUND),
|gate| {
camber::spawn_async(async move {
wait_scope_closing().await;
child_completed.store(release_rx.await.is_ok(), Ordering::SeqCst);
});
gate.arm(AWAITING_ONE);
VALUE
},
|watch| observe_cooperative_drain(watch, release_tx, observer_completed),
);
assert!(
awaited,
"the drain never paused while awaiting its cooperative child"
);
assert!(released, "the cooperative child's release was never sent");
assert_eq!(
value.expect("the cooperative drain failed the runtime"),
VALUE,
"the drain displaced the closure's value"
);
assert!(
completed.load(Ordering::SeqCst),
"the cooperative child never completed"
);
assert_eq!(
completed_at_zero,
Some(true),
"the drain reached zero before the child it was awaiting completed"
);
}
fn observe_cooperative_drain(
watch: &ArmedWatch<'_>,
release: tokio::sync::oneshot::Sender<()>,
completed: Arc<AtomicBool>,
) -> (bool, bool, Option<bool>) {
watch.wait_armed();
let awaited = watch.probe(AWAITING_ONE, |_| ()).is_some();
watch.controller().pause_once(DRAINED).unwrap();
let released = release.send(()).is_ok();
let completed_at_zero = watch.probe(DRAINED, |_| completed.load(Ordering::SeqCst));
(awaited, released, completed_at_zero)
}
#[test]
fn wedged_async_child_is_aborted_joined_and_reported() {
let wedged = WedgedHandle::new();
let closure_wedged = wedged.clone();
let (result, probe) = observe_drain_end(SHORT_DRAIN, move || {
let handle = camber::spawn_async(async { std::future::pending::<()>().await });
closure_wedged.record(handle);
});
assert!(
matches!(result, Err(RuntimeError::ScopeDrainTimeout(1))),
"the drain did not report one outstanding child: {result:?}"
);
let (joined, entries) = probe.expect("the drain never paused at its terminal observation");
assert_eq!(
joined, 1,
"the owner never awaited the aborted child's Tokio handle"
);
assert_eq!(entries, 0, "the aborted child left a handle behind");
let outcome = block_on_detached(join_bounded(wedged.take(), BOUND));
assert_forced_abort(&outcome);
}
#[test]
fn drain_timeout_counts_every_outstanding_child() {
let (park_tx, park_rx) = std::sync::mpsc::channel::<()>();
let result = scope_runtime(SHORT_DRAIN).run(move || {
camber::spawn_async(async { std::future::pending::<()>().await });
camber::spawn(move || {
let _ = park_rx.recv_timeout(BLOCKING_PARK);
});
});
assert!(
matches!(result, Err(RuntimeError::ScopeDrainTimeout(2))),
"the drain did not report both outstanding children: {result:?}"
);
drop(park_tx);
}
#[test]
fn nonpreemptible_blocking_child_still_yields_bounded_return() {
let observed_at_shutdown = WedgedHandle::new();
let teardown_at = WedgedHandle::new();
let closure_teardown = teardown_at.clone();
let resource_observation = observed_at_shutdown.clone();
let park_ended = Arc::new(AtomicBool::new(false));
let child_park_ended = Arc::clone(&park_ended);
let resource_park_ended = Arc::clone(&park_ended);
let (park_tx, park_rx) = std::sync::mpsc::channel::<()>();
let result = scope_runtime(SHORT_DRAIN)
.resource(RecordingResource::new(
"drain-marker",
ignore_hook,
move || {
resource_observation
.record((Instant::now(), resource_park_ended.load(Ordering::SeqCst)));
Ok(())
},
))
.run(move || {
camber::spawn(move || {
let _ = park_rx.recv_timeout(BLOCKING_PARK);
child_park_ended.store(true, Ordering::SeqCst);
});
closure_teardown.record(Instant::now());
});
assert!(
matches!(result, Err(RuntimeError::ScopeDrainTimeout(1))),
"the drain did not report the non-preemptible child outstanding: {result:?}"
);
let (shutdown, park_ended_at_shutdown) = observed_at_shutdown
.take_expecting("the resource shutdown hook never recorded the parked child");
assert!(
!park_ended_at_shutdown,
"resource shutdown ran only after the blocking child left its park"
);
let waited = shutdown.saturating_duration_since(
teardown_at.take_expecting("the closure never recorded its teardown start"),
);
assert!(
waited <= SHORT_DRAIN + SLACK,
"resource shutdown waited {waited:?} on a child the drain cannot stop"
);
drop(park_tx);
}
#[test]
fn resource_shutdown_runs_only_after_stoppable_children_are_drained_or_aborted() {
let (controller, builder) = probed_runtime(SHORT_DRAIN);
let joined_at_shutdown = WedgedHandle::new();
let resource_controller = Arc::clone(&controller);
let resource_joined = joined_at_shutdown.clone();
let result = builder
.resource(RecordingResource::new(
"join-order",
ignore_hook,
move || {
resource_joined.record(resource_controller.scope_joined_count()?);
Ok(())
},
))
.run(|| {
camber::spawn_async(async { std::future::pending::<()>().await });
});
assert!(
matches!(result, Err(RuntimeError::ScopeDrainTimeout(1))),
"the wedged child was not reported outstanding: {result:?}"
);
assert_eq!(
joined_at_shutdown
.take_expecting("the resource shutdown hook never recorded the join count"),
1,
"resource shutdown began before the aborted child's handle was joined"
);
}
#[test]
fn deadline_drains_registered_async_child_to_zero() {
let (result, (awaiting, drained)) = observe_armed_sequence(
|builder| builder.shutdown_timeout(SHORT_DRAIN),
|gate| {
camber::spawn_async(async { std::future::pending::<()>().await });
gate.arm(AWAITING_ONE);
},
observe_escalation_boundary,
);
assert!(
matches!(result, Err(RuntimeError::ScopeDrainTimeout(1))),
"the deadline did not report the registered child: {result:?}"
);
assert_eq!(
awaiting,
Some((0, 1)),
"before escalation the child was not registered and unjoined"
);
assert_eq!(
drained,
Some((1, 0)),
"after escalation the child was not joined with its handle removed"
);
}
fn observe_escalation_boundary(
watch: &ArmedWatch<'_>,
) -> (Option<(usize, usize)>, Option<(usize, usize)>) {
watch.wait_armed();
let awaiting = watch.probe(AWAITING_ONE, read_scope);
watch.controller().pause_once(DRAINED).unwrap();
let drained = watch.probe(DRAINED, read_scope);
(awaiting, drained)
}