nextest_runner/reporter/displayer/
unit_output.rs1use crate::{
7 errors::DisplayErrorChain,
8 reporter::{
9 ByteSubslice, TestOutputErrorSlice, UnitErrorDescription,
10 events::*,
11 helpers::{Styles, highlight_end},
12 },
13 test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
14};
15use bstr::ByteSlice;
16use indent_write::io::IndentWriter;
17use owo_colors::Style;
18use serde::Deserialize;
19use std::{
20 fmt,
21 io::{self, Write},
22};
23
24#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize)]
26#[cfg_attr(test, derive(test_strategy::Arbitrary))]
27#[serde(rename_all = "kebab-case")]
28pub enum TestOutputDisplay {
29 Immediate,
33
34 ImmediateFinal,
36
37 Final,
39
40 Never,
42}
43
44impl TestOutputDisplay {
45 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 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#[derive(Debug)]
67pub(super) struct ChildOutputSpec {
68 pub(super) kind: UnitKind,
69 pub(super) stdout_header: String,
70 pub(super) stderr_header: String,
71 pub(super) combined_header: String,
72 pub(super) exec_fail_header: String,
73 pub(super) output_indent: &'static str,
74}
75
76pub(super) struct UnitOutputReporter {
77 force_success_output: Option<TestOutputDisplay>,
78 force_failure_output: Option<TestOutputDisplay>,
79 display_empty_outputs: bool,
80}
81
82impl UnitOutputReporter {
83 pub(super) fn new(
84 force_success_output: Option<TestOutputDisplay>,
85 force_failure_output: Option<TestOutputDisplay>,
86 ) -> Self {
87 let display_empty_outputs =
91 std::env::var_os("__NEXTEST_DISPLAY_EMPTY_OUTPUTS").is_some_and(|v| v == "1");
92
93 Self {
94 force_success_output,
95 force_failure_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 #[cfg(test)]
111 pub(super) fn force_success_output(&self) -> Option<TestOutputDisplay> {
112 self.force_success_output
113 }
114
115 #[cfg(test)]
116 pub(super) fn force_failure_output(&self) -> Option<TestOutputDisplay> {
117 self.force_failure_output
118 }
119
120 pub(super) fn write_child_execution_output(
121 &self,
122 styles: &Styles,
123 spec: &ChildOutputSpec,
124 exec_output: &ChildExecutionOutput,
125 mut writer: &mut dyn Write,
126 ) -> io::Result<()> {
127 match exec_output {
128 ChildExecutionOutput::Output {
129 output,
130 result: _,
132 errors: _,
133 } => {
134 let desc = UnitErrorDescription::new(spec.kind, exec_output);
135
136 if let Some(errors) = desc.exec_fail_error_list() {
139 writeln!(writer, "{}", spec.exec_fail_header)?;
140
141 let error_chain = DisplayErrorChain::new(errors);
143 let mut indent_writer = IndentWriter::new(spec.output_indent, writer);
144 writeln!(indent_writer, "{error_chain}")?;
145 indent_writer.flush()?;
146 writer = indent_writer.into_inner();
147 }
148
149 let highlight_slice = if styles.is_colorized {
150 desc.output_slice()
151 } else {
152 None
153 };
154 self.write_child_output(styles, spec, output, highlight_slice, writer)?;
155 }
156
157 ChildExecutionOutput::StartError(error) => {
158 writeln!(writer, "{}", spec.exec_fail_header)?;
159
160 let error_chain = DisplayErrorChain::new(error);
162 let mut indent_writer = IndentWriter::new(spec.output_indent, writer);
163 writeln!(indent_writer, "{error_chain}")?;
164 indent_writer.flush()?;
165 writer = indent_writer.into_inner();
166 }
167 }
168
169 writeln!(writer)
170 }
171
172 pub(super) fn write_child_output(
173 &self,
174 styles: &Styles,
175 spec: &ChildOutputSpec,
176 output: &ChildOutput,
177 highlight_slice: Option<TestOutputErrorSlice<'_>>,
178 mut writer: &mut dyn Write,
179 ) -> io::Result<()> {
180 match output {
181 ChildOutput::Split(split) => {
182 if let Some(stdout) = &split.stdout {
183 if self.display_empty_outputs || !stdout.is_empty() {
184 writeln!(writer, "{}", spec.stdout_header)?;
185
186 let mut indent_writer = IndentWriter::new(spec.output_indent, writer);
191 self.write_test_single_output_with_description(
192 styles,
193 stdout,
194 highlight_slice.and_then(|d| d.stdout_subslice()),
195 &mut indent_writer,
196 )?;
197 indent_writer.flush()?;
198 writer = indent_writer.into_inner();
199 }
200 }
201
202 if let Some(stderr) = &split.stderr {
203 if self.display_empty_outputs || !stderr.is_empty() {
204 writeln!(writer, "{}", spec.stderr_header)?;
205
206 let mut indent_writer = IndentWriter::new(spec.output_indent, writer);
207 self.write_test_single_output_with_description(
208 styles,
209 stderr,
210 highlight_slice.and_then(|d| d.stderr_subslice()),
211 &mut indent_writer,
212 )?;
213 indent_writer.flush()?;
214 }
215 }
216 }
217 ChildOutput::Combined { output } => {
218 if self.display_empty_outputs || !output.is_empty() {
219 writeln!(writer, "{}", spec.combined_header)?;
220
221 let mut indent_writer = IndentWriter::new(spec.output_indent, writer);
222 self.write_test_single_output_with_description(
223 styles,
224 output,
225 highlight_slice.and_then(|d| d.combined_subslice()),
226 &mut indent_writer,
227 )?;
228 indent_writer.flush()?;
229 }
230 }
231 }
232
233 Ok(())
234 }
235
236 fn write_test_single_output_with_description(
241 &self,
242 styles: &Styles,
243 output: &ChildSingleOutput,
244 description: Option<ByteSubslice<'_>>,
245 writer: &mut dyn Write,
246 ) -> io::Result<()> {
247 if styles.is_colorized {
248 if let Some(subslice) = description {
249 write_output_with_highlight(&output.buf, subslice, &styles.fail, writer)?;
250 } else {
251 write_output_with_trailing_newline(&output.buf, RESET_COLOR, writer)?;
254 }
255 } else {
256 let mut no_color = strip_ansi_escapes::Writer::new(writer);
258 write_output_with_trailing_newline(&output.buf, b"", &mut no_color)?;
259 }
260
261 Ok(())
262 }
263}
264
265const RESET_COLOR: &[u8] = b"\x1b[0m";
266
267fn write_output_with_highlight(
268 output: &[u8],
269 ByteSubslice { slice, start }: ByteSubslice,
270 highlight_style: &Style,
271 mut writer: &mut dyn Write,
272) -> io::Result<()> {
273 let end = start + highlight_end(slice);
274
275 writer.write_all(&output[..start])?;
278 writer.write_all(RESET_COLOR)?;
279
280 for line in output[start..end].lines_with_terminator() {
284 write!(writer, "{}", FmtPrefix(highlight_style))?;
285
286 let mut no_color = strip_ansi_escapes::Writer::new(writer);
288 let trimmed = line.trim_end_with(|c| c == '\n' || c == '\r');
289 no_color.write_all(trimmed.as_bytes())?;
290 writer = no_color.into_inner()?;
291
292 write!(writer, "{}", FmtSuffix(highlight_style))?;
294
295 writer.write_all(&line[trimmed.len()..])?;
297 }
298
299 write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
303
304 Ok(())
305}
306
307fn write_output_with_trailing_newline(
312 mut output: &[u8],
313 trailer: &[u8],
314 writer: &mut dyn Write,
315) -> io::Result<()> {
316 if output.last() == Some(&b'\n') {
319 output = &output[..output.len() - 1];
320 }
321
322 writer.write_all(output)?;
323 writer.write_all(trailer)?;
324 writer.write_all(b"\n")
325}
326
327struct FmtPrefix<'a>(&'a Style);
328
329impl fmt::Display for FmtPrefix<'_> {
330 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
331 self.0.fmt_prefix(f)
332 }
333}
334
335struct FmtSuffix<'a>(&'a Style);
336
337impl fmt::Display for FmtSuffix<'_> {
338 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
339 self.0.fmt_suffix(f)
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn test_write_output_with_highlight() {
349 const RESET_COLOR: &str = "\u{1b}[0m";
350 const BOLD_RED: &str = "\u{1b}[31;1m";
351
352 assert_eq!(
353 write_output_with_highlight_buf("output", 0, Some(6)),
354 format!("{RESET_COLOR}{BOLD_RED}output{RESET_COLOR}{RESET_COLOR}\n")
355 );
356
357 assert_eq!(
358 write_output_with_highlight_buf("output", 1, Some(5)),
359 format!("o{RESET_COLOR}{BOLD_RED}utpu{RESET_COLOR}t{RESET_COLOR}\n")
360 );
361
362 assert_eq!(
363 write_output_with_highlight_buf("output\nhighlight 1\nhighlight 2\n", 7, None),
364 format!(
365 "output\n{RESET_COLOR}\
366 {BOLD_RED}highlight 1{RESET_COLOR}\n\
367 {BOLD_RED}highlight 2{RESET_COLOR}{RESET_COLOR}\n"
368 )
369 );
370
371 assert_eq!(
372 write_output_with_highlight_buf(
373 "output\nhighlight 1\nhighlight 2\nnot highlighted",
374 7,
375 None
376 ),
377 format!(
378 "output\n{RESET_COLOR}\
379 {BOLD_RED}highlight 1{RESET_COLOR}\n\
380 {BOLD_RED}highlight 2{RESET_COLOR}\n\
381 not highlighted{RESET_COLOR}\n"
382 )
383 );
384 }
385
386 fn write_output_with_highlight_buf(output: &str, start: usize, end: Option<usize>) -> String {
387 let mut buf = Vec::new();
390 let end = end.unwrap_or(output.len());
391
392 let subslice = ByteSubslice {
393 start,
394 slice: &output.as_bytes()[start..end],
395 };
396 write_output_with_highlight(
397 output.as_bytes(),
398 subslice,
399 &Style::new().red().bold(),
400 &mut buf,
401 )
402 .unwrap();
403 String::from_utf8(buf).unwrap()
404 }
405}