use super::*;
use std::sync::Arc;
use std::time::Duration;
#[tokio::test]
async fn shutdown_signal_before_wait_returns_immediately() {
let s = Shutdown::new();
s.signal();
tokio::time::timeout(Duration::from_millis(100), s.wait())
.await
.expect("wait must return immediately when already signaled");
assert!(s.is_set());
}
#[tokio::test]
async fn shutdown_wait_then_signal_wakes_waiter() {
let s = Arc::new(Shutdown::new());
let s_clone = Arc::clone(&s);
let waiter = tokio::spawn(async move { s_clone.wait().await });
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(!s.is_set());
s.signal();
tokio::time::timeout(Duration::from_millis(200), waiter)
.await
.expect("waiter must wake within timeout")
.expect("waiter task should not panic");
assert!(s.is_set());
}
#[tokio::test]
async fn shutdown_multiple_concurrent_waiters_all_wake() {
let s = Arc::new(Shutdown::new());
let mut handles = Vec::new();
for _ in 0..16 {
let s = Arc::clone(&s);
handles.push(tokio::spawn(async move { s.wait().await }));
}
tokio::time::sleep(Duration::from_millis(20)).await;
s.signal();
for h in handles {
tokio::time::timeout(Duration::from_millis(200), h)
.await
.expect("each waiter must wake within timeout")
.expect("waiter task should not panic");
}
}
#[tokio::test]
async fn shutdown_signal_is_idempotent() {
let s = Shutdown::new();
s.signal();
s.signal();
s.signal();
tokio::time::timeout(Duration::from_millis(100), s.wait())
.await
.expect("wait must still return on idempotent re-signal");
}
#[tokio::test]
async fn joinset_abort_all_makes_drain_finite() {
let mut set: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
set.spawn(async {
tokio::time::sleep(Duration::from_secs(60)).await;
});
let primary = tokio::time::timeout(Duration::from_millis(100), async {
while set.join_next().await.is_some() {}
})
.await;
assert!(
primary.is_err(),
"primary drain should time out while task is still sleeping"
);
set.abort_all();
let secondary = tokio::time::timeout(Duration::from_millis(500), async {
while set.join_next().await.is_some() {}
})
.await;
assert!(
secondary.is_ok(),
"drain after abort_all must complete quickly"
);
assert!(set.is_empty(), "JoinSet should be empty after drain");
}
#[tokio::test]
async fn joinset_panics_are_observable_via_try_join_next() {
let mut set: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
set.spawn(async {
panic!("simulated handler panic");
});
let deadline = std::time::Instant::now() + Duration::from_millis(500);
loop {
if let Some(res) = set.try_join_next() {
let err = res.expect_err("panicked task should yield Err");
assert!(
err.is_panic(),
"JoinError must report is_panic for panicking task; got: {err:?}"
);
return;
}
if std::time::Instant::now() >= deadline {
panic!("try_join_next never reported the panic within 500ms");
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
#[tokio::test]
async fn shutdown_no_lost_signal_under_race() {
for trial in 0..50 {
let s = Arc::new(Shutdown::new());
let s_waiter = Arc::clone(&s);
let s_signaler = Arc::clone(&s);
let waiter = tokio::spawn(async move { s_waiter.wait().await });
tokio::task::yield_now().await;
s_signaler.signal();
tokio::time::timeout(Duration::from_millis(500), waiter)
.await
.unwrap_or_else(|_| panic!("trial {trial}: waiter stranded by lost signal"))
.expect("waiter task should not panic");
}
}