1use 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#[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 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(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 pub(super) fn success_output(&self, event_setting: TestOutputDisplay) -> TestOutputDisplay {
79 self.force_success_output.unwrap_or(event_setting)
80 }
81
82 pub(super) fn failure_output(&self, event_setting: TestOutputDisplay) -> TestOutputDisplay {
84 self.force_failure_output.unwrap_or(event_setting)
85 }
86
87 pub(super) fn exec_fail_output(&self, event_setting: TestOutputDisplay) -> TestOutputDisplay {
89 self.force_exec_fail_output.unwrap_or(event_setting)
90 }
91
92 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#[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 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 pub(super) fn overrides(&self) -> OutputDisplayOverrides {
159 self.overrides
160 }
161
162 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: _,
186 errors: _,
187 } => {
188 let desc = UnitErrorDescription::new(spec.kind, exec_output);
189
190 if let Some(errors) = desc.exec_fail_error_list() {
193 writeln!(writer, "{}", spec.exec_fail_header)?;
194
195 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 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 if self.displayer_kind == DisplayerKind::Replay
238 && stdout.is_none()
239 && stderr.is_none()
240 {
241 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 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 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 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 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 write_output_with_trailing_newline(output_str, RESET_COLOR, writer)?;
337 }
338 } else {
339 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 writer.write_str(&output[..start])?;
361 writer.write_str(RESET_COLOR)?;
362
363 for line in output[start..end].split_inclusive('\n') {
367 write!(writer, "{}", FmtPrefix(highlight_style))?;
368
369 let trimmed = line.trim_end_matches(['\n', '\r']);
371 let stripped = strip_ansi_escapes::strip_str(trimmed);
372 writer.write_str(&stripped)?;
373
374 write!(writer, "{}", FmtSuffix(highlight_style))?;
376
377 writer.write_str(&line[trimmed.len()..])?;
379 }
380
381 write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
385
386 Ok(())
387}
388
389fn write_output_with_trailing_newline(
394 mut output: &str,
395 trailer: &str,
396 writer: &mut dyn WriteStr,
397) -> io::Result<()> {
398 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 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 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 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 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 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 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}