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    helpers::RESET_COLOR,
11    indenter::indented,
12    output_spec::{LiveSpec, OutputSpec},
13    reporter::{
14        ByteSubslice, TestOutputErrorSlice, UnitErrorDescription,
15        events::*,
16        helpers::{Styles, highlight_end},
17    },
18    test_output::ChildSingleOutput,
19    write_str::WriteStr,
20};
21use owo_colors::{OwoColorize, Style};
22use serde::Deserialize;
23use std::{fmt, io};
24
25/// When to display test output in the reporter.
26#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, serde::Serialize)]
27#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
28#[cfg_attr(test, derive(test_strategy::Arbitrary))]
29#[serde(rename_all = "kebab-case")]
30pub enum TestOutputDisplay {
31    /// Shows captured output as soon as the test completes.
32    Immediate,
33
34    /// Shows captured output when the test completes and again at the end of
35    /// the run.
36    ImmediateFinal,
37
38    /// Shows captured output only at the end of the run.
39    Final,
40
41    /// Never shows captured 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
360fn write_output_with_highlight(
361    output: &str,
362    ByteSubslice { slice, start }: ByteSubslice,
363    highlight_style: &Style,
364    writer: &mut dyn WriteStr,
365) -> io::Result<()> {
366    let end = start + highlight_end(slice);
367
368    // Output the start and end of the test without stripping ANSI escapes, then reset
369    // the color afterwards in case the output is malformed.
370    writer.write_str(&output[..start])?;
371    writer.write_str(RESET_COLOR)?;
372
373    // Some systems (e.g. GitHub Actions, Buildomat) don't handle multiline ANSI
374    // coloring -- they reset colors after each line. To work around that,
375    // we reset and re-apply colors for each line.
376    for line in output[start..end].split_inclusive('\n') {
377        write!(writer, "{}", FmtPrefix(highlight_style))?;
378
379        // Write everything before the newline, stripping ANSI escapes.
380        let trimmed = line.trim_end_matches(['\n', '\r']);
381        let stripped = strip_ansi_escapes::strip_str(trimmed);
382        writer.write_str(&stripped)?;
383
384        // End coloring.
385        write!(writer, "{}", FmtSuffix(highlight_style))?;
386
387        // Now write the newline, if present.
388        writer.write_str(&line[trimmed.len()..])?;
389    }
390
391    // `end` is guaranteed to be within the bounds of `output`. (It is actually safe
392    // for it to be equal to `output.len()` -- it gets treated as an empty string in
393    // that case.)
394    write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
395
396    Ok(())
397}
398
399/// Write output, always ensuring there's a trailing newline. (If there's no
400/// newline, one will be inserted.)
401///
402/// `trailer` is written immediately before the trailing newline if any.
403fn write_output_with_trailing_newline(
404    mut output: &str,
405    trailer: &str,
406    writer: &mut dyn WriteStr,
407) -> io::Result<()> {
408    // If there's a trailing newline in the output, insert the trailer right
409    // before it.
410    if output.ends_with('\n') {
411        output = &output[..output.len() - 1];
412    }
413
414    writer.write_str(output)?;
415    writer.write_str(trailer)?;
416    writeln!(writer)
417}
418
419struct FmtPrefix<'a>(&'a Style);
420
421impl fmt::Display for FmtPrefix<'_> {
422    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
423        self.0.fmt_prefix(f)
424    }
425}
426
427struct FmtSuffix<'a>(&'a Style);
428
429impl fmt::Display for FmtSuffix<'_> {
430    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
431        self.0.fmt_suffix(f)
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::reporter::events::UnitKind;
439
440    fn make_test_spec() -> ChildOutputSpec {
441        ChildOutputSpec {
442            kind: UnitKind::Test,
443            stdout_header: "--- STDOUT ---".to_string(),
444            stderr_header: "--- STDERR ---".to_string(),
445            combined_header: "--- OUTPUT ---".to_string(),
446            exec_fail_header: "--- EXEC FAIL ---".to_string(),
447            output_indent: "    ",
448        }
449    }
450
451    fn make_unit_output_reporter(displayer_kind: DisplayerKind) -> UnitOutputReporter {
452        UnitOutputReporter::new(
453            OutputDisplayOverrides {
454                force_success_output: None,
455                force_failure_output: None,
456                force_exec_fail_output: None,
457            },
458            displayer_kind,
459        )
460    }
461
462    #[test]
463    fn test_replay_output_not_captured() {
464        let reporter = make_unit_output_reporter(DisplayerKind::Replay);
465        let spec = make_test_spec();
466        let styles = Styles::default();
467
468        // Test: both stdout and stderr not captured.
469        let output = ChildOutputDescription::Split {
470            stdout: None,
471            stderr: None,
472        };
473        let mut buf = String::new();
474        reporter
475            .write_child_output(&styles, &spec, &output, None, &mut buf)
476            .unwrap();
477        insta::assert_snapshot!("replay_neither_captured", buf);
478    }
479
480    #[test]
481    fn test_replay_stdout_not_captured() {
482        let reporter = make_unit_output_reporter(DisplayerKind::Replay);
483        let spec = make_test_spec();
484        let styles = Styles::default();
485
486        // Test: only stdout not captured (stderr is captured).
487        let output = ChildOutputDescription::Split {
488            stdout: None,
489            stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
490                b"stderr output\n",
491            ))),
492        };
493        let mut buf = String::new();
494        reporter
495            .write_child_output(&styles, &spec, &output, None, &mut buf)
496            .unwrap();
497        insta::assert_snapshot!("replay_stdout_not_captured", buf);
498    }
499
500    #[test]
501    fn test_replay_stderr_not_captured() {
502        let reporter = make_unit_output_reporter(DisplayerKind::Replay);
503        let spec = make_test_spec();
504        let styles = Styles::default();
505
506        // Test: only stderr not captured (stdout is captured).
507        let output = ChildOutputDescription::Split {
508            stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
509                b"stdout output\n",
510            ))),
511            stderr: None,
512        };
513        let mut buf = String::new();
514        reporter
515            .write_child_output(&styles, &spec, &output, None, &mut buf)
516            .unwrap();
517        insta::assert_snapshot!("replay_stderr_not_captured", buf);
518    }
519
520    #[test]
521    fn test_replay_both_captured() {
522        let reporter = make_unit_output_reporter(DisplayerKind::Replay);
523        let spec = make_test_spec();
524        let styles = Styles::default();
525
526        // Test: both captured (no "not captured" message).
527        let output = ChildOutputDescription::Split {
528            stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
529                b"stdout output\n",
530            ))),
531            stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
532                b"stderr output\n",
533            ))),
534        };
535        let mut buf = String::new();
536        reporter
537            .write_child_output(&styles, &spec, &output, None, &mut buf)
538            .unwrap();
539        insta::assert_snapshot!("replay_both_captured", buf);
540    }
541
542    #[test]
543    fn test_live_output_not_captured_no_message() {
544        let reporter = make_unit_output_reporter(DisplayerKind::Live);
545        let spec = make_test_spec();
546        let styles = Styles::default();
547
548        // Test: live mode with neither captured should NOT show the message.
549        let output = ChildOutputDescription::Split {
550            stdout: None,
551            stderr: None,
552        };
553        let mut buf = String::new();
554        reporter
555            .write_child_output(&styles, &spec, &output, None, &mut buf)
556            .unwrap();
557        insta::assert_snapshot!("live_neither_captured", buf);
558    }
559
560    #[test]
561    fn test_write_output_with_highlight() {
562        const RESET_COLOR: &str = "\u{1b}[0m";
563        const BOLD_RED: &str = "\u{1b}[31;1m";
564
565        assert_eq!(
566            write_output_with_highlight_buf("output", 0, Some(6)),
567            format!("{RESET_COLOR}{BOLD_RED}output{RESET_COLOR}{RESET_COLOR}\n")
568        );
569
570        assert_eq!(
571            write_output_with_highlight_buf("output", 1, Some(5)),
572            format!("o{RESET_COLOR}{BOLD_RED}utpu{RESET_COLOR}t{RESET_COLOR}\n")
573        );
574
575        assert_eq!(
576            write_output_with_highlight_buf("output\nhighlight 1\nhighlight 2\n", 7, None),
577            format!(
578                "output\n{RESET_COLOR}\
579                {BOLD_RED}highlight 1{RESET_COLOR}\n\
580                {BOLD_RED}highlight 2{RESET_COLOR}{RESET_COLOR}\n"
581            )
582        );
583
584        assert_eq!(
585            write_output_with_highlight_buf(
586                "output\nhighlight 1\nhighlight 2\nnot highlighted",
587                7,
588                None
589            ),
590            format!(
591                "output\n{RESET_COLOR}\
592                {BOLD_RED}highlight 1{RESET_COLOR}\n\
593                {BOLD_RED}highlight 2{RESET_COLOR}\n\
594                not highlighted{RESET_COLOR}\n"
595            )
596        );
597    }
598
599    fn write_output_with_highlight_buf(output: &str, start: usize, end: Option<usize>) -> String {
600        // We're not really testing non-UTF-8 output here, and using strings results in much more
601        // readable error messages.
602        let mut buf = String::new();
603        let end = end.unwrap_or(output.len());
604
605        let subslice = ByteSubslice {
606            start,
607            slice: &output.as_bytes()[start..end],
608        };
609        write_output_with_highlight(output, subslice, &Style::new().red().bold(), &mut buf)
610            .unwrap();
611        buf
612    }
613}