use core::time::Duration;
use proptest::prelude::*;
use relentless::{RetryState, Wait, wait};
const NANOS_PER_SEC: u32 = 1_000_000_000;
const EXACT_NANOS_LIMIT: u64 = 1 << 20;
const EXACT_ATTEMPT_LIMIT: u32 = 33;
fn state(attempt: u32) -> RetryState {
RetryState::for_attempt(attempt)
}
fn arb_duration() -> impl Strategy<Value = Duration> {
(any::<u64>(), 0..NANOS_PER_SEC).prop_map(|(secs, nanos)| Duration::new(secs, nanos))
}
proptest! {
#[test]
fn exponential_never_panics(
initial in arb_duration(),
base in any::<f64>(),
attempt in any::<u32>(),
) {
let strategy = wait::exponential(initial).base(base);
let _ = strategy.next_wait(&state(attempt));
}
#[test]
fn exponential_matches_integer_reference_in_exact_window(
nanos in 0..EXACT_NANOS_LIMIT,
attempt in 1..=EXACT_ATTEMPT_LIMIT,
) {
let strategy = wait::exponential(Duration::from_nanos(nanos));
let expected = Duration::from_nanos(nanos << (attempt - 1));
prop_assert_eq!(strategy.next_wait(&state(attempt)), expected);
}
#[test]
fn exponential_zero_initial_is_always_zero(
base in any::<f64>(),
attempt in any::<u32>(),
) {
let strategy = wait::exponential(Duration::ZERO).base(base);
prop_assert_eq!(strategy.next_wait(&state(attempt)), Duration::ZERO);
}
#[test]
fn linear_matches_u128_reference(
initial in arb_duration(),
increment in arb_duration(),
attempt in any::<u32>(),
) {
let strategy = wait::linear(initial, increment);
let reference = initial.as_nanos()
+ u128::from(attempt.saturating_sub(1)) * increment.as_nanos();
let expected = if reference > Duration::MAX.as_nanos() {
Duration::MAX
} else {
duration_from_nanos_u128(reference)
};
prop_assert_eq!(strategy.next_wait(&state(attempt)), expected);
}
#[test]
fn full_jitter_stays_within_base(
base in arb_duration(),
seed in any::<u64>(),
attempt in any::<u32>(),
) {
let strategy = wait::fixed(base).full_jitter().with_seed(seed);
prop_assert!(strategy.next_wait(&state(attempt)) <= base);
}
#[test]
fn additive_jitter_stays_within_base_plus_max(
base in arb_duration(),
max_jitter in arb_duration(),
seed in any::<u64>(),
attempt in any::<u32>(),
) {
let strategy = wait::fixed(base).jitter(max_jitter).with_seed(seed);
let delay = strategy.next_wait(&state(attempt));
prop_assert!(delay >= base);
prop_assert!(delay <= base.saturating_add(max_jitter));
}
#[test]
fn equal_jitter_stays_within_half_to_base(
base in arb_duration(),
seed in any::<u64>(),
attempt in any::<u32>(),
) {
let strategy = wait::fixed(base).equal_jitter().with_seed(seed);
let delay = strategy.next_wait(&state(attempt));
prop_assert!(delay >= base / 2);
prop_assert!(delay <= base);
}
#[test]
fn decorrelated_jitter_stays_within_feedback_bounds(
base in arb_duration(),
previous in arb_duration(),
seed in any::<u64>(),
attempt in any::<u32>(),
) {
let strategy = wait::decorrelated_jitter(base).with_seed(seed);
let state = state(attempt).with_previous_delay(Some(previous));
let delay = strategy.next_wait(&state);
prop_assert!(delay >= base);
prop_assert!(delay <= base.max(previous.saturating_mul(3)));
}
}
fn duration_from_nanos_u128(nanos: u128) -> Duration {
Duration::new(
(nanos / u128::from(NANOS_PER_SEC)) as u64,
(nanos % u128::from(NANOS_PER_SEC)) as u32,
)
}