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