1use super::DisplayerKind;
7use crate::{
8 config::elements::{LeakTimeoutResult, SlowTimeoutResult},
9 errors::DisplayErrorChain,
10 indenter::indented,
11 output_spec::{LiveSpec, OutputSpec},
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(feature = "config-schema", derive(schemars::JsonSchema))]
27#[cfg_attr(test, derive(test_strategy::Arbitrary))]
28#[serde(rename_all = "kebab-case")]
29pub enum TestOutputDisplay {
30 Immediate,
34
35 ImmediateFinal,
37
38 Final,
40
41 Never,
43}
44
45impl TestOutputDisplay {
46 pub fn is_immediate(self) -> bool {
48 match self {
49 TestOutputDisplay::Immediate | TestOutputDisplay::ImmediateFinal => true,
50 TestOutputDisplay::Final | TestOutputDisplay::Never => false,
51 }
52 }
53
54 pub fn is_final(self) -> bool {
56 match self {
57 TestOutputDisplay::Final | TestOutputDisplay::ImmediateFinal => true,
58 TestOutputDisplay::Immediate | TestOutputDisplay::Never => false,
59 }
60 }
61}
62
63#[derive(Copy, Clone, Debug)]
70#[cfg_attr(test, derive(test_strategy::Arbitrary))]
71pub(super) struct OutputDisplayOverrides {
72 pub(super) force_success_output: Option<TestOutputDisplay>,
73 pub(super) force_failure_output: Option<TestOutputDisplay>,
74 pub(super) force_exec_fail_output: Option<TestOutputDisplay>,
75}
76
77impl OutputDisplayOverrides {
78 pub(super) fn success_output(&self, event_setting: TestOutputDisplay) -> TestOutputDisplay {
80 self.force_success_output.unwrap_or(event_setting)
81 }
82
83 pub(super) fn failure_output(&self, event_setting: TestOutputDisplay) -> TestOutputDisplay {
85 self.force_failure_output.unwrap_or(event_setting)
86 }
87
88 pub(super) fn exec_fail_output(&self, event_setting: TestOutputDisplay) -> TestOutputDisplay {
90 self.force_exec_fail_output.unwrap_or(event_setting)
91 }
92
93 pub(super) fn resolve_for_describe<S: OutputSpec>(
100 &self,
101 success_output: TestOutputDisplay,
102 failure_output: TestOutputDisplay,
103 describe: &ExecutionDescription<'_, S>,
104 ) -> TestOutputDisplay {
105 if describe.is_success_for_output() {
106 self.success_output(success_output)
107 } else {
108 self.resolve_test_output_display(
109 success_output,
110 failure_output,
111 &describe.last_status().result,
112 )
113 }
114 }
115
116 fn resolve_test_output_display(
119 &self,
120 success_output: TestOutputDisplay,
121 failure_output: TestOutputDisplay,
122 result: &ExecutionResultDescription,
123 ) -> TestOutputDisplay {
124 match result {
125 ExecutionResultDescription::Pass
126 | ExecutionResultDescription::Timeout {
127 result: SlowTimeoutResult::Pass,
128 }
129 | ExecutionResultDescription::Leak {
130 result: LeakTimeoutResult::Pass,
131 } => self.success_output(success_output),
132
133 ExecutionResultDescription::Leak {
134 result: LeakTimeoutResult::Fail,
135 }
136 | ExecutionResultDescription::Timeout {
137 result: SlowTimeoutResult::Fail,
138 }
139 | ExecutionResultDescription::Fail { .. } => self.failure_output(failure_output),
140
141 ExecutionResultDescription::ExecFail => self.exec_fail_output(failure_output),
142 }
143 }
144}
145
146#[derive(Debug)]
151pub(super) struct ChildOutputSpec {
152 pub(super) kind: UnitKind,
153 pub(super) stdout_header: String,
154 pub(super) stderr_header: String,
155 pub(super) combined_header: String,
156 pub(super) exec_fail_header: String,
157 pub(super) output_indent: &'static str,
158}
159
160pub(super) struct UnitOutputReporter {
161 overrides: OutputDisplayOverrides,
162 display_empty_outputs: bool,
163 displayer_kind: DisplayerKind,
164}
165
166impl UnitOutputReporter {
167 pub(super) fn new(overrides: OutputDisplayOverrides, displayer_kind: DisplayerKind) -> Self {
168 let display_empty_outputs =
172 std::env::var_os("__NEXTEST_DISPLAY_EMPTY_OUTPUTS").is_some_and(|v| v == "1");
173
174 Self {
175 overrides,
176 display_empty_outputs,
177 displayer_kind,
178 }
179 }
180
181 pub(super) fn overrides(&self) -> OutputDisplayOverrides {
183 self.overrides
184 }
185
186 pub(super) fn write_child_execution_output(
187 &self,
188 styles: &Styles,
189 spec: &ChildOutputSpec,
190 exec_output: &ChildExecutionOutputDescription<LiveSpec>,
191 mut writer: &mut dyn WriteStr,
192 ) -> io::Result<()> {
193 match exec_output {
194 ChildExecutionOutputDescription::Output {
195 output,
196 result: _,
198 errors: _,
199 } => {
200 let desc = UnitErrorDescription::new(spec.kind, exec_output);
201
202 if let Some(errors) = desc.exec_fail_error_list() {
205 writeln!(writer, "{}", spec.exec_fail_header)?;
206
207 let error_chain = DisplayErrorChain::new(errors);
209 let mut indent_writer = indented(writer).with_str(spec.output_indent);
210 writeln!(indent_writer, "{error_chain}")?;
211 indent_writer.write_str_flush()?;
212 writer = indent_writer.into_inner();
213 }
214
215 let highlight_slice = if styles.is_colorized {
216 desc.output_slice()
217 } else {
218 None
219 };
220 self.write_child_output(styles, spec, output, highlight_slice, writer)?;
221 }
222
223 ChildExecutionOutputDescription::StartError(error) => {
224 writeln!(writer, "{}", spec.exec_fail_header)?;
225
226 let error_chain = DisplayErrorChain::new(error);
228 let mut indent_writer = indented(writer).with_str(spec.output_indent);
229 writeln!(indent_writer, "{error_chain}")?;
230 indent_writer.write_str_flush()?;
231 writer = indent_writer.into_inner();
232 }
233 }
234
235 writeln!(writer)
236 }
237
238 pub(super) fn write_child_output(
239 &self,
240 styles: &Styles,
241 spec: &ChildOutputSpec,
242 output: &ChildOutputDescription,
243 highlight_slice: Option<TestOutputErrorSlice<'_>>,
244 mut writer: &mut dyn WriteStr,
245 ) -> io::Result<()> {
246 match output {
247 ChildOutputDescription::Split { stdout, stderr } => {
248 if self.displayer_kind == DisplayerKind::Replay
250 && stdout.is_none()
251 && stderr.is_none()
252 {
253 writeln!(writer, " (output {})", "not captured".style(styles.skip))?;
257 return Ok(());
258 }
259
260 if let Some(stdout) = stdout {
261 if self.display_empty_outputs || !stdout.is_empty() {
262 writeln!(writer, "{}", spec.stdout_header)?;
263
264 let mut indent_writer = indented(writer).with_str(spec.output_indent);
269 self.write_test_single_output_with_description(
270 styles,
271 stdout,
272 highlight_slice.and_then(|d| d.stdout_subslice()),
273 &mut indent_writer,
274 )?;
275 indent_writer.write_str_flush()?;
276 writer = indent_writer.into_inner();
277 }
278 } else if self.displayer_kind == DisplayerKind::Replay {
279 writeln!(writer, " (stdout {})", "not captured".style(styles.skip))?;
283 }
284
285 if let Some(stderr) = stderr {
286 if self.display_empty_outputs || !stderr.is_empty() {
287 writeln!(writer, "{}", spec.stderr_header)?;
288
289 let mut indent_writer = indented(writer).with_str(spec.output_indent);
290 self.write_test_single_output_with_description(
291 styles,
292 stderr,
293 highlight_slice.and_then(|d| d.stderr_subslice()),
294 &mut indent_writer,
295 )?;
296 indent_writer.write_str_flush()?;
297 }
298 } else if self.displayer_kind == DisplayerKind::Replay {
299 writeln!(writer, " (stderr {})", "not captured".style(styles.skip))?;
303 }
304 }
305 ChildOutputDescription::Combined { output } => {
306 if self.display_empty_outputs || !output.is_empty() {
307 writeln!(writer, "{}", spec.combined_header)?;
308
309 let mut indent_writer = indented(writer).with_str(spec.output_indent);
310 self.write_test_single_output_with_description(
311 styles,
312 output,
313 highlight_slice.and_then(|d| d.combined_subslice()),
314 &mut indent_writer,
315 )?;
316 indent_writer.write_str_flush()?;
317 }
318 }
319 ChildOutputDescription::NotLoaded => {
320 unreachable!(
321 "attempted to display output that was not loaded \
322 (the OutputLoadDecider should have returned Load for this event)"
323 );
324 }
325 }
326
327 Ok(())
328 }
329
330 fn write_test_single_output_with_description(
335 &self,
336 styles: &Styles,
337 output: &ChildSingleOutput,
338 description: Option<ByteSubslice<'_>>,
339 writer: &mut dyn WriteStr,
340 ) -> io::Result<()> {
341 let output_str = output.as_str_lossy();
342 if styles.is_colorized {
343 if let Some(subslice) = description {
344 write_output_with_highlight(output_str, subslice, &styles.fail, writer)?;
345 } else {
346 write_output_with_trailing_newline(output_str, RESET_COLOR, writer)?;
349 }
350 } else {
351 let output_no_color = strip_ansi_escapes::strip_str(output_str);
353 write_output_with_trailing_newline(&output_no_color, "", writer)?;
354 }
355
356 Ok(())
357 }
358}
359
360const RESET_COLOR: &str = "\x1b[0m";
361
362fn write_output_with_highlight(
363 output: &str,
364 ByteSubslice { slice, start }: ByteSubslice,
365 highlight_style: &Style,
366 writer: &mut dyn WriteStr,
367) -> io::Result<()> {
368 let end = start + highlight_end(slice);
369
370 writer.write_str(&output[..start])?;
373 writer.write_str(RESET_COLOR)?;
374
375 for line in output[start..end].split_inclusive('\n') {
379 write!(writer, "{}", FmtPrefix(highlight_style))?;
380
381 let trimmed = line.trim_end_matches(['\n', '\r']);
383 let stripped = strip_ansi_escapes::strip_str(trimmed);
384 writer.write_str(&stripped)?;
385
386 write!(writer, "{}", FmtSuffix(highlight_style))?;
388
389 writer.write_str(&line[trimmed.len()..])?;
391 }
392
393 write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
397
398 Ok(())
399}
400
401fn write_output_with_trailing_newline(
406 mut output: &str,
407 trailer: &str,
408 writer: &mut dyn WriteStr,
409) -> io::Result<()> {
410 if output.ends_with('\n') {
413 output = &output[..output.len() - 1];
414 }
415
416 writer.write_str(output)?;
417 writer.write_str(trailer)?;
418 writeln!(writer)
419}
420
421struct FmtPrefix<'a>(&'a Style);
422
423impl fmt::Display for FmtPrefix<'_> {
424 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
425 self.0.fmt_prefix(f)
426 }
427}
428
429struct FmtSuffix<'a>(&'a Style);
430
431impl fmt::Display for FmtSuffix<'_> {
432 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
433 self.0.fmt_suffix(f)
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::reporter::events::UnitKind;
441
442 fn make_test_spec() -> ChildOutputSpec {
443 ChildOutputSpec {
444 kind: UnitKind::Test,
445 stdout_header: "--- STDOUT ---".to_string(),
446 stderr_header: "--- STDERR ---".to_string(),
447 combined_header: "--- OUTPUT ---".to_string(),
448 exec_fail_header: "--- EXEC FAIL ---".to_string(),
449 output_indent: " ",
450 }
451 }
452
453 fn make_unit_output_reporter(displayer_kind: DisplayerKind) -> UnitOutputReporter {
454 UnitOutputReporter::new(
455 OutputDisplayOverrides {
456 force_success_output: None,
457 force_failure_output: None,
458 force_exec_fail_output: None,
459 },
460 displayer_kind,
461 )
462 }
463
464 #[test]
465 fn test_replay_output_not_captured() {
466 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
467 let spec = make_test_spec();
468 let styles = Styles::default();
469
470 let output = ChildOutputDescription::Split {
472 stdout: None,
473 stderr: None,
474 };
475 let mut buf = String::new();
476 reporter
477 .write_child_output(&styles, &spec, &output, None, &mut buf)
478 .unwrap();
479 insta::assert_snapshot!("replay_neither_captured", buf);
480 }
481
482 #[test]
483 fn test_replay_stdout_not_captured() {
484 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
485 let spec = make_test_spec();
486 let styles = Styles::default();
487
488 let output = ChildOutputDescription::Split {
490 stdout: None,
491 stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
492 b"stderr output\n",
493 ))),
494 };
495 let mut buf = String::new();
496 reporter
497 .write_child_output(&styles, &spec, &output, None, &mut buf)
498 .unwrap();
499 insta::assert_snapshot!("replay_stdout_not_captured", buf);
500 }
501
502 #[test]
503 fn test_replay_stderr_not_captured() {
504 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
505 let spec = make_test_spec();
506 let styles = Styles::default();
507
508 let output = ChildOutputDescription::Split {
510 stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
511 b"stdout output\n",
512 ))),
513 stderr: None,
514 };
515 let mut buf = String::new();
516 reporter
517 .write_child_output(&styles, &spec, &output, None, &mut buf)
518 .unwrap();
519 insta::assert_snapshot!("replay_stderr_not_captured", buf);
520 }
521
522 #[test]
523 fn test_replay_both_captured() {
524 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
525 let spec = make_test_spec();
526 let styles = Styles::default();
527
528 let output = ChildOutputDescription::Split {
530 stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
531 b"stdout output\n",
532 ))),
533 stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
534 b"stderr output\n",
535 ))),
536 };
537 let mut buf = String::new();
538 reporter
539 .write_child_output(&styles, &spec, &output, None, &mut buf)
540 .unwrap();
541 insta::assert_snapshot!("replay_both_captured", buf);
542 }
543
544 #[test]
545 fn test_live_output_not_captured_no_message() {
546 let reporter = make_unit_output_reporter(DisplayerKind::Live);
547 let spec = make_test_spec();
548 let styles = Styles::default();
549
550 let output = ChildOutputDescription::Split {
552 stdout: None,
553 stderr: None,
554 };
555 let mut buf = String::new();
556 reporter
557 .write_child_output(&styles, &spec, &output, None, &mut buf)
558 .unwrap();
559 insta::assert_snapshot!("live_neither_captured", buf);
560 }
561
562 #[test]
563 fn test_write_output_with_highlight() {
564 const RESET_COLOR: &str = "\u{1b}[0m";
565 const BOLD_RED: &str = "\u{1b}[31;1m";
566
567 assert_eq!(
568 write_output_with_highlight_buf("output", 0, Some(6)),
569 format!("{RESET_COLOR}{BOLD_RED}output{RESET_COLOR}{RESET_COLOR}\n")
570 );
571
572 assert_eq!(
573 write_output_with_highlight_buf("output", 1, Some(5)),
574 format!("o{RESET_COLOR}{BOLD_RED}utpu{RESET_COLOR}t{RESET_COLOR}\n")
575 );
576
577 assert_eq!(
578 write_output_with_highlight_buf("output\nhighlight 1\nhighlight 2\n", 7, None),
579 format!(
580 "output\n{RESET_COLOR}\
581 {BOLD_RED}highlight 1{RESET_COLOR}\n\
582 {BOLD_RED}highlight 2{RESET_COLOR}{RESET_COLOR}\n"
583 )
584 );
585
586 assert_eq!(
587 write_output_with_highlight_buf(
588 "output\nhighlight 1\nhighlight 2\nnot highlighted",
589 7,
590 None
591 ),
592 format!(
593 "output\n{RESET_COLOR}\
594 {BOLD_RED}highlight 1{RESET_COLOR}\n\
595 {BOLD_RED}highlight 2{RESET_COLOR}\n\
596 not highlighted{RESET_COLOR}\n"
597 )
598 );
599 }
600
601 fn write_output_with_highlight_buf(output: &str, start: usize, end: Option<usize>) -> String {
602 let mut buf = String::new();
605 let end = end.unwrap_or(output.len());
606
607 let subslice = ByteSubslice {
608 start,
609 slice: &output.as_bytes()[start..end],
610 };
611 write_output_with_highlight(output, subslice, &Style::new().red().bold(), &mut buf)
612 .unwrap();
613 buf
614 }
615}