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