1use super::DisplayerKind;
7use crate::{
8 errors::DisplayErrorChain,
9 indenter::indented,
10 reporter::{
11 ByteSubslice, TestOutputErrorSlice, UnitErrorDescription,
12 events::*,
13 helpers::{Styles, highlight_end},
14 },
15 test_output::ChildSingleOutput,
16 write_str::WriteStr,
17};
18use owo_colors::{OwoColorize, Style};
19use serde::Deserialize;
20use std::{fmt, io};
21
22#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, serde::Serialize)]
24#[cfg_attr(test, derive(test_strategy::Arbitrary))]
25#[serde(rename_all = "kebab-case")]
26pub enum TestOutputDisplay {
27 Immediate,
31
32 ImmediateFinal,
34
35 Final,
37
38 Never,
40}
41
42impl TestOutputDisplay {
43 pub fn is_immediate(self) -> bool {
45 match self {
46 TestOutputDisplay::Immediate | TestOutputDisplay::ImmediateFinal => true,
47 TestOutputDisplay::Final | TestOutputDisplay::Never => false,
48 }
49 }
50
51 pub fn is_final(self) -> bool {
53 match self {
54 TestOutputDisplay::Final | TestOutputDisplay::ImmediateFinal => true,
55 TestOutputDisplay::Immediate | TestOutputDisplay::Never => false,
56 }
57 }
58}
59
60#[derive(Debug)]
65pub(super) struct ChildOutputSpec {
66 pub(super) kind: UnitKind,
67 pub(super) stdout_header: String,
68 pub(super) stderr_header: String,
69 pub(super) combined_header: String,
70 pub(super) exec_fail_header: String,
71 pub(super) output_indent: &'static str,
72}
73
74pub(super) struct UnitOutputReporter {
75 force_success_output: Option<TestOutputDisplay>,
76 force_failure_output: Option<TestOutputDisplay>,
77 force_exec_fail_output: Option<TestOutputDisplay>,
78 display_empty_outputs: bool,
79 displayer_kind: DisplayerKind,
80}
81
82impl UnitOutputReporter {
83 pub(super) fn new(
84 force_success_output: Option<TestOutputDisplay>,
85 force_failure_output: Option<TestOutputDisplay>,
86 force_exec_fail_output: Option<TestOutputDisplay>,
87 displayer_kind: DisplayerKind,
88 ) -> Self {
89 let display_empty_outputs =
93 std::env::var_os("__NEXTEST_DISPLAY_EMPTY_OUTPUTS").is_some_and(|v| v == "1");
94
95 Self {
96 force_success_output,
97 force_failure_output,
98 force_exec_fail_output,
99 display_empty_outputs,
100 displayer_kind,
101 }
102 }
103
104 pub(super) fn success_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
105 self.force_success_output.unwrap_or(test_setting)
106 }
107
108 pub(super) fn failure_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
109 self.force_failure_output.unwrap_or(test_setting)
110 }
111
112 pub(super) fn exec_fail_output(&self, test_setting: TestOutputDisplay) -> TestOutputDisplay {
113 self.force_exec_fail_output.unwrap_or(test_setting)
114 }
115
116 #[cfg(test)]
119 pub(super) fn force_success_output(&self) -> Option<TestOutputDisplay> {
120 self.force_success_output
121 }
122
123 #[cfg(test)]
124 pub(super) fn force_failure_output(&self) -> Option<TestOutputDisplay> {
125 self.force_failure_output
126 }
127
128 pub(super) fn write_child_execution_output(
129 &self,
130 styles: &Styles,
131 spec: &ChildOutputSpec,
132 exec_output: &ChildExecutionOutputDescription<ChildSingleOutput>,
133 mut writer: &mut dyn WriteStr,
134 ) -> io::Result<()> {
135 match exec_output {
136 ChildExecutionOutputDescription::Output {
137 output,
138 result: _,
140 errors: _,
141 } => {
142 let desc = UnitErrorDescription::new(spec.kind, exec_output);
143
144 if let Some(errors) = desc.exec_fail_error_list() {
147 writeln!(writer, "{}", spec.exec_fail_header)?;
148
149 let error_chain = DisplayErrorChain::new(errors);
151 let mut indent_writer = indented(writer).with_str(spec.output_indent);
152 writeln!(indent_writer, "{error_chain}")?;
153 indent_writer.write_str_flush()?;
154 writer = indent_writer.into_inner();
155 }
156
157 let highlight_slice = if styles.is_colorized {
158 desc.output_slice()
159 } else {
160 None
161 };
162 self.write_child_output(styles, spec, output, highlight_slice, writer)?;
163 }
164
165 ChildExecutionOutputDescription::StartError(error) => {
166 writeln!(writer, "{}", spec.exec_fail_header)?;
167
168 let error_chain = DisplayErrorChain::new(error);
170 let mut indent_writer = indented(writer).with_str(spec.output_indent);
171 writeln!(indent_writer, "{error_chain}")?;
172 indent_writer.write_str_flush()?;
173 writer = indent_writer.into_inner();
174 }
175 }
176
177 writeln!(writer)
178 }
179
180 pub(super) fn write_child_output(
181 &self,
182 styles: &Styles,
183 spec: &ChildOutputSpec,
184 output: &ChildOutputDescription<ChildSingleOutput>,
185 highlight_slice: Option<TestOutputErrorSlice<'_>>,
186 mut writer: &mut dyn WriteStr,
187 ) -> io::Result<()> {
188 match output {
189 ChildOutputDescription::Split { stdout, stderr } => {
190 if self.displayer_kind == DisplayerKind::Replay
192 && stdout.is_none()
193 && stderr.is_none()
194 {
195 writeln!(writer, " (output {})", "not captured".style(styles.skip))?;
199 return Ok(());
200 }
201
202 if let Some(stdout) = stdout {
203 if self.display_empty_outputs || !stdout.is_empty() {
204 writeln!(writer, "{}", spec.stdout_header)?;
205
206 let mut indent_writer = indented(writer).with_str(spec.output_indent);
211 self.write_test_single_output_with_description(
212 styles,
213 stdout,
214 highlight_slice.and_then(|d| d.stdout_subslice()),
215 &mut indent_writer,
216 )?;
217 indent_writer.write_str_flush()?;
218 writer = indent_writer.into_inner();
219 }
220 } else if self.displayer_kind == DisplayerKind::Replay {
221 writeln!(writer, " (stdout {})", "not captured".style(styles.skip))?;
225 }
226
227 if let Some(stderr) = stderr {
228 if self.display_empty_outputs || !stderr.is_empty() {
229 writeln!(writer, "{}", spec.stderr_header)?;
230
231 let mut indent_writer = indented(writer).with_str(spec.output_indent);
232 self.write_test_single_output_with_description(
233 styles,
234 stderr,
235 highlight_slice.and_then(|d| d.stderr_subslice()),
236 &mut indent_writer,
237 )?;
238 indent_writer.write_str_flush()?;
239 }
240 } else if self.displayer_kind == DisplayerKind::Replay {
241 writeln!(writer, " (stderr {})", "not captured".style(styles.skip))?;
245 }
246 }
247 ChildOutputDescription::Combined { output } => {
248 if self.display_empty_outputs || !output.is_empty() {
249 writeln!(writer, "{}", spec.combined_header)?;
250
251 let mut indent_writer = indented(writer).with_str(spec.output_indent);
252 self.write_test_single_output_with_description(
253 styles,
254 output,
255 highlight_slice.and_then(|d| d.combined_subslice()),
256 &mut indent_writer,
257 )?;
258 indent_writer.write_str_flush()?;
259 }
260 }
261 }
262
263 Ok(())
264 }
265
266 fn write_test_single_output_with_description(
271 &self,
272 styles: &Styles,
273 output: &ChildSingleOutput,
274 description: Option<ByteSubslice<'_>>,
275 writer: &mut dyn WriteStr,
276 ) -> io::Result<()> {
277 let output_str = output.as_str_lossy();
278 if styles.is_colorized {
279 if let Some(subslice) = description {
280 write_output_with_highlight(output_str, subslice, &styles.fail, writer)?;
281 } else {
282 write_output_with_trailing_newline(output_str, RESET_COLOR, writer)?;
285 }
286 } else {
287 let output_no_color = strip_ansi_escapes::strip_str(output_str);
289 write_output_with_trailing_newline(&output_no_color, "", writer)?;
290 }
291
292 Ok(())
293 }
294}
295
296const RESET_COLOR: &str = "\x1b[0m";
297
298fn write_output_with_highlight(
299 output: &str,
300 ByteSubslice { slice, start }: ByteSubslice,
301 highlight_style: &Style,
302 writer: &mut dyn WriteStr,
303) -> io::Result<()> {
304 let end = start + highlight_end(slice);
305
306 writer.write_str(&output[..start])?;
309 writer.write_str(RESET_COLOR)?;
310
311 for line in output[start..end].split_inclusive('\n') {
315 write!(writer, "{}", FmtPrefix(highlight_style))?;
316
317 let trimmed = line.trim_end_matches(['\n', '\r']);
319 let stripped = strip_ansi_escapes::strip_str(trimmed);
320 writer.write_str(&stripped)?;
321
322 write!(writer, "{}", FmtSuffix(highlight_style))?;
324
325 writer.write_str(&line[trimmed.len()..])?;
327 }
328
329 write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
333
334 Ok(())
335}
336
337fn write_output_with_trailing_newline(
342 mut output: &str,
343 trailer: &str,
344 writer: &mut dyn WriteStr,
345) -> io::Result<()> {
346 if output.ends_with('\n') {
349 output = &output[..output.len() - 1];
350 }
351
352 writer.write_str(output)?;
353 writer.write_str(trailer)?;
354 writeln!(writer)
355}
356
357struct FmtPrefix<'a>(&'a Style);
358
359impl fmt::Display for FmtPrefix<'_> {
360 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
361 self.0.fmt_prefix(f)
362 }
363}
364
365struct FmtSuffix<'a>(&'a Style);
366
367impl fmt::Display for FmtSuffix<'_> {
368 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
369 self.0.fmt_suffix(f)
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::reporter::events::UnitKind;
377
378 fn make_test_spec() -> ChildOutputSpec {
379 ChildOutputSpec {
380 kind: UnitKind::Test,
381 stdout_header: "--- STDOUT ---".to_string(),
382 stderr_header: "--- STDERR ---".to_string(),
383 combined_header: "--- OUTPUT ---".to_string(),
384 exec_fail_header: "--- EXEC FAIL ---".to_string(),
385 output_indent: " ",
386 }
387 }
388
389 fn make_unit_output_reporter(displayer_kind: DisplayerKind) -> UnitOutputReporter {
390 UnitOutputReporter::new(None, None, None, displayer_kind)
391 }
392
393 #[test]
394 fn test_replay_output_not_captured() {
395 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
396 let spec = make_test_spec();
397 let styles = Styles::default();
398
399 let output = ChildOutputDescription::Split {
401 stdout: None,
402 stderr: None,
403 };
404 let mut buf = String::new();
405 reporter
406 .write_child_output(&styles, &spec, &output, None, &mut buf)
407 .unwrap();
408 insta::assert_snapshot!("replay_neither_captured", buf);
409 }
410
411 #[test]
412 fn test_replay_stdout_not_captured() {
413 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
414 let spec = make_test_spec();
415 let styles = Styles::default();
416
417 let output = ChildOutputDescription::Split {
419 stdout: None,
420 stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
421 b"stderr output\n",
422 ))),
423 };
424 let mut buf = String::new();
425 reporter
426 .write_child_output(&styles, &spec, &output, None, &mut buf)
427 .unwrap();
428 insta::assert_snapshot!("replay_stdout_not_captured", buf);
429 }
430
431 #[test]
432 fn test_replay_stderr_not_captured() {
433 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
434 let spec = make_test_spec();
435 let styles = Styles::default();
436
437 let output = ChildOutputDescription::Split {
439 stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
440 b"stdout output\n",
441 ))),
442 stderr: None,
443 };
444 let mut buf = String::new();
445 reporter
446 .write_child_output(&styles, &spec, &output, None, &mut buf)
447 .unwrap();
448 insta::assert_snapshot!("replay_stderr_not_captured", buf);
449 }
450
451 #[test]
452 fn test_replay_both_captured() {
453 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
454 let spec = make_test_spec();
455 let styles = Styles::default();
456
457 let output = ChildOutputDescription::Split {
459 stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
460 b"stdout output\n",
461 ))),
462 stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
463 b"stderr output\n",
464 ))),
465 };
466 let mut buf = String::new();
467 reporter
468 .write_child_output(&styles, &spec, &output, None, &mut buf)
469 .unwrap();
470 insta::assert_snapshot!("replay_both_captured", buf);
471 }
472
473 #[test]
474 fn test_live_output_not_captured_no_message() {
475 let reporter = make_unit_output_reporter(DisplayerKind::Live);
476 let spec = make_test_spec();
477 let styles = Styles::default();
478
479 let output = ChildOutputDescription::Split {
481 stdout: None,
482 stderr: None,
483 };
484 let mut buf = String::new();
485 reporter
486 .write_child_output(&styles, &spec, &output, None, &mut buf)
487 .unwrap();
488 insta::assert_snapshot!("live_neither_captured", buf);
489 }
490
491 #[test]
492 fn test_write_output_with_highlight() {
493 const RESET_COLOR: &str = "\u{1b}[0m";
494 const BOLD_RED: &str = "\u{1b}[31;1m";
495
496 assert_eq!(
497 write_output_with_highlight_buf("output", 0, Some(6)),
498 format!("{RESET_COLOR}{BOLD_RED}output{RESET_COLOR}{RESET_COLOR}\n")
499 );
500
501 assert_eq!(
502 write_output_with_highlight_buf("output", 1, Some(5)),
503 format!("o{RESET_COLOR}{BOLD_RED}utpu{RESET_COLOR}t{RESET_COLOR}\n")
504 );
505
506 assert_eq!(
507 write_output_with_highlight_buf("output\nhighlight 1\nhighlight 2\n", 7, None),
508 format!(
509 "output\n{RESET_COLOR}\
510 {BOLD_RED}highlight 1{RESET_COLOR}\n\
511 {BOLD_RED}highlight 2{RESET_COLOR}{RESET_COLOR}\n"
512 )
513 );
514
515 assert_eq!(
516 write_output_with_highlight_buf(
517 "output\nhighlight 1\nhighlight 2\nnot highlighted",
518 7,
519 None
520 ),
521 format!(
522 "output\n{RESET_COLOR}\
523 {BOLD_RED}highlight 1{RESET_COLOR}\n\
524 {BOLD_RED}highlight 2{RESET_COLOR}\n\
525 not highlighted{RESET_COLOR}\n"
526 )
527 );
528 }
529
530 fn write_output_with_highlight_buf(output: &str, start: usize, end: Option<usize>) -> String {
531 let mut buf = String::new();
534 let end = end.unwrap_or(output.len());
535
536 let subslice = ByteSubslice {
537 start,
538 slice: &output.as_bytes()[start..end],
539 };
540 write_output_with_highlight(output, subslice, &Style::new().red().bold(), &mut buf)
541 .unwrap();
542 buf
543 }
544}