Repository: zkat/miette
Pull request #325 (2023-12-23 15:41:11)

fix(handler): remove the two extra `is_terminal` sys call from `MietteHandlerOpts::build`
`GraphicalReportHandler::new()` calls `GraphicalTheme::default()`

https://github.com/zkat/miette/blob/7ff4f874d693a665af4df40f4e94505013e3e262/src/handlers/graphical.rs#L52

which calls `std::io::stdout().is_terminal()` and `std::io::stderr().is_terminal()`

https://github.com/zkat/miette/blob/7ff4f874d693a665af4df40f4e94505013e3e262/src/handlers/theme.rs#L72

but this default theme is overridden by `with_theme(theme)`.


Patch:
diff --git a/src/handler.rs b/src/handler.rs
index e32f3ef..3c09373 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -56,6 +56,7 @@ pub struct MietteHandlerOpts {
     pub(crate) tab_width: Option<usize>,
     pub(crate) with_cause_chain: Option<bool>,
     pub(crate) break_words: Option<bool>,
+    pub(crate) wrap_lines: Option<bool>,
     pub(crate) word_separator: Option<textwrap::WordSeparator>,
     pub(crate) word_splitter: Option<textwrap::WordSplitter>,
 }
@@ -89,6 +90,16 @@ impl MietteHandlerOpts {
         self
     }
 
+    /// If true, long lines can be wrapped.
+    ///
+    /// If false, long lines will not be broken when they exceed the width.
+    ///
+    /// Defaults to true.
+    pub fn wrap_lines(mut self, wrap_lines: bool) -> Self {
+        self.wrap_lines = Some(wrap_lines);
+        self
+    }
+
     /// If true, long words can be broken when wrapping.
     ///
     /// If false, long words will not be broken when they exceed the width.
@@ -98,7 +109,6 @@ impl MietteHandlerOpts {
         self.break_words = Some(break_words);
         self
     }
-
     /// Sets the `textwrap::WordSeparator` to use when determining wrap points.
     pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
         self.word_separator = Some(word_separator);
@@ -237,10 +247,9 @@ impl MietteHandlerOpts {
                 ThemeStyles::none()
             };
             let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles });
-            let mut handler = GraphicalReportHandler::new()
+            let mut handler = GraphicalReportHandler::new_themed(theme)
                 .with_width(width)
-                .with_links(linkify)
-                .with_theme(theme);
+                .with_links(linkify);
             if let Some(with_cause_chain) = self.with_cause_chain {
                 if with_cause_chain {
                     handler = handler.with_cause_chain();
@@ -260,6 +269,9 @@ impl MietteHandlerOpts {
             if let Some(b) = self.break_words {
                 handler = handler.with_break_words(b)
             }
+            if let Some(b) = self.wrap_lines {
+                handler = handler.with_wrap_lines(b)
+            }
             if let Some(s) = self.word_separator {
                 handler = handler.with_word_separator(s)
             }
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index 3193472..7812925 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -6,7 +6,7 @@ use unicode_width::UnicodeWidthChar;
 use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
 use crate::handlers::theme::*;
 use crate::protocol::{Diagnostic, Severity};
-use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
+use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents};
 
 /**
 A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a
@@ -30,6 +30,7 @@ pub struct GraphicalReportHandler {
     pub(crate) context_lines: usize,
     pub(crate) tab_width: usize,
     pub(crate) with_cause_chain: bool,
+    pub(crate) wrap_lines: bool,
     pub(crate) break_words: bool,
     pub(crate) word_separator: Option<textwrap::WordSeparator>,
     pub(crate) word_splitter: Option<textwrap::WordSplitter>,
@@ -54,6 +55,7 @@ impl GraphicalReportHandler {
             context_lines: 1,
             tab_width: 4,
             with_cause_chain: true,
+            wrap_lines: true,
             break_words: true,
             word_separator: None,
             word_splitter: None,
@@ -69,6 +71,7 @@ impl GraphicalReportHandler {
             footer: None,
             context_lines: 1,
             tab_width: 4,
+            wrap_lines: true,
             with_cause_chain: true,
             break_words: true,
             word_separator: None,
@@ -131,6 +134,12 @@ impl GraphicalReportHandler {
         self
     }
 
+    /// Enables or disables wrapping of lines to fit the width.
+    pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
+        self.wrap_lines = wrap_lines;
+        self
+    }
+
     /// Enables or disables breaking of words during wrapping.
     pub fn with_break_words(mut self, break_words: bool) -> Self {
         self.break_words = break_words;
@@ -197,7 +206,7 @@ impl GraphicalReportHandler {
                 opts = opts.word_splitter(word_splitter);
             }
 
-            writeln!(f, "{}", textwrap::fill(footer, opts))?;
+            writeln!(f, "{}", self.wrap(footer, opts))?;
         }
         Ok(())
     }
@@ -258,7 +267,7 @@ impl GraphicalReportHandler {
             opts = opts.word_splitter(word_splitter);
         }
 
-        writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
+        writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?;
 
         if !self.with_cause_chain {
             return Ok(());
@@ -308,16 +317,17 @@ impl GraphicalReportHandler {
                     ErrorKind::Diagnostic(diag) => {
                         let mut inner = String::new();
 
-                        // Don't print footer for inner errors
                         let mut inner_renderer = self.clone();
+                        // Don't print footer for inner errors
                         inner_renderer.footer = None;
+                        // Cause chains are already flattened, so don't double-print the nested error
                         inner_renderer.with_cause_chain = false;
                         inner_renderer.render_report(&mut inner, diag)?;
 
-                        writeln!(f, "{}", textwrap::fill(&inner, opts))?;
+                        writeln!(f, "{}", self.wrap(&inner, opts))?;
                     }
                     ErrorKind::StdError(err) => {
-                        writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?;
+                        writeln!(f, "{}", self.wrap(&err.to_string(), opts))?;
                     }
                 }
             }
@@ -341,7 +351,7 @@ impl GraphicalReportHandler {
                 opts = opts.word_splitter(word_splitter);
             }
 
-            writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
+            writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
         }
         Ok(())
     }
@@ -353,6 +363,9 @@ impl GraphicalReportHandler {
         parent_src: Option<&dyn SourceCode>,
     ) -> fmt::Result {
         if let Some(related) = diagnostic.related() {
+            let mut inner_renderer = self.clone();
+            // Re-enable the printing of nested cause chains for related errors
+            inner_renderer.with_cause_chain = true;
             writeln!(f)?;
             for rel in related {
                 match rel.severity() {
@@ -360,12 +373,12 @@ impl GraphicalReportHandler {
                     Some(Severity::Warning) => write!(f, "Warning: ")?,
                     Some(Severity::Advice) => write!(f, "Advice: ")?,
                 };
-                self.render_header(f, rel)?;
-                self.render_causes(f, rel)?;
+                inner_renderer.render_header(f, rel)?;
+                inner_renderer.render_causes(f, rel)?;
                 let src = rel.source_code().or(parent_src);
-                self.render_snippets(f, rel, src)?;
-                self.render_footer(f, rel)?;
-                self.render_related(f, rel, src)?;
+                inner_renderer.render_snippets(f, rel, src)?;
+                inner_renderer.render_footer(f, rel)?;
+                inner_renderer.render_related(f, rel, src)?;
             }
         }
         Ok(())
@@ -377,66 +390,58 @@ impl GraphicalReportHandler {
         diagnostic: &(dyn Diagnostic),
         opt_source: Option<&dyn SourceCode>,
     ) -> fmt::Result {
-        if let Some(source) = opt_source {
-            if let Some(labels) = diagnostic.labels() {
-                let mut labels = labels.collect::<Vec<_>>();
-                labels.sort_unstable_by_key(|l| l.inner().offset());
-                if !labels.is_empty() {
-                    let contents = labels
-                        .iter()
-                        .map(|label| {
-                            source.read_span(label.inner(), self.context_lines, self.context_lines)
-                        })
-                        .collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>()
-                        .map_err(|_| fmt::Error)?;
-                    let mut contexts = Vec::with_capacity(contents.len());
-                    for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) {
-                        if contexts.is_empty() {
-                            contexts.push((right, right_conts));
-                        } else {
-                            let (left, left_conts) = contexts.last().unwrap().clone();
-                            let left_end = left.offset() + left.len();
-                            let right_end = right.offset() + right.len();
-                            if left_conts.line() + left_conts.line_count() >= right_conts.line() {
-                                // The snippets will overlap, so we create one Big Chunky Boi
-                                let new_span = LabeledSpan::new(
-                                    left.label().map(String::from),
-                                    left.offset(),
-                                    if right_end >= left_end {
-                                        // Right end goes past left end
-                                        right_end - left.offset()
-                                    } else {
-                                        // right is contained inside left
-                                        left.len()
-                                    },
-                                );
-                                if source
-                                    .read_span(
-                                        new_span.inner(),
-                                        self.context_lines,
-                                        self.context_lines,
-                                    )
-                                    .is_ok()
-                                {
-                                    contexts.pop();
-                                    contexts.push((
-                                        // We'll throw this away later
-                                        new_span, left_conts,
-                                    ));
-                                } else {
-                                    contexts.push((right, right_conts));
-                                }
-                            } else {
-                                contexts.push((right, right_conts));
-                            }
-                        }
-                    }
-                    for (ctx, _) in contexts {
-                        self.render_context(f, source, &ctx, &labels[..])?;
-                    }
+        let source = match opt_source {
+            Some(source) => source,
+            None => return Ok(()),
+        };
+        let labels = match diagnostic.labels() {
+            Some(labels) => labels,
+            None => return Ok(()),
+        };
+
+        let mut labels = labels.collect::<Vec<_>>();
+        labels.sort_unstable_by_key(|l| l.inner().offset());
+
+        let mut contexts = Vec::with_capacity(labels.len());
+        for right in labels.iter().cloned() {
+            let right_conts = source
+                .read_span(right.inner(), self.context_lines, self.context_lines)
+                .map_err(|_| fmt::Error)?;
+
+            if contexts.is_empty() {
+                contexts.push((right, right_conts));
+                continue;
+            }
+
+            let (left, left_conts) = contexts.last().unwrap();
+            if left_conts.line() + left_conts.line_count() >= right_conts.line() {
+                // The snippets will overlap, so we create one Big Chunky Boi
+                let left_end = left.offset() + left.len();
+                let right_end = right.offset() + right.len();
+                let new_end = std::cmp::max(left_end, right_end);
+
+                let new_span = LabeledSpan::new(
+                    left.label().map(String::from),
+                    left.offset(),
+                    new_end - left.offset(),
+                );
+                // Check that the two contexts can be combined
+                if let Ok(new_conts) =
+                    source.read_span(new_span.inner(), self.context_lines, self.context_lines)
+                {
+                    contexts.pop();
+                    // We'll throw the contents away later
+                    contexts.push((new_span, new_conts));
+                    continue;
                 }
             }
+
+            contexts.push((right, right_conts));
+        }
+        for (ctx, _) in contexts {
+            self.render_context(f, source, &ctx, &labels[..])?;
         }
+
         Ok(())
     }
 
@@ -449,10 +454,16 @@ impl GraphicalReportHandler {
     ) -> fmt::Result {
         let (contents, lines) = self.get_lines(source, context.inner())?;
 
-        let primary_label = labels
-            .iter()
+        // only consider labels from the context as primary label
+        let ctx_labels = labels.iter().filter(|l| {
+            context.inner().offset() <= l.inner().offset()
+                && l.inner().offset() + l.inner().len()
+                    <= context.inner().offset() + context.inner().len()
+        });
+        let primary_label = ctx_labels
+            .clone()
             .find(|label| label.primary())
-            .or_else(|| labels.first());
+            .or_else(|| ctx_labels.clone().next());
 
         // sorting is your friend
         let labels = labels
@@ -810,6 +821,41 @@ impl GraphicalReportHandler {
         Ok(())
     }
 
+    fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String {
+        if self.wrap_lines {
+            textwrap::fill(text, opts)
+        } else {
+            // Format without wrapping, but retain the indentation options
+            // Implementation based on `textwrap::indent`
+            let mut result = String::with_capacity(2 * text.len());
+            let trimmed_indent = opts.subsequent_indent.trim_end();
+            for (idx, line) in text.split_terminator('\n').enumerate() {
+                if idx > 0 {
+                    result.push('\n');
+                }
+                if idx == 0 {
+                    if line.trim().is_empty() {
+                        result.push_str(opts.initial_indent.trim_end());
+                    } else {
+                        result.push_str(opts.initial_indent);
+                    }
+                } else {
+                    if line.trim().is_empty() {
+                        result.push_str(trimmed_indent);
+                    } else {
+                        result.push_str(opts.subsequent_indent);
+                    }
+                }
+                result.push_str(line);
+            }
+            if text.ends_with('\n') {
+                // split_terminator will have eaten the final '\n'.
+                result.push('\n');
+            }
+            result
+        }
+    }
+
     fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
         write!(
             f,


Test patch:
diff --git a/tests/color_format.rs b/tests/color_format.rs
index bb90708..95d40e0 100644
--- a/tests/color_format.rs
+++ b/tests/color_format.rs
@@ -3,6 +3,7 @@
 use lazy_static::lazy_static;
 use miette::{Diagnostic, MietteHandler, MietteHandlerOpts, ReportHandler, RgbColors};
 use regex::Regex;
+use std::ffi::OsString;
 use std::fmt::{self, Debug};
 use std::sync::Mutex;
 use thiserror::Error;
@@ -42,16 +43,29 @@ fn color_format(handler: MietteHandler) -> ColorFormat {
     }
 }
 
-/// Runs a function with an environment variable set to a specific value, then
-/// sets it back to it's original value once completed.
-fn with_env_var<F: FnOnce()>(var: &str, value: &str, body: F) {
-    let old_value = std::env::var_os(var);
-    std::env::set_var(var, value);
-    body();
-    if let Some(old_value) = old_value {
-        std::env::set_var(var, old_value);
-    } else {
-        std::env::remove_var(var);
+/// Store the current value of an environment variable on construction, and then
+/// restore that value when the guard is dropped.
+struct EnvVarGuard<'a> {
+    var: &'a str,
+    old_value: Option<OsString>,
+}
+
+impl EnvVarGuard<'_> {
+    fn new(var: &str) -> EnvVarGuard<'_> {
+        EnvVarGuard {
+            var,
+            old_value: std::env::var_os(var),
+        }
+    }
+}
+
+impl Drop for EnvVarGuard<'_> {
+    fn drop(&mut self) {
+        if let Some(old_value) = &self.old_value {
+            std::env::set_var(self.var, old_value);
+        } else {
+            std::env::remove_var(self.var);
+        }
     }
 }
 
@@ -72,22 +86,33 @@ fn check_colors<F: Fn(MietteHandlerOpts) -> MietteHandlerOpts>(
     //
     // Since environment variables are shared for the entire process, we need
     // to ensure that only one test that modifies these env vars runs at a time.
-    let guard = COLOR_ENV_VARS.lock().unwrap();
-
-    with_env_var("NO_COLOR", "1", || {
-        let handler = make_handler(MietteHandlerOpts::new()).build();
-        assert_eq!(color_format(handler), no_support);
-    });
-    with_env_var("FORCE_COLOR", "1", || {
-        let handler = make_handler(MietteHandlerOpts::new()).build();
-        assert_eq!(color_format(handler), ansi_support);
-    });
-    with_env_var("FORCE_COLOR", "3", || {
-        let handler = make_handler(MietteHandlerOpts::new()).build();
-        assert_eq!(color_format(handler), rgb_support);
-    });
-
-    drop(guard);
+    let lock = COLOR_ENV_VARS.lock().unwrap();
+
+    let guards = (
+        EnvVarGuard::new("NO_COLOR"),
+        EnvVarGuard::new("FORCE_COLOR"),
+    );
+    // Clear color environment variables that may be set outside of 'cargo test'
+    std::env::remove_var("NO_COLOR");
+    std::env::remove_var("FORCE_COLOR");
+
+    std::env::set_var("NO_COLOR", "1");
+    let handler = make_handler(MietteHandlerOpts::new()).build();
+    assert_eq!(color_format(handler), no_support);
+    std::env::remove_var("NO_COLOR");
+
+    std::env::set_var("FORCE_COLOR", "1");
+    let handler = make_handler(MietteHandlerOpts::new()).build();
+    assert_eq!(color_format(handler), ansi_support);
+    std::env::remove_var("FORCE_COLOR");
+
+    std::env::set_var("FORCE_COLOR", "3");
+    let handler = make_handler(MietteHandlerOpts::new()).build();
+    assert_eq!(color_format(handler), rgb_support);
+    std::env::remove_var("FORCE_COLOR");
+
+    drop(guards);
+    drop(lock);
 }
 
 #[test]
diff --git a/tests/graphical.rs b/tests/graphical.rs
index aabf167..4e8b737 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -218,6 +218,50 @@ fn word_wrap_options() -> Result<(), MietteError> {
     Ok(())
 }
 
+#[test]
+fn wrap_option() -> Result<(), MietteError> {
+    // A line should break on the width
+    let out = fmt_report_with_settings(
+        Report::msg("abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz"),
+        |handler| handler.with_width(15),
+    );
+    let expected = r#"  × abc def
+  │ ghi jkl
+  │ mno pqr
+  │ stu vwx
+  │ yz abc
+  │ def ghi
+  │ jkl mno
+  │ pqr stu
+  │ vwx yz
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    // Unless, wrapping is disabled
+    let out = fmt_report_with_settings(
+        Report::msg("abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz"),
+        |handler| handler.with_width(15).with_wrap_lines(false),
+    );
+    let expected =
+        "  × abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz\n".to_string();
+    assert_eq!(expected, out);
+
+    // Then, user-defined new lines should be preserved wrapping is disabled
+    let out = fmt_report_with_settings(
+      Report::msg("abc def ghi jkl mno pqr stu vwx yz\nabc def ghi jkl mno pqr stu vwx yz\nabc def ghi jkl mno pqr stu vwx yz"),
+      |handler| handler.with_width(15).with_wrap_lines(false),
+  );
+    let expected = r#"  × abc def ghi jkl mno pqr stu vwx yz
+  │ abc def ghi jkl mno pqr stu vwx yz
+  │ abc def ghi jkl mno pqr stu vwx yz
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    Ok(())
+}
+
 #[test]
 fn empty_source() -> Result<(), MietteError> {
     #[derive(Debug, Diagnostic, Error)]
@@ -1713,3 +1757,95 @@ fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError>
     assert_eq!(expected, out);
     Ok(())
 }
+
+#[test]
+fn triple_adjacent_highlight() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource,
+        #[label = "this bit here"]
+        highlight1: SourceSpan,
+        #[label = "also this bit"]
+        highlight2: SourceSpan,
+        #[label = "finally we got"]
+        highlight3: SourceSpan,
+    }
+
+    let src = "source\n\n\n  text\n\n\n    here".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight1: (0, 6).into(),
+        highlight2: (11, 4).into(),
+        highlight3: (22, 4).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = "oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:1:1]
+ 1 │ source
+   · ───┬──
+   ·    ╰── this bit here
+ 2 │ 
+ 3 │ 
+ 4 │   text
+   ·   ──┬─
+   ·     ╰── also this bit
+ 5 │ 
+ 6 │ 
+ 7 │     here
+   ·     ──┬─
+   ·       ╰── finally we got
+   ╰────
+  help: try doing it better next time?
+";
+    assert_eq!(expected, &out);
+    Ok(())
+}
+
+#[test]
+fn non_adjacent_highlight() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource,
+        #[label = "this bit here"]
+        highlight1: SourceSpan,
+        #[label = "also this bit"]
+        highlight2: SourceSpan,
+    }
+
+    let src = "source\n\n\n\n  text    here".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight1: (0, 6).into(),
+        highlight2: (12, 4).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = "oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:1:1]
+ 1 │ source
+   · ───┬──
+   ·    ╰── this bit here
+ 2 │ 
+   ╰────
+   ╭─[bad_file.rs:5:3]
+ 4 │ 
+ 5 │   text    here
+   ·   ──┬─
+   ·     ╰── also this bit
+   ╰────
+  help: try doing it better next time?
+";
+    assert_eq!(expected, &out);
+    Ok(())
+}
diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs
index 334ca34..e5305ac 100644
--- a/tests/test_diagnostic_source_macro.rs
+++ b/tests/test_diagnostic_source_macro.rs
@@ -194,3 +194,85 @@ fn test_nested_diagnostic_source_is_output() {
 
     assert_eq!(expected, out);
 }
+
+#[derive(Debug, miette::Diagnostic, thiserror::Error)]
+#[error("A multi-error happened")]
+struct MultiError {
+    #[related]
+    related_errs: Vec<Box<dyn Diagnostic>>,
+}
+
+#[cfg(feature = "fancy-no-backtrace")]
+#[test]
+fn test_nested_cause_chains_for_related_errors_are_output() {
+    let inner_error = TestStructError {
+        asdf_inner_foo: SourceError {
+            code: String::from("This is another error"),
+            help: String::from("You should fix this"),
+            label: (3, 4),
+        },
+    };
+    let first_error = NestedError {
+        code: String::from("right here"),
+        label: (6, 4),
+        the_other_err: Box::new(inner_error),
+    };
+    let second_error = SourceError {
+        code: String::from("You're actually a mess"),
+        help: String::from("Get a grip..."),
+        label: (3, 4),
+    };
+    let multi_error = MultiError {
+        related_errs: vec![Box::new(first_error), Box::new(second_error)],
+    };
+    let diag = NestedError {
+        code: String::from("the outside world"),
+        label: (6, 4),
+        the_other_err: Box::new(multi_error),
+    };
+    let mut out = String::new();
+    miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
+        .with_width(80)
+        .with_footer("Yooo, a footer".to_string())
+        .render_report(&mut out, &diag)
+        .unwrap();
+    println!("{}", out);
+
+    let expected = r#"  × A nested error happened
+  ╰─▶   × A multi-error happened
+      
+      Error:   × A nested error happened
+        ├─▶   × TestError
+        │
+        ╰─▶   × A complex error happened
+               ╭────
+             1 │ This is another error
+               ·    ──┬─
+               ·      ╰── here
+               ╰────
+              help: You should fix this
+      
+         ╭────
+       1 │ right here
+         ·       ──┬─
+         ·         ╰── here
+         ╰────
+      Error:   × A complex error happened
+         ╭────
+       1 │ You're actually a mess
+         ·    ──┬─
+         ·      ╰── here
+         ╰────
+        help: Get a grip...
+      
+   ╭────
+ 1 │ the outside world
+   ·       ──┬─
+   ·         ╰── here
+   ╰────
+
+  Yooo, a footer
+"#;
+
+    assert_eq!(expected, out);
+}