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 force_exec_fail_output: Option<TestOutputDisplay>,
77 display_empty_outputs: bool,
78}
79
80impl UnitOutputReporter {
81 pub(super) fn new(
82 force_success_output: Option<TestOutputDisplay>,
83 force_failure_output: Option<TestOutputDisplay>,
84 force_exec_fail_output: Option<TestOutputDisplay>,
85 ) -> Self {
86 let display_empty_outputs =
90 std::env::var_os("__NEXTEST_DISPLAY_EMPTY_OUTPUTS").is_some_and(|v| v == "1");
91
92 Self {
93 force_success_output,
94 force_failure_output,
95 force_exec_fail_output,
96 display_empty_outputs,
97 }
98 }
99
100 pub(super) fn success_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
101 self.force_success_output.unwrap_or(test_setting)
102 }
103
104 pub(super) fn failure_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
105 self.force_failure_output.unwrap_or(test_setting)
106 }
107
108 pub(super) fn exec_fail_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
109 self.force_exec_fail_output.unwrap_or(test_setting)
110 }
111
112 #[cfg(test)]
115 pub(super) fn force_success_output(&self) -> Option<TestOutputDisplay> {
116 self.force_success_output
117 }
118
119 #[cfg(test)]
120 pub(super) fn force_failure_output(&self) -> Option<TestOutputDisplay> {
121 self.force_failure_output
122 }
123
124 pub(super) fn write_child_execution_output(
125 &self,
126 styles: &Styles,
127 spec: &ChildOutputSpec,
128 exec_output: &ChildExecutionOutput,
129 mut writer: &mut dyn WriteStr,
130 ) -> io::Result<()> {
131 match exec_output {
132 ChildExecutionOutput::Output {
133 output,
134 result: _,
136 errors: _,
137 } => {
138 let desc = UnitErrorDescription::new(spec.kind, exec_output);
139
140 if let Some(errors) = desc.exec_fail_error_list() {
143 writeln!(writer, "{}", spec.exec_fail_header)?;
144
145 let error_chain = DisplayErrorChain::new(errors);
147 let mut indent_writer = indented(writer).with_str(spec.output_indent);
148 writeln!(indent_writer, "{error_chain}")?;
149 indent_writer.write_str_flush()?;
150 writer = indent_writer.into_inner();
151 }
152
153 let highlight_slice = if styles.is_colorized {
154 desc.output_slice()
155 } else {
156 None
157 };
158 self.write_child_output(styles, spec, output, highlight_slice, writer)?;
159 }
160
161 ChildExecutionOutput::StartError(error) => {
162 writeln!(writer, "{}", spec.exec_fail_header)?;
163
164 let error_chain = DisplayErrorChain::new(error);
166 let mut indent_writer = indented(writer).with_str(spec.output_indent);
167 writeln!(indent_writer, "{error_chain}")?;
168 indent_writer.write_str_flush()?;
169 writer = indent_writer.into_inner();
170 }
171 }
172
173 writeln!(writer)
174 }
175
176 pub(super) fn write_child_output(
177 &self,
178 styles: &Styles,
179 spec: &ChildOutputSpec,
180 output: &ChildOutput,
181 highlight_slice: Option<TestOutputErrorSlice<'_>>,
182 mut writer: &mut dyn WriteStr,
183 ) -> io::Result<()> {
184 match output {
185 ChildOutput::Split(split) => {
186 if let Some(stdout) = &split.stdout
187 && (self.display_empty_outputs || !stdout.is_empty())
188 {
189 writeln!(writer, "{}", spec.stdout_header)?;
190
191 let mut indent_writer = indented(writer).with_str(spec.output_indent);
196 self.write_test_single_output_with_description(
197 styles,
198 stdout,
199 highlight_slice.and_then(|d| d.stdout_subslice()),
200 &mut indent_writer,
201 )?;
202 indent_writer.write_str_flush()?;
203 writer = indent_writer.into_inner();
204 }
205
206 if let Some(stderr) = &split.stderr
207 && (self.display_empty_outputs || !stderr.is_empty())
208 {
209 writeln!(writer, "{}", spec.stderr_header)?;
210
211 let mut indent_writer = indented(writer).with_str(spec.output_indent);
212 self.write_test_single_output_with_description(
213 styles,
214 stderr,
215 highlight_slice.and_then(|d| d.stderr_subslice()),
216 &mut indent_writer,
217 )?;
218 indent_writer.write_str_flush()?;
219 }
220 }
221 ChildOutput::Combined { output } => {
222 if self.display_empty_outputs || !output.is_empty() {
223 writeln!(writer, "{}", spec.combined_header)?;
224
225 let mut indent_writer = indented(writer).with_str(spec.output_indent);
226 self.write_test_single_output_with_description(
227 styles,
228 output,
229 highlight_slice.and_then(|d| d.combined_subslice()),
230 &mut indent_writer,
231 )?;
232 indent_writer.write_str_flush()?;
233 }
234 }
235 }
236
237 Ok(())
238 }
239
240 fn write_test_single_output_with_description(
245 &self,
246 styles: &Styles,
247 output: &ChildSingleOutput,
248 description: Option<ByteSubslice<'_>>,
249 writer: &mut dyn WriteStr,
250 ) -> io::Result<()> {
251 let output_str = output.as_str_lossy();
252 if styles.is_colorized {
253 if let Some(subslice) = description {
254 write_output_with_highlight(output_str, subslice, &styles.fail, writer)?;
255 } else {
256 write_output_with_trailing_newline(output_str, RESET_COLOR, writer)?;
259 }
260 } else {
261 let output_no_color = strip_ansi_escapes::strip_str(output_str);
263 write_output_with_trailing_newline(&output_no_color, "", writer)?;
264 }
265
266 Ok(())
267 }
268}
269
270const RESET_COLOR: &str = "\x1b[0m";
271
272fn write_output_with_highlight(
273 output: &str,
274 ByteSubslice { slice, start }: ByteSubslice,
275 highlight_style: &Style,
276 writer: &mut dyn WriteStr,
277) -> io::Result<()> {
278 let end = start + highlight_end(slice);
279
280 writer.write_str(&output[..start])?;
283 writer.write_str(RESET_COLOR)?;
284
285 for line in output[start..end].split_inclusive('\n') {
289 write!(writer, "{}", FmtPrefix(highlight_style))?;
290
291 let trimmed = line.trim_end_matches(['\n', '\r']);
293 let stripped = strip_ansi_escapes::strip_str(trimmed);
294 writer.write_str(&stripped)?;
295
296 write!(writer, "{}", FmtSuffix(highlight_style))?;
298
299 writer.write_str(&line[trimmed.len()..])?;
301 }
302
303 write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
307
308 Ok(())
309}
310
311fn write_output_with_trailing_newline(
316 mut output: &str,
317 trailer: &str,
318 writer: &mut dyn WriteStr,
319) -> io::Result<()> {
320 if output.ends_with('\n') {
323 output = &output[..output.len() - 1];
324 }
325
326 writer.write_str(output)?;
327 writer.write_str(trailer)?;
328 writeln!(writer)
329}
330
331struct FmtPrefix<'a>(&'a Style);
332
333impl fmt::Display for FmtPrefix<'_> {
334 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
335 self.0.fmt_prefix(f)
336 }
337}
338
339struct FmtSuffix<'a>(&'a Style);
340
341impl fmt::Display for FmtSuffix<'_> {
342 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
343 self.0.fmt_suffix(f)
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn test_write_output_with_highlight() {
353 const RESET_COLOR: &str = "\u{1b}[0m";
354 const BOLD_RED: &str = "\u{1b}[31;1m";
355
356 assert_eq!(
357 write_output_with_highlight_buf("output", 0, Some(6)),
358 format!("{RESET_COLOR}{BOLD_RED}output{RESET_COLOR}{RESET_COLOR}\n")
359 );
360
361 assert_eq!(
362 write_output_with_highlight_buf("output", 1, Some(5)),
363 format!("o{RESET_COLOR}{BOLD_RED}utpu{RESET_COLOR}t{RESET_COLOR}\n")
364 );
365
366 assert_eq!(
367 write_output_with_highlight_buf("output\nhighlight 1\nhighlight 2\n", 7, None),
368 format!(
369 "output\n{RESET_COLOR}\
370 {BOLD_RED}highlight 1{RESET_COLOR}\n\
371 {BOLD_RED}highlight 2{RESET_COLOR}{RESET_COLOR}\n"
372 )
373 );
374
375 assert_eq!(
376 write_output_with_highlight_buf(
377 "output\nhighlight 1\nhighlight 2\nnot highlighted",
378 7,
379 None
380 ),
381 format!(
382 "output\n{RESET_COLOR}\
383 {BOLD_RED}highlight 1{RESET_COLOR}\n\
384 {BOLD_RED}highlight 2{RESET_COLOR}\n\
385 not highlighted{RESET_COLOR}\n"
386 )
387 );
388 }
389
390 fn write_output_with_highlight_buf(output: &str, start: usize, end: Option<usize>) -> String {
391 let mut buf = String::new();
394 let end = end.unwrap_or(output.len());
395
396 let subslice = ByteSubslice {
397 start,
398 slice: &output.as_bytes()[start..end],
399 };
400 write_output_with_highlight(output, subslice, &Style::new().red().bold(), &mut buf)
401 .unwrap();
402 buf
403 }
404}