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 super::DisplayerKind;
7use crate::{
8    errors::DisplayErrorChain,
9    indenter::indented,
10    reporter::{
11        ByteSubslice, TestOutputErrorSlice, UnitErrorDescription,
12        events::*,
13        helpers::{Styles, highlight_end},
14    },
15    test_output::ChildSingleOutput,
16    write_str::WriteStr,
17};
18use owo_colors::{OwoColorize, Style};
19use serde::Deserialize;
20use std::{fmt, io};
21
22/// When to display test output in the reporter.
23#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, serde::Serialize)]
24#[cfg_attr(test, derive(test_strategy::Arbitrary))]
25#[serde(rename_all = "kebab-case")]
26pub enum TestOutputDisplay {
27    /// Show output immediately on execution completion.
28    ///
29    /// This is the default for failing tests.
30    Immediate,
31
32    /// Show output immediately, and at the end of a test run.
33    ImmediateFinal,
34
35    /// Show output at the end of execution.
36    Final,
37
38    /// Never show output.
39    Never,
40}
41
42impl TestOutputDisplay {
43    /// Returns true if test output is shown immediately.
44    pub fn is_immediate(self) -> bool {
45        match self {
46            TestOutputDisplay::Immediate | TestOutputDisplay::ImmediateFinal => true,
47            TestOutputDisplay::Final | TestOutputDisplay::Never => false,
48        }
49    }
50
51    /// Returns true if test output is shown at the end of the run.
52    pub fn is_final(self) -> bool {
53        match self {
54            TestOutputDisplay::Final | TestOutputDisplay::ImmediateFinal => true,
55            TestOutputDisplay::Immediate | TestOutputDisplay::Never => false,
56        }
57    }
58}
59
60/// Formatting options for writing out child process output.
61///
62/// TODO: should these be lazily generated? Can't imagine this ever being
63/// measurably slow.
64#[derive(Debug)]
65pub(super) struct ChildOutputSpec {
66    pub(super) kind: UnitKind,
67    pub(super) stdout_header: String,
68    pub(super) stderr_header: String,
69    pub(super) combined_header: String,
70    pub(super) exec_fail_header: String,
71    pub(super) output_indent: &'static str,
72}
73
74pub(super) struct UnitOutputReporter {
75    force_success_output: Option<TestOutputDisplay>,
76    force_failure_output: Option<TestOutputDisplay>,
77    force_exec_fail_output: Option<TestOutputDisplay>,
78    display_empty_outputs: bool,
79    displayer_kind: DisplayerKind,
80}
81
82impl UnitOutputReporter {
83    pub(super) fn new(
84        force_success_output: Option<TestOutputDisplay>,
85        force_failure_output: Option<TestOutputDisplay>,
86        force_exec_fail_output: Option<TestOutputDisplay>,
87        displayer_kind: DisplayerKind,
88    ) -> Self {
89        // Ordinarily, empty stdout and stderr are not displayed. This
90        // environment variable is set in integration tests to ensure that they
91        // are.
92        let display_empty_outputs =
93            std::env::var_os("__NEXTEST_DISPLAY_EMPTY_OUTPUTS").is_some_and(|v| v == "1");
94
95        Self {
96            force_success_output,
97            force_failure_output,
98            force_exec_fail_output,
99            display_empty_outputs,
100            displayer_kind,
101        }
102    }
103
104    pub(super) fn success_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
105        self.force_success_output.unwrap_or(test_setting)
106    }
107
108    pub(super) fn failure_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
109        self.force_failure_output.unwrap_or(test_setting)
110    }
111
112    pub(super) fn exec_fail_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
113        self.force_exec_fail_output.unwrap_or(test_setting)
114    }
115
116    // These are currently only used by tests, but there's no principled
117    // objection to using these functions elsewhere in the displayer.
118    #[cfg(test)]
119    pub(super) fn force_success_output(&self) -> Option<TestOutputDisplay> {
120        self.force_success_output
121    }
122
123    #[cfg(test)]
124    pub(super) fn force_failure_output(&self) -> Option<TestOutputDisplay> {
125        self.force_failure_output
126    }
127
128    pub(super) fn write_child_execution_output(
129        &self,
130        styles: &Styles,
131        spec: &ChildOutputSpec,
132        exec_output: &ChildExecutionOutputDescription<ChildSingleOutput>,
133        mut writer: &mut dyn WriteStr,
134    ) -> io::Result<()> {
135        match exec_output {
136            ChildExecutionOutputDescription::Output {
137                output,
138                // result and errors are captured by desc.
139                result: _,
140                errors: _,
141            } => {
142                let desc = UnitErrorDescription::new(spec.kind, exec_output);
143
144                // Show execution failures first so that they show up
145                // immediately after the failure notification.
146                if let Some(errors) = desc.exec_fail_error_list() {
147                    writeln!(writer, "{}", spec.exec_fail_header)?;
148
149                    // Indent the displayed error chain.
150                    let error_chain = DisplayErrorChain::new(errors);
151                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
152                    writeln!(indent_writer, "{error_chain}")?;
153                    indent_writer.write_str_flush()?;
154                    writer = indent_writer.into_inner();
155                }
156
157                let highlight_slice = if styles.is_colorized {
158                    desc.output_slice()
159                } else {
160                    None
161                };
162                self.write_child_output(styles, spec, output, highlight_slice, writer)?;
163            }
164
165            ChildExecutionOutputDescription::StartError(error) => {
166                writeln!(writer, "{}", spec.exec_fail_header)?;
167
168                // Indent the displayed error chain.
169                let error_chain = DisplayErrorChain::new(error);
170                let mut indent_writer = indented(writer).with_str(spec.output_indent);
171                writeln!(indent_writer, "{error_chain}")?;
172                indent_writer.write_str_flush()?;
173                writer = indent_writer.into_inner();
174            }
175        }
176
177        writeln!(writer)
178    }
179
180    pub(super) fn write_child_output(
181        &self,
182        styles: &Styles,
183        spec: &ChildOutputSpec,
184        output: &ChildOutputDescription<ChildSingleOutput>,
185        highlight_slice: Option<TestOutputErrorSlice<'_>>,
186        mut writer: &mut dyn WriteStr,
187    ) -> io::Result<()> {
188        match output {
189            ChildOutputDescription::Split { stdout, stderr } => {
190                // In replay mode, show a message if output was not captured.
191                if self.displayer_kind == DisplayerKind::Replay
192                    && stdout.is_none()
193                    && stderr.is_none()
194                {
195                    // Use a hardcoded 4-space indentation even if there's no
196                    // output indent. That makes replay --nocapture look a bit
197                    // better.
198                    writeln!(writer, "    (output {})", "not captured".style(styles.skip))?;
199                    return Ok(());
200                }
201
202                if let Some(stdout) = stdout {
203                    if self.display_empty_outputs || !stdout.is_empty() {
204                        writeln!(writer, "{}", spec.stdout_header)?;
205
206                        // If there's no output indent, this is a no-op, though
207                        // it will bear the perf cost of a vtable indirection +
208                        // whatever internal state IndentWriter tracks. Doubt
209                        // this will be an issue in practice though!
210                        let mut indent_writer = indented(writer).with_str(spec.output_indent);
211                        self.write_test_single_output_with_description(
212                            styles,
213                            stdout,
214                            highlight_slice.and_then(|d| d.stdout_subslice()),
215                            &mut indent_writer,
216                        )?;
217                        indent_writer.write_str_flush()?;
218                        writer = indent_writer.into_inner();
219                    }
220                } else if self.displayer_kind == DisplayerKind::Replay {
221                    // Use a hardcoded 4-space indentation even if there's no
222                    // output indent. That makes replay --nocapture look a bit
223                    // better.
224                    writeln!(writer, "    (stdout {})", "not captured".style(styles.skip))?;
225                }
226
227                if let Some(stderr) = stderr {
228                    if self.display_empty_outputs || !stderr.is_empty() {
229                        writeln!(writer, "{}", spec.stderr_header)?;
230
231                        let mut indent_writer = indented(writer).with_str(spec.output_indent);
232                        self.write_test_single_output_with_description(
233                            styles,
234                            stderr,
235                            highlight_slice.and_then(|d| d.stderr_subslice()),
236                            &mut indent_writer,
237                        )?;
238                        indent_writer.write_str_flush()?;
239                    }
240                } else if self.displayer_kind == DisplayerKind::Replay {
241                    // Use a hardcoded 4-space indentation even if there's no
242                    // output indent. That makes replay --nocapture look a bit
243                    // better.
244                    writeln!(writer, "    (stderr {})", "not captured".style(styles.skip))?;
245                }
246            }
247            ChildOutputDescription::Combined { output } => {
248                if self.display_empty_outputs || !output.is_empty() {
249                    writeln!(writer, "{}", spec.combined_header)?;
250
251                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
252                    self.write_test_single_output_with_description(
253                        styles,
254                        output,
255                        highlight_slice.and_then(|d| d.combined_subslice()),
256                        &mut indent_writer,
257                    )?;
258                    indent_writer.write_str_flush()?;
259                }
260            }
261        }
262
263        Ok(())
264    }
265
266    /// Writes a test output to the writer, along with optionally a subslice of the output to
267    /// highlight.
268    ///
269    /// The description must be a subslice of the output.
270    fn write_test_single_output_with_description(
271        &self,
272        styles: &Styles,
273        output: &ChildSingleOutput,
274        description: Option<ByteSubslice<'_>>,
275        writer: &mut dyn WriteStr,
276    ) -> io::Result<()> {
277        let output_str = output.as_str_lossy();
278        if styles.is_colorized {
279            if let Some(subslice) = description {
280                write_output_with_highlight(output_str, subslice, &styles.fail, writer)?;
281            } else {
282                // Output the text without stripping ANSI escapes, then reset the color afterwards
283                // in case the output is malformed.
284                write_output_with_trailing_newline(output_str, RESET_COLOR, writer)?;
285            }
286        } else {
287            // Strip ANSI escapes from the output if nextest itself isn't colorized.
288            let output_no_color = strip_ansi_escapes::strip_str(output_str);
289            write_output_with_trailing_newline(&output_no_color, "", writer)?;
290        }
291
292        Ok(())
293    }
294}
295
296const RESET_COLOR: &str = "\x1b[0m";
297
298fn write_output_with_highlight(
299    output: &str,
300    ByteSubslice { slice, start }: ByteSubslice,
301    highlight_style: &Style,
302    writer: &mut dyn WriteStr,
303) -> io::Result<()> {
304    let end = start + highlight_end(slice);
305
306    // Output the start and end of the test without stripping ANSI escapes, then reset
307    // the color afterwards in case the output is malformed.
308    writer.write_str(&output[..start])?;
309    writer.write_str(RESET_COLOR)?;
310
311    // Some systems (e.g. GitHub Actions, Buildomat) don't handle multiline ANSI
312    // coloring -- they reset colors after each line. To work around that,
313    // we reset and re-apply colors for each line.
314    for line in output[start..end].split_inclusive('\n') {
315        write!(writer, "{}", FmtPrefix(highlight_style))?;
316
317        // Write everything before the newline, stripping ANSI escapes.
318        let trimmed = line.trim_end_matches(['\n', '\r']);
319        let stripped = strip_ansi_escapes::strip_str(trimmed);
320        writer.write_str(&stripped)?;
321
322        // End coloring.
323        write!(writer, "{}", FmtSuffix(highlight_style))?;
324
325        // Now write the newline, if present.
326        writer.write_str(&line[trimmed.len()..])?;
327    }
328
329    // `end` is guaranteed to be within the bounds of `output`. (It is actually safe
330    // for it to be equal to `output.len()` -- it gets treated as an empty string in
331    // that case.)
332    write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
333
334    Ok(())
335}
336
337/// Write output, always ensuring there's a trailing newline. (If there's no
338/// newline, one will be inserted.)
339///
340/// `trailer` is written immediately before the trailing newline if any.
341fn write_output_with_trailing_newline(
342    mut output: &str,
343    trailer: &str,
344    writer: &mut dyn WriteStr,
345) -> io::Result<()> {
346    // If there's a trailing newline in the output, insert the trailer right
347    // before it.
348    if output.ends_with('\n') {
349        output = &output[..output.len() - 1];
350    }
351
352    writer.write_str(output)?;
353    writer.write_str(trailer)?;
354    writeln!(writer)
355}
356
357struct FmtPrefix<'a>(&'a Style);
358
359impl fmt::Display for FmtPrefix<'_> {
360    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
361        self.0.fmt_prefix(f)
362    }
363}
364
365struct FmtSuffix<'a>(&'a Style);
366
367impl fmt::Display for FmtSuffix<'_> {
368    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
369        self.0.fmt_suffix(f)
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::reporter::events::UnitKind;
377
378    fn make_test_spec() -> ChildOutputSpec {
379        ChildOutputSpec {
380            kind: UnitKind::Test,
381            stdout_header: "--- STDOUT ---".to_string(),
382            stderr_header: "--- STDERR ---".to_string(),
383            combined_header: "--- OUTPUT ---".to_string(),
384            exec_fail_header: "--- EXEC FAIL ---".to_string(),
385            output_indent: "    ",
386        }
387    }
388
389    fn make_unit_output_reporter(displayer_kind: DisplayerKind) -> UnitOutputReporter {
390        UnitOutputReporter::new(None, None, None, displayer_kind)
391    }
392
393    #[test]
394    fn test_replay_output_not_captured() {
395        let reporter = make_unit_output_reporter(DisplayerKind::Replay);
396        let spec = make_test_spec();
397        let styles = Styles::default();
398
399        // Test: both stdout and stderr not captured.
400        let output = ChildOutputDescription::Split {
401            stdout: None,
402            stderr: None,
403        };
404        let mut buf = String::new();
405        reporter
406            .write_child_output(&styles, &spec, &output, None, &mut buf)
407            .unwrap();
408        insta::assert_snapshot!("replay_neither_captured", buf);
409    }
410
411    #[test]
412    fn test_replay_stdout_not_captured() {
413        let reporter = make_unit_output_reporter(DisplayerKind::Replay);
414        let spec = make_test_spec();
415        let styles = Styles::default();
416
417        // Test: only stdout not captured (stderr is captured).
418        let output = ChildOutputDescription::Split {
419            stdout: None,
420            stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
421                b"stderr output\n",
422            ))),
423        };
424        let mut buf = String::new();
425        reporter
426            .write_child_output(&styles, &spec, &output, None, &mut buf)
427            .unwrap();
428        insta::assert_snapshot!("replay_stdout_not_captured", buf);
429    }
430
431    #[test]
432    fn test_replay_stderr_not_captured() {
433        let reporter = make_unit_output_reporter(DisplayerKind::Replay);
434        let spec = make_test_spec();
435        let styles = Styles::default();
436
437        // Test: only stderr not captured (stdout is captured).
438        let output = ChildOutputDescription::Split {
439            stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
440                b"stdout output\n",
441            ))),
442            stderr: None,
443        };
444        let mut buf = String::new();
445        reporter
446            .write_child_output(&styles, &spec, &output, None, &mut buf)
447            .unwrap();
448        insta::assert_snapshot!("replay_stderr_not_captured", buf);
449    }
450
451    #[test]
452    fn test_replay_both_captured() {
453        let reporter = make_unit_output_reporter(DisplayerKind::Replay);
454        let spec = make_test_spec();
455        let styles = Styles::default();
456
457        // Test: both captured (no "not captured" message).
458        let output = ChildOutputDescription::Split {
459            stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
460                b"stdout output\n",
461            ))),
462            stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
463                b"stderr output\n",
464            ))),
465        };
466        let mut buf = String::new();
467        reporter
468            .write_child_output(&styles, &spec, &output, None, &mut buf)
469            .unwrap();
470        insta::assert_snapshot!("replay_both_captured", buf);
471    }
472
473    #[test]
474    fn test_live_output_not_captured_no_message() {
475        let reporter = make_unit_output_reporter(DisplayerKind::Live);
476        let spec = make_test_spec();
477        let styles = Styles::default();
478
479        // Test: live mode with neither captured should NOT show the message.
480        let output = ChildOutputDescription::Split {
481            stdout: None,
482            stderr: None,
483        };
484        let mut buf = String::new();
485        reporter
486            .write_child_output(&styles, &spec, &output, None, &mut buf)
487            .unwrap();
488        insta::assert_snapshot!("live_neither_captured", buf);
489    }
490
491    #[test]
492    fn test_write_output_with_highlight() {
493        const RESET_COLOR: &str = "\u{1b}[0m";
494        const BOLD_RED: &str = "\u{1b}[31;1m";
495
496        assert_eq!(
497            write_output_with_highlight_buf("output", 0, Some(6)),
498            format!("{RESET_COLOR}{BOLD_RED}output{RESET_COLOR}{RESET_COLOR}\n")
499        );
500
501        assert_eq!(
502            write_output_with_highlight_buf("output", 1, Some(5)),
503            format!("o{RESET_COLOR}{BOLD_RED}utpu{RESET_COLOR}t{RESET_COLOR}\n")
504        );
505
506        assert_eq!(
507            write_output_with_highlight_buf("output\nhighlight 1\nhighlight 2\n", 7, None),
508            format!(
509                "output\n{RESET_COLOR}\
510                {BOLD_RED}highlight 1{RESET_COLOR}\n\
511                {BOLD_RED}highlight 2{RESET_COLOR}{RESET_COLOR}\n"
512            )
513        );
514
515        assert_eq!(
516            write_output_with_highlight_buf(
517                "output\nhighlight 1\nhighlight 2\nnot highlighted",
518                7,
519                None
520            ),
521            format!(
522                "output\n{RESET_COLOR}\
523                {BOLD_RED}highlight 1{RESET_COLOR}\n\
524                {BOLD_RED}highlight 2{RESET_COLOR}\n\
525                not highlighted{RESET_COLOR}\n"
526            )
527        );
528    }
529
530    fn write_output_with_highlight_buf(output: &str, start: usize, end: Option<usize>) -> String {
531        // We're not really testing non-UTF-8 output here, and using strings results in much more
532        // readable error messages.
533        let mut buf = String::new();
534        let end = end.unwrap_or(output.len());
535
536        let subslice = ByteSubslice {
537            start,
538            slice: &output.as_bytes()[start..end],
539        };
540        write_output_with_highlight(output, subslice, &Style::new().red().bold(), &mut buf)
541            .unwrap();
542        buf
543    }
544}