Skip to main content

shipper_retry/
lib.rs

1//! Retry strategies and backoff policies for distributed systems.
2//!
3//! This crate provides configurable retry strategies with support for:
4//! - Multiple backoff strategies (immediate, exponential, linear, constant)
5//! - Jitter for avoiding thundering herd problems
6//! - Per-error-type configuration
7//! - Predefined policies for common use cases
8//!
9//! # Example
10//!
11//! ```
12//! use shipper_retry::{RetryPolicy, RetryStrategyConfig, calculate_delay};
13//! use std::time::Duration;
14//!
15//! // Use a predefined policy
16//! let config = RetryPolicy::Default.to_config();
17//! let delay = calculate_delay(&config, 2);
18//! println!("Retry after: {:?}", delay);
19//!
20//! // Custom configuration
21//! let custom = RetryStrategyConfig {
22//!     max_attempts: 5,
23//!     base_delay: Duration::from_secs(1),
24//!     max_delay: Duration::from_secs(30),
25//!     ..Default::default()
26//! };
27//! ```
28
29use std::time::Duration;
30
31use serde::{Deserialize, Serialize};
32
33/// Strategy type for retry behavior.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum RetryStrategyType {
37    /// No delay between retries - retry immediately
38    Immediate,
39    /// Exponential backoff: delay doubles each attempt (default)
40    #[default]
41    Exponential,
42    /// Linear backoff: delay increases linearly each attempt
43    Linear,
44    /// Constant delay: same delay every attempt
45    Constant,
46}
47
48/// Predefined retry policies with sensible defaults for different use cases.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum RetryPolicy {
52    /// Default balanced retry behavior - good for most scenarios
53    #[default]
54    Default,
55    /// Aggressive retries - more attempts, faster recovery
56    Aggressive,
57    /// Conservative retries - fewer attempts, longer delays
58    Conservative,
59    /// Fully custom configuration via retry.strategy settings
60    Custom,
61}
62
63impl RetryPolicy {
64    /// Get the default retry configuration for this policy.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use shipper_retry::RetryPolicy;
70    /// use std::time::Duration;
71    ///
72    /// let config = RetryPolicy::Default.to_config();
73    /// assert_eq!(config.max_attempts, 6);
74    /// assert_eq!(config.base_delay, Duration::from_secs(2));
75    /// ```
76    pub fn to_config(&self) -> RetryStrategyConfig {
77        match self {
78            RetryPolicy::Default => RetryStrategyConfig {
79                strategy: RetryStrategyType::Exponential,
80                max_attempts: 6,
81                base_delay: Duration::from_secs(2),
82                max_delay: Duration::from_secs(120),
83                jitter: 0.5,
84            },
85            RetryPolicy::Aggressive => RetryStrategyConfig {
86                strategy: RetryStrategyType::Exponential,
87                max_attempts: 10,
88                base_delay: Duration::from_millis(500),
89                max_delay: Duration::from_secs(30),
90                jitter: 0.3,
91            },
92            RetryPolicy::Conservative => RetryStrategyConfig {
93                strategy: RetryStrategyType::Linear,
94                max_attempts: 3,
95                base_delay: Duration::from_secs(5),
96                max_delay: Duration::from_secs(60),
97                jitter: 0.1,
98            },
99            RetryPolicy::Custom => {
100                // Custom uses the explicitly configured values
101                RetryStrategyConfig::default()
102            }
103        }
104    }
105}
106
107/// Configuration for a retry strategy.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct RetryStrategyConfig {
110    /// Strategy type for calculating delay between retries.
111    #[serde(default)]
112    pub strategy: RetryStrategyType,
113    /// Maximum number of retry attempts.
114    #[serde(default)]
115    pub max_attempts: u32,
116    /// Base delay for backoff calculations.
117    #[serde(default = "default_base_delay")]
118    #[serde(with = "humantime_serde")]
119    pub base_delay: Duration,
120    /// Maximum delay cap for backoff.
121    #[serde(default = "default_max_delay")]
122    #[serde(with = "humantime_serde")]
123    pub max_delay: Duration,
124    /// Jitter factor for randomized delays (0.0 = no jitter, 1.0 = full jitter).
125    #[serde(default = "default_jitter")]
126    pub jitter: f64,
127}
128
129fn default_base_delay() -> Duration {
130    Duration::from_secs(2)
131}
132
133fn default_max_delay() -> Duration {
134    Duration::from_secs(120)
135}
136
137fn default_jitter() -> f64 {
138    0.5
139}
140
141impl Default for RetryStrategyConfig {
142    fn default() -> Self {
143        Self {
144            strategy: RetryStrategyType::Exponential,
145            max_attempts: 6,
146            base_delay: Duration::from_secs(2),
147            max_delay: Duration::from_secs(120),
148            jitter: 0.5,
149        }
150    }
151}
152
153/// Error classification for retry decisions.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
155#[serde(rename_all = "snake_case")]
156pub enum ErrorClass {
157    /// Error is transient and should be retried
158    #[default]
159    Retryable,
160    /// Error outcome is unknown (may have succeeded)
161    Ambiguous,
162    /// Error is permanent and should not be retried
163    Permanent,
164}
165
166/// Per-error-type retry configuration.
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
168pub struct PerErrorConfig {
169    /// Retry configuration for retryable errors (e.g., network issues, rate limiting).
170    #[serde(default, rename = "retryable")]
171    pub retryable: Option<RetryStrategyConfig>,
172    /// Retry configuration for ambiguous errors (e.g., unknown if publish succeeded).
173    #[serde(default, rename = "ambiguous")]
174    pub ambiguous: Option<RetryStrategyConfig>,
175    /// Retry configuration for permanent errors (e.g., authentication failure).
176    /// Permanent errors are typically not retried, but this can be customized.
177    #[serde(default, rename = "permanent")]
178    pub permanent: Option<RetryStrategyConfig>,
179}
180
181/// Calculate the delay for the next retry attempt based on the strategy configuration.
182///
183/// # Arguments
184///
185/// * `config` - The retry strategy configuration
186/// * `attempt` - The current attempt number (1-indexed)
187///
188/// # Returns
189///
190/// The duration to wait before the next retry attempt.
191///
192/// # Example
193///
194/// ```
195/// use shipper_retry::{RetryStrategyConfig, RetryStrategyType, calculate_delay};
196/// use std::time::Duration;
197///
198/// let config = RetryStrategyConfig {
199///     strategy: RetryStrategyType::Exponential,
200///     base_delay: Duration::from_secs(1),
201///     max_delay: Duration::from_secs(60),
202///     jitter: 0.0,
203///     max_attempts: 10,
204/// };
205///
206/// let delay = calculate_delay(&config, 1);
207/// assert_eq!(delay, Duration::from_secs(1));
208///
209/// let delay = calculate_delay(&config, 2);
210/// assert_eq!(delay, Duration::from_secs(2));
211/// ```
212pub fn calculate_delay(config: &RetryStrategyConfig, attempt: u32) -> Duration {
213    let delay = match config.strategy {
214        RetryStrategyType::Immediate => Duration::ZERO,
215        RetryStrategyType::Exponential => {
216            let pow = attempt.saturating_sub(1).min(16);
217            config.base_delay.saturating_mul(2_u32.saturating_pow(pow))
218        }
219        RetryStrategyType::Linear => config.base_delay.saturating_mul(attempt),
220        RetryStrategyType::Constant => config.base_delay,
221    };
222
223    // Cap at max_delay
224    let capped = delay.min(config.max_delay);
225
226    // Apply jitter if enabled
227    if config.jitter > 0.0 {
228        apply_jitter(capped, config.jitter)
229    } else {
230        capped
231    }
232}
233
234/// Apply jitter to a delay value.
235/// Jitter factor of 0.5 means delay * (0.5 to 1.5).
236fn apply_jitter(delay: Duration, jitter: f64) -> Duration {
237    // Generate a random factor between (1 - jitter) and (1 + jitter)
238    let jitter_range = 2.0 * jitter;
239    let random_value: f64 = rand::random();
240    let random_factor = 1.0 - jitter + (random_value * jitter_range);
241    let millis = (delay.as_millis() as f64 * random_factor).round() as u64;
242    Duration::from_millis(millis)
243}
244
245/// Get the retry configuration for a specific error class.
246/// Falls back to the default config if no per-error config is specified.
247///
248/// # Arguments
249///
250/// * `default_config` - The default retry configuration
251/// * `per_error_config` - Optional per-error-type configuration
252/// * `error_class` - The classification of the error
253///
254/// # Returns
255///
256/// The appropriate retry configuration for the error class.
257///
258/// # Examples
259///
260/// ```
261/// use shipper_retry::{RetryStrategyConfig, RetryStrategyType, ErrorClass, PerErrorConfig, config_for_error};
262///
263/// let default = RetryStrategyConfig::default();
264/// let per_error = PerErrorConfig {
265///     retryable: Some(RetryStrategyConfig {
266///         strategy: RetryStrategyType::Immediate,
267///         max_attempts: 10,
268///         ..Default::default()
269///     }),
270///     ..Default::default()
271/// };
272///
273/// // Uses per-error config for retryable errors
274/// let config = config_for_error(&default, Some(&per_error), ErrorClass::Retryable);
275/// assert_eq!(config.strategy, RetryStrategyType::Immediate);
276///
277/// // Falls back to default for ambiguous errors
278/// let config = config_for_error(&default, Some(&per_error), ErrorClass::Ambiguous);
279/// assert_eq!(config.strategy, RetryStrategyType::Exponential);
280/// ```
281pub fn config_for_error(
282    default_config: &RetryStrategyConfig,
283    per_error_config: Option<&PerErrorConfig>,
284    error_class: ErrorClass,
285) -> RetryStrategyConfig {
286    if let Some(per_error) = per_error_config {
287        match error_class {
288            ErrorClass::Retryable => {
289                if let Some(config) = &per_error.retryable {
290                    return config.clone();
291                }
292            }
293            ErrorClass::Ambiguous => {
294                if let Some(config) = &per_error.ambiguous {
295                    return config.clone();
296                }
297            }
298            ErrorClass::Permanent => {
299                if let Some(config) = &per_error.permanent {
300                    return config.clone();
301                }
302            }
303        }
304    }
305    default_config.clone()
306}
307
308/// A retry executor that runs a fallible operation with configured retry behavior.
309pub struct RetryExecutor {
310    config: RetryStrategyConfig,
311}
312
313impl RetryExecutor {
314    /// Create a new retry executor with the given configuration.
315    pub fn new(config: RetryStrategyConfig) -> Self {
316        Self { config }
317    }
318
319    /// Create a retry executor from a predefined policy.
320    pub fn from_policy(policy: RetryPolicy) -> Self {
321        Self::new(policy.to_config())
322    }
323
324    /// Execute a fallible operation with retry behavior.
325    ///
326    /// The operation receives the current attempt number (starting at 1).
327    /// Return `Ok(T)` on success, `Err(E)` on failure.
328    ///
329    /// # Example
330    ///
331    /// ```no_run
332    /// use shipper_retry::{RetryExecutor, RetryPolicy};
333    ///
334    /// let executor = RetryExecutor::from_policy(RetryPolicy::Default);
335    /// let result = executor.run(|attempt| {
336    ///     // Your fallible operation here
337    ///     if attempt < 3 {
338    ///         Err("transient error")
339    ///     } else {
340    ///         Ok("success")
341    ///     }
342    /// });
343    /// ```
344    pub fn run<T, E, F>(&self, mut operation: F) -> Result<T, E>
345    where
346        F: FnMut(u32) -> Result<T, E>,
347    {
348        let mut attempt = 1;
349
350        loop {
351            match operation(attempt) {
352                Ok(result) => return Ok(result),
353                Err(e) => {
354                    if attempt >= self.config.max_attempts {
355                        return Err(e);
356                    }
357
358                    let delay = calculate_delay(&self.config, attempt);
359                    std::thread::sleep(delay);
360                    attempt += 1;
361                }
362            }
363        }
364    }
365
366    /// Execute a fallible operation with retry behavior and custom error classification.
367    ///
368    /// The operation returns a tuple of (result, should_retry).
369    /// This allows the operation to indicate whether an error is retryable.
370    pub fn run_with_classification<T, E, F>(&self, mut operation: F) -> Result<T, E>
371    where
372        F: FnMut(u32) -> Result<(T, bool), E>,
373    {
374        let mut attempt = 1;
375
376        loop {
377            match operation(attempt) {
378                Ok((result, _)) => return Ok(result),
379                Err(e) => {
380                    if attempt >= self.config.max_attempts {
381                        return Err(e);
382                    }
383
384                    let delay = calculate_delay(&self.config, attempt);
385                    std::thread::sleep(delay);
386                    attempt += 1;
387                }
388            }
389        }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn test_retry_policy_to_config_default() {
399        let config = RetryPolicy::Default.to_config();
400        assert_eq!(config.strategy, RetryStrategyType::Exponential);
401        assert_eq!(config.max_attempts, 6);
402        assert_eq!(config.base_delay, Duration::from_secs(2));
403        assert_eq!(config.max_delay, Duration::from_secs(120));
404    }
405
406    #[test]
407    fn test_retry_policy_to_config_aggressive() {
408        let config = RetryPolicy::Aggressive.to_config();
409        assert_eq!(config.strategy, RetryStrategyType::Exponential);
410        assert_eq!(config.max_attempts, 10);
411        assert_eq!(config.base_delay, Duration::from_millis(500));
412        assert_eq!(config.max_delay, Duration::from_secs(30));
413    }
414
415    #[test]
416    fn test_retry_policy_to_config_conservative() {
417        let config = RetryPolicy::Conservative.to_config();
418        assert_eq!(config.strategy, RetryStrategyType::Linear);
419        assert_eq!(config.max_attempts, 3);
420        assert_eq!(config.base_delay, Duration::from_secs(5));
421        assert_eq!(config.max_delay, Duration::from_secs(60));
422    }
423
424    #[test]
425    fn test_calculate_delay_immediate() {
426        let config = RetryStrategyConfig {
427            strategy: RetryStrategyType::Immediate,
428            base_delay: Duration::from_secs(1),
429            max_delay: Duration::from_secs(60),
430            jitter: 0.0,
431            max_attempts: 3,
432        };
433
434        assert_eq!(calculate_delay(&config, 1), Duration::ZERO);
435        assert_eq!(calculate_delay(&config, 5), Duration::ZERO);
436    }
437
438    #[test]
439    fn test_calculate_delay_exponential() {
440        let config = RetryStrategyConfig {
441            strategy: RetryStrategyType::Exponential,
442            base_delay: Duration::from_secs(1),
443            max_delay: Duration::from_secs(60),
444            jitter: 0.0,
445            max_attempts: 10,
446        };
447
448        // Attempt 1: base_delay * 2^0 = 1s
449        assert_eq!(calculate_delay(&config, 1), Duration::from_secs(1));
450
451        // Attempt 2: base_delay * 2^1 = 2s
452        assert_eq!(calculate_delay(&config, 2), Duration::from_secs(2));
453
454        // Attempt 3: base_delay * 2^2 = 4s
455        assert_eq!(calculate_delay(&config, 3), Duration::from_secs(4));
456
457        // Attempt 10: should be capped at max_delay
458        assert_eq!(calculate_delay(&config, 10), Duration::from_secs(60));
459    }
460
461    #[test]
462    fn test_calculate_delay_linear() {
463        let config = RetryStrategyConfig {
464            strategy: RetryStrategyType::Linear,
465            base_delay: Duration::from_secs(1),
466            max_delay: Duration::from_secs(10),
467            jitter: 0.0,
468            max_attempts: 10,
469        };
470
471        assert_eq!(calculate_delay(&config, 1), Duration::from_secs(1));
472        assert_eq!(calculate_delay(&config, 2), Duration::from_secs(2));
473        assert_eq!(calculate_delay(&config, 5), Duration::from_secs(5));
474        assert_eq!(calculate_delay(&config, 15), Duration::from_secs(10));
475    }
476
477    #[test]
478    fn test_calculate_delay_constant() {
479        let config = RetryStrategyConfig {
480            strategy: RetryStrategyType::Constant,
481            base_delay: Duration::from_secs(2),
482            max_delay: Duration::from_secs(10),
483            jitter: 0.0,
484            max_attempts: 10,
485        };
486
487        assert_eq!(calculate_delay(&config, 1), Duration::from_secs(2));
488        assert_eq!(calculate_delay(&config, 5), Duration::from_secs(2));
489        assert_eq!(calculate_delay(&config, 10), Duration::from_secs(2));
490    }
491
492    #[test]
493    fn test_calculate_delay_capped_at_max() {
494        let config = RetryStrategyConfig {
495            strategy: RetryStrategyType::Exponential,
496            base_delay: Duration::from_secs(10),
497            max_delay: Duration::from_secs(30),
498            jitter: 0.0,
499            max_attempts: 10,
500        };
501
502        assert_eq!(calculate_delay(&config, 1), Duration::from_secs(10));
503        assert_eq!(calculate_delay(&config, 2), Duration::from_secs(20));
504        assert_eq!(calculate_delay(&config, 3), Duration::from_secs(30));
505        assert_eq!(calculate_delay(&config, 10), Duration::from_secs(30));
506    }
507
508    #[test]
509    fn test_config_for_error_uses_defaults() {
510        let default_config = RetryStrategyConfig {
511            strategy: RetryStrategyType::Exponential,
512            max_attempts: 5,
513            base_delay: Duration::from_secs(1),
514            max_delay: Duration::from_secs(30),
515            jitter: 0.5,
516        };
517
518        let result = config_for_error(&default_config, None, ErrorClass::Retryable);
519        assert_eq!(result.max_attempts, 5);
520
521        let result = config_for_error(&default_config, None, ErrorClass::Permanent);
522        assert_eq!(result.max_attempts, 5);
523    }
524
525    #[test]
526    fn test_config_for_error_uses_per_error() {
527        let default_config = RetryStrategyConfig::default();
528
529        let per_error = PerErrorConfig {
530            retryable: Some(RetryStrategyConfig {
531                strategy: RetryStrategyType::Immediate,
532                max_attempts: 10,
533                base_delay: Duration::ZERO,
534                max_delay: Duration::ZERO,
535                jitter: 0.0,
536            }),
537            ambiguous: None,
538            permanent: None,
539        };
540
541        // Should use per-error config for retryable
542        let result = config_for_error(&default_config, Some(&per_error), ErrorClass::Retryable);
543        assert_eq!(result.strategy, RetryStrategyType::Immediate);
544
545        // Should fall back to default for ambiguous
546        let result = config_for_error(&default_config, Some(&per_error), ErrorClass::Ambiguous);
547        assert_eq!(result.strategy, RetryStrategyType::Exponential);
548    }
549
550    #[test]
551    fn test_retry_executor_success_on_first_try() {
552        let executor = RetryExecutor::from_policy(RetryPolicy::Aggressive);
553        let result = executor.run(|_attempt| Ok::<_, &str>("success"));
554        assert_eq!(result, Ok("success"));
555    }
556
557    #[test]
558    fn test_retry_executor_success_after_retries() {
559        let executor = RetryExecutor::new(RetryStrategyConfig {
560            strategy: RetryStrategyType::Immediate,
561            max_attempts: 5,
562            base_delay: Duration::ZERO,
563            max_delay: Duration::ZERO,
564            jitter: 0.0,
565        });
566
567        let mut attempts = 0;
568        let result = executor.run(|attempt| {
569            attempts = attempt;
570            if attempt < 3 {
571                Err("transient error")
572            } else {
573                Ok("success")
574            }
575        });
576
577        assert_eq!(result, Ok("success"));
578        assert_eq!(attempts, 3);
579    }
580
581    #[test]
582    fn test_retry_executor_fails_after_max_attempts() {
583        let executor = RetryExecutor::new(RetryStrategyConfig {
584            strategy: RetryStrategyType::Immediate,
585            max_attempts: 3,
586            base_delay: Duration::ZERO,
587            max_delay: Duration::ZERO,
588            jitter: 0.0,
589        });
590
591        let result = executor.run(|_attempt| Err::<&str, _>("permanent error"));
592        assert_eq!(result, Err("permanent error"));
593    }
594
595    #[test]
596    fn test_jitter_applied_correctly() {
597        let config = RetryStrategyConfig {
598            strategy: RetryStrategyType::Constant,
599            base_delay: Duration::from_secs(10),
600            max_delay: Duration::from_secs(60),
601            jitter: 0.5,
602            max_attempts: 10,
603        };
604
605        // With jitter of 0.5, delay should be between 5s and 15s
606        for _ in 0..100 {
607            let delay = calculate_delay(&config, 1);
608            assert!(delay >= Duration::from_millis(5000));
609            assert!(delay <= Duration::from_millis(15000));
610        }
611    }
612
613    // --- Edge-case tests ---
614
615    #[test]
616    fn test_zero_max_retries_does_not_retry() {
617        let executor = RetryExecutor::new(RetryStrategyConfig {
618            strategy: RetryStrategyType::Immediate,
619            max_attempts: 0,
620            base_delay: Duration::ZERO,
621            max_delay: Duration::ZERO,
622            jitter: 0.0,
623        });
624
625        let mut call_count = 0u32;
626        let result = executor.run(|_attempt| {
627            call_count += 1;
628            Err::<(), _>("fail")
629        });
630
631        assert_eq!(result, Err("fail"));
632        // With max_attempts=0 the operation is called once but never retried
633        assert_eq!(call_count, 1);
634    }
635
636    #[test]
637    fn test_zero_max_retries_succeeds_on_first_try() {
638        let executor = RetryExecutor::new(RetryStrategyConfig {
639            strategy: RetryStrategyType::Exponential,
640            max_attempts: 0,
641            base_delay: Duration::from_secs(1),
642            max_delay: Duration::from_secs(60),
643            jitter: 0.0,
644        });
645
646        let result = executor.run(|_| Ok::<_, &str>("ok"));
647        assert_eq!(result, Ok("ok"));
648    }
649
650    #[test]
651    fn test_very_large_max_retries_immediate_success() {
652        let executor = RetryExecutor::new(RetryStrategyConfig {
653            strategy: RetryStrategyType::Exponential,
654            max_attempts: 100,
655            base_delay: Duration::from_secs(60),
656            max_delay: Duration::from_secs(3600),
657            jitter: 0.5,
658        });
659
660        let mut call_count = 0u32;
661        let result = executor.run(|_attempt| {
662            call_count += 1;
663            Ok::<_, &str>("first try")
664        });
665
666        assert_eq!(result, Ok("first try"));
667        assert_eq!(call_count, 1);
668    }
669
670    #[test]
671    fn test_backoff_overflow_exponential_saturates() {
672        // Use a large base delay so exponential computation would overflow
673        let config = RetryStrategyConfig {
674            strategy: RetryStrategyType::Exponential,
675            base_delay: Duration::from_secs(u64::MAX / 2),
676            max_delay: Duration::from_secs(u64::MAX / 2),
677            jitter: 0.0,
678            max_attempts: 100,
679        };
680
681        // High attempt number: 2^16 * huge base would overflow without saturating_mul
682        let delay = calculate_delay(&config, 17);
683        // Must not panic; should be capped at max_delay
684        assert!(delay <= config.max_delay);
685    }
686
687    #[test]
688    fn test_backoff_overflow_linear_saturates() {
689        let config = RetryStrategyConfig {
690            strategy: RetryStrategyType::Linear,
691            base_delay: Duration::from_secs(u64::MAX / 2),
692            max_delay: Duration::from_secs(100),
693            jitter: 0.0,
694            max_attempts: 100,
695        };
696
697        let delay = calculate_delay(&config, u32::MAX);
698        assert!(delay <= config.max_delay);
699    }
700
701    #[test]
702    fn test_jitter_zero_produces_exact_delays() {
703        let config = RetryStrategyConfig {
704            strategy: RetryStrategyType::Exponential,
705            base_delay: Duration::from_millis(100),
706            max_delay: Duration::from_secs(60),
707            jitter: 0.0,
708            max_attempts: 10,
709        };
710
711        // With jitter=0.0, repeated calls must return the exact same value
712        for attempt in 1..=5 {
713            let first = calculate_delay(&config, attempt);
714            for _ in 0..50 {
715                assert_eq!(calculate_delay(&config, attempt), first);
716            }
717        }
718    }
719
720    #[test]
721    fn test_jitter_one_full_range() {
722        let config = RetryStrategyConfig {
723            strategy: RetryStrategyType::Constant,
724            base_delay: Duration::from_secs(10),
725            max_delay: Duration::from_secs(60),
726            jitter: 1.0,
727            max_attempts: 10,
728        };
729
730        // With jitter=1.0, delay = base * random_factor where random_factor in [0.0, 2.0]
731        // So delay should be in [0ms, 20_000ms]
732        for _ in 0..200 {
733            let delay = calculate_delay(&config, 1);
734            assert!(delay <= Duration::from_millis(20_000));
735        }
736    }
737
738    #[test]
739    fn test_initial_delay_zero() {
740        // base_delay=ZERO means all computed delays are zero for every strategy
741        for strategy in [
742            RetryStrategyType::Exponential,
743            RetryStrategyType::Linear,
744            RetryStrategyType::Constant,
745            RetryStrategyType::Immediate,
746        ] {
747            let config = RetryStrategyConfig {
748                strategy,
749                base_delay: Duration::ZERO,
750                max_delay: Duration::from_secs(60),
751                jitter: 0.0,
752                max_attempts: 10,
753            };
754
755            for attempt in 1..=5 {
756                assert_eq!(
757                    calculate_delay(&config, attempt),
758                    Duration::ZERO,
759                    "strategy {:?} attempt {} should be zero with base_delay=ZERO",
760                    strategy,
761                    attempt
762                );
763            }
764        }
765    }
766
767    #[test]
768    fn test_max_delay_zero_caps_everything() {
769        let config = RetryStrategyConfig {
770            strategy: RetryStrategyType::Exponential,
771            base_delay: Duration::from_secs(10),
772            max_delay: Duration::ZERO,
773            jitter: 0.0,
774            max_attempts: 10,
775        };
776
777        for attempt in 1..=10 {
778            assert_eq!(
779                calculate_delay(&config, attempt),
780                Duration::ZERO,
781                "attempt {} should be capped to zero by max_delay=ZERO",
782                attempt
783            );
784        }
785    }
786
787    #[test]
788    fn test_max_delay_less_than_initial_delay() {
789        let config = RetryStrategyConfig {
790            strategy: RetryStrategyType::Exponential,
791            base_delay: Duration::from_secs(10),
792            max_delay: Duration::from_secs(5),
793            jitter: 0.0,
794            max_attempts: 10,
795        };
796
797        // Every attempt should be capped at max_delay
798        for attempt in 1..=5 {
799            assert_eq!(
800                calculate_delay(&config, attempt),
801                Duration::from_secs(5),
802                "attempt {} should be capped at max_delay when max_delay < base_delay",
803                attempt
804            );
805        }
806
807        // Also test with linear and constant strategies
808        let linear = RetryStrategyConfig {
809            strategy: RetryStrategyType::Linear,
810            ..config.clone()
811        };
812        assert_eq!(calculate_delay(&linear, 1), Duration::from_secs(5));
813
814        let constant = RetryStrategyConfig {
815            strategy: RetryStrategyType::Constant,
816            ..config
817        };
818        assert_eq!(calculate_delay(&constant, 1), Duration::from_secs(5));
819    }
820
821    // --- Backoff curve validation ---
822
823    #[test]
824    fn test_exponential_growth_doubles_each_attempt() {
825        let config = RetryStrategyConfig {
826            strategy: RetryStrategyType::Exponential,
827            base_delay: Duration::from_millis(100),
828            max_delay: Duration::from_secs(3600),
829            jitter: 0.0,
830            max_attempts: 20,
831        };
832        for attempt in 2..=10 {
833            let prev = calculate_delay(&config, attempt - 1);
834            let curr = calculate_delay(&config, attempt);
835            assert_eq!(
836                curr,
837                prev * 2,
838                "attempt {} should be double attempt {}",
839                attempt,
840                attempt - 1
841            );
842        }
843    }
844
845    #[test]
846    fn test_exponential_pow_clamped_at_16() {
847        // The exponent is clamped at 16, so attempts 18 and 19 produce the same
848        // uncapped delay as attempt 17 (base * 2^16).
849        let config = RetryStrategyConfig {
850            strategy: RetryStrategyType::Exponential,
851            base_delay: Duration::from_millis(1),
852            max_delay: Duration::from_secs(3600),
853            jitter: 0.0,
854            max_attempts: 30,
855        };
856        let at_17 = calculate_delay(&config, 17);
857        let at_18 = calculate_delay(&config, 18);
858        let at_25 = calculate_delay(&config, 25);
859        assert_eq!(at_17, at_18);
860        assert_eq!(at_17, at_25);
861        // 2^16 ms = 65536 ms
862        assert_eq!(at_17, Duration::from_millis(65536));
863    }
864
865    // --- Strategy selection ---
866
867    #[test]
868    fn test_strategy_selection_produces_distinct_delays() {
869        let base = Duration::from_secs(2);
870        let max = Duration::from_secs(3600);
871        let make = |s| RetryStrategyConfig {
872            strategy: s,
873            base_delay: base,
874            max_delay: max,
875            jitter: 0.0,
876            max_attempts: 10,
877        };
878        let attempt = 3;
879        let imm = calculate_delay(&make(RetryStrategyType::Immediate), attempt);
880        let exp = calculate_delay(&make(RetryStrategyType::Exponential), attempt);
881        let lin = calculate_delay(&make(RetryStrategyType::Linear), attempt);
882        let con = calculate_delay(&make(RetryStrategyType::Constant), attempt);
883
884        // immediate = 0, exponential = 8s, linear = 6s, constant = 2s — all distinct
885        assert_eq!(imm, Duration::ZERO);
886        assert_eq!(exp, Duration::from_secs(8));
887        assert_eq!(lin, Duration::from_secs(6));
888        assert_eq!(con, Duration::from_secs(2));
889        // All four are distinct
890        let vals = [imm, exp, lin, con];
891        for i in 0..vals.len() {
892            for j in (i + 1)..vals.len() {
893                assert_ne!(vals[i], vals[j], "strategies {} and {} collided", i, j);
894            }
895        }
896    }
897
898    #[test]
899    fn test_immediate_always_zero_regardless_of_config() {
900        let config = RetryStrategyConfig {
901            strategy: RetryStrategyType::Immediate,
902            base_delay: Duration::from_secs(999),
903            max_delay: Duration::from_secs(9999),
904            jitter: 0.0,
905            max_attempts: 50,
906        };
907        for attempt in [1, 5, 10, 50, u32::MAX] {
908            assert_eq!(calculate_delay(&config, attempt), Duration::ZERO);
909        }
910    }
911
912    #[test]
913    fn test_constant_ignores_attempt_number() {
914        let config = RetryStrategyConfig {
915            strategy: RetryStrategyType::Constant,
916            base_delay: Duration::from_millis(750),
917            max_delay: Duration::from_secs(60),
918            jitter: 0.0,
919            max_attempts: 100,
920        };
921        let first = calculate_delay(&config, 1);
922        for attempt in 2..=20 {
923            assert_eq!(
924                calculate_delay(&config, attempt),
925                first,
926                "constant delay should not change with attempt number"
927            );
928        }
929    }
930
931    // --- Edge cases ---
932
933    #[test]
934    fn test_attempt_zero_does_not_panic() {
935        for strategy in [
936            RetryStrategyType::Immediate,
937            RetryStrategyType::Exponential,
938            RetryStrategyType::Linear,
939            RetryStrategyType::Constant,
940        ] {
941            let config = RetryStrategyConfig {
942                strategy,
943                base_delay: Duration::from_secs(1),
944                max_delay: Duration::from_secs(60),
945                jitter: 0.0,
946                max_attempts: 10,
947            };
948            // Should not panic
949            let _ = calculate_delay(&config, 0);
950        }
951    }
952
953    #[test]
954    fn test_executor_max_attempts_one_no_retry() {
955        let executor = RetryExecutor::new(RetryStrategyConfig {
956            strategy: RetryStrategyType::Immediate,
957            max_attempts: 1,
958            base_delay: Duration::ZERO,
959            max_delay: Duration::ZERO,
960            jitter: 0.0,
961        });
962        let mut call_count = 0u32;
963        let result = executor.run(|_| {
964            call_count += 1;
965            Err::<(), _>("fail")
966        });
967        assert_eq!(result, Err("fail"));
968        assert_eq!(call_count, 1, "max_attempts=1 should call exactly once");
969    }
970
971    #[test]
972    fn test_executor_run_with_classification_success() {
973        let executor = RetryExecutor::new(RetryStrategyConfig {
974            strategy: RetryStrategyType::Immediate,
975            max_attempts: 5,
976            base_delay: Duration::ZERO,
977            max_delay: Duration::ZERO,
978            jitter: 0.0,
979        });
980        let result = executor.run_with_classification(|attempt| {
981            if attempt < 3 {
982                Err("transient")
983            } else {
984                Ok(("done", true))
985            }
986        });
987        assert_eq!(result, Ok("done"));
988    }
989
990    #[test]
991    fn test_executor_run_with_classification_exhausted() {
992        let executor = RetryExecutor::new(RetryStrategyConfig {
993            strategy: RetryStrategyType::Immediate,
994            max_attempts: 2,
995            base_delay: Duration::ZERO,
996            max_delay: Duration::ZERO,
997            jitter: 0.0,
998        });
999        let result =
1000            executor.run_with_classification(|_| Err::<(&str, bool), _>("permanent failure"));
1001        assert_eq!(result, Err("permanent failure"));
1002    }
1003
1004    #[test]
1005    fn test_config_for_error_all_three_overrides() {
1006        let default = RetryStrategyConfig::default();
1007        let per_error = PerErrorConfig {
1008            retryable: Some(RetryStrategyConfig {
1009                max_attempts: 10,
1010                ..Default::default()
1011            }),
1012            ambiguous: Some(RetryStrategyConfig {
1013                max_attempts: 20,
1014                ..Default::default()
1015            }),
1016            permanent: Some(RetryStrategyConfig {
1017                max_attempts: 30,
1018                ..Default::default()
1019            }),
1020        };
1021        assert_eq!(
1022            config_for_error(&default, Some(&per_error), ErrorClass::Retryable).max_attempts,
1023            10
1024        );
1025        assert_eq!(
1026            config_for_error(&default, Some(&per_error), ErrorClass::Ambiguous).max_attempts,
1027            20
1028        );
1029        assert_eq!(
1030            config_for_error(&default, Some(&per_error), ErrorClass::Permanent).max_attempts,
1031            30
1032        );
1033    }
1034
1035    #[test]
1036    fn test_default_config_matches_default_policy() {
1037        let from_default = RetryStrategyConfig::default();
1038        let from_policy = RetryPolicy::Default.to_config();
1039        assert_eq!(from_default.strategy, from_policy.strategy);
1040        assert_eq!(from_default.max_attempts, from_policy.max_attempts);
1041        assert_eq!(from_default.base_delay, from_policy.base_delay);
1042        assert_eq!(from_default.max_delay, from_policy.max_delay);
1043        assert!(
1044            (from_default.jitter - from_policy.jitter).abs() < f64::EPSILON,
1045            "jitter should match"
1046        );
1047    }
1048
1049    #[test]
1050    fn test_jitter_bounds_with_exponential() {
1051        let config = RetryStrategyConfig {
1052            strategy: RetryStrategyType::Exponential,
1053            base_delay: Duration::from_secs(1),
1054            max_delay: Duration::from_secs(3600),
1055            jitter: 0.3,
1056            max_attempts: 10,
1057        };
1058        // attempt 3 => base * 2^2 = 4s, jitter 0.3 => [2800ms, 5200ms]
1059        for _ in 0..200 {
1060            let delay = calculate_delay(&config, 3);
1061            assert!(
1062                delay >= Duration::from_millis(2800),
1063                "delay {:?} below lower bound",
1064                delay
1065            );
1066            assert!(
1067                delay <= Duration::from_millis(5200),
1068                "delay {:?} above upper bound",
1069                delay
1070            );
1071        }
1072    }
1073
1074    // --- Determinism ---
1075
1076    #[test]
1077    fn test_jitter_zero_is_deterministic_across_strategies() {
1078        // Zero jitter must produce identical results across repeated calls
1079        for strategy in [
1080            RetryStrategyType::Exponential,
1081            RetryStrategyType::Linear,
1082            RetryStrategyType::Constant,
1083        ] {
1084            let config = RetryStrategyConfig {
1085                strategy,
1086                base_delay: Duration::from_millis(500),
1087                max_delay: Duration::from_secs(120),
1088                jitter: 0.0,
1089                max_attempts: 20,
1090            };
1091            for attempt in 1..=10 {
1092                let a = calculate_delay(&config, attempt);
1093                let b = calculate_delay(&config, attempt);
1094                assert_eq!(a, b, "{:?} attempt {} not deterministic", strategy, attempt);
1095            }
1096        }
1097    }
1098}
1099
1100#[cfg(test)]
1101mod property_tests {
1102    use super::*;
1103    use proptest::prelude::*;
1104
1105    fn expected_exponential(base: Duration, max: Duration, attempt: u32) -> Duration {
1106        let delay = base.saturating_mul(2_u32.saturating_pow(attempt.saturating_sub(1).min(16)));
1107        delay.min(max)
1108    }
1109
1110    fn expected_linear(base: Duration, max: Duration, attempt: u32) -> Duration {
1111        base.saturating_mul(attempt).min(max)
1112    }
1113
1114    proptest! {
1115        #[test]
1116        fn exponential_delay_matches_formula_with_no_jitter(
1117            base_ms in 1u64..10_000,
1118            extra_ms in 0u64..290_000,
1119            attempt in 1u32..40,
1120        ) {
1121            let base_delay = Duration::from_millis(base_ms);
1122            let max_delay = Duration::from_millis(base_ms.saturating_add(extra_ms).min(300_000));
1123
1124            let config = RetryStrategyConfig {
1125                strategy: RetryStrategyType::Exponential,
1126                max_attempts: 100,
1127                base_delay,
1128                max_delay,
1129                jitter: 0.0,
1130            };
1131
1132            let expected = expected_exponential(base_delay, max_delay, attempt);
1133            prop_assert_eq!(calculate_delay(&config, attempt), expected);
1134            prop_assert!(calculate_delay(&config, attempt) <= max_delay);
1135        }
1136
1137        #[test]
1138        fn linear_delay_matches_formula_with_no_jitter(
1139            base_ms in 1u64..5_000,
1140            extra_ms in 0u64..295_000,
1141            attempt in 1u32..60,
1142        ) {
1143            let base_delay = Duration::from_millis(base_ms);
1144            let max_delay = Duration::from_millis(base_ms.saturating_add(extra_ms).min(300_000));
1145
1146            let config = RetryStrategyConfig {
1147                strategy: RetryStrategyType::Linear,
1148                max_attempts: 100,
1149                base_delay,
1150                max_delay,
1151                jitter: 0.0,
1152            };
1153
1154            let expected = expected_linear(base_delay, max_delay, attempt);
1155            prop_assert_eq!(calculate_delay(&config, attempt), expected);
1156            prop_assert!(calculate_delay(&config, attempt) <= max_delay);
1157        }
1158
1159        #[test]
1160        fn constant_and_immediate_hold_edge_invariants(
1161            base_ms in 0u64..20_000,
1162            extra_ms in 0u64..50_000,
1163            jitter_byte in 0u8..=255,
1164        ) {
1165            let base_delay = Duration::from_millis(base_ms);
1166            let max_delay = Duration::from_millis((base_ms + extra_ms).min(300_000));
1167            let jitter = (jitter_byte as f64) / 255.0;
1168
1169            let constant = RetryStrategyConfig {
1170                strategy: RetryStrategyType::Constant,
1171                max_attempts: 100,
1172                base_delay,
1173                max_delay,
1174                jitter,
1175            };
1176            let delay = calculate_delay(&constant, 4);
1177            prop_assert!(delay <= max_delay.saturating_mul(2));
1178
1179            let no_jitter = RetryStrategyConfig {
1180                jitter: 0.0,
1181                ..constant.clone()
1182            };
1183            prop_assert_eq!(calculate_delay(&no_jitter, 5), base_delay.min(max_delay));
1184
1185            let immediate = RetryStrategyConfig {
1186                strategy: RetryStrategyType::Immediate,
1187                ..constant
1188            };
1189            prop_assert_eq!(calculate_delay(&immediate, 9), Duration::ZERO);
1190        }
1191
1192        #[test]
1193        fn backoff_never_exceeds_max_delay(
1194            strategy_idx in 0u8..4,
1195            base_ms in 0u64..100_000,
1196            max_ms in 0u64..300_000,
1197            attempt in 1u32..100,
1198        ) {
1199            let strategy = match strategy_idx {
1200                0 => RetryStrategyType::Immediate,
1201                1 => RetryStrategyType::Exponential,
1202                2 => RetryStrategyType::Linear,
1203                _ => RetryStrategyType::Constant,
1204            };
1205
1206            let max_delay = Duration::from_millis(max_ms);
1207
1208            let config = RetryStrategyConfig {
1209                strategy,
1210                max_attempts: 100,
1211                base_delay: Duration::from_millis(base_ms),
1212                max_delay,
1213                jitter: 0.0,
1214            };
1215
1216            let delay = calculate_delay(&config, attempt);
1217            prop_assert!(
1218                delay <= max_delay,
1219                "strategy={:?} base={}ms max={}ms attempt={} => delay={:?} exceeded max_delay={:?}",
1220                strategy, base_ms, max_ms, attempt, delay, max_delay
1221            );
1222        }
1223
1224        #[test]
1225        fn jitter_delay_within_bounds(
1226            base_ms in 100u64..10_000,
1227            jitter_pct in 1u8..100,
1228            attempt in 1u32..10,
1229        ) {
1230            let jitter = (jitter_pct as f64) / 100.0;
1231            let base_delay = Duration::from_millis(base_ms);
1232            let max_delay = Duration::from_secs(3600);
1233
1234            let config = RetryStrategyConfig {
1235                strategy: RetryStrategyType::Constant,
1236                max_attempts: 100,
1237                base_delay,
1238                max_delay,
1239                jitter,
1240            };
1241
1242            let nominal = base_delay.min(max_delay);
1243            let nominal_ms = nominal.as_millis() as f64;
1244            // epsilon accounts for floating-point rounding in Duration::mul_f64
1245            let eps = 1.0;
1246            let low = Duration::from_millis(((nominal_ms * (1.0 - jitter)) - eps).max(0.0) as u64);
1247            let high = Duration::from_millis((nominal_ms * (1.0 + jitter) + eps).ceil() as u64);
1248
1249            for _ in 0..20 {
1250                let delay = calculate_delay(&config, attempt);
1251                prop_assert!(
1252                    delay >= low && delay <= high,
1253                    "jitter={} base={}ms => delay={:?} outside [{:?}, {:?}]",
1254                    jitter, base_ms, delay, low, high
1255                );
1256            }
1257        }
1258
1259        #[test]
1260        fn exponential_delays_non_decreasing_without_jitter(
1261            base_ms in 1u64..5_000,
1262            max_ms in 1u64..300_000,
1263        ) {
1264            let config = RetryStrategyConfig {
1265                strategy: RetryStrategyType::Exponential,
1266                max_attempts: 100,
1267                base_delay: Duration::from_millis(base_ms),
1268                max_delay: Duration::from_millis(max_ms),
1269                jitter: 0.0,
1270            };
1271
1272            let mut prev = Duration::ZERO;
1273            for attempt in 1..=20 {
1274                let curr = calculate_delay(&config, attempt);
1275                prop_assert!(
1276                    curr >= prev,
1277                    "delay decreased at attempt {}: {:?} < {:?}",
1278                    attempt, curr, prev
1279                );
1280                prev = curr;
1281            }
1282        }
1283
1284        #[test]
1285        fn delays_never_negative_or_panic(
1286            strategy_idx in 0u8..4,
1287            base_ms in 0u64..u64::MAX / 2,
1288            max_ms in 0u64..300_000,
1289            attempt in 0u32..200,
1290        ) {
1291            let strategy = match strategy_idx {
1292                0 => RetryStrategyType::Immediate,
1293                1 => RetryStrategyType::Exponential,
1294                2 => RetryStrategyType::Linear,
1295                _ => RetryStrategyType::Constant,
1296            };
1297
1298            let config = RetryStrategyConfig {
1299                strategy,
1300                max_attempts: 100,
1301                base_delay: Duration::from_millis(base_ms.min(300_000)),
1302                max_delay: Duration::from_millis(max_ms),
1303                jitter: 0.0,
1304            };
1305
1306            // Must not panic
1307            let delay = calculate_delay(&config, attempt);
1308            // Duration can't be negative, but verify it's bounded
1309            prop_assert!(delay <= Duration::from_millis(max_ms));
1310        }
1311    }
1312}
1313
1314#[cfg(test)]
1315mod snapshot_tests {
1316    use super::*;
1317    use insta::assert_yaml_snapshot;
1318
1319    #[test]
1320    fn snapshot_default_policy_config() {
1321        assert_yaml_snapshot!(RetryPolicy::Default.to_config());
1322    }
1323
1324    #[test]
1325    fn snapshot_aggressive_policy_config() {
1326        assert_yaml_snapshot!(RetryPolicy::Aggressive.to_config());
1327    }
1328
1329    #[test]
1330    fn snapshot_conservative_policy_config() {
1331        assert_yaml_snapshot!(RetryPolicy::Conservative.to_config());
1332    }
1333
1334    #[test]
1335    fn snapshot_custom_policy_config() {
1336        assert_yaml_snapshot!(RetryPolicy::Custom.to_config());
1337    }
1338
1339    #[test]
1340    fn snapshot_error_classes() {
1341        assert_yaml_snapshot!("retryable", ErrorClass::Retryable);
1342        assert_yaml_snapshot!("ambiguous", ErrorClass::Ambiguous);
1343        assert_yaml_snapshot!("permanent", ErrorClass::Permanent);
1344    }
1345
1346    #[test]
1347    fn snapshot_per_error_config_empty() {
1348        assert_yaml_snapshot!(PerErrorConfig::default());
1349    }
1350
1351    #[test]
1352    fn snapshot_per_error_config_with_retryable_override() {
1353        let config = PerErrorConfig {
1354            retryable: Some(RetryStrategyConfig {
1355                strategy: RetryStrategyType::Immediate,
1356                max_attempts: 10,
1357                base_delay: Duration::ZERO,
1358                max_delay: Duration::from_secs(5),
1359                jitter: 0.0,
1360            }),
1361            ambiguous: None,
1362            permanent: None,
1363        };
1364        assert_yaml_snapshot!(config);
1365    }
1366
1367    #[test]
1368    fn snapshot_delay_sequence_exponential() {
1369        let config = RetryStrategyConfig {
1370            strategy: RetryStrategyType::Exponential,
1371            base_delay: Duration::from_secs(1),
1372            max_delay: Duration::from_secs(60),
1373            jitter: 0.0,
1374            max_attempts: 10,
1375        };
1376        let delays: Vec<String> = (1..=7)
1377            .map(|a| format!("attempt {}: {:?}", a, calculate_delay(&config, a)))
1378            .collect();
1379        assert_yaml_snapshot!(delays);
1380    }
1381
1382    #[test]
1383    fn snapshot_delay_sequence_linear() {
1384        let config = RetryStrategyConfig {
1385            strategy: RetryStrategyType::Linear,
1386            base_delay: Duration::from_secs(2),
1387            max_delay: Duration::from_secs(20),
1388            jitter: 0.0,
1389            max_attempts: 10,
1390        };
1391        let delays: Vec<String> = (1..=10)
1392            .map(|a| format!("attempt {}: {:?}", a, calculate_delay(&config, a)))
1393            .collect();
1394        assert_yaml_snapshot!(delays);
1395    }
1396
1397    #[test]
1398    fn snapshot_strategy_types() {
1399        assert_yaml_snapshot!("immediate", RetryStrategyType::Immediate);
1400        assert_yaml_snapshot!("exponential", RetryStrategyType::Exponential);
1401        assert_yaml_snapshot!("linear", RetryStrategyType::Linear);
1402        assert_yaml_snapshot!("constant", RetryStrategyType::Constant);
1403    }
1404
1405    #[test]
1406    fn snapshot_debug_default_config() {
1407        insta::assert_debug_snapshot!(RetryStrategyConfig::default());
1408    }
1409
1410    #[test]
1411    fn snapshot_debug_all_policies() {
1412        insta::assert_debug_snapshot!("debug_default", RetryPolicy::Default.to_config());
1413        insta::assert_debug_snapshot!("debug_aggressive", RetryPolicy::Aggressive.to_config());
1414        insta::assert_debug_snapshot!("debug_conservative", RetryPolicy::Conservative.to_config());
1415    }
1416
1417    #[test]
1418    fn snapshot_debug_retry_policy_variants() {
1419        insta::assert_debug_snapshot!(vec![
1420            RetryPolicy::Default,
1421            RetryPolicy::Aggressive,
1422            RetryPolicy::Conservative,
1423            RetryPolicy::Custom,
1424        ]);
1425    }
1426
1427    #[test]
1428    fn snapshot_delay_sequence_constant() {
1429        let config = RetryStrategyConfig {
1430            strategy: RetryStrategyType::Constant,
1431            base_delay: Duration::from_millis(500),
1432            max_delay: Duration::from_secs(60),
1433            jitter: 0.0,
1434            max_attempts: 10,
1435        };
1436        let delays: Vec<String> = (1..=8)
1437            .map(|a| format!("attempt {}: {:?}", a, calculate_delay(&config, a)))
1438            .collect();
1439        assert_yaml_snapshot!(delays);
1440    }
1441
1442    #[test]
1443    fn snapshot_all_strategies_at_attempt_5() {
1444        let make = |s| RetryStrategyConfig {
1445            strategy: s,
1446            base_delay: Duration::from_secs(1),
1447            max_delay: Duration::from_secs(120),
1448            jitter: 0.0,
1449            max_attempts: 10,
1450        };
1451        let result: Vec<String> = [
1452            RetryStrategyType::Immediate,
1453            RetryStrategyType::Exponential,
1454            RetryStrategyType::Linear,
1455            RetryStrategyType::Constant,
1456        ]
1457        .iter()
1458        .map(|&s| format!("{:?}: {:?}", s, calculate_delay(&make(s), 5)))
1459        .collect();
1460        assert_yaml_snapshot!(result);
1461    }
1462}