use std::sync::Arc;
use std::time::Duration;
use cordis_core::{Context, Plugin, PreparedPlugin};
use cordis_timer::{TimerCancelled, TimerExt};
use futures::StreamExt;
use parking_lot::Mutex;
use std::convert::Infallible;
use std::panic::AssertUnwindSafe;
use std::task::{Context as TaskContext, Waker};
struct ContextGrabber {
captured: Arc<Mutex<Option<Context>>>,
}
impl Plugin for ContextGrabber {
type Config = ();
type Input = ();
type PrepareError = Infallible;
type ApplyError = Infallible;
fn prepare(&self, (): ()) -> Result<(), Infallible> {
Ok(())
}
async fn apply(&self, ctx: Context, _: &()) -> Result<(), Infallible> {
*self.captured.lock() = Some(ctx);
Ok(())
}
}
async fn scoped_ctx(root: &Context) -> (cordis_core::FiberHandle, Context) {
let captured: Arc<Mutex<Option<Context>>> = Default::default();
let fiber_handle = root
.spawn(PreparedPlugin::from_input(
ContextGrabber {
captured: captured.clone(),
},
(),
))
.await
.unwrap();
let ctx = captured.lock().clone().unwrap();
(fiber_handle, ctx)
}
async fn settle() {
for _ in 0..3 {
tokio::task::yield_now().await;
}
}
#[tokio::test(start_paused = true)]
async fn sleep_resolves() {
let root = Context::new();
let (_fiber_handle, ctx) = scoped_ctx(&root).await;
let pending = ctx.sleep(Duration::from_millis(10)).unwrap();
let result = pending.await;
assert!(matches!(result, Ok(())), "the delay must resolve Ok");
}
#[tokio::test(start_paused = true)]
async fn sleep_cancelled_by_fiber_dispose() {
let root = Context::new();
let (fiber_handle, ctx) = scoped_ctx(&root).await;
let pending = ctx.sleep(Duration::from_millis(10_000)).unwrap();
let task = tokio::spawn(pending);
settle().await;
fiber_handle.dispose().await.unwrap();
let result = task.await.unwrap();
assert!(
matches!(result, Err(TimerCancelled)),
"fiber disposal must cancel outstanding sleeps"
);
}
#[tokio::test(start_paused = true)]
async fn sleep_on_dead_fiber_context_refuses_synchronously() {
let root = Context::new();
let (fiber_handle, ctx) = scoped_ctx(&root).await;
fiber_handle.dispose().await.unwrap();
let result = ctx.sleep(Duration::from_secs(10_000));
assert!(
matches!(
result,
Err(cordis_timer::TimerRegistrationError::InactiveContext)
),
"a dead fiber refuses Sleep construction synchronously"
);
}
#[tokio::test(start_paused = true)]
async fn sleep_cancellation_wins_ready_but_uncommitted_expiry() {
let root = Context::new();
let (fiber_handle, ctx) = scoped_ctx(&root).await;
let sleep = ctx.sleep(Duration::from_millis(10)).unwrap();
tokio::time::advance(Duration::from_millis(10)).await;
fiber_handle.dispose().await.unwrap();
assert!(matches!(sleep.await, Err(TimerCancelled)));
}
#[tokio::test(start_paused = true)]
async fn completed_sleep_repoll_panics() {
let root = Context::new();
let (_fiber_handle, ctx) = scoped_ctx(&root).await;
let mut sleep = Box::pin(ctx.sleep(Duration::ZERO).unwrap());
assert!(sleep.as_mut().await.is_ok());
let waker = Waker::noop();
let mut task_cx = TaskContext::from_waker(waker);
let repoll = std::panic::catch_unwind(AssertUnwindSafe(|| {
std::future::Future::poll(sleep.as_mut(), &mut task_cx)
}));
assert!(repoll.is_err(), "completed Sleep must reject a second poll");
}
#[tokio::test(start_paused = true)]
async fn completed_sleep_is_not_reacted_to_by_later_generation_disposal() {
let root = Context::new();
let (fiber_handle, ctx) = scoped_ctx(&root).await;
let sleep = ctx.sleep(Duration::from_millis(10)).unwrap();
tokio::time::advance(Duration::from_millis(10)).await;
assert!(matches!(sleep.await, Ok(())));
fiber_handle.dispose().await.unwrap();
}
fn assert_interval_type(_: cordis_timer::Interval) {}
#[tokio::test(start_paused = true)]
async fn interval_first_tick_is_anchored_at_construction() {
let root = Context::new();
let (_fiber_handle, ctx) = scoped_ctx(&root).await;
let anchor = tokio::time::Instant::now();
let interval = ctx.interval(Duration::from_millis(10)).unwrap();
assert_interval_type(interval);
let mut interval = Box::pin(ctx.interval(Duration::from_millis(10)).unwrap());
tokio::time::advance(Duration::from_millis(9)).await;
assert!(futures::poll!(interval.as_mut().next()).is_pending());
tokio::time::advance(Duration::from_millis(1)).await;
assert!(matches!(interval.next().await, Some(Ok(()))));
assert_eq!(
tokio::time::Instant::now(),
anchor + Duration::from_millis(10)
);
}
#[tokio::test(start_paused = true)]
async fn interval_on_time_ticks_are_ok() {
let root = Context::new();
let (_fiber_handle, ctx) = scoped_ctx(&root).await;
let mut interval = Box::pin(ctx.interval(Duration::from_millis(5)).unwrap());
for _ in 0..3 {
tokio::time::advance(Duration::from_millis(5)).await;
assert!(matches!(interval.next().await, Some(Ok(()))));
}
}
#[tokio::test(start_paused = true)]
async fn interval_late_poll_coalesces_without_burst_or_phase_shift() {
let root = Context::new();
let (_fiber_handle, ctx) = scoped_ctx(&root).await;
let anchor = tokio::time::Instant::now();
let mut interval = Box::pin(ctx.interval(Duration::from_millis(10)).unwrap());
tokio::time::advance(Duration::from_millis(35)).await;
assert!(matches!(interval.next().await, Some(Ok(()))));
assert_eq!(
tokio::time::Instant::now(),
anchor + Duration::from_millis(35)
);
assert!(
futures::poll!(interval.as_mut().next()).is_pending(),
"missed ticks must not burst after the one coalesced overdue tick"
);
tokio::time::advance(Duration::from_millis(4)).await;
assert!(futures::poll!(interval.as_mut().next()).is_pending());
tokio::time::advance(Duration::from_millis(1)).await;
assert!(matches!(interval.next().await, Some(Ok(()))));
assert_eq!(
tokio::time::Instant::now(),
anchor + Duration::from_millis(40),
"cadence must remain on the construction-time phase"
);
}
#[tokio::test(start_paused = true)]
async fn interval_cancellation_yields_one_error_then_ends() {
let root = Context::new();
let (fiber_handle, ctx) = scoped_ctx(&root).await;
let mut interval = Box::pin(ctx.interval(Duration::from_millis(10)).unwrap());
fiber_handle.dispose().await.unwrap();
assert!(matches!(interval.next().await, Some(Err(TimerCancelled))));
assert!(interval.next().await.is_none());
tokio::time::advance(Duration::from_millis(100)).await;
assert!(interval.next().await.is_none());
}
#[tokio::test(start_paused = true)]
async fn interval_cancellation_wins_uncommitted_boundary_tick() {
let root = Context::new();
let (fiber_handle, ctx) = scoped_ctx(&root).await;
let mut interval = Box::pin(ctx.interval(Duration::from_millis(10)).unwrap());
tokio::time::advance(Duration::from_millis(10)).await;
fiber_handle.dispose().await.unwrap();
assert!(matches!(interval.next().await, Some(Err(TimerCancelled))));
assert!(interval.next().await.is_none());
}
#[tokio::test(start_paused = true)]
async fn dropping_interval_emits_nothing() {
let root = Context::new();
let (fiber_handle, ctx) = scoped_ctx(&root).await;
let interval = ctx.interval(Duration::from_millis(10)).unwrap();
drop(interval);
fiber_handle.dispose().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn interval_on_dead_fiber_context_refuses_synchronously() {
let root = Context::new();
let (fiber_handle, ctx) = scoped_ctx(&root).await;
fiber_handle.dispose().await.unwrap();
let result = ctx.interval(Duration::from_secs(10_000));
assert!(matches!(
result,
Err(cordis_timer::TimerRegistrationError::InactiveContext)
));
}