1
#![cfg_attr(docsrs, feature(doc_cfg))]
2
#![doc = include_str!("../README.md")]
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6
#![warn(missing_docs)]
7
#![warn(noop_method_call)]
8
#![warn(unreachable_pub)]
9
#![warn(clippy::all)]
10
#![deny(clippy::await_holding_lock)]
11
#![deny(clippy::cargo_common_metadata)]
12
#![deny(clippy::cast_lossless)]
13
#![deny(clippy::checked_conversions)]
14
#![warn(clippy::cognitive_complexity)]
15
#![deny(clippy::debug_assert_with_mut_call)]
16
#![deny(clippy::exhaustive_enums)]
17
#![deny(clippy::exhaustive_structs)]
18
#![deny(clippy::expl_impl_clone_on_copy)]
19
#![deny(clippy::fallible_impl_from)]
20
#![deny(clippy::implicit_clone)]
21
#![deny(clippy::large_stack_arrays)]
22
#![warn(clippy::manual_ok_or)]
23
#![deny(clippy::missing_docs_in_private_items)]
24
#![warn(clippy::needless_borrow)]
25
#![warn(clippy::needless_pass_by_value)]
26
#![warn(clippy::option_option)]
27
#![deny(clippy::print_stderr)]
28
#![deny(clippy::print_stdout)]
29
#![warn(clippy::rc_buffer)]
30
#![deny(clippy::ref_option_ref)]
31
#![warn(clippy::semicolon_if_nothing_returned)]
32
#![warn(clippy::trait_duplication_in_bounds)]
33
#![deny(clippy::unchecked_time_subtraction)]
34
#![deny(clippy::unnecessary_wraps)]
35
#![warn(clippy::unseparated_literal_suffix)]
36
#![deny(clippy::unwrap_used)]
37
#![deny(clippy::mod_module_files)]
38
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39
#![allow(clippy::uninlined_format_args)]
40
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43
#![allow(clippy::needless_lifetimes)] // See arti#1765
44
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45
#![allow(clippy::collapsible_if)] // See arti#2342
46
#![deny(clippy::unused_async)]
47
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
48

            
49
use std::error::Error;
50
use std::fmt::{self, Debug, Display, Error as FmtError, Formatter};
51
use std::iter;
52
use std::time::{Duration, Instant, SystemTime};
53

            
54
/// An error type for use when we're going to do something a few times,
55
/// and they might all fail.
56
///
57
/// To use this error type, initialize a new RetryError before you
58
/// start trying to do whatever it is.  Then, every time the operation
59
/// fails, use [`RetryError::push()`] to add a new error to the list
60
/// of errors.  If the operation fails too many times, you can use
61
/// RetryError as an [`Error`] itself.
62
///
63
/// This type now tracks timestamps for each error occurrence, allowing
64
/// users to see when errors occurred and how long the retry process took.
65
#[derive(Debug, Clone)]
66
pub struct RetryError<E> {
67
    /// The operation we were trying to do.
68
    doing: String,
69
    /// The errors that we encountered when doing the operation.
70
    errors: Vec<(Attempt, E, Instant)>,
71
    /// The total number of errors we encountered.
72
    ///
73
    /// This can differ from errors.len() if the errors have been
74
    /// deduplicated.
75
    n_errors: usize,
76
    /// The wall-clock time when the first error occurred.
77
    ///
78
    /// This is used for human-readable display of absolute timestamps.
79
    ///
80
    /// We store both types because they serve different purposes:
81
    /// - `Instant` (in the errors vec): Monotonic clock for reliable duration calculations.
82
    ///   Immune to clock adjustments, but can't be displayed as wall-clock time.
83
    /// - `SystemTime` (here): Wall-clock time for displaying when the first error occurred
84
    ///   in a human-readable format (e.g., "2025-12-09T10:24:02Z").
85
    ///
86
    /// We only store `SystemTime` for the first error to show users *when* the problem
87
    /// started. Subsequent errors are displayed relative to the first ("+2m 30s"),
88
    /// using the reliable `Instant` timestamps.
89
    first_error_at: Option<SystemTime>,
90
}
91

            
92
/// Represents which attempts, in sequence, failed to complete.
93
#[derive(Debug, Clone)]
94
enum Attempt {
95
    /// A single attempt that failed.
96
    Single(usize),
97
    /// A range of consecutive attempts that failed.
98
    Range(usize, usize),
99
}
100

            
101
// TODO: Should we declare that some error is the 'source' of this one?
102
// If so, should it be the first failure?  The last?
103
impl<E: Debug + AsRef<dyn Error>> Error for RetryError<E> {}
104

            
105
impl<E> RetryError<E> {
106
    /// Create a new RetryError, with no failed attempts.
107
    ///
108
    /// The provided `doing` argument is a short string that describes
109
    /// what we were trying to do when we failed too many times.  It
110
    /// will be used to format the final error message; it should be a
111
    /// phrase that can go after "while trying to".
112
    ///
113
    /// This RetryError should not be used as-is, since when no
114
    /// [`Error`]s have been pushed into it, it doesn't represent an
115
    /// actual failure.
116
796
    pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
117
796
        RetryError {
118
796
            doing: doing.into(),
119
796
            errors: Vec::new(),
120
796
            n_errors: 0,
121
796
            first_error_at: None,
122
796
        }
123
796
    }
124
    /// Add an error to this RetryError with explicit timestamps.
125
    ///
126
    /// You should call this method when an attempt at the underlying operation
127
    /// has failed.
128
    ///
129
    /// The `instant` parameter should be the monotonic time when the error
130
    /// occurred, typically obtained from a runtime's `now()` method.
131
    ///
132
    /// The `wall_clock` parameter is the wall-clock time when the error occurred,
133
    /// used for human-readable display. Pass `None` to skip wall-clock tracking,
134
    /// or `Some(SystemTime::now())` for the current time.
135
    ///
136
    /// # Example
137
    /// ```
138
    /// # use retry_error::RetryError;
139
    /// # use std::time::{Instant, SystemTime};
140
    /// let mut retry_err: RetryError<&str> = RetryError::in_attempt_to("connect");
141
    /// let now = Instant::now();
142
    /// retry_err.push_timed("connection failed", now, Some(SystemTime::now()));
143
    /// ```
144
256
    pub fn push_timed<T>(&mut self, err: T, instant: Instant, wall_clock: Option<SystemTime>)
145
256
    where
146
256
        T: Into<E>,
147
    {
148
256
        if self.n_errors < usize::MAX {
149
254
            self.n_errors += 1;
150
254
            let attempt = Attempt::Single(self.n_errors);
151

            
152
254
            if self.first_error_at.is_none() {
153
218
                self.first_error_at = wall_clock;
154
240
            }
155

            
156
254
            self.errors.push((attempt, err.into(), instant));
157
2
        }
158
256
    }
159

            
160
    /// Add an error to this RetryError using the current time.
161
    ///
162
    /// You should call this method when an attempt at the underlying operation
163
    /// has failed.
164
    ///
165
    /// This is a convenience wrapper around [`push_timed()`](Self::push_timed)
166
    /// that uses `Instant::now()` and `SystemTime::now()` for the timestamps.
167
    /// For code that needs mockable time (such as in tests), prefer `push_timed()`.
168
20
    pub fn push<T>(&mut self, err: T)
169
20
    where
170
20
        T: Into<E>,
171
    {
172
20
        self.push_timed(err, Instant::now(), Some(SystemTime::now()));
173
20
    }
174

            
175
    /// Return an iterator over all of the reasons that the attempt
176
    /// behind this RetryError has failed.
177
84
    pub fn sources(&self) -> impl Iterator<Item = &E> {
178
84
        self.errors.iter().map(|(.., e, _)| e)
179
84
    }
180

            
181
    /// Return the number of underlying errors.
182
16
    pub fn len(&self) -> usize {
183
16
        self.errors.len()
184
16
    }
185

            
186
    /// Return true if no underlying errors have been added.
187
6
    pub fn is_empty(&self) -> bool {
188
6
        self.errors.is_empty()
189
6
    }
190

            
191
    /// Add multiple errors to this RetryError using the current time.
192
    ///
193
    /// This method uses [`push()`](Self::push) internally, which captures
194
    /// `SystemTime::now()`. For code that needs mockable time (such as in tests),
195
    /// iterate manually and call [`push_timed()`](Self::push_timed) instead.
196
    ///
197
    /// # Example
198
    /// ```
199
    /// # use retry_error::RetryError;
200
    /// let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("parse");
201
    /// let errors = vec!["error1", "error2"].into_iter().map(anyhow::Error::msg);
202
    /// err.extend(errors);
203
    /// ```
204
    #[allow(clippy::disallowed_methods)] // This method intentionally uses push()
205
2
    pub fn extend<T>(&mut self, iter: impl IntoIterator<Item = T>)
206
2
    where
207
2
        T: Into<E>,
208
    {
209
8
        for item in iter {
210
6
            self.push(item);
211
6
        }
212
2
    }
213

            
214
    /// Group up consecutive errors of the same kind, for easier display.
215
    ///
216
    /// Two errors have "the same kind" if they return `true` when passed
217
    /// to the provided `dedup` function.
218
4
    pub fn dedup_by<F>(&mut self, same_err: F)
219
4
    where
220
4
        F: Fn(&E, &E) -> bool,
221
    {
222
4
        let mut old_errs = Vec::new();
223
4
        std::mem::swap(&mut old_errs, &mut self.errors);
224

            
225
16
        for (attempt, err, timestamp) in old_errs {
226
12
            if let Some((last_attempt, last_err, ..)) = self.errors.last_mut() {
227
8
                if same_err(last_err, &err) {
228
8
                    last_attempt.grow();
229
8
                } else {
230
                    self.errors.push((attempt, err, timestamp));
231
                }
232
4
            } else {
233
4
                self.errors.push((attempt, err, timestamp));
234
4
            }
235
        }
236
4
    }
237

            
238
    /// Add multiple errors to this RetryError, preserving their original timestamps.
239
    ///
240
    /// The errors from other will be added to this RetryError, with their original
241
    /// timestamps retained. The `Attempt` counters will be updated to continue from
242
    /// the current state of this RetryError. `Attempt::Range` entries are preserved as ranges
243
84
    pub fn extend_from_retry_error(&mut self, other: RetryError<E>) {
244
84
        if self.first_error_at.is_none() {
245
14
            self.first_error_at = other.first_error_at;
246
70
        }
247

            
248
170
        for (attempt, err, timestamp) in other.errors {
249
86
            let new_attempt = match attempt {
250
                Attempt::Single(_) => {
251
84
                    let Some(new_n_errors) = self.n_errors.checked_add(1) else {
252
                        break;
253
                    };
254
84
                    self.n_errors = new_n_errors;
255
84
                    Attempt::Single(new_n_errors)
256
                }
257
2
                Attempt::Range(first, last) => {
258
2
                    let count = last - first + 1;
259
2
                    let Some(new_n_errors) = self.n_errors.checked_add(count) else {
260
                        break;
261
                    };
262
2
                    let start = self.n_errors + 1;
263
2
                    self.n_errors = new_n_errors;
264
2
                    Attempt::Range(start, new_n_errors)
265
                }
266
            };
267

            
268
86
            self.errors.push((new_attempt, err, timestamp));
269
        }
270
84
    }
271
}
272

            
273
impl<E: PartialEq<E>> RetryError<E> {
274
    /// Group up consecutive errors of the same kind, according to the
275
    /// `PartialEq` implementation.
276
2
    pub fn dedup(&mut self) {
277
2
        self.dedup_by(PartialEq::eq);
278
2
    }
279
}
280

            
281
impl Attempt {
282
    /// Extend this attempt by a single additional failure.
283
8
    fn grow(&mut self) {
284
8
        *self = match *self {
285
4
            Attempt::Single(idx) => Attempt::Range(idx, idx + 1),
286
4
            Attempt::Range(first, last) => Attempt::Range(first, last + 1),
287
        };
288
8
    }
289
}
290

            
291
impl<E> IntoIterator for RetryError<E> {
292
    type Item = E;
293
    type IntoIter = std::vec::IntoIter<E>;
294
    #[allow(clippy::needless_collect)]
295
    // TODO We have to use collect/into_iter here for now, since
296
    // the actual Map<> type can't be named.  Once Rust lets us say
297
    // `type IntoIter = impl Iterator<Item=E>` then we fix the code
298
    // and turn the Clippy warning back on.
299
2
    fn into_iter(self) -> Self::IntoIter {
300
2
        self.errors
301
2
            .into_iter()
302
2
            .map(|(.., e, _)| e)
303
2
            .collect::<Vec<_>>()
304
2
            .into_iter()
305
2
    }
306
}
307

            
308
impl Display for Attempt {
309
16
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
310
16
        match self {
311
12
            Attempt::Single(idx) => write!(f, "Attempt {}", idx),
312
4
            Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
313
        }
314
16
    }
315
}
316

            
317
impl<E: AsRef<dyn Error>> Display for RetryError<E> {
318
14
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
319
14
        let show_timestamps = f.alternate();
320

            
321
14
        match self.n_errors {
322
2
            0 => write!(f, "Unable to {}. (No errors given)", self.doing),
323
            1 => {
324
4
                write!(f, "Unable to {}", self.doing)?;
325

            
326
4
                if show_timestamps {
327
2
                    if let (Some((.., timestamp)), Some(first_at)) =
328
2
                        (self.errors.first(), self.first_error_at)
329
                    {
330
2
                        write!(
331
2
                            f,
332
2
                            " at {} ({})",
333
2
                            humantime::format_rfc3339(first_at),
334
2
                            FormatTimeAgo(timestamp.elapsed())
335
                        )?;
336
                    }
337
2
                }
338

            
339
4
                write!(f, ": ")?;
340
4
                fmt_error_with_sources(self.errors[0].1.as_ref(), f)
341
            }
342
8
            n => {
343
8
                write!(
344
8
                    f,
345
8
                    "Tried to {} {} times, but all attempts failed",
346
                    self.doing, n
347
                )?;
348

            
349
8
                if show_timestamps {
350
4
                    if let (Some(first_at), Some((.., first_ts)), Some((.., last_ts))) =
351
4
                        (self.first_error_at, self.errors.first(), self.errors.last())
352
                    {
353
4
                        let duration = last_ts.saturating_duration_since(*first_ts);
354

            
355
4
                        write!(f, " (from {} ", humantime::format_rfc3339(first_at))?;
356

            
357
4
                        if duration.as_secs() > 0 {
358
                            write!(f, "to {}", humantime::format_rfc3339(first_at + duration))?;
359
4
                        }
360

            
361
4
                        write!(f, ", {})", FormatTimeAgo(last_ts.elapsed()))?;
362
                    }
363
4
                }
364

            
365
8
                let first_ts = self.errors.first().map(|(.., ts)| ts);
366
24
                for (attempt, e, timestamp) in &self.errors {
367
16
                    write!(f, "\n{}", attempt)?;
368

            
369
16
                    if show_timestamps {
370
8
                        if let Some(first_ts) = first_ts {
371
8
                            let offset = timestamp.saturating_duration_since(*first_ts);
372
8
                            if offset.as_secs() > 0 {
373
                                write!(f, " (+{})", FormatDuration(offset))?;
374
8
                            }
375
                        }
376
8
                    }
377

            
378
16
                    write!(f, ": ")?;
379
16
                    fmt_error_with_sources(e.as_ref(), f)?;
380
                }
381
8
                Ok(())
382
            }
383
        }
384
14
    }
385
}
386

            
387
/// A wrapper for formatting a [`Duration`] in a human-readable way.
388
/// Produces output like "2m 30s", "5h 12m", "45s", "500ms".
389
///
390
/// We use this instead of `humantime::format_duration` because humantime tends to produce overly verbose output.
391
struct FormatDuration(Duration);
392

            
393
impl Display for FormatDuration {
394
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
395
        fmt_duration_impl(self.0, f)
396
    }
397
}
398

            
399
/// A wrapper for formatting a [`Duration`] with "ago" suffix.
400
struct FormatTimeAgo(Duration);
401

            
402
impl Display for FormatTimeAgo {
403
6
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
404
6
        let secs = self.0.as_secs();
405
6
        let millis = self.0.as_millis();
406

            
407
        // Special case: very recent times show as "just now" rather than "0s ago" or "0ms ago"
408
6
        if secs == 0 && millis == 0 {
409
6
            return write!(f, "just now");
410
        }
411

            
412
        fmt_duration_impl(self.0, f)?;
413
        write!(f, " ago")
414
6
    }
415
}
416

            
417
/// Internal helper to format a duration.
418
///
419
/// This function contains the actual formatting logic to avoid duplication
420
/// between `FormatDuration` and `FormatTimeAgo`.
421
fn fmt_duration_impl(duration: Duration, f: &mut Formatter<'_>) -> fmt::Result {
422
    let secs = duration.as_secs();
423

            
424
    if secs == 0 {
425
        let millis = duration.as_millis();
426
        if millis == 0 {
427
            write!(f, "0s")
428
        } else {
429
            write!(f, "{}ms", millis)
430
        }
431
    } else if secs < 60 {
432
        write!(f, "{}s", secs)
433
    } else if secs < 3600 {
434
        let mins = secs / 60;
435
        let rem_secs = secs % 60;
436
        if rem_secs == 0 {
437
            write!(f, "{}m", mins)
438
        } else {
439
            write!(f, "{}m {}s", mins, rem_secs)
440
        }
441
    } else {
442
        let hours = secs / 3600;
443
        let mins = (secs % 3600) / 60;
444
        if mins == 0 {
445
            write!(f, "{}h", hours)
446
        } else {
447
            write!(f, "{}h {}m", hours, mins)
448
        }
449
    }
450
}
451

            
452
/// Helper: formats a [`std::error::Error`] and its sources (as `"error: source"`)
453
///
454
/// Avoids duplication in messages by not printing messages which are
455
/// wholly-contained (textually) within already-printed messages.
456
///
457
/// Offered as a `fmt` function:
458
/// this is for use in more-convenient higher-level error handling functionality,
459
/// rather than directly in application/functional code.
460
///
461
/// This is used by `RetryError`'s impl of `Display`,
462
/// but will be useful for other error-handling situations.
463
///
464
/// # Example
465
///
466
/// ```
467
/// use std::fmt::{self, Display};
468
///
469
/// #[derive(Debug, thiserror::Error)]
470
/// #[error("some pernickety problem")]
471
/// struct Pernickety;
472
///
473
/// #[derive(Debug, thiserror::Error)]
474
/// enum ApplicationError {
475
///     #[error("everything is terrible")]
476
///     Terrible(#[source] Pernickety),
477
/// }
478
///
479
/// struct Wrapper(Box<dyn std::error::Error>);
480
/// impl Display for Wrapper {
481
///     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
482
///         retry_error::fmt_error_with_sources(&*self.0, f)
483
///     }
484
/// }
485
///
486
/// let bad = Pernickety;
487
/// let err = ApplicationError::Terrible(bad);
488
///
489
/// let printed = Wrapper(err.into()).to_string();
490
/// assert_eq!(printed, "everything is terrible: some pernickety problem");
491
/// ```
492
5090
pub fn fmt_error_with_sources(mut e: &dyn Error, f: &mut fmt::Formatter) -> fmt::Result {
493
    // We deduplicate the errors here under the assumption that the `Error` trait is poorly defined
494
    // and contradictory, and that some error types will duplicate error messages. This is
495
    // controversial, and since there isn't necessarily agreement, we should stick with the status
496
    // quo here and avoid changing this behaviour without further discussion.
497
5090
    let mut last = String::new();
498
5090
    let mut sep = iter::once("").chain(iter::repeat(": "));
499

            
500
    // Note that this loop does not use tor_basic_utils::ErrorSources.  We can't, because `e` is not
501
    // `Error + 'static`.  But we shouldn't use ErrorSources here, since io::Error will format
502
    // its inner by_ref() error, and so it's desirable that `source` skips over it.
503
    loop {
504
6780
        let this = e.to_string();
505
6780
        if !last.contains(&this) {
506
6585
            write!(f, "{}{}", sep.next().expect("repeat ended"), &this)?;
507
195
        }
508
6780
        last = this;
509

            
510
6780
        if let Some(ne) = e.source() {
511
1690
            e = ne;
512
1690
        } else {
513
5090
            break;
514
        }
515
    }
516
5090
    Ok(())
517
5090
}
518

            
519
#[cfg(test)]
520
mod test {
521
    // @@ begin test lint list maintained by maint/add_warning @@
522
    #![allow(clippy::bool_assert_comparison)]
523
    #![allow(clippy::clone_on_copy)]
524
    #![allow(clippy::dbg_macro)]
525
    #![allow(clippy::mixed_attributes_style)]
526
    #![allow(clippy::print_stderr)]
527
    #![allow(clippy::print_stdout)]
528
    #![allow(clippy::single_char_pattern)]
529
    #![allow(clippy::unwrap_used)]
530
    #![allow(clippy::unchecked_time_subtraction)]
531
    #![allow(clippy::useless_vec)]
532
    #![allow(clippy::needless_pass_by_value)]
533
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
534
    #![allow(clippy::disallowed_methods)]
535
    use super::*;
536
    use derive_more::From;
537

            
538
    #[test]
539
    fn bad_parse1() {
540
        let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
541
        if let Err(e) = "maybe".parse::<bool>() {
542
            err.push(e);
543
        }
544
        if let Err(e) = "a few".parse::<u32>() {
545
            err.push(e);
546
        }
547
        if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
548
            err.push(e);
549
        }
550

            
551
        let disp = format!("{}", err);
552
        assert_eq!(
553
            disp,
554
            "\
555
Tried to convert some things 3 times, but all attempts failed
556
Attempt 1: provided string was not `true` or `false`
557
Attempt 2: invalid digit found in string
558
Attempt 3: invalid IP address syntax"
559
        );
560

            
561
        let disp_alt = format!("{:#}", err);
562
        assert!(disp_alt.contains("Tried to convert some things 3 times, but all attempts failed"));
563
        assert!(disp_alt.contains("(from 20")); // Year prefix for timestamp
564
    }
565

            
566
    #[test]
567
    fn no_problems() {
568
        let empty: RetryError<anyhow::Error> =
569
            RetryError::in_attempt_to("immanentize the eschaton");
570
        let disp = format!("{}", empty);
571
        assert_eq!(
572
            disp,
573
            "Unable to immanentize the eschaton. (No errors given)"
574
        );
575
    }
576

            
577
    #[test]
578
    fn one_problem() {
579
        let mut err: RetryError<anyhow::Error> =
580
            RetryError::in_attempt_to("connect to torproject.org");
581
        if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
582
            err.push(e);
583
        }
584
        let disp = format!("{}", err);
585
        assert_eq!(
586
            disp,
587
            "Unable to connect to torproject.org: invalid IP address syntax"
588
        );
589

            
590
        let disp_alt = format!("{:#}", err);
591
        assert!(disp_alt.contains("Unable to connect to torproject.org at 20")); // Year prefix
592
        assert!(disp_alt.contains("invalid IP address syntax"));
593
    }
594

            
595
    #[test]
596
    fn operations() {
597
        use std::num::ParseIntError;
598

            
599
        #[derive(From, Clone, Debug, Eq, PartialEq)]
600
        struct Wrapper(ParseIntError);
601

            
602
        impl AsRef<dyn Error + 'static> for Wrapper {
603
            fn as_ref(&self) -> &(dyn Error + 'static) {
604
                &self.0
605
            }
606
        }
607

            
608
        let mut err: RetryError<Wrapper> = RetryError::in_attempt_to("parse some integers");
609
        assert!(err.is_empty());
610
        assert_eq!(err.len(), 0);
611
        err.extend(
612
            vec!["not", "your", "number"]
613
                .iter()
614
                .filter_map(|s| s.parse::<u16>().err())
615
                .map(Wrapper),
616
        );
617
        assert!(!err.is_empty());
618
        assert_eq!(err.len(), 3);
619

            
620
        let cloned = err.clone();
621
        for (s1, s2) in err.sources().zip(cloned.sources()) {
622
            assert_eq!(s1, s2);
623
        }
624

            
625
        err.dedup();
626

            
627
        let disp = format!("{}", err);
628
        assert_eq!(
629
            disp,
630
            "\
631
Tried to parse some integers 3 times, but all attempts failed
632
Attempts 1..3: invalid digit found in string"
633
        );
634

            
635
        let disp_alt = format!("{:#}", err);
636
        assert!(disp_alt.contains("Tried to parse some integers 3 times, but all attempts failed"));
637
        assert!(disp_alt.contains("(from 20")); // Year prefix for timestamp
638
    }
639

            
640
    #[test]
641
    fn overflow() {
642
        use std::num::ParseIntError;
643
        let mut err: RetryError<ParseIntError> =
644
            RetryError::in_attempt_to("parse too many integers");
645
        assert!(err.is_empty());
646
        let mut errors: Vec<ParseIntError> = vec!["no", "numbers"]
647
            .iter()
648
            .filter_map(|s| s.parse::<u16>().err())
649
            .collect();
650
        err.n_errors = usize::MAX;
651
        err.errors.push((
652
            Attempt::Range(1, err.n_errors),
653
            errors.pop().expect("parser did not fail"),
654
            Instant::now(),
655
        ));
656
        assert!(err.n_errors == usize::MAX);
657
        assert!(err.len() == 1);
658

            
659
        err.push(errors.pop().expect("parser did not fail"));
660
        assert!(err.n_errors == usize::MAX);
661
        assert!(err.len() == 1);
662
    }
663

            
664
    #[test]
665
    fn extend_from_retry_preserve_timestamps() {
666
        let n1 = Instant::now();
667
        let n2 = n1 + Duration::from_secs(10);
668
        let n3 = n1 + Duration::from_secs(20);
669

            
670
        let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do first thing");
671
        let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do second thing");
672

            
673
        err2.push_timed(anyhow::Error::msg("e1"), n1, None);
674
        err2.push_timed(anyhow::Error::msg("e2"), n2, None);
675

            
676
        // err1 is empty initially
677
        assert!(err1.first_error_at.is_none());
678

            
679
        err1.extend_from_retry_error(err2);
680

            
681
        assert_eq!(err1.len(), 2);
682
        // The timestamps should be preserved
683
        assert_eq!(err1.errors[0].2, n1);
684
        assert_eq!(err1.errors[1].2, n2);
685

            
686
        // Add another error to err1 to ensure mixed sources work
687
        err1.push_timed(anyhow::Error::msg("e3"), n3, None);
688
        assert_eq!(err1.len(), 3);
689
        assert_eq!(err1.errors[2].2, n3);
690
    }
691

            
692
    #[test]
693
    fn extend_from_retry_preserve_ranges() {
694
        let n1 = Instant::now();
695
        let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 1");
696

            
697
        // Push 2 errors
698
        err1.push(anyhow::Error::msg("e1"));
699
        err1.push(anyhow::Error::msg("e2"));
700
        assert_eq!(err1.n_errors, 2);
701

            
702
        let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 2");
703
        // Push 3 identical errors to create a range
704
        let _err_msg = anyhow::Error::msg("repeated");
705
        err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
706
        err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
707
        err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
708

            
709
        // Dedup err2 so it has a range
710
        err2.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
711
        assert_eq!(err2.len(), 1); // collapsed to 1 entry
712
        match err2.errors[0].0 {
713
            Attempt::Range(1, 3) => {}
714
            _ => panic!("Expected range 1..3"),
715
        }
716

            
717
        // Extend err1 with err2
718
        err1.extend_from_retry_error(err2);
719

            
720
        assert_eq!(err1.len(), 3); // 2 singles + 1 range
721
        assert_eq!(err1.n_errors, 5); // 2 + 3 = 5 total attempts
722

            
723
        // Check the range indices
724
        match err1.errors[2].0 {
725
            Attempt::Range(3, 5) => {}
726
            ref x => panic!("Expected range 3..5, got {:?}", x),
727
        }
728
    }
729
}