nextest_runner/reporter/displayer/
unit_output.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Code to write out test and script outputs to the displayer.
5
6use crate::{
7    errors::DisplayErrorChain,
8    indenter::indented,
9    reporter::{
10        ByteSubslice, TestOutputErrorSlice, UnitErrorDescription,
11        events::*,
12        helpers::{Styles, highlight_end},
13    },
14    test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
15    write_str::WriteStr,
16};
17use owo_colors::Style;
18use serde::Deserialize;
19use std::{fmt, io};
20
21/// When to display test output in the reporter.
22#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize)]
23#[cfg_attr(test, derive(test_strategy::Arbitrary))]
24#[serde(rename_all = "kebab-case")]
25pub enum TestOutputDisplay {
26    /// Show output immediately on execution completion.
27    ///
28    /// This is the default for failing tests.
29    Immediate,
30
31    /// Show output immediately, and at the end of a test run.
32    ImmediateFinal,
33
34    /// Show output at the end of execution.
35    Final,
36
37    /// Never show output.
38    Never,
39}
40
41impl TestOutputDisplay {
42    /// Returns true if test output is shown immediately.
43    pub fn is_immediate(self) -> bool {
44        match self {
45            TestOutputDisplay::Immediate | TestOutputDisplay::ImmediateFinal => true,
46            TestOutputDisplay::Final | TestOutputDisplay::Never => false,
47        }
48    }
49
50    /// Returns true if test output is shown at the end of the run.
51    pub fn is_final(self) -> bool {
52        match self {
53            TestOutputDisplay::Final | TestOutputDisplay::ImmediateFinal => true,
54            TestOutputDisplay::Immediate | TestOutputDisplay::Never => false,
55        }
56    }
57}
58
59/// Formatting options for writing out child process output.
60///
61/// TODO: should these be lazily generated? Can't imagine this ever being
62/// measurably slow.
63#[derive(Debug)]
64pub(super) struct ChildOutputSpec {
65    pub(super) kind: UnitKind,
66    pub(super) stdout_header: String,
67    pub(super) stderr_header: String,
68    pub(super) combined_header: String,
69    pub(super) exec_fail_header: String,
70    pub(super) output_indent: &'static str,
71}
72
73pub(super) struct UnitOutputReporter {
74    force_success_output: Option<TestOutputDisplay>,
75    force_failure_output: Option<TestOutputDisplay>,
76    force_exec_fail_output: Option<TestOutputDisplay>,
77    display_empty_outputs: bool,
78}
79
80impl UnitOutputReporter {
81    pub(super) fn new(
82        force_success_output: Option<TestOutputDisplay>,
83        force_failure_output: Option<TestOutputDisplay>,
84        force_exec_fail_output: Option<TestOutputDisplay>,
85    ) -> Self {
86        // Ordinarily, empty stdout and stderr are not displayed. This
87        // environment variable is set in integration tests to ensure that they
88        // are.
89        let display_empty_outputs =
90            std::env::var_os("__NEXTEST_DISPLAY_EMPTY_OUTPUTS").is_some_and(|v| v == "1");
91
92        Self {
93            force_success_output,
94            force_failure_output,
95            force_exec_fail_output,
96            display_empty_outputs,
97        }
98    }
99
100    pub(super) fn success_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
101        self.force_success_output.unwrap_or(test_setting)
102    }
103
104    pub(super) fn failure_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
105        self.force_failure_output.unwrap_or(test_setting)
106    }
107
108    pub(super) fn exec_fail_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
109        self.force_exec_fail_output.unwrap_or(test_setting)
110    }
111
112    // These are currently only used by tests, but there's no principled
113    // objection to using these functions elsewhere in the displayer.
114    #[cfg(test)]
115    pub(super) fn force_success_output(&self) -> Option<TestOutputDisplay> {
116        self.force_success_output
117    }
118
119    #[cfg(test)]
120    pub(super) fn force_failure_output(&self) -> Option<TestOutputDisplay> {
121        self.force_failure_output
122    }
123
124    pub(super) fn write_child_execution_output(
125        &self,
126        styles: &Styles,
127        spec: &ChildOutputSpec,
128        exec_output: &ChildExecutionOutput,
129        mut writer: &mut dyn WriteStr,
130    ) -> io::Result<()> {
131        match exec_output {
132            ChildExecutionOutput::Output {
133                output,
134                // result and errors are captured by desc.
135                result: _,
136                errors: _,
137            } => {
138                let desc = UnitErrorDescription::new(spec.kind, exec_output);
139
140                // Show execution failures first so that they show up
141                // immediately after the failure notification.
142                if let Some(errors) = desc.exec_fail_error_list() {
143                    writeln!(writer, "{}", spec.exec_fail_header)?;
144
145                    // Indent the displayed error chain.
146                    let error_chain = DisplayErrorChain::new(errors);
147                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
148                    writeln!(indent_writer, "{error_chain}")?;
149                    indent_writer.write_str_flush()?;
150                    writer = indent_writer.into_inner();
151                }
152
153                let highlight_slice = if styles.is_colorized {
154                    desc.output_slice()
155                } else {
156                    None
157                };
158                self.write_child_output(styles, spec, output, highlight_slice, writer)?;
159            }
160
161            ChildExecutionOutput::StartError(error) => {
162                writeln!(writer, "{}", spec.exec_fail_header)?;
163
164                // Indent the displayed error chain.
165                let error_chain = DisplayErrorChain::new(error);
166                let mut indent_writer = indented(writer).with_str(spec.output_indent);
167                writeln!(indent_writer, "{error_chain}")?;
168                indent_writer.write_str_flush()?;
169                writer = indent_writer.into_inner();
170            }
171        }
172
173        writeln!(writer)
174    }
175
176    pub(super) fn write_child_output(
177        &self,
178        styles: &Styles,
179        spec: &ChildOutputSpec,
180        output: &ChildOutput,
181        highlight_slice: Option<TestOutputErrorSlice<'_>>,
182        mut writer: &mut dyn WriteStr,
183    ) -> io::Result<()> {
184        match output {
185            ChildOutput::Split(split) => {
186                if let Some(stdout) = &split.stdout
187                    && (self.display_empty_outputs || !stdout.is_empty())
188                {
189                    writeln!(writer, "{}", spec.stdout_header)?;
190
191                    // If there's no output indent, this is a no-op, though
192                    // it will bear the perf cost of a vtable indirection +
193                    // whatever internal state IndentWriter tracks. Doubt
194                    // this will be an issue in practice though!
195                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
196                    self.write_test_single_output_with_description(
197                        styles,
198                        stdout,
199                        highlight_slice.and_then(|d| d.stdout_subslice()),
200                        &mut indent_writer,
201                    )?;
202                    indent_writer.write_str_flush()?;
203                    writer = indent_writer.into_inner();
204                }
205
206                if let Some(stderr) = &split.stderr
207                    && (self.display_empty_outputs || !stderr.is_empty())
208                {
209                    writeln!(writer, "{}", spec.stderr_header)?;
210
211                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
212                    self.write_test_single_output_with_description(
213                        styles,
214                        stderr,
215                        highlight_slice.and_then(|d| d.stderr_subslice()),
216                        &mut indent_writer,
217                    )?;
218                    indent_writer.write_str_flush()?;
219                }
220            }
221            ChildOutput::Combined { output } => {
222                if self.display_empty_outputs || !output.is_empty() {
223                    writeln!(writer, "{}", spec.combined_header)?;
224
225                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
226                    self.write_test_single_output_with_description(
227                        styles,
228                        output,
229                        highlight_slice.and_then(|d| d.combined_subslice()),
230                        &mut indent_writer,
231                    )?;
232                    indent_writer.write_str_flush()?;
233                }
234            }
235        }
236
237        Ok(())
238    }
239
240    /// Writes a test output to the writer, along with optionally a subslice of the output to
241    /// highlight.
242    ///
243    /// The description must be a subslice of the output.
244    fn write_test_single_output_with_description(
245        &self,
246        styles: &Styles,
247        output: &ChildSingleOutput,
248        description: Option<ByteSubslice<'_>>,
249        writer: &mut dyn WriteStr,
250    ) -> io::Result<()> {
251        let output_str = output.as_str_lossy();
252        if styles.is_colorized {
253            if let Some(subslice) = description {
254                write_output_with_highlight(output_str, subslice, &styles.fail, writer)?;
255            } else {
256                // Output the text without stripping ANSI escapes, then reset the color afterwards
257                // in case the output is malformed.
258                write_output_with_trailing_newline(output_str, RESET_COLOR, writer)?;
259            }
260        } else {
261            // Strip ANSI escapes from the output if nextest itself isn't colorized.
262            let output_no_color = strip_ansi_escapes::strip_str(output_str);
263            write_output_with_trailing_newline(&output_no_color, "", writer)?;
264        }
265
266        Ok(())
267    }
268}
269
270const RESET_COLOR: &str = "\x1b[0m";
271
272fn write_output_with_highlight(
273    output: &str,
274    ByteSubslice { slice, start }: ByteSubslice,
275    highlight_style: &Style,
276    writer: &mut dyn WriteStr,
277) -> io::Result<()> {
278    let end = start + highlight_end(slice);
279
280    // Output the start and end of the test without stripping ANSI escapes, then reset
281    // the color afterwards in case the output is malformed.
282    writer.write_str(&output[..start])?;
283    writer.write_str(RESET_COLOR)?;
284
285    // Some systems (e.g. GitHub Actions, Buildomat) don't handle multiline ANSI
286    // coloring -- they reset colors after each line. To work around that,
287    // we reset and re-apply colors for each line.
288    for line in output[start..end].split_inclusive('\n') {
289        write!(writer, "{}", FmtPrefix(highlight_style))?;
290
291        // Write everything before the newline, stripping ANSI escapes.
292        let trimmed = line.trim_end_matches(['\n', '\r']);
293        let stripped = strip_ansi_escapes::strip_str(trimmed);
294        writer.write_str(&stripped)?;
295
296        // End coloring.
297        write!(writer, "{}", FmtSuffix(highlight_style))?;
298
299        // Now write the newline, if present.
300        writer.write_str(&line[trimmed.len()..])?;
301    }
302
303    // `end` is guaranteed to be within the bounds of `output`. (It is actually safe
304    // for it to be equal to `output.len()` -- it gets treated as an empty string in
305    // that case.)
306    write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
307
308    Ok(())
309}
310
311/// Write output, always ensuring there's a trailing newline. (If there's no
312/// newline, one will be inserted.)
313///
314/// `trailer` is written immediately before the trailing newline if any.
315fn write_output_with_trailing_newline(
316    mut output: &str,
317    trailer: &str,
318    writer: &mut dyn WriteStr,
319) -> io::Result<()> {
320    // If there's a trailing newline in the output, insert the trailer right
321    // before it.
322    if output.ends_with('\n') {
323        output = &output[..output.len() - 1];
324    }
325
326    writer.write_str(output)?;
327    writer.write_str(trailer)?;
328    writeln!(writer)
329}
330
331struct FmtPrefix<'a>(&'a Style);
332
333impl fmt::Display for FmtPrefix<'_> {
334    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
335        self.0.fmt_prefix(f)
336    }
337}
338
339struct FmtSuffix<'a>(&'a Style);
340
341impl fmt::Display for FmtSuffix<'_> {
342    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
343        self.0.fmt_suffix(f)
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_write_output_with_highlight() {
353        const RESET_COLOR: &str = "\u{1b}[0m";
354        const BOLD_RED: &str = "\u{1b}[31;1m";
355
356        assert_eq!(
357            write_output_with_highlight_buf("output", 0, Some(6)),
358            format!("{RESET_COLOR}{BOLD_RED}output{RESET_COLOR}{RESET_COLOR}\n")
359        );
360
361        assert_eq!(
362            write_output_with_highlight_buf("output", 1, Some(5)),
363            format!("o{RESET_COLOR}{BOLD_RED}utpu{RESET_COLOR}t{RESET_COLOR}\n")
364        );
365
366        assert_eq!(
367            write_output_with_highlight_buf("output\nhighlight 1\nhighlight 2\n", 7, None),
368            format!(
369                "output\n{RESET_COLOR}\
370                {BOLD_RED}highlight 1{RESET_COLOR}\n\
371                {BOLD_RED}highlight 2{RESET_COLOR}{RESET_COLOR}\n"
372            )
373        );
374
375        assert_eq!(
376            write_output_with_highlight_buf(
377                "output\nhighlight 1\nhighlight 2\nnot highlighted",
378                7,
379                None
380            ),
381            format!(
382                "output\n{RESET_COLOR}\
383                {BOLD_RED}highlight 1{RESET_COLOR}\n\
384                {BOLD_RED}highlight 2{RESET_COLOR}\n\
385                not highlighted{RESET_COLOR}\n"
386            )
387        );
388    }
389
390    fn write_output_with_highlight_buf(output: &str, start: usize, end: Option<usize>) -> String {
391        // We're not really testing non-UTF-8 output here, and using strings results in much more
392        // readable error messages.
393        let mut buf = String::new();
394        let end = end.unwrap_or(output.len());
395
396        let subslice = ByteSubslice {
397            start,
398            slice: &output.as_bytes()[start..end],
399        };
400        write_output_with_highlight(output, subslice, &Style::new().red().bold(), &mut buf)
401            .unwrap();
402        buf
403    }
404}