From 6e9ba7bf2dbf75572acdb6098a8978452273f55b Mon Sep 17 00:00:00 2001 From: Azeem Shaik Date: Sun, 9 Aug 2026 17:06:20 +0530 Subject: [PATCH] Give the blocking driver the same coverage as the async one Two gaps, both found by asking whether blocking was actually done. The blocking driver had no test driving a randomized backoff at all. Both drivers take any `Backoff`, and ADR001 decision 3 says they must not diverge, but the coverage had: async proved jitter and decorrelated work through the real driver and blocking proved neither. And the `Clock for &C` / `Arc` impls this release adds had no in-repo test on either side. They were verified from an external crate while writing them, which is not the same as being guarded. Deleting them now fails the build with four E0599s instead of passing silently. Both drivers now carry the same three tests: jittered, decorrelated, and a borrowed-or-shared clock. --- src/blocking/retry.rs | 64 +++++++++++++++++++++++++++++++++++++++++++ src/retry.rs | 19 +++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/blocking/retry.rs b/src/blocking/retry.rs index e39dbeb..9fdd664 100644 --- a/src/blocking/retry.rs +++ b/src/blocking/retry.rs @@ -248,6 +248,70 @@ mod tests { assert_eq!(clock.slept(), vec![secs(1), secs(2), secs(4)]); } + #[test] + fn drives_a_jittered_backoff() { + // The async twin of this lives in src/retry.rs. Both drivers take any `Backoff`, so both + // have to be shown driving a randomized one (ADR001 decision 3). + let clock = MockClock::new(); + let out: Result = retry(|| Err("boom")) + .backoff(backoff(3).jittered_with_seed(42)) + .clock(&clock) + .call(); + + assert_eq!(out, Err("boom")); + let slept = clock.slept(); + assert_eq!(slept.len(), 3); + // Underlying exponential is 1s, 2s, 4s; full jitter can only shrink each one. + for (d, cap) in slept.iter().zip([secs(1), secs(2), secs(4)]) { + assert!(*d <= cap, "jittered delay {d:?} exceeded {cap:?}"); + } + } + + #[test] + fn drives_a_decorrelated_backoff() { + use crate::backoff::{DecorrelatedBackoff, DecorrelatedBackoffConfig}; + + let clock = MockClock::new(); + let out: Result = retry(|| Err("boom")) + .backoff( + DecorrelatedBackoff::with_seed( + DecorrelatedBackoffConfig { + base: secs(1), + max_retries: 4, + max_delay: secs(20), + }, + 7, + ) + .unwrap(), + ) + .clock(&clock) + .call(); + + assert_eq!(out, Err("boom")); + let slept = clock.slept(); + assert_eq!(slept.len(), 4); + assert!( + slept.iter().all(|d| *d >= secs(1) && *d <= secs(20)), + "delays escaped [base, max_delay]: {slept:?}" + ); + } + + #[test] + fn accepts_a_borrowed_or_shared_clock() { + // `.clock(c)` takes the clock by value, so without the reference impls a test could hand + // over its mock and never read it back. Both forms must reach the same mock. + let clock = MockClock::new(); + let _: Result = retry(|| Err("x")).backoff(backoff(2)).clock(&clock).call(); + assert_eq!(clock.slept(), vec![secs(1), secs(2)]); + + let shared = std::sync::Arc::new(MockClock::new()); + let _: Result = retry(|| Err("x")) + .backoff(backoff(2)) + .clock(Arc::clone(&shared)) + .call(); + assert_eq!(shared.slept(), vec![secs(1), secs(2)]); + } + #[test] fn emits_a_tracing_event_per_retry() { // The op fails twice then succeeds → exactly two `mettle::retry` events. diff --git a/src/retry.rs b/src/retry.rs index c19d458..4cfc6ad 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -720,6 +720,25 @@ mod tests { ); } + #[tokio::test] + async fn accepts_a_borrowed_or_shared_clock() { + // `.clock(c)` takes the clock by value, so without the reference impls a test could hand + // over its mock and never read it back. The blocking twin of this is in blocking/retry.rs. + let clock = MockClock::new(); + let _: Result = retry(|| async { Err("x") }) + .backoff(backoff(2)) + .clock(&clock) + .await; + assert_eq!(clock.slept(), vec![secs(1), secs(2)]); + + let shared = Arc::new(MockClock::new()); + let _: Result = retry(|| async { Err("x") }) + .backoff(backoff(2)) + .clock(Arc::clone(&shared)) + .await; + assert_eq!(shared.slept(), vec![secs(1), secs(2)]); + } + #[tokio::test] async fn drives_a_jittered_backoff() { let clock = MockClock::new();