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