use crate::types::{CancelReason, Outcome, Time};
use core::fmt;
use std::marker::PhantomData;
use std::time::Duration;
#[inline]
fn duration_to_nanos(duration: Duration) -> u64 {
duration.as_nanos().min(u128::from(u64::MAX)) as u64
}
#[derive(Debug)]
pub struct Timeout<T> {
pub deadline: Time,
_t: PhantomData<T>,
}
impl<T> Timeout<T> {
#[inline]
#[must_use]
pub const fn new(deadline: Time) -> Self {
Self {
deadline,
_t: PhantomData,
}
}
#[inline]
#[must_use]
pub const fn after_nanos(now: Time, nanos: u64) -> Self {
Self::new(now.saturating_add_nanos(nanos))
}
#[inline]
#[must_use]
pub const fn after_millis(now: Time, millis: u64) -> Self {
Self::after_nanos(now, millis.saturating_mul(1_000_000))
}
#[inline]
#[must_use]
pub const fn after_secs(now: Time, secs: u64) -> Self {
Self::after_nanos(now, secs.saturating_mul(1_000_000_000))
}
#[inline]
#[must_use]
pub fn after(now: Time, duration: Duration) -> Self {
Self::after_nanos(now, duration_to_nanos(duration))
}
#[inline]
#[must_use]
pub fn is_expired(&self, now: Time) -> bool {
now >= self.deadline
}
#[inline]
#[must_use]
pub fn remaining(&self, now: Time) -> Duration {
if now >= self.deadline {
Duration::ZERO
} else {
let nanos = self.deadline.as_nanos().saturating_sub(now.as_nanos());
Duration::from_nanos(nanos)
}
}
}
impl<T> Clone for Timeout<T> {
#[inline]
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Timeout<T> {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimeoutError {
pub deadline: Time,
pub message: Option<&'static str>,
}
impl TimeoutError {
#[inline]
#[must_use]
pub const fn new(deadline: Time) -> Self {
Self {
deadline,
message: None,
}
}
#[inline]
#[must_use]
pub const fn with_message(deadline: Time, message: &'static str) -> Self {
Self {
deadline,
message: Some(message),
}
}
#[inline]
#[must_use]
pub const fn into_cancel_reason(self) -> CancelReason {
CancelReason::timeout()
}
}
impl fmt::Display for TimeoutError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.message {
Some(msg) => write!(f, "timeout: {} (deadline: {:?})", msg, self.deadline),
None => write!(f, "operation timed out at {:?}", self.deadline),
}
}
}
impl std::error::Error for TimeoutError {}
#[derive(Debug, Clone)]
pub enum TimedResult<T, E> {
Completed(Outcome<T, E>),
TimedOut(TimeoutError),
}
impl<T, E> TimedResult<T, E> {
#[inline]
#[must_use]
pub const fn is_completed(&self) -> bool {
matches!(self, Self::Completed(_))
}
#[inline]
#[must_use]
pub const fn is_timed_out(&self) -> bool {
matches!(self, Self::TimedOut(_))
}
#[inline]
pub fn into_outcome(self) -> Outcome<T, E> {
match self {
Self::Completed(outcome) => outcome,
Self::TimedOut(err) => Outcome::Cancelled(err.into_cancel_reason()),
}
}
#[inline]
pub fn into_result(self) -> Result<T, TimedError<E>> {
match self {
Self::Completed(outcome) => match outcome {
Outcome::Ok(v) => Ok(v),
Outcome::Err(e) => Err(TimedError::Error(e)),
Outcome::Cancelled(r) => Err(TimedError::Cancelled(r)),
Outcome::Panicked(p) => Err(TimedError::Panicked(p)),
},
Self::TimedOut(err) => Err(TimedError::TimedOut(err)),
}
}
}
#[derive(Debug, Clone)]
pub enum TimedError<E> {
Error(E),
Cancelled(CancelReason),
Panicked(crate::types::outcome::PanicPayload),
TimedOut(TimeoutError),
}
impl<E: fmt::Display> fmt::Display for TimedError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Error(e) => write!(f, "{e}"),
Self::Cancelled(r) => write!(f, "cancelled: {r}"),
Self::Panicked(p) => write!(f, "panicked: {p}"),
Self::TimedOut(t) => write!(f, "{t}"),
}
}
}
impl<E: fmt::Debug + fmt::Display> std::error::Error for TimedError<E> {}
#[inline]
#[must_use]
pub fn make_timed_result<T, E>(
outcome: Outcome<T, E>,
deadline: Time,
completed_in_time: bool,
) -> TimedResult<T, E> {
if completed_in_time {
return TimedResult::Completed(outcome);
}
match outcome {
Outcome::Ok(_) | Outcome::Err(_) | Outcome::Panicked(_) => {
TimedResult::Completed(outcome)
}
Outcome::Cancelled(_) => {
TimedResult::TimedOut(TimeoutError::new(deadline))
}
}
}
#[inline]
#[must_use]
pub const fn effective_deadline(requested: Time, existing: Option<Time>) -> Time {
match existing {
Some(e) if e.as_nanos() < requested.as_nanos() => e,
_ => requested,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeoutConfig {
pub deadline: Time,
pub use_effective: bool,
}
impl TimeoutConfig {
#[inline]
#[must_use]
pub const fn new(deadline: Time) -> Self {
Self {
deadline,
use_effective: true,
}
}
#[inline]
#[must_use]
pub const fn absolute(deadline: Time) -> Self {
Self {
deadline,
use_effective: false,
}
}
#[inline]
#[must_use]
pub const fn resolve(&self, existing: Option<Time>) -> Time {
if self.use_effective {
effective_deadline(self.deadline, existing)
} else {
self.deadline
}
}
}
#[macro_export]
macro_rules! timeout {
($duration:expr, $future:expr) => {{ compile_error!("timeout! requires a Cx context: timeout!(cx, duration, future)") }};
($cx:expr, $duration:expr, $future:expr) => {
$crate::time::TimeoutFuture::after($cx.now(), $duration, $future)
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timeout_creation() {
let now = Time::ZERO;
let timeout = Timeout::<()>::after_secs(now, 5);
assert_eq!(timeout.deadline.as_nanos(), 5_000_000_000);
}
#[test]
fn timeout_after_millis() {
let now = Time::ZERO;
let timeout = Timeout::<()>::after_millis(now, 100);
assert_eq!(timeout.deadline.as_nanos(), 100_000_000);
}
#[test]
fn timeout_after_duration() {
let now = Time::ZERO;
let timeout = Timeout::<()>::after(now, Duration::from_millis(250));
assert_eq!(timeout.deadline.as_nanos(), 250_000_000);
}
#[test]
fn timeout_after_duration_saturates_large_duration() {
let now = Time::from_nanos(1);
let timeout = Timeout::<()>::after(now, Duration::MAX);
assert_eq!(timeout.deadline, Time::MAX);
}
#[test]
fn timeout_is_expired() {
let now = Time::from_nanos(1000);
let past = Time::from_nanos(500);
let future = Time::from_nanos(2000);
let timeout_past = Timeout::<()>::new(past);
let timeout_future = Timeout::<()>::new(future);
assert!(timeout_past.is_expired(now));
assert!(!timeout_future.is_expired(now));
}
#[test]
fn timeout_remaining() {
let now = Time::from_nanos(1000);
let deadline = Time::from_nanos(1500);
let timeout = Timeout::<()>::new(deadline);
assert_eq!(timeout.remaining(now), Duration::from_nanos(500));
let later = Time::from_nanos(2000);
assert_eq!(timeout.remaining(later), Duration::ZERO);
}
#[test]
fn timeout_error_display() {
let err = TimeoutError::new(Time::from_nanos(1000));
assert!(err.to_string().contains("timed out"));
let err_with_msg = TimeoutError::with_message(Time::from_nanos(1000), "fetch failed");
assert!(err_with_msg.to_string().contains("fetch failed"));
}
#[test]
fn timed_result_completed() {
let result: TimedResult<i32, &str> = TimedResult::Completed(Outcome::Ok(42));
assert!(result.is_completed());
assert!(!result.is_timed_out());
let outcome = result.into_outcome();
assert!(outcome.is_ok());
}
#[test]
fn timed_result_timed_out() {
let result: TimedResult<i32, &str> =
TimedResult::TimedOut(TimeoutError::new(Time::from_nanos(1000)));
assert!(!result.is_completed());
assert!(result.is_timed_out());
let outcome = result.into_outcome();
assert!(outcome.is_cancelled());
}
#[test]
fn timed_result_into_result_ok() {
let result: TimedResult<i32, &str> = TimedResult::Completed(Outcome::Ok(42));
let res = result.into_result();
assert!(res.is_ok());
assert_eq!(res.unwrap(), 42);
}
#[test]
fn timed_result_into_result_timeout() {
let result: TimedResult<i32, &str> =
TimedResult::TimedOut(TimeoutError::new(Time::from_nanos(1000)));
let res = result.into_result();
assert!(matches!(res, Err(TimedError::TimedOut(_))));
}
#[test]
fn timed_result_into_result_error() {
let result: TimedResult<i32, &str> = TimedResult::Completed(Outcome::Err("failed"));
let res = result.into_result();
assert!(matches!(res, Err(TimedError::Error("failed"))));
}
#[test]
fn timed_result_into_result_cancelled() {
let result: TimedResult<i32, &str> =
TimedResult::Completed(Outcome::Cancelled(CancelReason::shutdown()));
let res = result.into_result();
assert!(matches!(res, Err(TimedError::Cancelled(_))));
}
#[test]
fn effective_deadline_uses_tighter() {
let requested = Time::from_nanos(1000);
let existing = Some(Time::from_nanos(500));
assert_eq!(effective_deadline(requested, existing).as_nanos(), 500);
let existing2 = Some(Time::from_nanos(2000));
assert_eq!(effective_deadline(requested, existing2).as_nanos(), 1000);
assert_eq!(effective_deadline(requested, None).as_nanos(), 1000);
}
#[test]
fn timeout_config_resolve() {
let config = TimeoutConfig::new(Time::from_nanos(1000));
let existing = Some(Time::from_nanos(500));
assert_eq!(config.resolve(existing).as_nanos(), 500);
let abs_config = TimeoutConfig::absolute(Time::from_nanos(1000));
assert_eq!(abs_config.resolve(existing).as_nanos(), 1000);
}
#[test]
fn make_timed_result_completed() {
let outcome: Outcome<i32, &str> = Outcome::Ok(42);
let deadline = Time::from_nanos(1000);
let result = make_timed_result(outcome, deadline, true);
assert!(result.is_completed());
}
#[test]
fn make_timed_result_timed_out() {
let outcome: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::timeout());
let deadline = Time::from_nanos(1000);
let result = make_timed_result(outcome, deadline, false);
assert!(result.is_timed_out());
}
#[test]
fn timed_error_display() {
let err: TimedError<&str> = TimedError::Error("test");
assert_eq!(err.to_string(), "test");
let err: TimedError<&str> = TimedError::Cancelled(CancelReason::shutdown());
assert!(err.to_string().contains("cancelled"));
let err: TimedError<&str> = TimedError::TimedOut(TimeoutError::new(Time::from_nanos(1000)));
assert!(err.to_string().contains("timed out"));
}
#[test]
fn timeout_clone_and_copy() {
let t1 = Timeout::<()>::new(Time::from_nanos(1000));
let t2 = t1; let t3 = t1;
assert_eq!(t1.deadline, t2.deadline);
assert_eq!(t1.deadline, t3.deadline);
}
#[test]
fn test_timeout_race_complete_before_deadline() {
let outcome: Outcome<i32, &str> = Outcome::Ok(42);
let deadline = Time::from_nanos(5000);
let result = make_timed_result(outcome, deadline, true);
assert!(result.is_completed());
assert!(!result.is_timed_out());
assert_eq!(result.into_result().unwrap(), 42);
}
#[test]
fn test_timeout_race_deadline_fires_first() {
let outcome: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::timeout());
let deadline = Time::from_nanos(1000);
let result = make_timed_result(outcome, deadline, false);
assert!(result.is_timed_out());
assert!(!result.is_completed());
let err = result.into_result().unwrap_err();
assert!(matches!(err, TimedError::TimedOut(_)));
}
#[test]
fn test_timeout_race_deadline_fires_first_preserves_panics() {
let outcome: Outcome<i32, &str> =
Outcome::Panicked(crate::types::outcome::PanicPayload::new("boom"));
let deadline = Time::from_nanos(1000);
let result = make_timed_result(outcome, deadline, false);
assert!(result.is_completed());
let err = result.into_result().unwrap_err();
assert!(matches!(err, TimedError::Panicked(_)));
}
#[test]
fn test_timeout_race_error_outcome_before_deadline() {
let outcome: Outcome<i32, &str> = Outcome::Err("db failure");
let deadline = Time::from_nanos(5000);
let result = make_timed_result(outcome, deadline, true);
assert!(result.is_completed());
let err = result.into_result().unwrap_err();
assert!(matches!(err, TimedError::Error("db failure")));
}
#[test]
fn test_timeout_race_panic_outcome_before_deadline() {
let outcome: Outcome<i32, &str> =
Outcome::Panicked(crate::types::outcome::PanicPayload::new("boom"));
let deadline = Time::from_nanos(5000);
let result = make_timed_result(outcome, deadline, true);
assert!(result.is_completed());
let err = result.into_result().unwrap_err();
assert!(matches!(err, TimedError::Panicked(_)));
}
#[test]
fn test_timeout_race_cancelled_outcome_before_deadline() {
let outcome: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::shutdown());
let deadline = Time::from_nanos(5000);
let result = make_timed_result(outcome, deadline, true);
assert!(result.is_completed());
let err = result.into_result().unwrap_err();
assert!(matches!(err, TimedError::Cancelled(_)));
}
#[test]
fn test_timeout_into_outcome_timeout_becomes_cancelled() {
let result: TimedResult<i32, &str> =
TimedResult::TimedOut(TimeoutError::new(Time::from_nanos(1000)));
let outcome = result.into_outcome();
assert!(outcome.is_cancelled());
}
#[test]
fn test_zero_duration_timeout() {
let now = Time::ZERO;
let timeout = Timeout::<()>::after_nanos(now, 0);
assert_eq!(timeout.deadline, Time::ZERO);
assert!(timeout.is_expired(now));
assert_eq!(timeout.remaining(now), Duration::ZERO);
}
#[test]
fn test_zero_duration_timeout_from_millis() {
let now = Time::from_nanos(5000);
let timeout = Timeout::<()>::after_millis(now, 0);
assert_eq!(timeout.deadline.as_nanos(), 5000);
assert!(timeout.is_expired(now));
}
#[test]
fn test_timeout_boundary_exact_deadline() {
let t = Time::from_nanos(1000);
let timeout = Timeout::<()>::new(t);
assert!(timeout.is_expired(t));
assert_eq!(timeout.remaining(t), Duration::ZERO);
}
#[test]
fn test_timeout_boundary_one_nano_before() {
let deadline = Time::from_nanos(1000);
let now = Time::from_nanos(999);
let timeout = Timeout::<()>::new(deadline);
assert!(!timeout.is_expired(now));
assert_eq!(timeout.remaining(now), Duration::from_nanos(1));
}
#[test]
fn test_timeout_boundary_one_nano_after() {
let deadline = Time::from_nanos(1000);
let now = Time::from_nanos(1001);
let timeout = Timeout::<()>::new(deadline);
assert!(timeout.is_expired(now));
assert_eq!(timeout.remaining(now), Duration::ZERO);
}
#[test]
fn test_nested_timeout_inner_tighter() {
let outer = Time::from_nanos(5000);
let inner = Time::from_nanos(2000);
assert_eq!(effective_deadline(outer, Some(inner)).as_nanos(), 2000);
}
#[test]
fn test_nested_timeout_outer_tighter() {
let outer = Time::from_nanos(2000);
let inner = Time::from_nanos(5000);
assert_eq!(effective_deadline(outer, Some(inner)).as_nanos(), 2000);
}
#[test]
fn test_nested_timeout_equal_deadlines() {
let d = Time::from_nanos(3000);
assert_eq!(effective_deadline(d, Some(d)).as_nanos(), 3000);
}
#[test]
fn test_nested_timeout_none_existing() {
let requested = Time::from_nanos(4000);
assert_eq!(effective_deadline(requested, None).as_nanos(), 4000);
}
#[test]
fn test_triple_nested_timeout_min_wins() {
let d1 = Time::from_nanos(5000);
let d2 = Time::from_nanos(3000);
let d3 = Time::from_nanos(7000);
let eff1 = effective_deadline(d3, None);
let eff2 = effective_deadline(d2, Some(eff1));
let eff3 = effective_deadline(d1, Some(eff2));
assert_eq!(eff3.as_nanos(), 3000); }
#[test]
fn test_timeout_config_effective_respects_tighter() {
let config = TimeoutConfig::new(Time::from_nanos(5000));
assert_eq!(
config.resolve(Some(Time::from_nanos(2000))).as_nanos(),
2000
);
assert_eq!(
config.resolve(Some(Time::from_nanos(8000))).as_nanos(),
5000
);
}
#[test]
fn test_timeout_config_absolute_ignores_existing() {
let config = TimeoutConfig::absolute(Time::from_nanos(5000));
assert_eq!(
config.resolve(Some(Time::from_nanos(2000))).as_nanos(),
5000
);
}
#[test]
fn test_timeout_config_equality() {
let a = TimeoutConfig::new(Time::from_nanos(1000));
let b = TimeoutConfig::new(Time::from_nanos(1000));
let c = TimeoutConfig::absolute(Time::from_nanos(1000));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn test_timeout_error_into_cancel_reason() {
let err = TimeoutError::new(Time::from_nanos(1000));
let reason = err.into_cancel_reason();
assert!(matches!(
reason.kind(),
crate::types::cancel::CancelKind::Timeout
));
}
#[test]
fn test_timeout_error_equality() {
let a = TimeoutError::new(Time::from_nanos(1000));
let b = TimeoutError::new(Time::from_nanos(1000));
let c = TimeoutError::new(Time::from_nanos(2000));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn test_timeout_after_nanos_saturating() {
let now = Time::from_nanos(u64::MAX - 10);
let timeout = Timeout::<()>::after_nanos(now, 100);
assert!(timeout.deadline.as_nanos() >= now.as_nanos());
}
#[test]
fn test_timeout_after_secs_large_value() {
let now = Time::ZERO;
let timeout = Timeout::<()>::after_secs(now, 1_000_000);
assert_eq!(
timeout.deadline.as_nanos(),
1_000_000u64.saturating_mul(1_000_000_000)
);
}
use proptest::prelude::*;
#[derive(Debug, Clone)]
enum MockOperationOutcome {
Complete(i32),
Error(&'static str),
Cancel,
Panic,
}
impl MockOperationOutcome {
fn into_outcome(self) -> Outcome<i32, &'static str> {
match self {
Self::Complete(val) => Outcome::Ok(val),
Self::Error(msg) => Outcome::Err(msg),
Self::Cancel => Outcome::Cancelled(CancelReason::shutdown()),
Self::Panic => Outcome::Panicked(crate::types::outcome::PanicPayload::new("test panic")),
}
}
}
fn mock_operation_strategy() -> impl Strategy<Value = MockOperationOutcome> {
prop_oneof![
any::<i16>().prop_map(|v| MockOperationOutcome::Complete(i32::from(v))),
Just(MockOperationOutcome::Error("mock error")),
Just(MockOperationOutcome::Cancel),
Just(MockOperationOutcome::Panic),
]
}
#[test]
fn metamorphic_timeout_single_fire_idempotence() {
proptest!(|(
base_time in 0u64..1_000_000_000,
timeout_duration in 1u64..10_000_000,
elapsed_time in 10_000_000u64..20_000_000,
)| {
let now = Time::from_nanos(base_time);
let timeout = Timeout::<()>::after_nanos(now, timeout_duration);
let future_time = Time::from_nanos(base_time + elapsed_time);
let expired_1 = timeout.is_expired(future_time);
let expired_2 = timeout.is_expired(future_time);
let expired_3 = timeout.is_expired(future_time);
prop_assert_eq!(expired_1, expired_2);
prop_assert_eq!(expired_2, expired_3);
let remaining_1 = timeout.remaining(future_time);
let remaining_2 = timeout.remaining(future_time);
prop_assert_eq!(remaining_1, remaining_2);
if elapsed_time > timeout_duration {
prop_assert!(expired_1);
prop_assert_eq!(remaining_1, Duration::ZERO);
}
});
}
#[test]
fn metamorphic_cancel_before_timeout_consistency() {
proptest!(|(
base_time in 0u64..1_000_000_000,
timeout_duration in 10_000_000u64..50_000_000,
completion_time in 1_000_000u64..9_999_999,
)| {
let now = Time::from_nanos(base_time);
let deadline = now.saturating_add_nanos(timeout_duration);
let completion_offset = Time::from_nanos(base_time + completion_time);
let cancelled_outcome: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::shutdown());
let completed_in_time = completion_offset < deadline;
prop_assert!(completed_in_time);
let result = make_timed_result(cancelled_outcome, deadline, completed_in_time);
prop_assert!(result.is_completed());
prop_assert!(!result.is_timed_out());
match result {
TimedResult::Completed(outcome) => {
prop_assert!(outcome.is_cancelled());
}
TimedResult::TimedOut(_) => {
prop_assert!(false, "Early cancellation should not be TimedOut");
}
}
});
}
#[test]
fn metamorphic_concurrent_timeout_race_determinism() {
proptest!(|(
timeout_durations in prop::collection::vec(1_000_000u64..100_000_000, 2..8),
base_time in 0u64..1_000_000_000,
)| {
let now = Time::from_nanos(base_time);
let deadlines: Vec<Time> = timeout_durations.iter()
.map(|&duration| now.saturating_add_nanos(duration))
.collect();
let min_deadline = deadlines.iter().min().copied().unwrap();
let mut effective = deadlines[0];
for &deadline in deadlines.iter().skip(1) {
effective = effective_deadline(deadline, Some(effective));
}
prop_assert_eq!(effective, min_deadline);
let mut reverse_effective = deadlines[deadlines.len() - 1];
for &deadline in deadlines.iter().rev().skip(1) {
reverse_effective = effective_deadline(deadline, Some(reverse_effective));
}
prop_assert_eq!(effective, reverse_effective);
for &deadline in &deadlines {
prop_assert!(effective.as_nanos() <= deadline.as_nanos());
}
});
}
#[test]
fn metamorphic_nested_timeout_composition_law() {
proptest!(|(
d1 in 1_000_000u64..50_000_000,
d2 in 1_000_000u64..50_000_000,
d3 in 1_000_000u64..50_000_000,
base_time in 0u64..1_000_000_000,
)| {
let now = Time::from_nanos(base_time);
let deadline1 = now.saturating_add_nanos(d1);
let deadline2 = now.saturating_add_nanos(d2);
let deadline3 = now.saturating_add_nanos(d3);
let nested_12 = effective_deadline(deadline1, Some(deadline2));
let nested_21 = effective_deadline(deadline2, Some(deadline1));
let min_12 = if deadline1.as_nanos() <= deadline2.as_nanos() {
deadline1
} else {
deadline2
};
prop_assert_eq!(nested_12, min_12);
prop_assert_eq!(nested_21, min_12);
prop_assert_eq!(nested_12, nested_21);
let triple_nested = effective_deadline(deadline1,
Some(effective_deadline(deadline2, Some(deadline3))));
let min_123 = [deadline1, deadline2, deadline3].iter().min().copied().unwrap();
prop_assert_eq!(triple_nested, min_123);
});
}
#[test]
fn metamorphic_operation_timeout_race_correctness() {
proptest!(|(
operation_outcome in mock_operation_strategy(),
timeout_duration in 10_000_000u64..100_000_000,
completion_duration in 1_000_000u64..150_000_000,
base_time in 0u64..1_000_000_000,
)| {
let now = Time::from_nanos(base_time);
let deadline = now.saturating_add_nanos(timeout_duration);
let completion_time = now.saturating_add_nanos(completion_duration);
let outcome = operation_outcome.into_outcome();
let completed_in_time = completion_time < deadline;
let result = make_timed_result(outcome.clone(), deadline, completed_in_time);
if completed_in_time {
prop_assert!(result.is_completed());
prop_assert!(!result.is_timed_out());
match result {
TimedResult::Completed(completed_outcome) => {
prop_assert_eq!(format!("{:?}", completed_outcome), format!("{:?}", outcome));
}
TimedResult::TimedOut(_) => {
prop_assert!(false, "Operation that completed in time should not be TimedOut");
}
}
} else {
match &outcome {
Outcome::Ok(_) | Outcome::Err(_) | Outcome::Panicked(_) => {
prop_assert!(result.is_completed());
prop_assert!(!result.is_timed_out());
}
Outcome::Cancelled(_) => {
prop_assert!(result.is_timed_out());
prop_assert!(!result.is_completed());
}
}
}
});
}
#[test]
fn metamorphic_timeout_drain_invariant_preservation() {
proptest!(|(
timeout_duration in 1_000_000u64..100_000_000,
base_time in 0u64..1_000_000_000,
message_present in any::<bool>(),
)| {
let now = Time::from_nanos(base_time);
let deadline = now.saturating_add_nanos(timeout_duration);
let timeout_error = if message_present {
TimeoutError::with_message(deadline, "operation timed out")
} else {
TimeoutError::new(deadline)
};
let timed_result: TimedResult<i32, &str> = TimedResult::TimedOut(timeout_error.clone());
prop_assert!(timed_result.is_timed_out());
prop_assert!(!timed_result.is_completed());
let outcome = timed_result.clone().into_outcome();
prop_assert!(outcome.is_cancelled());
if let Outcome::Cancelled(reason) = outcome {
prop_assert!(matches!(reason.kind(), crate::types::cancel::CancelKind::Timeout));
} else {
prop_assert!(false, "TimedOut should convert to Cancelled outcome");
}
let result = timed_result.into_result();
prop_assert!(result.is_err());
if let Err(err) = result {
prop_assert!(matches!(err, TimedError::TimedOut(_)));
if let TimedError::TimedOut(timeout_err) = err {
prop_assert_eq!(timeout_err.deadline, deadline);
prop_assert_eq!(timeout_err.message.is_some(), message_present);
}
}
});
}
#[test]
fn metamorphic_timeout_boundary_consistency() {
proptest!(|(
base_time in 1_000_000u64..1_000_000_000,
timeout_duration in 1_000_000u64..100_000_000,
)| {
let now = Time::from_nanos(base_time);
let timeout = Timeout::<()>::after_nanos(now, timeout_duration);
let deadline = now.saturating_add_nanos(timeout_duration);
prop_assert_eq!(timeout.deadline, deadline);
let before = Time::from_nanos(deadline.as_nanos().saturating_sub(1));
prop_assert!(!timeout.is_expired(before));
prop_assert!(timeout.remaining(before) > Duration::ZERO);
prop_assert!(timeout.is_expired(deadline));
prop_assert_eq!(timeout.remaining(deadline), Duration::ZERO);
let after = Time::from_nanos(deadline.as_nanos().saturating_add(1));
prop_assert!(timeout.is_expired(after));
prop_assert_eq!(timeout.remaining(after), Duration::ZERO);
if before < deadline {
let remaining_before = timeout.remaining(before);
let remaining_at = timeout.remaining(deadline);
prop_assert!(remaining_before >= remaining_at);
}
});
}
#[test]
fn metamorphic_timeout_config_resolution_invariants() {
proptest!(|(
requested_time in 1_000_000u64..100_000_000,
existing_time in 1_000_000u64..100_000_000,
base_time in 0u64..1_000_000_000,
use_effective in any::<bool>(),
)| {
let now = Time::from_nanos(base_time);
let requested = now.saturating_add_nanos(requested_time);
let existing = now.saturating_add_nanos(existing_time);
let config = if use_effective {
TimeoutConfig::new(requested)
} else {
TimeoutConfig::absolute(requested)
};
let resolved_with_existing = config.resolve(Some(existing));
let resolved_without_existing = config.resolve(None);
prop_assert_eq!(resolved_without_existing, requested);
if use_effective {
let expected = if requested.as_nanos() <= existing.as_nanos() {
requested
} else {
existing
};
prop_assert_eq!(resolved_with_existing, expected);
prop_assert!(resolved_with_existing.as_nanos() <= requested.as_nanos());
prop_assert!(resolved_with_existing.as_nanos() <= existing.as_nanos());
} else {
prop_assert_eq!(resolved_with_existing, requested);
}
let resolved_twice = if use_effective {
TimeoutConfig::new(resolved_with_existing).resolve(Some(existing))
} else {
TimeoutConfig::absolute(resolved_with_existing).resolve(Some(existing))
};
prop_assert_eq!(resolved_twice, resolved_with_existing);
});
}
}