#![cfg(feature = "producer")]
use hook0_client::{MAX_ATTEMPTS_CAP, RetryPolicy};
use proptest::prelude::*;
use proptest::test_runner::FileFailurePersistence;
use std::time::Duration;
const REGRESSIONS: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/proptest-regressions/retry_backoff.txt"
);
const INSPECTED_RETRIES: u32 = 64;
fn config() -> ProptestConfig {
ProptestConfig {
cases: 512,
failure_persistence: Some(Box::new(FileFailurePersistence::Direct(REGRESSIONS))),
..ProptestConfig::default()
}
}
prop_compose! {
fn a_policy()(
max_attempts in 0u32..40,
initial_backoff_ms in 0u64..5_000,
max_backoff_ms in 0u64..10_000,
max_total_delay_ms in 0u64..60_000,
) -> RetryPolicy {
RetryPolicy {
max_attempts,
initial_backoff: Duration::from_millis(initial_backoff_ms),
max_backoff: Duration::from_millis(max_backoff_ms),
max_total_delay: Duration::from_millis(max_total_delay_ms),
}
}
}
prop_compose! {
fn some_draws()(
draws in prop::collection::vec(
prop_oneof![
90 => 0.0f64..1.0,
10 => prop_oneof![
Just(f64::NAN),
Just(f64::INFINITY),
Just(-1.0),
Just(2.0),
],
],
0..48,
),
) -> Vec<f64> {
draws
}
}
proptest! {
#![proptest_config(config())]
#[test]
fn a_policy_makes_between_one_attempt_and_the_cap(policy in a_policy()) {
let attempts = policy.attempts();
prop_assert!(
(1..=MAX_ATTEMPTS_CAP).contains(&attempts),
"{policy:?} makes {attempts} attempts"
);
}
#[test]
fn ceilings_never_shrink_and_never_pass_the_maximum_backoff(policy in a_policy()) {
for retry in 1..=INSPECTED_RETRIES {
let ceiling = policy.backoff_ceiling(retry);
let next = policy.backoff_ceiling(retry + 1);
prop_assert!(
ceiling <= policy.max_backoff,
"the ceiling of retry {retry} of {policy:?} is {ceiling:?}"
);
prop_assert!(
ceiling <= next,
"the ceiling of {policy:?} shrinks from {ceiling:?} at retry {retry} to {next:?}"
);
}
}
#[test]
fn a_schedule_stays_inside_every_bound_of_its_policy(
policy in a_policy(),
draws in some_draws(),
) {
let delays = policy.delays(&draws);
prop_assert!(
delays.len() < policy.attempts() as usize,
"{policy:?} scheduled {} delays for {} attempts",
delays.len(),
policy.attempts()
);
let mut total = Duration::ZERO;
for (index, delay) in delays.iter().enumerate() {
let retry = index as u32 + 1;
let ceiling = policy.backoff_ceiling(retry);
prop_assert!(
*delay <= ceiling,
"retry {retry} of {policy:?} waits {delay:?}, above its {ceiling:?} ceiling"
);
total = total.saturating_add(*delay);
}
prop_assert!(
total <= policy.max_total_delay,
"{policy:?} waits {total:?} in total, above its {:?} budget",
policy.max_total_delay
);
}
#[test]
fn a_disabled_policy_schedules_no_delay(draws in some_draws()) {
let policy = RetryPolicy::disabled();
prop_assert_eq!(policy.attempts(), 1);
prop_assert!(policy.delays(&draws).is_empty());
}
}