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