1use super::DisplayerKind;
7use crate::{
8 config::elements::{LeakTimeoutResult, SlowTimeoutResult},
9 errors::DisplayErrorChain,
10 helpers::RESET_COLOR,
11 indenter::indented,
12 output_spec::{LiveSpec, OutputSpec},
13 reporter::{
14 ByteSubslice, TestOutputErrorSlice, UnitErrorDescription,
15 events::*,
16 helpers::{Styles, highlight_end},
17 },
18 test_output::ChildSingleOutput,
19 write_str::WriteStr,
20};
21use owo_colors::{OwoColorize, Style};
22use serde::Deserialize;
23use std::{fmt, io};
24
25#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, serde::Serialize)]
27#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
28#[cfg_attr(test, derive(test_strategy::Arbitrary))]
29#[serde(rename_all = "kebab-case")]
30pub enum TestOutputDisplay {
31 Immediate,
33
34 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
360fn write_output_with_highlight(
361 output: &str,
362 ByteSubslice { slice, start }: ByteSubslice,
363 highlight_style: &Style,
364 writer: &mut dyn WriteStr,
365) -> io::Result<()> {
366 let end = start + highlight_end(slice);
367
368 writer.write_str(&output[..start])?;
371 writer.write_str(RESET_COLOR)?;
372
373 for line in output[start..end].split_inclusive('\n') {
377 write!(writer, "{}", FmtPrefix(highlight_style))?;
378
379 let trimmed = line.trim_end_matches(['\n', '\r']);
381 let stripped = strip_ansi_escapes::strip_str(trimmed);
382 writer.write_str(&stripped)?;
383
384 write!(writer, "{}", FmtSuffix(highlight_style))?;
386
387 writer.write_str(&line[trimmed.len()..])?;
389 }
390
391 write_output_with_trailing_newline(&output[end..], RESET_COLOR, writer)?;
395
396 Ok(())
397}
398
399fn write_output_with_trailing_newline(
404 mut output: &str,
405 trailer: &str,
406 writer: &mut dyn WriteStr,
407) -> io::Result<()> {
408 if output.ends_with('\n') {
411 output = &output[..output.len() - 1];
412 }
413
414 writer.write_str(output)?;
415 writer.write_str(trailer)?;
416 writeln!(writer)
417}
418
419struct FmtPrefix<'a>(&'a Style);
420
421impl fmt::Display for FmtPrefix<'_> {
422 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
423 self.0.fmt_prefix(f)
424 }
425}
426
427struct FmtSuffix<'a>(&'a Style);
428
429impl fmt::Display for FmtSuffix<'_> {
430 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
431 self.0.fmt_suffix(f)
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::reporter::events::UnitKind;
439
440 fn make_test_spec() -> ChildOutputSpec {
441 ChildOutputSpec {
442 kind: UnitKind::Test,
443 stdout_header: "--- STDOUT ---".to_string(),
444 stderr_header: "--- STDERR ---".to_string(),
445 combined_header: "--- OUTPUT ---".to_string(),
446 exec_fail_header: "--- EXEC FAIL ---".to_string(),
447 output_indent: " ",
448 }
449 }
450
451 fn make_unit_output_reporter(displayer_kind: DisplayerKind) -> UnitOutputReporter {
452 UnitOutputReporter::new(
453 OutputDisplayOverrides {
454 force_success_output: None,
455 force_failure_output: None,
456 force_exec_fail_output: None,
457 },
458 displayer_kind,
459 )
460 }
461
462 #[test]
463 fn test_replay_output_not_captured() {
464 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
465 let spec = make_test_spec();
466 let styles = Styles::default();
467
468 let output = ChildOutputDescription::Split {
470 stdout: None,
471 stderr: None,
472 };
473 let mut buf = String::new();
474 reporter
475 .write_child_output(&styles, &spec, &output, None, &mut buf)
476 .unwrap();
477 insta::assert_snapshot!("replay_neither_captured", buf);
478 }
479
480 #[test]
481 fn test_replay_stdout_not_captured() {
482 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
483 let spec = make_test_spec();
484 let styles = Styles::default();
485
486 let output = ChildOutputDescription::Split {
488 stdout: None,
489 stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
490 b"stderr output\n",
491 ))),
492 };
493 let mut buf = String::new();
494 reporter
495 .write_child_output(&styles, &spec, &output, None, &mut buf)
496 .unwrap();
497 insta::assert_snapshot!("replay_stdout_not_captured", buf);
498 }
499
500 #[test]
501 fn test_replay_stderr_not_captured() {
502 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
503 let spec = make_test_spec();
504 let styles = Styles::default();
505
506 let output = ChildOutputDescription::Split {
508 stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
509 b"stdout output\n",
510 ))),
511 stderr: None,
512 };
513 let mut buf = String::new();
514 reporter
515 .write_child_output(&styles, &spec, &output, None, &mut buf)
516 .unwrap();
517 insta::assert_snapshot!("replay_stderr_not_captured", buf);
518 }
519
520 #[test]
521 fn test_replay_both_captured() {
522 let reporter = make_unit_output_reporter(DisplayerKind::Replay);
523 let spec = make_test_spec();
524 let styles = Styles::default();
525
526 let output = ChildOutputDescription::Split {
528 stdout: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
529 b"stdout output\n",
530 ))),
531 stderr: Some(ChildSingleOutput::from(bytes::Bytes::from_static(
532 b"stderr output\n",
533 ))),
534 };
535 let mut buf = String::new();
536 reporter
537 .write_child_output(&styles, &spec, &output, None, &mut buf)
538 .unwrap();
539 insta::assert_snapshot!("replay_both_captured", buf);
540 }
541
542 #[test]
543 fn test_live_output_not_captured_no_message() {
544 let reporter = make_unit_output_reporter(DisplayerKind::Live);
545 let spec = make_test_spec();
546 let styles = Styles::default();
547
548 let output = ChildOutputDescription::Split {
550 stdout: None,
551 stderr: None,
552 };
553 let mut buf = String::new();
554 reporter
555 .write_child_output(&styles, &spec, &output, None, &mut buf)
556 .unwrap();
557 insta::assert_snapshot!("live_neither_captured", buf);
558 }
559
560 #[test]
561 fn test_write_output_with_highlight() {
562 const RESET_COLOR: &str = "\u{1b}[0m";
563 const BOLD_RED: &str = "\u{1b}[31;1m";
564
565 assert_eq!(
566 write_output_with_highlight_buf("output", 0, Some(6)),
567 format!("{RESET_COLOR}{BOLD_RED}output{RESET_COLOR}{RESET_COLOR}\n")
568 );
569
570 assert_eq!(
571 write_output_with_highlight_buf("output", 1, Some(5)),
572 format!("o{RESET_COLOR}{BOLD_RED}utpu{RESET_COLOR}t{RESET_COLOR}\n")
573 );
574
575 assert_eq!(
576 write_output_with_highlight_buf("output\nhighlight 1\nhighlight 2\n", 7, None),
577 format!(
578 "output\n{RESET_COLOR}\
579 {BOLD_RED}highlight 1{RESET_COLOR}\n\
580 {BOLD_RED}highlight 2{RESET_COLOR}{RESET_COLOR}\n"
581 )
582 );
583
584 assert_eq!(
585 write_output_with_highlight_buf(
586 "output\nhighlight 1\nhighlight 2\nnot highlighted",
587 7,
588 None
589 ),
590 format!(
591 "output\n{RESET_COLOR}\
592 {BOLD_RED}highlight 1{RESET_COLOR}\n\
593 {BOLD_RED}highlight 2{RESET_COLOR}\n\
594 not highlighted{RESET_COLOR}\n"
595 )
596 );
597 }
598
599 fn write_output_with_highlight_buf(output: &str, start: usize, end: Option<usize>) -> String {
600 let mut buf = String::new();
603 let end = end.unwrap_or(output.len());
604
605 let subslice = ByteSubslice {
606 start,
607 slice: &output.as_bytes()[start..end],
608 };
609 write_output_with_highlight(output, subslice, &Style::new().red().bold(), &mut buf)
610 .unwrap();
611 buf
612 }
613}