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
#![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
#![deny(clippy::string_slice)] // See arti#2571
48
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
49
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
50

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

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

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

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

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

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

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

            
161
396
            if self.first_error_at.is_none() {
162
248
                self.first_error_at = wall_clock;
163
358
            }
164

            
165
396
            self.errors.push((attempt, err.into(), instant));
166
2
        }
167
398
    }
168

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

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

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

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

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

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

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

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

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

            
262
88
            let new_attempt = match attempt {
263
84
                Attempt::Single(_) => Attempt::Single(new_n_errors),
264
4
                Attempt::Range(_, _) => Attempt::Range(self.n_errors + 1, new_n_errors),
265
            };
266

            
267
88
            self.errors.push((new_attempt, err, timestamp));
268
88
            self.n_errors = new_n_errors;
269
        }
270
86
    }
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 additional failures.
283
12
    fn grow(&mut self, count: usize) {
284
12
        *self = match *self {
285
8
            Attempt::Single(idx) => Attempt::Range(idx, idx + count),
286
4
            Attempt::Range(first, last) => Attempt::Range(first, last + count),
287
        };
288
12
    }
289

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
518
7886
        if let Some(ne) = e.source() {
519
2139
            e = ne;
520
2139
        } else {
521
5747
            break;
522
        }
523
    }
524
5747
    Ok(())
525
5747
}
526

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
660
        err.dedup();
661

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

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

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

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

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

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

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

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

            
714
        err1.extend_from_retry_error(err2);
715

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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