nextest_runner/reporter/displayer/
unit_output.rs1use crate::{
7    errors::DisplayErrorChain,
8    indenter::indented,
9    reporter::{
10        ByteSubslice, TestOutputErrorSlice, UnitErrorDescription,
11        events::*,
12        helpers::{Styles, highlight_end},
13    },
14    test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
15    write_str::WriteStr,
16};
17use owo_colors::Style;
18use serde::Deserialize;
19use std::{fmt, io};
20
21#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize)]
23#[cfg_attr(test, derive(test_strategy::Arbitrary))]
24#[serde(rename_all = "kebab-case")]
25pub enum TestOutputDisplay {
26    Immediate,
30
31    ImmediateFinal,
33
34    Final,
36
37    Never,
39}
40
41impl TestOutputDisplay {
42    pub fn is_immediate(self) -> bool {
44        match self {
45            TestOutputDisplay::Immediate | TestOutputDisplay::ImmediateFinal => true,
46            TestOutputDisplay::Final | TestOutputDisplay::Never => false,
47        }
48    }
49
50    pub fn is_final(self) -> bool {
52        match self {
53            TestOutputDisplay::Final | TestOutputDisplay::ImmediateFinal => true,
54            TestOutputDisplay::Immediate | TestOutputDisplay::Never => false,
55        }
56    }
57}
58
59#[derive(Debug)]
64pub(super) struct ChildOutputSpec {
65    pub(super) kind: UnitKind,
66    pub(super) stdout_header: String,
67    pub(super) stderr_header: String,
68    pub(super) combined_header: String,
69    pub(super) exec_fail_header: String,
70    pub(super) output_indent: &'static str,
71}
72
73pub(super) struct UnitOutputReporter {
74    force_success_output: Option<TestOutputDisplay>,
75    force_failure_output: Option<TestOutputDisplay>,
76    display_empty_outputs: bool,
77}
78
79impl UnitOutputReporter {
80    pub(super) fn new(
81        force_success_output: Option<TestOutputDisplay>,
82        force_failure_output: Option<TestOutputDisplay>,
83    ) -> Self {
84        let display_empty_outputs =
88            std::env::var_os("__NEXTEST_DISPLAY_EMPTY_OUTPUTS").is_some_and(|v| v == "1");
89
90        Self {
91            force_success_output,
92            force_failure_output,
93            display_empty_outputs,
94        }
95    }
96
97    pub(super) fn success_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
98        self.force_success_output.unwrap_or(test_setting)
99    }
100
101    pub(super) fn failure_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
102        self.force_failure_output.unwrap_or(test_setting)
103    }
104
105    #[cfg(test)]
108    pub(super) fn force_success_output(&self) -> Option<TestOutputDisplay> {
109        self.force_success_output
110    }
111
112    #[cfg(test)]
113    pub(super) fn force_failure_output(&self) -> Option<TestOutputDisplay> {
114        self.force_failure_output
115    }
116
117    pub(super) fn write_child_execution_output(
118        &self,
119        styles: &Styles,
120        spec: &ChildOutputSpec,
121        exec_output: &ChildExecutionOutput,
122        mut writer: &mut dyn WriteStr,
123    ) -> io::Result<()> {
124        match exec_output {
125            ChildExecutionOutput::Output {
126                output,
127                result: _,
129                errors: _,
130            } => {
131                let desc = UnitErrorDescription::new(spec.kind, exec_output);
132
133                if let Some(errors) = desc.exec_fail_error_list() {
136                    writeln!(writer, "{}", spec.exec_fail_header)?;
137
138                    let error_chain = DisplayErrorChain::new(errors);
140                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
141                    writeln!(indent_writer, "{error_chain}")?;
142                    indent_writer.write_str_flush()?;
143                    writer = indent_writer.into_inner();
144                }
145
146                let highlight_slice = if styles.is_colorized {
147                    desc.output_slice()
148                } else {
149                    None
150                };
151                self.write_child_output(styles, spec, output, highlight_slice, writer)?;
152            }
153
154            ChildExecutionOutput::StartError(error) => {
155                writeln!(writer, "{}", spec.exec_fail_header)?;
156
157                let error_chain = DisplayErrorChain::new(error);
159                let mut indent_writer = indented(writer).with_str(spec.output_indent);
160                writeln!(indent_writer, "{error_chain}")?;
161                indent_writer.write_str_flush()?;
162                writer = indent_writer.into_inner();
163            }
164        }
165
166        writeln!(writer)
167    }
168
169    pub(super) fn write_child_output(
170        &self,
171        styles: &Styles,
172        spec: &ChildOutputSpec,
173        output: &ChildOutput,
174        highlight_slice: Option<TestOutputErrorSlice<'_>>,
175        mut writer: &mut dyn WriteStr,
176    ) -> io::Result<()> {
177        match output {
178            ChildOutput::Split(split) => {
179                if let Some(stdout) = &split.stdout
180                    && (self.display_empty_outputs || !stdout.is_empty())
181                {
182                    writeln!(writer, "{}", spec.stdout_header)?;
183
184                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
189                    self.write_test_single_output_with_description(
190                        styles,
191                        stdout,
192                        highlight_slice.and_then(|d| d.stdout_subslice()),
193                        &mut indent_writer,
194                    )?;
195                    indent_writer.write_str_flush()?;
196                    writer = indent_writer.into_inner();
197                }
198
199                if let Some(stderr) = &split.stderr
200                    && (self.display_empty_outputs || !stderr.is_empty())
201                {
202                    writeln!(writer, "{}", spec.stderr_header)?;
203
204                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
205                    self.write_test_single_output_with_description(
206                        styles,
207                        stderr,
208                        highlight_slice.and_then(|d| d.stderr_subslice()),
209                        &mut indent_writer,
210                    )?;
211                    indent_writer.write_str_flush()?;
212                }
213            }
214            ChildOutput::Combined { output } => {
215                if self.display_empty_outputs || !output.is_empty() {
216                    writeln!(writer, "{}", spec.combined_header)?;
217
218                    let mut indent_writer = indented(writer).with_str(spec.output_indent);
219                    self.write_test_single_output_with_description(
220                        styles,
221                        output,
222                        highlight_slice.and_then(|d| d.combined_subslice()),
223                        &mut indent_writer,
224                    )?;
225                    indent_writer.write_str_flush()?;
226                }
227            }
228        }
229
230        Ok(())
231    }
232
233    fn write_test_single_output_with_description(
238        &self,
239        styles: &Styles,
240        output: &ChildSingleOutput,
241        description: Option<ByteSubslice<'_>>,
242        writer: &mut dyn WriteStr,
243    ) -> io::Result<()> {
244        let output_str = output.as_str_lossy();
245        if styles.is_colorized {
246            if let Some(subslice) = description {
247                write_output_with_highlight(output_str, subslice, &styles.fail, writer)?;
248            } else {
249                write_output_with_trailing_newline(output_str, RESET_COLOR, writer)?;
252            }
253        } else {
254            let output_no_color = strip_ansi_escapes::strip_str(output_str);
256            write_output_with_trailing_newline(&output_no_color, "", writer)?;
257        }
258
259        Ok(())
260    }
261}
262
263const RESET_COLOR: &str = "\x1b[0m";
264
265fn write_output_with_highlight(
266    output: &str,
267    ByteSubslice { slice, start }: ByteSubslice,
268    highlight_style: &Style,
269    writer: &mut dyn WriteStr,
270) -> io::Result<()> {
271    let end = start + highlight_end(slice);
272
273    writer.write_str(&output[..start])?;
276    writer.write_str(RESET_COLOR)?;
277
278    for line in output[start..end].split_inclusive('\n') {
282        write!(writer, "{}", FmtPrefix(highlight_style))?;
283
284        let trimmed = line.trim_end_matches(['\n', '\r']);
286        let stripped = strip_ansi_escapes::strip_str(trimmed);
287        writer.write_str(&stripped)?;
288
289        write!(writer, "{}", FmtSuffix(highlight_style))?;
291
292        writer.write_str(&line[trimmed.len()..])?;
294    }
295
296    write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
300
301    Ok(())
302}
303
304fn write_output_with_trailing_newline(
309    mut output: &str,
310    trailer: &str,
311    writer: &mut dyn WriteStr,
312) -> io::Result<()> {
313    if output.ends_with('\n') {
316        output = &output[..output.len() - 1];
317    }
318
319    writer.write_str(output)?;
320    writer.write_str(trailer)?;
321    writeln!(writer)
322}
323
324struct FmtPrefix<'a>(&'a Style);
325
326impl fmt::Display for FmtPrefix<'_> {
327    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
328        self.0.fmt_prefix(f)
329    }
330}
331
332struct FmtSuffix<'a>(&'a Style);
333
334impl fmt::Display for FmtSuffix<'_> {
335    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
336        self.0.fmt_suffix(f)
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_write_output_with_highlight() {
346        const RESET_COLOR: &str = "\u{1b}[0m";
347        const BOLD_RED: &str = "\u{1b}[31;1m";
348
349        assert_eq!(
350            write_output_with_highlight_buf("output", 0, Some(6)),
351            format!("{RESET_COLOR}{BOLD_RED}output{RESET_COLOR}{RESET_COLOR}\n")
352        );
353
354        assert_eq!(
355            write_output_with_highlight_buf("output", 1, Some(5)),
356            format!("o{RESET_COLOR}{BOLD_RED}utpu{RESET_COLOR}t{RESET_COLOR}\n")
357        );
358
359        assert_eq!(
360            write_output_with_highlight_buf("output\nhighlight 1\nhighlight 2\n", 7, None),
361            format!(
362                "output\n{RESET_COLOR}\
363                {BOLD_RED}highlight 1{RESET_COLOR}\n\
364                {BOLD_RED}highlight 2{RESET_COLOR}{RESET_COLOR}\n"
365            )
366        );
367
368        assert_eq!(
369            write_output_with_highlight_buf(
370                "output\nhighlight 1\nhighlight 2\nnot highlighted",
371                7,
372                None
373            ),
374            format!(
375                "output\n{RESET_COLOR}\
376                {BOLD_RED}highlight 1{RESET_COLOR}\n\
377                {BOLD_RED}highlight 2{RESET_COLOR}\n\
378                not highlighted{RESET_COLOR}\n"
379            )
380        );
381    }
382
383    fn write_output_with_highlight_buf(output: &str, start: usize, end: Option<usize>) -> String {
384        let mut buf = String::new();
387        let end = end.unwrap_or(output.len());
388
389        let subslice = ByteSubslice {
390            start,
391            slice: &output.as_bytes()[start..end],
392        };
393        write_output_with_highlight(output, subslice, &Style::new().red().bold(), &mut buf)
394            .unwrap();
395        buf
396    }
397}