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
#![allow(clippy::cognitive_complexity)] // See arti#2556
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
#![warn(clippy::or_fun_call)]
28
#![deny(clippy::print_stderr)]
29
#![deny(clippy::print_stdout)]
30
#![warn(clippy::rc_buffer)]
31
#![deny(clippy::ref_option_ref)]
32
#![warn(clippy::semicolon_if_nothing_returned)]
33
#![warn(clippy::trait_duplication_in_bounds)]
34
#![deny(clippy::unchecked_time_subtraction)]
35
#![deny(clippy::unnecessary_wraps)]
36
#![warn(clippy::unseparated_literal_suffix)]
37
#![deny(clippy::unwrap_used)]
38
#![deny(clippy::mod_module_files)]
39
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
40
#![allow(clippy::uninlined_format_args)]
41
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
42
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
43
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
44
#![allow(clippy::needless_lifetimes)] // See arti#1765
45
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
46
#![allow(clippy::collapsible_if)] // See arti#2342
47
#![deny(clippy::unused_async)]
48
#![deny(clippy::string_slice)] // See arti#2571
49
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
50
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
51

            
52
use std::error::Error;
53
use std::fmt::{self, Debug, Display, Error as FmtError, Formatter};
54
use std::iter;
55
use std::time::{Duration, SystemTime};
56

            
57
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
58
use web_time::Instant;
59

            
60
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
61
use std::time::Instant;
62

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

            
101
/// Represents which attempts, in sequence, failed to complete.
102
#[derive(Debug, Clone)]
103
enum Attempt {
104
    /// A single attempt that failed.
105
    Single(usize),
106
    /// A range of consecutive attempts that failed.
107
    Range(usize, usize),
108
}
109

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

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

            
162
198
            if self.first_error_at.is_none() {
163
124
                self.first_error_at = wall_clock;
164
179
            }
165

            
166
198
            self.errors.push((attempt, err.into(), instant));
167
1
        }
168
199
    }
169

            
170
    /// Add an error to this RetryError using the current time.
171
    ///
172
    /// You should call this method when an attempt at the underlying operation
173
    /// has failed.
174
    ///
175
    /// This is a convenience wrapper around [`push_timed()`](Self::push_timed)
176
    /// that uses `Instant::now()` and `SystemTime::now()` for the timestamps.
177
    /// For code that needs mockable time (such as in tests), prefer `push_timed()`.
178
11
    pub fn push<T>(&mut self, err: T)
179
11
    where
180
11
        T: Into<E>,
181
    {
182
11
        self.push_timed(err, current_instant(), Some(current_system_time()));
183
11
    }
184

            
185
    /// Return an iterator over all of the reasons that the attempt
186
    /// behind this RetryError has failed.
187
42
    pub fn sources(&self) -> impl Iterator<Item = &E> {
188
42
        self.errors.iter().map(|(.., e, _)| e)
189
42
    }
190

            
191
    /// Return the number of underlying errors.
192
11
    pub fn len(&self) -> usize {
193
11
        self.errors.len()
194
11
    }
195

            
196
    /// Return true if no underlying errors have been added.
197
3
    pub fn is_empty(&self) -> bool {
198
3
        self.errors.is_empty()
199
3
    }
200

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

            
224
    /// Group up consecutive errors of the same kind, for easier display.
225
    ///
226
    /// Two errors have "the same kind" if they return `true` when passed
227
    /// to the provided `same_err` function.
228
4
    pub fn dedup_by<F>(&mut self, same_err: F)
229
4
    where
230
4
        F: Fn(&E, &E) -> bool,
231
    {
232
4
        let mut old_errs = Vec::new();
233
4
        std::mem::swap(&mut old_errs, &mut self.errors);
234

            
235
10
        for (attempt, err, timestamp) in old_errs {
236
10
            if let Some((last_attempt, last_err, ..)) = self.errors.last_mut() {
237
6
                if same_err(last_err, &err) {
238
6
                    last_attempt.grow(attempt.count());
239
6
                } else {
240
                    self.errors.push((attempt, err, timestamp));
241
                }
242
4
            } else {
243
4
                self.errors.push((attempt, err, timestamp));
244
4
            }
245
        }
246
4
    }
247

            
248
    /// Add multiple errors to this RetryError, preserving their original timestamps.
249
    ///
250
    /// The errors from other will be added to this RetryError, with their original
251
    /// timestamps retained. The `Attempt` counters will be updated to continue from
252
    /// the current state of this RetryError. `Attempt::Range` entries are preserved as ranges
253
43
    pub fn extend_from_retry_error(&mut self, other: RetryError<E>) {
254
43
        if self.first_error_at.is_none() {
255
7
            self.first_error_at = other.first_error_at;
256
36
        }
257

            
258
44
        for (attempt, err, timestamp) in other.errors {
259
44
            let Some(new_n_errors) = self.n_errors.checked_add(attempt.count()) else {
260
                break;
261
            };
262

            
263
44
            let new_attempt = match attempt {
264
42
                Attempt::Single(_) => Attempt::Single(new_n_errors),
265
2
                Attempt::Range(_, _) => Attempt::Range(self.n_errors + 1, new_n_errors),
266
            };
267

            
268
44
            self.errors.push((new_attempt, err, timestamp));
269
44
            self.n_errors = new_n_errors;
270
        }
271
43
    }
272
}
273

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

            
282
impl Attempt {
283
    /// Extend this attempt by additional failures.
284
6
    fn grow(&mut self, count: usize) {
285
6
        *self = match *self {
286
4
            Attempt::Single(idx) => Attempt::Range(idx, idx + count),
287
2
            Attempt::Range(first, last) => Attempt::Range(first, last + count),
288
        };
289
6
    }
290

            
291
    /// Return amount of failures.
292
3610
    fn count(&self) -> usize {
293
3610
        match *self {
294
3607
            Attempt::Single(_) => 1,
295
3
            Attempt::Range(first, last) => last - first + 1,
296
        }
297
3610
    }
298
}
299

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

            
317
impl Display for Attempt {
318
8
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
319
8
        match self {
320
6
            Attempt::Single(idx) => write!(f, "Attempt {}", idx),
321
2
            Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
322
        }
323
8
    }
324
}
325

            
326
impl<E: AsRef<dyn Error>> Display for RetryError<E> {
327
7
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
328
7
        let show_timestamps = f.alternate();
329

            
330
7
        match self.n_errors {
331
1
            0 => write!(f, "Unable to {}. (No errors given)", self.doing),
332
            1 => {
333
2
                write!(f, "Unable to {}", self.doing)?;
334

            
335
2
                if show_timestamps {
336
1
                    if let (Some((.., timestamp)), Some(first_at)) =
337
1
                        (self.errors.first(), self.first_error_at)
338
                    {
339
1
                        write!(
340
1
                            f,
341
                            " at {} ({})",
342
1
                            humantime::format_rfc3339(first_at),
343
1
                            FormatTimeAgo(timestamp.elapsed())
344
                        )?;
345
                    }
346
1
                }
347

            
348
2
                write!(f, ": ")?;
349
2
                fmt_error_with_sources(self.errors[0].1.as_ref(), f)
350
            }
351
4
            n => {
352
4
                write!(
353
4
                    f,
354
                    "Tried to {} {} times, but all attempts failed",
355
                    self.doing, n
356
                )?;
357

            
358
4
                if show_timestamps {
359
2
                    if let (Some(first_at), Some((.., first_ts)), Some((.., last_ts))) =
360
2
                        (self.first_error_at, self.errors.first(), self.errors.last())
361
                    {
362
2
                        let duration = last_ts.saturating_duration_since(*first_ts);
363

            
364
2
                        write!(f, " (from {} ", humantime::format_rfc3339(first_at))?;
365

            
366
2
                        if duration.as_secs() > 0 {
367
                            write!(f, "to {}", humantime::format_rfc3339(first_at + duration))?;
368
2
                        }
369

            
370
2
                        write!(f, ", {})", FormatTimeAgo(last_ts.elapsed()))?;
371
                    }
372
2
                }
373

            
374
4
                let first_ts = self.errors.first().map(|(.., ts)| ts);
375
8
                for (attempt, e, timestamp) in &self.errors {
376
8
                    write!(f, "\n{}", attempt)?;
377

            
378
8
                    if show_timestamps {
379
4
                        if let Some(first_ts) = first_ts {
380
4
                            let offset = timestamp.saturating_duration_since(*first_ts);
381
4
                            if offset.as_secs() > 0 {
382
                                write!(f, " (+{})", FormatDuration(offset))?;
383
4
                            }
384
                        }
385
4
                    }
386

            
387
8
                    write!(f, ": ")?;
388
8
                    fmt_error_with_sources(e.as_ref(), f)?;
389
                }
390
4
                Ok(())
391
            }
392
        }
393
7
    }
394
}
395

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

            
402
impl Display for FormatDuration {
403
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
404
        fmt_duration_impl(self.0, f)
405
    }
406
}
407

            
408
/// A wrapper for formatting a [`Duration`] with "ago" suffix.
409
struct FormatTimeAgo(Duration);
410

            
411
impl Display for FormatTimeAgo {
412
3
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
413
3
        let secs = self.0.as_secs();
414
3
        let millis = self.0.as_millis();
415

            
416
        // Special case: very recent times show as "just now" rather than "0s ago" or "0ms ago"
417
3
        if secs == 0 && millis == 0 {
418
3
            return write!(f, "just now");
419
        }
420

            
421
        fmt_duration_impl(self.0, f)?;
422
        write!(f, " ago")
423
3
    }
424
}
425

            
426
/// Internal helper to format a duration.
427
///
428
/// This function contains the actual formatting logic to avoid duplication
429
/// between `FormatDuration` and `FormatTimeAgo`.
430
fn fmt_duration_impl(duration: Duration, f: &mut Formatter<'_>) -> fmt::Result {
431
    let secs = duration.as_secs();
432

            
433
    if secs == 0 {
434
        let millis = duration.as_millis();
435
        if millis == 0 {
436
            write!(f, "0s")
437
        } else {
438
            write!(f, "{}ms", millis)
439
        }
440
    } else if secs < 60 {
441
        write!(f, "{}s", secs)
442
    } else if secs < 3600 {
443
        let mins = secs / 60;
444
        let rem_secs = secs % 60;
445
        if rem_secs == 0 {
446
            write!(f, "{}m", mins)
447
        } else {
448
            write!(f, "{}m {}s", mins, rem_secs)
449
        }
450
    } else {
451
        let hours = secs / 3600;
452
        let mins = (secs % 3600) / 60;
453
        if mins == 0 {
454
            write!(f, "{}h", hours)
455
        } else {
456
            write!(f, "{}h {}m", hours, mins)
457
        }
458
    }
459
}
460

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

            
509
    // Note that this loop does not use tor_basic_utils::ErrorSources.  We can't, because `e` is not
510
    // `Error + 'static`.  But we shouldn't use ErrorSources here, since io::Error will format
511
    // its inner by_ref() error, and so it's desirable that `source` skips over it.
512
    loop {
513
10270
        let this = e.to_string();
514
10270
        if !last.contains(&this) {
515
10000
            write!(f, "{}{}", sep.next().expect("repeat ended"), this)?;
516
270
        }
517
10270
        last = this;
518

            
519
10270
        if let Some(ne) = e.source() {
520
2790
            e = ne;
521
2790
        } else {
522
7480
            break;
523
        }
524
    }
525
7480
    Ok(())
526
7480
}
527

            
528
/// Return the current system time.
529
///
530
/// (This is a separate method for compatibility with wasm32.)
531
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
532
fn current_system_time() -> SystemTime {
533
    use web_time::web::SystemTimeExt as _;
534
    web_time::SystemTime::now().to_std()
535
}
536

            
537
/// Return the current system time.
538
///
539
/// (This is a separate method for compatibility with wasm32.)
540
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
541
11
fn current_system_time() -> SystemTime {
542
    #![allow(clippy::disallowed_methods)]
543
11
    SystemTime::now()
544
11
}
545

            
546
/// Return the current Instant.
547
///
548
/// (This is a separate method for compatibility with wasm32.)
549
11
fn current_instant() -> Instant {
550
    #![allow(clippy::disallowed_methods)]
551
11
    Instant::now()
552
11
}
553

            
554
#[cfg(test)]
555
mod test {
556
    // @@ begin test lint list maintained by maint/add_warning @@
557
    #![allow(clippy::bool_assert_comparison)]
558
    #![allow(clippy::clone_on_copy)]
559
    #![allow(clippy::dbg_macro)]
560
    #![allow(clippy::mixed_attributes_style)]
561
    #![allow(clippy::print_stderr)]
562
    #![allow(clippy::print_stdout)]
563
    #![allow(clippy::single_char_pattern)]
564
    #![allow(clippy::unwrap_used)]
565
    #![allow(clippy::unchecked_time_subtraction)]
566
    #![allow(clippy::useless_vec)]
567
    #![allow(clippy::needless_pass_by_value)]
568
    #![allow(clippy::string_slice)] // See arti#2571
569
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
570
    #![allow(clippy::disallowed_methods)]
571
    use super::*;
572
    use derive_more::From;
573

            
574
    #[test]
575
    fn bad_parse1() {
576
        let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
577
        if let Err(e) = "maybe".parse::<bool>() {
578
            err.push(e);
579
        }
580
        if let Err(e) = "a few".parse::<u32>() {
581
            err.push(e);
582
        }
583
        if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
584
            err.push(e);
585
        }
586

            
587
        let disp = format!("{}", err);
588
        assert_eq!(
589
            disp,
590
            "\
591
Tried to convert some things 3 times, but all attempts failed
592
Attempt 1: provided string was not `true` or `false`
593
Attempt 2: invalid digit found in string
594
Attempt 3: invalid IP address syntax"
595
        );
596

            
597
        let disp_alt = format!("{:#}", err);
598
        assert!(disp_alt.contains("Tried to convert some things 3 times, but all attempts failed"));
599
        assert!(disp_alt.contains("(from 20")); // Year prefix for timestamp
600
    }
601

            
602
    #[test]
603
    fn no_problems() {
604
        let empty: RetryError<anyhow::Error> =
605
            RetryError::in_attempt_to("immanentize the eschaton");
606
        let disp = format!("{}", empty);
607
        assert_eq!(
608
            disp,
609
            "Unable to immanentize the eschaton. (No errors given)"
610
        );
611
    }
612

            
613
    #[test]
614
    fn one_problem() {
615
        let mut err: RetryError<anyhow::Error> =
616
            RetryError::in_attempt_to("connect to torproject.org");
617
        if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
618
            err.push(e);
619
        }
620
        let disp = format!("{}", err);
621
        assert_eq!(
622
            disp,
623
            "Unable to connect to torproject.org: invalid IP address syntax"
624
        );
625

            
626
        let disp_alt = format!("{:#}", err);
627
        assert!(disp_alt.contains("Unable to connect to torproject.org at 20")); // Year prefix
628
        assert!(disp_alt.contains("invalid IP address syntax"));
629
    }
630

            
631
    #[test]
632
    fn operations() {
633
        use std::num::ParseIntError;
634

            
635
        #[derive(From, Clone, Debug, Eq, PartialEq)]
636
        struct Wrapper(ParseIntError);
637

            
638
        impl AsRef<dyn Error + 'static> for Wrapper {
639
            fn as_ref(&self) -> &(dyn Error + 'static) {
640
                &self.0
641
            }
642
        }
643

            
644
        let mut err: RetryError<Wrapper> = RetryError::in_attempt_to("parse some integers");
645
        assert!(err.is_empty());
646
        assert_eq!(err.len(), 0);
647
        err.extend(
648
            vec!["not", "your", "number"]
649
                .iter()
650
                .filter_map(|s| s.parse::<u16>().err())
651
                .map(Wrapper),
652
        );
653
        assert!(!err.is_empty());
654
        assert_eq!(err.len(), 3);
655

            
656
        let cloned = err.clone();
657
        for (s1, s2) in err.sources().zip(cloned.sources()) {
658
            assert_eq!(s1, s2);
659
        }
660

            
661
        err.dedup();
662

            
663
        let disp = format!("{}", err);
664
        assert_eq!(
665
            disp,
666
            "\
667
Tried to parse some integers 3 times, but all attempts failed
668
Attempts 1..3: invalid digit found in string"
669
        );
670

            
671
        let disp_alt = format!("{:#}", err);
672
        assert!(disp_alt.contains("Tried to parse some integers 3 times, but all attempts failed"));
673
        assert!(disp_alt.contains("(from 20")); // Year prefix for timestamp
674
    }
675

            
676
    #[test]
677
    fn overflow() {
678
        use std::num::ParseIntError;
679
        let mut err: RetryError<ParseIntError> =
680
            RetryError::in_attempt_to("parse too many integers");
681
        assert!(err.is_empty());
682
        let mut errors: Vec<ParseIntError> = vec!["no", "numbers"]
683
            .iter()
684
            .filter_map(|s| s.parse::<u16>().err())
685
            .collect();
686
        err.n_errors = usize::MAX;
687
        err.errors.push((
688
            Attempt::Range(1, err.n_errors),
689
            errors.pop().expect("parser did not fail"),
690
            Instant::now(),
691
        ));
692
        assert!(err.n_errors == usize::MAX);
693
        assert!(err.len() == 1);
694

            
695
        err.push(errors.pop().expect("parser did not fail"));
696
        assert!(err.n_errors == usize::MAX);
697
        assert!(err.len() == 1);
698
    }
699

            
700
    #[test]
701
    fn extend_from_retry_preserve_timestamps() {
702
        let n1 = Instant::now();
703
        let n2 = n1 + Duration::from_secs(10);
704
        let n3 = n1 + Duration::from_secs(20);
705

            
706
        let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do first thing");
707
        let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do second thing");
708

            
709
        err2.push_timed(anyhow::Error::msg("e1"), n1, None);
710
        err2.push_timed(anyhow::Error::msg("e2"), n2, None);
711

            
712
        // err1 is empty initially
713
        assert!(err1.first_error_at.is_none());
714

            
715
        err1.extend_from_retry_error(err2);
716

            
717
        assert_eq!(err1.len(), 2);
718
        // The timestamps should be preserved
719
        assert_eq!(err1.errors[0].2, n1);
720
        assert_eq!(err1.errors[1].2, n2);
721

            
722
        // Add another error to err1 to ensure mixed sources work
723
        err1.push_timed(anyhow::Error::msg("e3"), n3, None);
724
        assert_eq!(err1.len(), 3);
725
        assert_eq!(err1.errors[2].2, n3);
726
    }
727

            
728
    #[test]
729
    fn extend_from_retry_preserve_ranges() {
730
        let n1 = Instant::now();
731
        let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 1");
732

            
733
        // Push 2 errors
734
        err1.push(anyhow::Error::msg("e1"));
735
        err1.push(anyhow::Error::msg("e2"));
736
        assert_eq!(err1.n_errors, 2);
737

            
738
        let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 2");
739
        // Push 3 identical errors to create a range
740
        err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
741
        err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
742
        err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
743

            
744
        // Dedup err2 so it has a range
745
        err2.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
746
        assert_eq!(err2.len(), 1); // collapsed to 1 entry
747
        match err2.errors[0].0 {
748
            Attempt::Range(1, 3) => {}
749
            _ => panic!("Expected range 1..3"),
750
        }
751

            
752
        // Extend err1 with err2
753
        err1.extend_from_retry_error(err2);
754

            
755
        assert_eq!(err1.len(), 3); // 2 singles + 1 range
756
        assert_eq!(err1.n_errors, 5); // 2 + 3 = 5 total attempts
757

            
758
        // Check the range indices
759
        match err1.errors[2].0 {
760
            Attempt::Range(3, 5) => {}
761
            ref x => panic!("Expected range 3..5, got {:?}", x),
762
        }
763
    }
764

            
765
    #[test]
766
    fn dedup_after_extend_same_doing() {
767
        let doing = "do thing";
768
        let message = "error";
769
        let n1 = Instant::now();
770
        let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to(doing);
771

            
772
        // Push 1 error
773
        err1.push(anyhow::Error::msg(message));
774
        assert_eq!(err1.n_errors, 1);
775

            
776
        let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to(doing);
777
        // Push 2 identical errors to create a range
778
        err2.push_timed(anyhow::Error::msg(message), n1, None);
779
        err2.push_timed(anyhow::Error::msg(message), n1, None);
780

            
781
        // Dedup err2 so it has a range
782
        err2.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
783
        assert_eq!(err2.len(), 1); // collapsed to 1 entry
784
        match err2.errors[0].0 {
785
            Attempt::Range(1, 2) => {}
786
            _ => panic!("Expected range 1..2"),
787
        }
788

            
789
        // Extend err1 with err2
790
        err1.extend_from_retry_error(err2);
791
        assert_eq!(err1.len(), 2); // 1 single + 1 range
792
        assert_eq!(err1.n_errors, 3); // 1 + 2 = 3 total attempts
793

            
794
        // Dedup err1 so it has only one range
795
        err1.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
796
        assert_eq!(err1.len(), 1); // collapsed to 1 entry
797
        assert_eq!(err1.n_errors, 3); // 3 total attempts
798

            
799
        // Check the range indices
800
        match err1.errors[0].0 {
801
            Attempt::Range(1, 3) => {}
802
            ref x => panic!("Expected range 1..3, got {:?}", x),
803
        }
804
    }
805
}