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