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