1use super::TestOutputDisplay;
9use crate::reporter::events::{CancelReason, ExecutionResultDescription};
10use serde::Deserialize;
11
12#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
17#[cfg_attr(test, derive(test_strategy::Arbitrary))]
18#[serde(rename_all = "kebab-case")]
19#[non_exhaustive]
20pub enum StatusLevel {
21 None,
23
24 Fail,
26
27 Retry,
29
30 Slow,
32
33 Leak,
35
36 Pass,
38
39 Skip,
41
42 All,
44}
45
46#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
54#[cfg_attr(test, derive(test_strategy::Arbitrary))]
55#[serde(rename_all = "kebab-case")]
56#[non_exhaustive]
57pub enum FinalStatusLevel {
58 None,
60
61 Fail,
63
64 #[serde(alias = "retry")]
66 Flaky,
67
68 Slow,
70
71 Skip,
73
74 Leak,
76
77 Pass,
79
80 All,
82}
83
84pub(crate) struct StatusLevels {
85 pub(crate) status_level: StatusLevel,
86 pub(crate) final_status_level: FinalStatusLevel,
87}
88
89impl StatusLevels {
90 pub(super) fn compute_output_on_test_finished(
91 &self,
92 display: TestOutputDisplay,
93 cancel_status: Option<CancelReason>,
94 test_status_level: StatusLevel,
95 test_final_status_level: FinalStatusLevel,
96 execution_result: &ExecutionResultDescription,
97 ) -> OutputOnTestFinished {
98 let write_status_line = self.status_level >= test_status_level;
99
100 let is_immediate = display.is_immediate();
101 let is_final = display.is_final() || self.final_status_level >= test_final_status_level;
104
105 let terminated_by_nextest = cancel_status == Some(CancelReason::TestFailureImmediate)
109 && execution_result.is_termination_failure();
110
111 let show_immediate =
147 is_immediate && cancel_status <= Some(CancelReason::Signal) && !terminated_by_nextest;
148
149 let store_final = if cancel_status == Some(CancelReason::Interrupt) || terminated_by_nextest
150 {
151 OutputStoreFinal::No
153 } else if is_final && cancel_status < Some(CancelReason::Signal)
154 || !is_immediate && is_final && cancel_status == Some(CancelReason::Signal)
155 {
156 OutputStoreFinal::Yes {
157 display_output: display.is_final(),
158 }
159 } else if is_immediate && is_final && cancel_status == Some(CancelReason::Signal) {
160 OutputStoreFinal::Yes {
163 display_output: false,
164 }
165 } else {
166 OutputStoreFinal::No
167 };
168
169 OutputOnTestFinished {
170 write_status_line,
171 show_immediate,
172 store_final,
173 }
174 }
175}
176
177#[derive(Debug, PartialEq, Eq)]
178pub(super) struct OutputOnTestFinished {
179 pub(super) write_status_line: bool,
180 pub(super) show_immediate: bool,
181 pub(super) store_final: OutputStoreFinal,
182}
183
184#[derive(Debug, PartialEq, Eq)]
185pub(super) enum OutputStoreFinal {
186 No,
188
189 Yes { display_output: bool },
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::{
198 output_spec::RecordingSpec,
199 record::{LoadOutput, OutputEventKind},
200 reporter::{
201 displayer::{OutputLoadDecider, unit_output::OutputDisplayOverrides},
202 events::ExecutionStatuses,
203 },
204 };
205 use test_strategy::{Arbitrary, proptest};
206
207 #[proptest(cases = 64)]
213 fn on_test_finished_dont_write_status_line(
214 display: TestOutputDisplay,
215 cancel_status: Option<CancelReason>,
216 #[filter(StatusLevel::Pass < #test_status_level)] test_status_level: StatusLevel,
217 test_final_status_level: FinalStatusLevel,
218 ) {
219 let status_levels = StatusLevels {
220 status_level: StatusLevel::Pass,
221 final_status_level: FinalStatusLevel::Fail,
222 };
223
224 let actual = status_levels.compute_output_on_test_finished(
225 display,
226 cancel_status,
227 test_status_level,
228 test_final_status_level,
229 &ExecutionResultDescription::Pass,
230 );
231
232 assert!(!actual.write_status_line);
233 }
234
235 #[proptest(cases = 64)]
236 fn on_test_finished_write_status_line(
237 display: TestOutputDisplay,
238 cancel_status: Option<CancelReason>,
239 #[filter(StatusLevel::Pass >= #test_status_level)] test_status_level: StatusLevel,
240 test_final_status_level: FinalStatusLevel,
241 ) {
242 let status_levels = StatusLevels {
243 status_level: StatusLevel::Pass,
244 final_status_level: FinalStatusLevel::Fail,
245 };
246
247 let actual = status_levels.compute_output_on_test_finished(
248 display,
249 cancel_status,
250 test_status_level,
251 test_final_status_level,
252 &ExecutionResultDescription::Pass,
253 );
254 assert!(actual.write_status_line);
255 }
256
257 #[proptest(cases = 64)]
258 fn on_test_finished_with_interrupt(
259 display: TestOutputDisplay,
261 test_status_level: StatusLevel,
265 test_final_status_level: FinalStatusLevel,
266 ) {
267 let status_levels = StatusLevels {
268 status_level: StatusLevel::Pass,
269 final_status_level: FinalStatusLevel::Fail,
270 };
271
272 let actual = status_levels.compute_output_on_test_finished(
273 display,
274 Some(CancelReason::Interrupt),
275 test_status_level,
276 test_final_status_level,
277 &ExecutionResultDescription::Pass,
278 );
279 assert!(!actual.show_immediate);
280 assert_eq!(actual.store_final, OutputStoreFinal::No);
281 }
282
283 #[proptest(cases = 64)]
284 fn on_test_finished_dont_show_immediate(
285 #[filter(!#display.is_immediate())] display: TestOutputDisplay,
286 cancel_status: Option<CancelReason>,
287 test_status_level: StatusLevel,
289 test_final_status_level: FinalStatusLevel,
290 ) {
291 let status_levels = StatusLevels {
292 status_level: StatusLevel::Pass,
293 final_status_level: FinalStatusLevel::Fail,
294 };
295
296 let actual = status_levels.compute_output_on_test_finished(
297 display,
298 cancel_status,
299 test_status_level,
300 test_final_status_level,
301 &ExecutionResultDescription::Pass,
302 );
303 assert!(!actual.show_immediate);
304 }
305
306 #[proptest(cases = 64)]
307 fn on_test_finished_show_immediate(
308 #[filter(#display.is_immediate())] display: TestOutputDisplay,
309 #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
310 test_status_level: StatusLevel,
312 test_final_status_level: FinalStatusLevel,
313 ) {
314 let status_levels = StatusLevels {
315 status_level: StatusLevel::Pass,
316 final_status_level: FinalStatusLevel::Fail,
317 };
318
319 let actual = status_levels.compute_output_on_test_finished(
320 display,
321 cancel_status,
322 test_status_level,
323 test_final_status_level,
324 &ExecutionResultDescription::Pass,
325 );
326 assert!(actual.show_immediate);
327 }
328
329 #[proptest(cases = 64)]
332 fn on_test_finished_dont_store_final(
333 #[filter(!#display.is_final())] display: TestOutputDisplay,
334 cancel_status: Option<CancelReason>,
335 test_status_level: StatusLevel,
337 #[filter(FinalStatusLevel::Fail < #test_final_status_level)]
339 test_final_status_level: FinalStatusLevel,
340 ) {
341 let status_levels = StatusLevels {
342 status_level: StatusLevel::Pass,
343 final_status_level: FinalStatusLevel::Fail,
344 };
345
346 let actual = status_levels.compute_output_on_test_finished(
347 display,
348 cancel_status,
349 test_status_level,
350 test_final_status_level,
351 &ExecutionResultDescription::Pass,
352 );
353 assert_eq!(actual.store_final, OutputStoreFinal::No);
354 }
355
356 #[proptest(cases = 64)]
359 fn on_test_finished_store_final_1(
360 #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
361 test_status_level: StatusLevel,
363 test_final_status_level: FinalStatusLevel,
364 ) {
365 let status_levels = StatusLevels {
366 status_level: StatusLevel::Pass,
367 final_status_level: FinalStatusLevel::Fail,
368 };
369
370 let actual = status_levels.compute_output_on_test_finished(
371 TestOutputDisplay::Final,
372 cancel_status,
373 test_status_level,
374 test_final_status_level,
375 &ExecutionResultDescription::Pass,
376 );
377 assert_eq!(
378 actual.store_final,
379 OutputStoreFinal::Yes {
380 display_output: true
381 }
382 );
383 }
384
385 #[proptest(cases = 64)]
388 fn on_test_finished_store_final_2(
389 #[filter(#cancel_status < Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
390 test_status_level: StatusLevel,
391 test_final_status_level: FinalStatusLevel,
392 ) {
393 let status_levels = StatusLevels {
394 status_level: StatusLevel::Pass,
395 final_status_level: FinalStatusLevel::Fail,
396 };
397
398 let actual = status_levels.compute_output_on_test_finished(
399 TestOutputDisplay::ImmediateFinal,
400 cancel_status,
401 test_status_level,
402 test_final_status_level,
403 &ExecutionResultDescription::Pass,
404 );
405 assert_eq!(
406 actual.store_final,
407 OutputStoreFinal::Yes {
408 display_output: true
409 }
410 );
411 }
412
413 #[proptest(cases = 64)]
416 fn on_test_finished_store_final_3(
417 test_status_level: StatusLevel,
418 test_final_status_level: FinalStatusLevel,
419 ) {
420 let status_levels = StatusLevels {
421 status_level: StatusLevel::Pass,
422 final_status_level: FinalStatusLevel::Fail,
423 };
424
425 let actual = status_levels.compute_output_on_test_finished(
426 TestOutputDisplay::ImmediateFinal,
427 Some(CancelReason::Signal),
428 test_status_level,
429 test_final_status_level,
430 &ExecutionResultDescription::Pass,
431 );
432 assert_eq!(
433 actual.store_final,
434 OutputStoreFinal::Yes {
435 display_output: false,
436 }
437 );
438 }
439
440 #[proptest(cases = 64)]
442 fn on_test_finished_store_final_4(
443 #[filter(!#display.is_final())] display: TestOutputDisplay,
444 #[filter(#cancel_status <= Some(CancelReason::Signal))] cancel_status: Option<CancelReason>,
445 test_status_level: StatusLevel,
447 #[filter(FinalStatusLevel::Fail >= #test_final_status_level)]
449 test_final_status_level: FinalStatusLevel,
450 ) {
451 let status_levels = StatusLevels {
452 status_level: StatusLevel::Pass,
453 final_status_level: FinalStatusLevel::Fail,
454 };
455
456 let actual = status_levels.compute_output_on_test_finished(
457 display,
458 cancel_status,
459 test_status_level,
460 test_final_status_level,
461 &ExecutionResultDescription::Pass,
462 );
463 assert_eq!(
464 actual.store_final,
465 OutputStoreFinal::Yes {
466 display_output: false,
467 }
468 );
469 }
470
471 #[test]
472 fn on_test_finished_terminated_by_nextest() {
473 use crate::reporter::events::{AbortDescription, FailureDescription, SIGTERM};
474
475 let status_levels = StatusLevels {
476 status_level: StatusLevel::Pass,
477 final_status_level: FinalStatusLevel::Fail,
478 };
479
480 {
482 let execution_result = ExecutionResultDescription::Fail {
483 failure: FailureDescription::Abort {
484 abort: AbortDescription::UnixSignal {
485 signal: SIGTERM,
486 name: Some("TERM".into()),
487 },
488 },
489 leaked: false,
490 };
491
492 let actual = status_levels.compute_output_on_test_finished(
493 TestOutputDisplay::ImmediateFinal,
494 Some(CancelReason::TestFailureImmediate),
495 StatusLevel::Fail,
496 FinalStatusLevel::Fail,
497 &execution_result,
498 );
499
500 assert!(
501 !actual.show_immediate,
502 "should not show immediate for SIGTERM during TestFailureImmediate"
503 );
504 assert_eq!(
505 actual.store_final,
506 OutputStoreFinal::No,
507 "should not store final for SIGTERM during TestFailureImmediate"
508 );
509 }
510
511 {
513 let execution_result = ExecutionResultDescription::Fail {
514 failure: FailureDescription::Abort {
515 abort: AbortDescription::WindowsJobObject,
516 },
517 leaked: false,
518 };
519
520 let actual = status_levels.compute_output_on_test_finished(
521 TestOutputDisplay::ImmediateFinal,
522 Some(CancelReason::TestFailureImmediate),
523 StatusLevel::Fail,
524 FinalStatusLevel::Fail,
525 &execution_result,
526 );
527
528 assert!(
529 !actual.show_immediate,
530 "should not show immediate for JobObject during TestFailureImmediate"
531 );
532 assert_eq!(
533 actual.store_final,
534 OutputStoreFinal::No,
535 "should not store final for JobObject during TestFailureImmediate"
536 );
537 }
538
539 let execution_result = ExecutionResultDescription::Fail {
541 failure: FailureDescription::ExitCode { code: 1 },
542 leaked: false,
543 };
544
545 let actual = status_levels.compute_output_on_test_finished(
546 TestOutputDisplay::ImmediateFinal,
547 Some(CancelReason::TestFailureImmediate),
548 StatusLevel::Fail,
549 FinalStatusLevel::Fail,
550 &execution_result,
551 );
552
553 assert!(
554 actual.show_immediate,
555 "should show immediate for natural failure during TestFailureImmediate"
556 );
557 assert_eq!(
558 actual.store_final,
559 OutputStoreFinal::Yes {
560 display_output: true
561 },
562 "should store final for natural failure"
563 );
564
565 {
567 let execution_result = ExecutionResultDescription::Fail {
568 failure: FailureDescription::Abort {
569 abort: AbortDescription::UnixSignal {
570 signal: SIGTERM,
571 name: Some("TERM".into()),
572 },
573 },
574 leaked: false,
575 };
576
577 let actual = status_levels.compute_output_on_test_finished(
578 TestOutputDisplay::ImmediateFinal,
579 Some(CancelReason::Signal), StatusLevel::Fail,
581 FinalStatusLevel::Fail,
582 &execution_result,
583 );
584
585 assert!(
586 actual.show_immediate,
587 "should show immediate for user-initiated SIGTERM"
588 );
589 assert_eq!(
590 actual.store_final,
591 OutputStoreFinal::Yes {
592 display_output: false
593 },
594 "should store but not display final"
595 );
596 }
597 }
598
599 #[proptest(cases = 512)]
624 fn cancellation_only_hides_output(
625 display: TestOutputDisplay,
626 cancel_status: Option<CancelReason>,
627 test_status_level: StatusLevel,
628 test_final_status_level: FinalStatusLevel,
629 execution_result: ExecutionResultDescription,
630 status_level: StatusLevel,
631 final_status_level: FinalStatusLevel,
632 ) {
633 let status_levels = StatusLevels {
634 status_level,
635 final_status_level,
636 };
637
638 let baseline = status_levels.compute_output_on_test_finished(
639 display,
640 None,
641 test_status_level,
642 test_final_status_level,
643 &execution_result,
644 );
645
646 let with_cancel = status_levels.compute_output_on_test_finished(
647 display,
648 cancel_status,
649 test_status_level,
650 test_final_status_level,
651 &execution_result,
652 );
653
654 if !baseline.show_immediate {
656 assert!(
657 !with_cancel.show_immediate,
658 "cancel_status={cancel_status:?} caused immediate output that \
659 wouldn't appear without cancellation"
660 );
661 }
662
663 match (&baseline.store_final, &with_cancel.store_final) {
671 (OutputStoreFinal::No, OutputStoreFinal::Yes { display_output }) => {
673 panic!(
674 "cancel_status={cancel_status:?} caused final output storage \
675 (display_output={display_output}) that wouldn't happen \
676 without cancellation"
677 );
678 }
679 (
682 OutputStoreFinal::Yes {
683 display_output: false,
684 },
685 OutputStoreFinal::Yes {
686 display_output: true,
687 },
688 ) => {
689 panic!(
690 "cancel_status={cancel_status:?} caused final output display \
691 that wouldn't happen without cancellation"
692 );
693 }
694
695 (OutputStoreFinal::No, OutputStoreFinal::No)
697 | (
698 OutputStoreFinal::Yes {
699 display_output: false,
700 },
701 OutputStoreFinal::No,
702 )
703 | (
704 OutputStoreFinal::Yes {
705 display_output: false,
706 },
707 OutputStoreFinal::Yes {
708 display_output: false,
709 },
710 )
711 | (
712 OutputStoreFinal::Yes {
713 display_output: true,
714 },
715 _,
716 ) => {}
717 }
718 }
719
720 #[derive(Debug, Arbitrary)]
728 struct TestFinishedLoadDeciderInput {
729 status_level: StatusLevel,
730 final_status_level: FinalStatusLevel,
731 success_output: TestOutputDisplay,
732 failure_output: TestOutputDisplay,
733 force_success_output: Option<TestOutputDisplay>,
734 force_failure_output: Option<TestOutputDisplay>,
735 force_exec_fail_output: Option<TestOutputDisplay>,
736 run_statuses: ExecutionStatuses<RecordingSpec>,
737 }
738
739 #[proptest(cases = 512)]
751 fn load_decider_test_finished_skip_implies_no_output(input: TestFinishedLoadDeciderInput) {
752 let TestFinishedLoadDeciderInput {
753 status_level,
754 final_status_level,
755 success_output,
756 failure_output,
757 force_success_output,
758 force_failure_output,
759 force_exec_fail_output,
760 run_statuses,
761 } = input;
762
763 let decider = OutputLoadDecider {
764 status_level,
765 overrides: OutputDisplayOverrides {
766 force_success_output,
767 force_failure_output,
768 force_exec_fail_output,
769 },
770 };
771
772 let load_decision =
773 decider.should_load_for_test_finished(success_output, failure_output, &run_statuses);
774
775 if load_decision == LoadOutput::Skip {
776 let describe = run_statuses.describe();
778 let last_status = describe.last_status();
779
780 let display = decider.overrides.resolve_test_output_display(
781 success_output,
782 failure_output,
783 &last_status.result,
784 );
785
786 let test_status_level = describe.status_level();
787 let test_final_status_level = describe.final_status_level();
788
789 let status_levels = StatusLevels {
790 status_level,
791 final_status_level,
792 };
793
794 let output = status_levels.compute_output_on_test_finished(
795 display,
796 None, test_status_level,
798 test_final_status_level,
799 &last_status.result,
800 );
801
802 assert!(
803 !output.show_immediate,
804 "load decider returned Skip but displayer would show immediate output \
805 (display={display:?}, test_status_level={test_status_level:?}, \
806 test_final_status_level={test_final_status_level:?})"
807 );
808 if let OutputStoreFinal::Yes {
811 display_output: true,
812 } = output.store_final
813 {
814 panic!(
815 "load decider returned Skip but displayer would display final output \
816 (display={display:?}, test_status_level={test_status_level:?}, \
817 test_final_status_level={test_final_status_level:?})"
818 );
819 }
820 }
821 }
822
823 #[proptest(cases = 64)]
842 fn load_decider_matches_retry_output(
843 status_level: StatusLevel,
844 failure_output: TestOutputDisplay,
845 force_failure_output: Option<TestOutputDisplay>,
846 ) {
847 let decider = OutputLoadDecider {
848 status_level,
849 overrides: OutputDisplayOverrides {
850 force_success_output: None,
851 force_failure_output,
852 force_exec_fail_output: None,
853 },
854 };
855
856 let resolved = decider.overrides.failure_output(failure_output);
857 let displayer_would_show = resolved.is_immediate() && status_level >= StatusLevel::Retry;
858
859 let expected = if displayer_would_show {
860 LoadOutput::Load
861 } else {
862 LoadOutput::Skip
863 };
864
865 let actual = OutputLoadDecider::should_load_for_retry(resolved, status_level);
866 assert_eq!(actual, expected);
867 }
868
869 #[proptest(cases = 64)]
872 fn load_decider_matches_setup_script_output(execution_result: ExecutionResultDescription) {
873 let expected = if execution_result.is_success() {
874 LoadOutput::Skip
875 } else {
876 LoadOutput::Load
877 };
878 let actual = OutputLoadDecider::should_load_for_setup_script(&execution_result);
879 assert_eq!(actual, expected);
880 }
881
882 #[proptest(cases = 256)]
892 fn should_load_output_consistent_with_helpers(
893 status_level: StatusLevel,
894 force_success_output: Option<TestOutputDisplay>,
895 force_failure_output: Option<TestOutputDisplay>,
896 force_exec_fail_output: Option<TestOutputDisplay>,
897 event_kind: OutputEventKind<RecordingSpec>,
898 ) {
899 let decider = OutputLoadDecider {
900 status_level,
901 overrides: OutputDisplayOverrides {
902 force_success_output,
903 force_failure_output,
904 force_exec_fail_output,
905 },
906 };
907
908 let actual = decider.should_load_output(&event_kind);
909
910 let expected = match &event_kind {
911 OutputEventKind::SetupScriptFinished { run_status, .. } => {
912 OutputLoadDecider::should_load_for_setup_script(&run_status.result)
913 }
914 OutputEventKind::TestAttemptFailedWillRetry { failure_output, .. } => {
915 let display = decider.overrides.failure_output(*failure_output);
916 OutputLoadDecider::should_load_for_retry(display, status_level)
917 }
918 OutputEventKind::TestFinished {
919 success_output,
920 failure_output,
921 run_statuses,
922 ..
923 } => decider.should_load_for_test_finished(
924 *success_output,
925 *failure_output,
926 run_statuses,
927 ),
928 };
929
930 assert_eq!(
931 actual, expected,
932 "should_load_output disagrees with individual helper for event kind"
933 );
934 }
935}