Skip to main content

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