#![cfg_attr(coverage_nightly, coverage(off))]
use crate::lifecycle::{Lifecycle, Status, StopReason};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::time::Duration;
#[tokio::test]
async fn test_ready_signal_success() {
let lc = Lifecycle::new();
let ready_signaled = Arc::new(AtomicBool::new(false));
let ready_signaled_inner = ready_signaled.clone();
lc.start_with_ready(move |cancel, ready| async move {
tokio::time::sleep(Duration::from_millis(10)).await;
ready.notify();
ready_signaled_inner.store(true, Ordering::SeqCst);
cancel.cancelled().await;
Ok(())
})
.unwrap();
tokio::time::timeout(Duration::from_millis(100), async {
while lc.status() != Status::Running {
tokio::task::yield_now().await;
}
})
.await
.expect("Should become ready");
assert!(ready_signaled.load(Ordering::SeqCst));
assert_eq!(lc.status(), Status::Running);
let reason = lc.stop(Duration::from_secs(1)).await.unwrap();
assert!(matches!(
reason,
StopReason::Cancelled | StopReason::Finished
));
}
#[tokio::test]
async fn test_ready_timeout() {
let lc = Lifecycle::new();
lc.start_with_ready(|cancel, _ready| async move {
cancel.cancelled().await;
Ok(())
})
.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(lc.status(), Status::Starting);
let reason = lc.stop(Duration::from_millis(100)).await.unwrap();
assert!(matches!(
reason,
StopReason::Cancelled | StopReason::Finished
));
}
#[tokio::test]
async fn test_cancel_before_ready() {
let lc = Lifecycle::new();
let cancel_received = Arc::new(AtomicBool::new(false));
let cancel_received_inner = cancel_received.clone();
lc.start_with_ready(move |cancel, ready| async move {
tokio::select! {
() = tokio::time::sleep(Duration::from_millis(100)) => {
ready.notify();
}
() = cancel.cancelled() => {
cancel_received_inner.store(true, Ordering::SeqCst);
}
}
Ok(())
})
.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let reason = lc.stop(Duration::from_millis(100)).await.unwrap();
assert!(cancel_received.load(Ordering::SeqCst));
assert!(matches!(
reason,
StopReason::Cancelled | StopReason::Finished
));
assert_eq!(lc.status(), Status::Stopped);
}
#[tokio::test]
async fn test_timeout_during_stop() {
let lc = Lifecycle::new();
lc.start(|_cancel| async move {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok(())
})
.unwrap();
let reason = lc.stop(Duration::from_millis(50)).await.unwrap();
assert_eq!(reason, StopReason::Timeout);
assert_eq!(lc.status(), Status::Stopped);
}
#[tokio::test]
async fn test_graceful_cancel_with_cleanup() {
let lc = Lifecycle::new();
let cleanup_done = Arc::new(AtomicBool::new(false));
let cleanup_done_inner = cleanup_done.clone();
lc.start_with_ready(move |cancel, ready| {
async move {
ready.notify();
cancel.cancelled().await;
tokio::time::sleep(Duration::from_millis(10)).await;
cleanup_done_inner.store(true, Ordering::SeqCst);
Ok(())
}
})
.unwrap();
tokio::time::timeout(Duration::from_millis(100), async {
while lc.status() != Status::Running {
tokio::task::yield_now().await;
}
})
.await
.expect("Should become ready");
let reason = lc.stop(Duration::from_secs(1)).await.unwrap();
assert!(cleanup_done.load(Ordering::SeqCst));
assert!(matches!(
reason,
StopReason::Finished | StopReason::Cancelled
));
assert_eq!(lc.status(), Status::Stopped);
}
#[tokio::test]
async fn test_ready_signal_single_use() {
let lc = Lifecycle::new();
let ready_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let ready_count_inner = ready_count.clone();
lc.start_with_ready(move |cancel, ready| {
async move {
ready.notify();
ready_count_inner.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(10)).await;
ready_count_inner.fetch_add(1, Ordering::SeqCst);
cancel.cancelled().await;
Ok(())
}
})
.unwrap();
tokio::time::timeout(Duration::from_millis(100), async {
while lc.status() != Status::Running {
tokio::task::yield_now().await;
}
})
.await
.expect("Should become ready");
assert_eq!(lc.status(), Status::Running);
let reason = lc.stop(Duration::from_secs(1)).await.unwrap();
assert!(matches!(
reason,
StopReason::Cancelled | StopReason::Finished
));
assert_eq!(ready_count.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn test_concurrent_stop_calls() {
let lc = Arc::new(Lifecycle::new());
lc.start(|cancel| async move {
cancel.cancelled().await;
Ok(())
})
.unwrap();
let lc1 = lc.clone();
let lc2 = lc.clone();
let lc3 = lc.clone();
let (r1, r2, r3) = tokio::join!(
lc1.stop(Duration::from_secs(1)),
lc2.stop(Duration::from_secs(1)),
lc3.stop(Duration::from_secs(1))
);
assert!(r1.is_ok());
assert!(r2.is_ok());
assert!(r3.is_ok());
assert_eq!(lc.status(), Status::Stopped);
}
#[tokio::test]
async fn test_task_panic_handling() {
let lc = Lifecycle::new();
lc.start_with_ready(|_cancel, ready| async move {
ready.notify();
panic!("Test panic in lifecycle task");
})
.unwrap();
tokio::time::timeout(Duration::from_millis(100), async {
while lc.status() != Status::Running {
tokio::task::yield_now().await;
}
})
.await
.expect("Should become ready despite upcoming panic");
tokio::time::sleep(Duration::from_millis(50)).await;
let reason = lc.stop(Duration::from_millis(100)).await.unwrap();
assert!(matches!(reason, StopReason::Finished | StopReason::Timeout));
assert_eq!(lc.status(), Status::Stopped);
}