Skip to main content

uv_errors/
lib.rs

1mod line_wrap;
2
3use std::borrow::Cow;
4use std::error::Error;
5use std::fmt;
6use std::iter;
7
8use owo_colors::{AnsiColors, DynColor, OwoColorize};
9
10use line_wrap::{get_wrap_width, wrap_text};
11
12/// An error that may carry user-facing hints.
13///
14/// Implement this on error types that want to surface contextual suggestions
15/// (e.g., "try `--prerelease=allow`") to the diagnostics layer. Hints are
16/// rendered after the error output, each prefixed with `hint:`.
17pub trait Hinted {
18    /// Return any hints associated with this error.
19    fn hints(&self) -> Hints<'_> {
20        Hints::none()
21    }
22}
23
24/// The display order of a user-facing hint.
25#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26pub enum HintOrdering {
27    /// Advice that should be shown before other hints.
28    First,
29    /// Advice with no preferred placement.
30    #[default]
31    Any,
32    /// General advice that should follow more specific hints.
33    Last,
34}
35
36/// A user-facing hint and its preferred display order.
37pub struct Hint<'a> {
38    message: Cow<'a, str>,
39    ordering: HintOrdering,
40}
41
42impl<'a> Hint<'a> {
43    /// Create a hint with no preferred placement.
44    pub fn new(message: impl Into<Cow<'a, str>>) -> Self {
45        Self {
46            message: message.into(),
47            ordering: HintOrdering::default(),
48        }
49    }
50
51    /// Set the preferred display order of this hint.
52    #[must_use]
53    pub fn with_ordering(mut self, ordering: HintOrdering) -> Self {
54        self.ordering = ordering;
55        self
56    }
57
58    /// Convert a borrowed hint to owned, extending its lifetime to `'static`.
59    fn into_owned(self) -> Hint<'static> {
60        Hint {
61            message: Cow::Owned(self.message.into_owned()),
62            ordering: self.ordering,
63        }
64    }
65}
66
67impl<'a> From<&'a str> for Hint<'a> {
68    fn from(message: &'a str) -> Self {
69        Self::new(message)
70    }
71}
72
73impl From<String> for Hint<'_> {
74    fn from(message: String) -> Self {
75        Self::new(message)
76    }
77}
78
79/// A collection of user-facing hint messages.
80///
81/// Each hint is rendered on its own line, prefixed with the styled `hint:` label.
82/// Hints are grouped by [`HintOrdering`], retaining insertion order within each group.
83pub struct Hints<'a>(Vec<Hint<'a>>);
84
85impl<'a> Hints<'a> {
86    /// No hints.
87    pub fn none() -> Self {
88        Self(Vec::new())
89    }
90
91    /// Add a single hint.
92    pub fn push(&mut self, hint: impl Into<Hint<'a>>) {
93        self.0.push(hint.into());
94    }
95
96    /// Set the display order of every hint in this collection.
97    #[must_use]
98    pub fn with_ordering(mut self, ordering: HintOrdering) -> Self {
99        for hint in &mut self.0 {
100            hint.ordering = ordering;
101        }
102        self
103    }
104
105    /// Convert all borrowed hints to owned, extending the lifetime to `'static`.
106    pub fn into_owned(self) -> Hints<'static> {
107        Hints(self.0.into_iter().map(Hint::into_owned).collect())
108    }
109
110    /// Whether the collection is empty.
111    pub fn is_empty(&self) -> bool {
112        self.0.is_empty()
113    }
114
115    /// Iterate over hint messages in display order.
116    pub fn iter(&self) -> HintsIter<'_, 'a> {
117        HintsIter {
118            hints: &self.0,
119            current: self.0.iter(),
120            ordering: HintOrdering::First,
121            remaining: [HintOrdering::Any, HintOrdering::Last].into_iter(),
122        }
123    }
124
125    /// Extend with another set of hints, converting borrowed hints to owned.
126    ///
127    /// Duplicate messages retain their first insertion position and earliest ordering.
128    pub fn extend(&mut self, other: Hints<'_>) {
129        for hint in other.0 {
130            if let Some(existing) = self
131                .0
132                .iter_mut()
133                .find(|existing| existing.message == hint.message)
134            {
135                existing.ordering = existing.ordering.min(hint.ordering);
136            } else {
137                self.0.push(hint.into_owned());
138            }
139        }
140    }
141}
142
143/// A borrowed iterator over hint messages in display order.
144pub struct HintsIter<'h, 'a> {
145    hints: &'h [Hint<'a>],
146    current: std::slice::Iter<'h, Hint<'a>>,
147    ordering: HintOrdering,
148    remaining: std::array::IntoIter<HintOrdering, 2>,
149}
150
151impl<'h> Iterator for HintsIter<'h, '_> {
152    type Item = &'h str;
153
154    fn next(&mut self) -> Option<Self::Item> {
155        loop {
156            if let Some(hint) = self.current.find(|hint| hint.ordering == self.ordering) {
157                return Some(hint.message.as_ref());
158            }
159            self.ordering = self.remaining.next()?;
160            self.current = self.hints.iter();
161        }
162    }
163}
164
165impl<'h, 'a> IntoIterator for &'h Hints<'a> {
166    type Item = &'h str;
167    type IntoIter = HintsIter<'h, 'a>;
168
169    fn into_iter(self) -> Self::IntoIter {
170        self.iter()
171    }
172}
173
174impl<'a> IntoIterator for Hints<'a> {
175    type Item = Cow<'a, str>;
176    type IntoIter = std::iter::Map<std::vec::IntoIter<Hint<'a>>, fn(Hint<'a>) -> Cow<'a, str>>;
177
178    fn into_iter(mut self) -> Self::IntoIter {
179        self.0.sort_by_key(|hint| hint.ordering);
180        self.0.into_iter().map(|hint| hint.message)
181    }
182}
183
184/// A display adapter for an error followed by its hints.
185///
186/// Error renderers line-terminate the error before rendering [`Hints`]. Use
187/// this adapter when an error and its hints need to be formatted together.
188pub struct ErrorWithHints<'a, E> {
189    error: E,
190    hints: Hints<'a>,
191}
192
193impl<'a, E> ErrorWithHints<'a, E> {
194    /// Format an error followed by any hints.
195    pub fn new(error: E, hints: Hints<'a>) -> Self {
196        Self { error, hints }
197    }
198}
199
200impl<E: fmt::Display> fmt::Display for ErrorWithHints<'_, E> {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        write!(f, "{}", self.error)?;
203        if !self.hints.is_empty() {
204            writeln!(f)?;
205            write!(f, "{}", self.hints)?;
206        }
207        Ok(())
208    }
209}
210
211impl<'a> From<&'a str> for Hints<'a> {
212    fn from(hint: &'a str) -> Self {
213        Self::from(Hint::from(hint))
214    }
215}
216
217impl From<String> for Hints<'_> {
218    fn from(hint: String) -> Self {
219        Self::from(Hint::from(hint))
220    }
221}
222
223impl<'a> From<Hint<'a>> for Hints<'a> {
224    fn from(hint: Hint<'a>) -> Self {
225        Self(vec![hint])
226    }
227}
228
229impl<'a, T: Into<Hint<'a>>> FromIterator<T> for Hints<'a> {
230    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
231        Self(iter.into_iter().map(Into::into).collect())
232    }
233}
234
235impl fmt::Display for Hints<'_> {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        for hint in self {
238            write!(f, "\n{HintPrefix} {hint}")?;
239        }
240        Ok(())
241    }
242}
243
244/// A styled `hint:` prefix for use in user-facing messages.
245pub struct HintPrefix;
246
247impl fmt::Display for HintPrefix {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        write!(f, "{}{}", "hint".bold().cyan(), ":".bold())
250    }
251}
252
253/// Options for formatting an error chain.
254#[must_use]
255pub struct ErrorOptions<'a, C = AnsiColors, W = Stderr> {
256    level: Cow<'a, str>,
257    color: C,
258    width_override: Option<usize>,
259    stream: W,
260}
261
262/// A standard-error writer for formatted error chains.
263#[derive(Debug, Clone, Copy, Default)]
264pub struct Stderr;
265
266impl fmt::Write for Stderr {
267    fn write_str(&mut self, output: &str) -> fmt::Result {
268        anstream::eprint!("{output}");
269        Ok(())
270    }
271}
272
273impl Default for ErrorOptions<'_, AnsiColors, Stderr> {
274    fn default() -> Self {
275        Self {
276            level: Cow::Borrowed("error"),
277            color: AnsiColors::Red,
278            width_override: None,
279            stream: Stderr,
280        }
281    }
282}
283
284impl<'a, C, W> ErrorOptions<'a, C, W> {
285    /// Use a custom level prefix, such as `warning`.
286    pub fn with_level(mut self, level: impl Into<Cow<'a, str>>) -> Self {
287        self.level = level.into();
288        self
289    }
290
291    /// Use a custom color for the level and cause prefixes.
292    pub fn with_color<D>(self, color: D) -> ErrorOptions<'a, D, W> {
293        ErrorOptions {
294            level: self.level,
295            color,
296            width_override: self.width_override,
297            stream: self.stream,
298        }
299    }
300
301    /// Override the terminal width used for wrapping.
302    ///
303    /// This is primarily useful for testing.
304    #[cfg(test)]
305    fn with_width_override(mut self, width_override: usize) -> Self {
306        self.width_override = Some(width_override);
307        self
308    }
309
310    /// Write the rendered error chain to a custom stream.
311    pub fn with_stream<D>(self, stream: D) -> ErrorOptions<'a, C, D> {
312        ErrorOptions {
313            level: self.level,
314            color: self.color,
315            width_override: self.width_override,
316            stream,
317        }
318    }
319}
320
321/// Format an error chain and explicitly supplied hints to standard error using the default level
322/// and color.
323pub fn write_error_chain(err: &dyn Error, hints: &Hints<'_>) -> fmt::Result {
324    write_error_chain_with_options(err, hints, ErrorOptions::default())
325}
326
327/// Format the [`Debug`] representation of every error in an error chain.
328pub fn debug_error_chain(err: &dyn Error) -> impl fmt::Display + '_ {
329    DebugErrorChain(err)
330}
331
332struct DebugErrorChain<'a>(&'a dyn Error);
333
334impl fmt::Display for DebugErrorChain<'_> {
335    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
336        for (index, error) in iter::successors(Some(self.0), |&error| error.source()).enumerate() {
337            if index > 0 {
338                formatter.write_str("\n")?;
339            }
340            write!(formatter, "{index}: {error:?}")?;
341        }
342        Ok(())
343    }
344}
345
346/// Formats an error or warning chain with custom options.
347///
348/// Each hint is rendered on its own line, prefixed with the styled `hint:` label.
349pub fn write_error_chain_with_options<C: DynColor + Copy, W: fmt::Write>(
350    err: &dyn Error,
351    hints: &Hints<'_>,
352    options: ErrorOptions<'_, C, W>,
353) -> fmt::Result {
354    let ErrorOptions {
355        level,
356        color,
357        width_override,
358        mut stream,
359    } = options;
360    let width = get_wrap_width(width_override);
361
362    let main_msg = err.to_string();
363    let main_padding = " ".repeat(level.len() + 2);
364    let wrapped_main = wrap_text(&main_msg, width, &main_padding, &main_padding, "");
365    writeln!(
366        &mut stream,
367        "{}{} {}",
368        level.as_ref().color(color).bold(),
369        ":".bold(),
370        wrapped_main.trim()
371    )?;
372
373    for source in iter::successors(err.source(), |&err| err.source()) {
374        let msg = source.to_string();
375        // Reserve the display width of the prefix before wrapping the message. Authored lines
376        // retain their own indentation beneath it.
377        let wrapped = wrap_text(&msg, width.map(|width| width.saturating_sub(9)), "", "", "");
378
379        let mut lines = wrapped.lines();
380        if let Some(first) = lines.next() {
381            writeln!(
382                &mut stream,
383                "  {}{} {}",
384                "cause".color(color).bold(),
385                ":".bold(),
386                first.trim()
387            )?;
388            for line in lines {
389                if line.trim().is_empty() {
390                    writeln!(&mut stream)?;
391                } else {
392                    writeln!(&mut stream, "         {line}")?;
393                }
394            }
395        }
396    }
397
398    for hint in hints {
399        writeln!(&mut stream, "\n{HintPrefix} {hint}")?;
400    }
401
402    Ok(())
403}
404
405#[cfg(test)]
406mod tests {
407    use anyhow::anyhow;
408    use indoc::indoc;
409    use insta::{assert_debug_snapshot, assert_snapshot};
410    use owo_colors::AnsiColors;
411
412    use super::{
413        ErrorOptions, ErrorWithHints, Hint, HintOrdering, Hints, debug_error_chain,
414        write_error_chain_with_options,
415    };
416
417    #[test]
418    fn extend_deduplicates_matching_hints() {
419        let mut hints = Hints::from("same");
420        hints.extend(Hints::from("same"));
421        hints.extend(Hints::from("other"));
422
423        let hints = hints.iter().collect::<Vec<_>>();
424        assert_debug_snapshot!(hints, @r#"
425        [
426            "same",
427            "other",
428        ]
429        "#);
430    }
431
432    #[test]
433    fn hint_ordering_retains_insertion_order() {
434        let mut hints = Hints::from("any 1");
435        hints.push(Hint::new("last 1").with_ordering(HintOrdering::Last));
436        hints.push(Hint::new("first 1").with_ordering(HintOrdering::First));
437        hints.push("any 2".to_string());
438        hints.extend(Hints::from("last 2").with_ordering(HintOrdering::Last));
439        hints.extend(Hints::from("first 2").with_ordering(HintOrdering::First));
440
441        assert_snapshot!(anstream::adapter::strip_str(&hints.to_string()), @"
442        hint: first 1
443        hint: first 2
444        hint: any 1
445        hint: any 2
446        hint: last 1
447        hint: last 2
448        ");
449        assert_debug_snapshot!((&hints).into_iter().collect::<Vec<_>>(), @r#"
450        [
451            "first 1",
452            "first 2",
453            "any 1",
454            "any 2",
455            "last 1",
456            "last 2",
457        ]
458        "#);
459        assert_debug_snapshot!(hints.into_iter().collect::<Vec<_>>(), @r#"
460        [
461            "first 1",
462            "first 2",
463            "any 1",
464            "any 2",
465            "last 1",
466            "last 2",
467        ]
468        "#);
469    }
470
471    #[test]
472    fn changing_ordering_retains_insertion_order() {
473        let hints = [
474            Hint::new("last").with_ordering(HintOrdering::Last),
475            Hint::new("first").with_ordering(HintOrdering::First),
476            Hint::new("any"),
477        ]
478        .into_iter()
479        .collect::<Hints<'_>>()
480        .with_ordering(HintOrdering::Any);
481
482        assert_debug_snapshot!(hints.iter().collect::<Vec<_>>(), @r#"
483        [
484            "last",
485            "first",
486            "any",
487        ]
488        "#);
489    }
490
491    #[test]
492    fn duplicate_hints_retain_the_earliest_ordering() {
493        let message = String::from("shared");
494        let mut hints = Hints::from(message.as_str())
495            .with_ordering(HintOrdering::Last)
496            .into_owned();
497        drop(message);
498
499        hints.extend(Hints::from("first").with_ordering(HintOrdering::First));
500        hints.extend(Hints::from("shared").with_ordering(HintOrdering::First));
501        hints.extend(Hints::from("shared").with_ordering(HintOrdering::Last));
502        hints.extend(Hints::from("any"));
503
504        assert_snapshot!(anstream::adapter::strip_str(&hints.to_string()), @"
505        hint: shared
506        hint: first
507        hint: any
508        ");
509    }
510
511    #[test]
512    fn error_with_hints_separates_hints_from_error() {
513        let output = ErrorWithHints::new("error", Hints::from("fix it")).to_string();
514        assert_snapshot!(anstream::adapter::strip_str(&output), @"
515        error
516
517        hint: fix it
518        ");
519        assert_snapshot!(ErrorWithHints::new("error", Hints::none()), @"error");
520    }
521
522    #[test]
523    fn test_error_wrapping_with_columns() {
524        #[derive(Debug, thiserror::Error)]
525        #[error(
526            "Because fiasobfhuasbf was not found in the package registry and you require fiasobfhuasbf, we can conclude that your requirements are unsatisfiable."
527        )]
528        struct Inner;
529
530        #[derive(Debug, thiserror::Error)]
531        #[error("No solution found when resolving dependencies")]
532        struct Outer {
533            #[source]
534            source: Inner,
535        }
536
537        let error = Outer { source: Inner };
538        let mut output = String::new();
539        write_error_chain_with_options(
540            &error,
541            &Hints::none(),
542            ErrorOptions::default()
543                .with_width_override(80)
544                .with_stream(&mut output),
545        )
546        .unwrap();
547        let output = anstream::adapter::strip_str(&output);
548
549        assert_snapshot!(output, @"
550        error: No solution found when resolving dependencies
551          cause: Because fiasobfhuasbf was not found in the package registry and you
552                 require fiasobfhuasbf, we can conclude that your requirements are
553                 unsatisfiable.
554        ");
555    }
556
557    #[test]
558    fn test_error_chain_with_cause() {
559        #[derive(Debug, thiserror::Error)]
560        #[error("Permission denied")]
561        struct Inner;
562
563        #[derive(Debug, thiserror::Error)]
564        #[error("Failed to write file")]
565        struct Outer {
566            #[source]
567            source: Inner,
568        }
569
570        let error = Outer { source: Inner };
571        let mut output = String::new();
572        write_error_chain_with_options(
573            &error,
574            &Hints::none(),
575            ErrorOptions::default().with_stream(&mut output),
576        )
577        .unwrap();
578        assert_snapshot!(format!("{output:?}"), @r#""\u{1b}[1m\u{1b}[31merror\u{1b}[39m\u{1b}[0m\u{1b}[1m:\u{1b}[0m Failed to write file\n  \u{1b}[1m\u{1b}[31mcause\u{1b}[39m\u{1b}[0m\u{1b}[1m:\u{1b}[0m Permission denied\n""#);
579        let output = anstream::adapter::strip_str(&output);
580
581        assert_snapshot!(output, @"
582        error: Failed to write file
583          cause: Permission denied
584        ");
585    }
586
587    #[test]
588    fn formats_debug_error_chain() {
589        #[derive(Debug, thiserror::Error)]
590        #[error("inner error")]
591        struct InnerError {
592            code: u8,
593        }
594
595        #[derive(Debug, thiserror::Error)]
596        #[error("outer error")]
597        struct OuterError {
598            #[source]
599            source: InnerError,
600        }
601
602        let error = OuterError {
603            source: InnerError { code: 42 },
604        };
605
606        assert_eq!(
607            debug_error_chain(&error).to_string(),
608            "0: OuterError { source: InnerError { code: 42 } }\n1: InnerError { code: 42 }"
609        );
610    }
611
612    #[test]
613    fn format_with_custom_level() {
614        let error = anyhow!("Failed to create registry entry");
615        let mut output = String::new();
616        write_error_chain_with_options(
617            error.as_ref(),
618            &Hints::none(),
619            ErrorOptions::default()
620                .with_level("warning")
621                .with_color(AnsiColors::Yellow)
622                .with_stream(&mut output),
623        )
624        .unwrap();
625        let output = anstream::adapter::strip_str(&output);
626
627        assert_snapshot!(output, @"warning: Failed to create registry entry
628");
629    }
630
631    #[test]
632    fn test_no_hyphenation() {
633        #[derive(Debug, thiserror::Error)]
634        #[error(
635            "Failed to download package from https://files.pythonhosted.org/packages/verylongpackagename"
636        )]
637        struct LongWord;
638
639        let error = LongWord;
640        let mut output = String::new();
641        write_error_chain_with_options(
642            &error,
643            &Hints::none(),
644            ErrorOptions::default()
645                .with_width_override(50)
646                .with_stream(&mut output),
647        )
648        .unwrap();
649        let output = anstream::adapter::strip_str(&output);
650        assert_snapshot!(output, @r"
651        error: Failed to download package from
652               https://files.pythonhosted.org/packages/verylongpackagename
653        ");
654    }
655
656    #[test]
657    fn test_long_words_not_broken() {
658        #[derive(Debug, thiserror::Error)]
659        #[error(
660            "The package supercalifragilisticexpialidocious-extraordinarily-long-name was not found"
661        )]
662        struct VeryLongWord;
663
664        let error = VeryLongWord;
665        let mut output = String::new();
666        write_error_chain_with_options(
667            &error,
668            &Hints::none(),
669            ErrorOptions::default()
670                .with_width_override(40)
671                .with_stream(&mut output),
672        )
673        .unwrap();
674        let output = anstream::adapter::strip_str(&output);
675        assert_snapshot!(output, @r"
676        error: The package
677               supercalifragilisticexpialidocious-extraordinarily-long-name
678               was not found
679        ");
680    }
681
682    #[test]
683    fn test_multiple_error_sources() {
684        #[derive(Debug, thiserror::Error)]
685        #[error("Network connection timeout after multiple retry attempts")]
686        struct DeepError;
687
688        #[derive(Debug, thiserror::Error)]
689        #[error("Failed to fetch package metadata from registry")]
690        struct MiddleError {
691            #[source]
692            source: DeepError,
693        }
694
695        #[derive(Debug, thiserror::Error)]
696        #[error("Unable to resolve package dependencies")]
697        struct TopError {
698            #[source]
699            source: MiddleError,
700        }
701
702        let error = TopError {
703            source: MiddleError { source: DeepError },
704        };
705        let mut output = String::new();
706        write_error_chain_with_options(
707            &error,
708            &Hints::none(),
709            ErrorOptions::default()
710                .with_width_override(40)
711                .with_stream(&mut output),
712        )
713        .unwrap();
714        let output = anstream::adapter::strip_str(&output);
715        assert_snapshot!(output, @"
716        error: Unable to resolve package
717               dependencies
718          cause: Failed to fetch package
719                 metadata from registry
720          cause: Network connection timeout
721                 after multiple retry attempts
722        ");
723    }
724
725    #[test]
726    fn format_cause_with_narrow_width() {
727        let error = anyhow!("one two").context("root");
728        let mut output = String::new();
729        write_error_chain_with_options(
730            error.as_ref(),
731            &Hints::none(),
732            ErrorOptions::default()
733                .with_width_override(4)
734                .with_stream(&mut output),
735        )
736        .unwrap();
737        let output = anstream::adapter::strip_str(&output);
738
739        assert_snapshot!(output, @"
740        error: root
741          cause: one
742                 two
743        ");
744    }
745
746    #[test]
747    fn test_multiline_main_message_wraps_each_line() {
748        #[derive(Debug, thiserror::Error)]
749        #[error(
750            "There is no command `foobar` for `uv`. Did you mean one of:\n    auth\n    run\n    init"
751        )]
752        struct Suggestions;
753
754        let error = Suggestions;
755        let mut output = String::new();
756        write_error_chain_with_options(
757            &error,
758            &Hints::none(),
759            ErrorOptions::default()
760                .with_width_override(50)
761                .with_stream(&mut output),
762        )
763        .unwrap();
764        let output = anstream::adapter::strip_str(&output);
765
766        assert_snapshot!(output, @r"
767        error: There is no command `foobar` for `uv`. Did
768               you mean one of:
769            auth
770            run
771            init
772        ");
773    }
774
775    #[test]
776    fn test_wrap_only_on_ascii_space() {
777        #[derive(Debug, thiserror::Error)]
778        #[error("Path /usr/local/lib/python3.12/site-packages not found in filesystem hierarchy")]
779        struct SpecialChars;
780
781        let error = SpecialChars;
782        let mut output = String::new();
783        write_error_chain_with_options(
784            &error,
785            &Hints::none(),
786            ErrorOptions::default()
787                .with_width_override(50)
788                .with_stream(&mut output),
789        )
790        .unwrap();
791        let output = anstream::adapter::strip_str(&output);
792        assert_snapshot!(output, @r"
793        error: Path /usr/local/lib/python3.12/site-packages
794               not found in filesystem hierarchy
795        ");
796    }
797
798    #[test]
799    fn format_with_hints() {
800        let err = anyhow!("Permission denied").context("Failed to fetch package");
801
802        let hints = [
803            "Try running with `--verbose` for more information.".to_string(),
804            "Try running without --offline.".to_string(),
805        ]
806        .into_iter()
807        .collect();
808
809        let mut rendered = String::new();
810        write_error_chain_with_options(
811            err.as_ref(),
812            &hints,
813            ErrorOptions::default().with_stream(&mut rendered),
814        )
815        .unwrap();
816        let rendered = anstream::adapter::strip_str(&rendered);
817
818        assert_snapshot!(rendered, @"
819        error: Failed to fetch package
820          cause: Permission denied
821
822        hint: Try running with `--verbose` for more information.
823
824        hint: Try running without --offline.
825        ");
826    }
827
828    #[test]
829    fn format_multiline_message() {
830        let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
831        Server says: This endpoint only support POST requests.
832
833        For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
834        let err = anyhow!("Caused By: HTTP Error 400")
835            .context(err_middle)
836            .context("Failed to download Python 3.12");
837
838        let mut rendered = String::new();
839        write_error_chain_with_options(
840            err.as_ref(),
841            &Hints::none(),
842            ErrorOptions::default().with_stream(&mut rendered),
843        )
844        .unwrap();
845        let rendered = anstream::adapter::strip_str(&rendered);
846
847        assert_snapshot!(rendered, @"
848        error: Failed to download Python 3.12
849          cause: Failed to fetch https://example.com/upload/python3.13.tar.zst
850                 Server says: This endpoint only support POST requests.
851
852                 For downloads, please refer to https://example.com/download/python3.13.tar.zst
853          cause: Caused By: HTTP Error 400
854        ");
855    }
856}