1use super::{FinalStatusLevel, StatusLevel, TestOutputDisplay};
10#[cfg(test)]
11use crate::output_spec::ArbitraryOutputSpec;
12use crate::{
13 config::{
14 elements::{
15 FlakyResult, JunitFlakyFailStatus, LeakTimeoutResult, ReportSkipPolicy,
16 SlowTimeoutResult, TestGroup,
17 },
18 scripts::ScriptId,
19 },
20 errors::{ChildError, ChildFdError, ChildStartError, ErrorList},
21 list::{OwnedTestInstanceId, TestInstanceId, TestList},
22 output_spec::{LiveSpec, OutputSpec, SerializableOutputSpec},
23 runner::{StressCondition, StressCount},
24 test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
25};
26use chrono::{DateTime, FixedOffset};
27use nextest_metadata::MismatchReason;
28use quick_junit::ReportUuid;
29use serde::{Deserialize, Serialize};
30use smol_str::SmolStr;
31use std::{
32 collections::BTreeMap, ffi::c_int, fmt, num::NonZero, process::ExitStatus, time::Duration,
33};
34
35pub const SIGTERM: c_int = 15;
40
41#[derive(Clone, Debug)]
43pub enum ReporterEvent<'a> {
44 Tick,
46
47 Test(Box<TestEvent<'a>>),
49}
50#[derive(Clone, Debug)]
55pub struct TestEvent<'a> {
56 pub timestamp: DateTime<FixedOffset>,
58
59 pub elapsed: Duration,
61
62 pub kind: TestEventKind<'a>,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "kebab-case")]
72pub struct TestSlotAssignment {
73 pub global_slot: u64,
76
77 pub group_slot: Option<u64>,
80
81 pub test_group: TestGroup,
83}
84
85#[derive(Clone, Debug)]
89pub enum TestEventKind<'a> {
90 RunStarted {
92 test_list: &'a TestList<'a>,
96
97 run_id: ReportUuid,
99
100 profile_name: String,
102
103 cli_args: Vec<String>,
105
106 stress_condition: Option<StressCondition>,
108 },
109
110 StressSubRunStarted {
112 progress: StressProgress,
114 },
115
116 SetupScriptStarted {
118 stress_index: Option<StressIndex>,
120
121 index: usize,
123
124 total: usize,
126
127 script_id: ScriptId,
129
130 program: String,
132
133 args: Vec<String>,
135
136 no_capture: bool,
138 },
139
140 SetupScriptSlow {
142 stress_index: Option<StressIndex>,
144
145 script_id: ScriptId,
147
148 program: String,
150
151 args: Vec<String>,
153
154 elapsed: Duration,
156
157 will_terminate: bool,
159 },
160
161 SetupScriptFinished {
163 stress_index: Option<StressIndex>,
165
166 index: usize,
168
169 total: usize,
171
172 script_id: ScriptId,
174
175 program: String,
177
178 args: Vec<String>,
180
181 junit_store_success_output: bool,
183
184 junit_store_failure_output: bool,
186
187 no_capture: bool,
189
190 run_status: SetupScriptExecuteStatus<LiveSpec>,
192 },
193
194 TestStarted {
199 stress_index: Option<StressIndex>,
201
202 test_instance: TestInstanceId<'a>,
204
205 slot_assignment: TestSlotAssignment,
207
208 current_stats: RunStats,
210
211 running: usize,
213
214 command_line: Vec<String>,
216 },
217
218 TestSlow {
220 stress_index: Option<StressIndex>,
222
223 test_instance: TestInstanceId<'a>,
225
226 retry_data: RetryData,
228
229 elapsed: Duration,
231
232 will_terminate: bool,
234 },
235
236 TestAttemptFailedWillRetry {
240 stress_index: Option<StressIndex>,
242
243 test_instance: TestInstanceId<'a>,
245
246 run_status: ExecuteStatus<LiveSpec>,
248
249 delay_before_next_attempt: Duration,
251
252 failure_output: TestOutputDisplay,
254
255 running: usize,
257 },
258
259 TestRetryStarted {
261 stress_index: Option<StressIndex>,
263
264 test_instance: TestInstanceId<'a>,
266
267 slot_assignment: TestSlotAssignment,
270
271 retry_data: RetryData,
273
274 running: usize,
276
277 command_line: Vec<String>,
279 },
280
281 TestFinished {
283 stress_index: Option<StressIndex>,
285
286 test_instance: TestInstanceId<'a>,
288
289 success_output: TestOutputDisplay,
291
292 failure_output: TestOutputDisplay,
294
295 junit_store_success_output: bool,
297
298 junit_store_failure_output: bool,
300
301 junit_flaky_fail_status: JunitFlakyFailStatus,
303
304 run_statuses: ExecutionStatuses<LiveSpec>,
306
307 current_stats: RunStats,
309
310 running: usize,
312 },
313
314 TestSkipped {
316 stress_index: Option<StressIndex>,
318
319 test_instance: TestInstanceId<'a>,
321
322 reason: MismatchReason,
324
325 junit_report_skipped: ReportSkipPolicy,
328 },
329
330 InfoStarted {
332 total: usize,
335
336 run_stats: RunStats,
338 },
339
340 InfoResponse {
342 index: usize,
344
345 total: usize,
347
348 response: InfoResponse<'a>,
350 },
351
352 InfoFinished {
354 missing: usize,
357 },
358
359 InputEnter {
362 current_stats: RunStats,
364
365 running: usize,
367 },
368
369 RunBeginCancel {
371 setup_scripts_running: usize,
373
374 current_stats: RunStats,
378
379 running: usize,
381 },
382
383 RunBeginKill {
385 setup_scripts_running: usize,
387
388 current_stats: RunStats,
392
393 running: usize,
395 },
396
397 RunPaused {
399 setup_scripts_running: usize,
401
402 running: usize,
404 },
405
406 RunContinued {
408 setup_scripts_running: usize,
410
411 running: usize,
413 },
414
415 StressSubRunFinished {
417 progress: StressProgress,
419
420 sub_elapsed: Duration,
422
423 sub_stats: RunStats,
425 },
426
427 RunFinished {
429 run_id: ReportUuid,
431
432 start_time: DateTime<FixedOffset>,
434
435 elapsed: Duration,
437
438 run_stats: RunFinishedStats,
440
441 outstanding_not_seen: Option<TestsNotSeen>,
446 },
447}
448
449#[derive(Clone, Debug)]
451pub struct TestsNotSeen {
452 pub not_seen: Vec<OwnedTestInstanceId>,
459
460 pub total_not_seen: usize,
462}
463
464#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
466#[serde(tag = "progress-type", rename_all = "kebab-case")]
467#[cfg_attr(test, derive(test_strategy::Arbitrary))]
468pub enum StressProgress {
469 Count {
471 total: StressCount,
473
474 elapsed: Duration,
476
477 completed: u32,
479 },
480
481 Time {
483 total: Duration,
485
486 elapsed: Duration,
488
489 completed: u32,
491 },
492}
493
494impl StressProgress {
495 pub fn remaining(&self) -> Option<StressRemaining> {
498 match self {
499 Self::Count {
500 total: StressCount::Count { count },
501 elapsed: _,
502 completed,
503 } => count
504 .get()
505 .checked_sub(*completed)
506 .and_then(|remaining| NonZero::try_from(remaining).ok())
507 .map(StressRemaining::Count),
508 Self::Count {
509 total: StressCount::Infinite,
510 ..
511 } => Some(StressRemaining::Infinite),
512 Self::Time {
513 total,
514 elapsed,
515 completed: _,
516 } => total
517 .checked_sub(*elapsed)
518 .and_then(NonZeroDuration::new)
519 .map(StressRemaining::Time),
520 }
521 }
522
523 pub fn unique_id(&self, run_id: ReportUuid) -> String {
525 let stress_current = match self {
526 Self::Count { completed, .. } | Self::Time { completed, .. } => *completed,
527 };
528 format!("{}:@stress-{}", run_id, stress_current)
529 }
530}
531
532#[derive(Clone, Debug, PartialEq, Eq)]
534pub enum StressRemaining {
535 Count(NonZero<u32>),
537
538 Infinite,
540
541 Time(NonZeroDuration),
543}
544
545#[derive(Clone, Copy, Debug, PartialEq, Eq)]
547pub struct NonZeroDuration(Duration);
548
549impl NonZeroDuration {
550 pub fn new(duration: Duration) -> Option<Self> {
554 if duration.is_zero() {
555 None
556 } else {
557 Some(Self(duration))
558 }
559 }
560
561 pub fn get(self) -> Duration {
563 self.0
564 }
565}
566
567#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
569#[serde(rename_all = "kebab-case")]
570#[cfg_attr(test, derive(test_strategy::Arbitrary))]
571pub struct StressIndex {
572 pub current: u32,
574
575 pub total: Option<NonZero<u32>>,
577}
578
579impl StressIndex {
580 pub fn total_get(&self) -> Option<u32> {
582 self.total.map(|t| t.get())
583 }
584}
585
586#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
588#[serde(tag = "type", rename_all = "kebab-case")]
589#[cfg_attr(test, derive(test_strategy::Arbitrary))]
590pub enum RunFinishedStats {
591 Single(RunStats),
593
594 Stress(StressRunStats),
596}
597
598impl RunFinishedStats {
599 pub fn final_stats(&self) -> FinalRunStats {
602 match self {
603 Self::Single(stats) => stats.summarize_final(),
604 Self::Stress(stats) => stats.last_final_stats,
605 }
606 }
607}
608
609#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
611#[serde(rename_all = "kebab-case")]
612#[cfg_attr(test, derive(test_strategy::Arbitrary))]
613pub struct RunStats {
614 pub initial_run_count: usize,
618
619 pub finished_count: usize,
621
622 pub setup_scripts_initial_count: usize,
626
627 pub setup_scripts_finished_count: usize,
629
630 pub setup_scripts_passed: usize,
632
633 pub setup_scripts_failed: usize,
635
636 pub setup_scripts_exec_failed: usize,
638
639 pub setup_scripts_timed_out: usize,
641
642 pub passed: usize,
645
646 pub passed_slow: usize,
648
649 pub passed_timed_out: usize,
651
652 pub flaky: usize,
654
655 pub failed: usize,
658
659 pub failed_slow: usize,
661
662 pub failed_timed_out: usize,
664
665 pub leaky: usize,
667
668 pub leaky_failed: usize,
673
674 pub exec_failed: usize,
676
677 pub skipped: usize,
679
680 pub cancel_reason: Option<CancelReason>,
682}
683
684impl RunStats {
685 pub fn has_failures(&self) -> bool {
687 self.failed_setup_script_count() > 0 || self.failed_count() > 0
688 }
689
690 pub fn failed_setup_script_count(&self) -> usize {
692 self.setup_scripts_failed + self.setup_scripts_exec_failed + self.setup_scripts_timed_out
693 }
694
695 pub fn failed_count(&self) -> usize {
697 self.failed + self.exec_failed + self.failed_timed_out
698 }
699
700 pub fn summarize_final(&self) -> FinalRunStats {
702 if self.failed_setup_script_count() > 0 {
705 self.summarize_failure(RunStatsFailureKind::SetupScript)
706 } else if self.setup_scripts_initial_count > self.setup_scripts_finished_count {
707 FinalRunStats::Cancelled {
708 reason: self.cancel_reason,
709 kind: RunStatsFailureKind::SetupScript,
710 }
711 } else if self.failed_count() > 0 {
712 self.summarize_failure(RunStatsFailureKind::Test {
713 initial_run_count: self.initial_run_count,
714 not_run: self.initial_run_count.saturating_sub(self.finished_count),
715 })
716 } else if self.initial_run_count > self.finished_count {
717 FinalRunStats::Cancelled {
718 reason: self.cancel_reason,
719 kind: RunStatsFailureKind::Test {
720 initial_run_count: self.initial_run_count,
721 not_run: self.initial_run_count.saturating_sub(self.finished_count),
722 },
723 }
724 } else if self.finished_count == 0 {
725 FinalRunStats::NoTestsRun
726 } else {
727 FinalRunStats::Success
728 }
729 }
730
731 fn summarize_failure(&self, kind: RunStatsFailureKind) -> FinalRunStats {
732 match self.cancel_reason {
733 None
734 | Some(
735 CancelReason::SetupScriptFailure
736 | CancelReason::TestFailure
737 | CancelReason::TestFailureImmediate,
738 ) => FinalRunStats::Failed { kind },
739 Some(
740 CancelReason::ReportError
741 | CancelReason::GlobalTimeout
742 | CancelReason::Signal
743 | CancelReason::Interrupt
744 | CancelReason::SecondSignal,
745 ) => FinalRunStats::Cancelled {
746 reason: self.cancel_reason,
747 kind,
748 },
749 }
750 }
751
752 pub(crate) fn on_setup_script_finished(&mut self, status: &SetupScriptExecuteStatus<LiveSpec>) {
753 self.setup_scripts_finished_count += 1;
754
755 match status.result {
756 ExecutionResultDescription::Pass
757 | ExecutionResultDescription::Leak {
758 result: LeakTimeoutResult::Pass,
759 } => {
760 self.setup_scripts_passed += 1;
761 }
762 ExecutionResultDescription::Fail { .. }
763 | ExecutionResultDescription::Leak {
764 result: LeakTimeoutResult::Fail,
765 } => {
766 self.setup_scripts_failed += 1;
767 }
768 ExecutionResultDescription::ExecFail => {
769 self.setup_scripts_exec_failed += 1;
770 }
771 ExecutionResultDescription::Timeout { .. } => {
773 self.setup_scripts_timed_out += 1;
774 }
775 }
776 }
777
778 pub(crate) fn on_test_finished(&mut self, run_statuses: &ExecutionStatuses<LiveSpec>) {
779 self.finished_count += 1;
780 let last_status = run_statuses.last_status();
789 match last_status.result {
790 ExecutionResultDescription::Pass => {
791 let is_flaky = run_statuses.len() > 1;
795 if is_flaky {
796 match run_statuses.flaky_result() {
797 FlakyResult::Fail => {
798 self.failed += 1;
799 if last_status.is_slow {
800 self.failed_slow += 1;
801 }
802 }
803 FlakyResult::Pass => {
804 self.passed += 1;
805 if last_status.is_slow {
806 self.passed_slow += 1;
807 }
808 self.flaky += 1;
809 }
810 }
811 } else {
812 self.passed += 1;
813 if last_status.is_slow {
814 self.passed_slow += 1;
815 }
816 }
817 }
818 ExecutionResultDescription::Leak {
819 result: LeakTimeoutResult::Pass,
820 } => {
821 let is_flaky = run_statuses.len() > 1;
822 if is_flaky {
823 match run_statuses.flaky_result() {
824 FlakyResult::Fail => {
825 self.failed += 1;
826 if last_status.is_slow {
827 self.failed_slow += 1;
828 }
829 self.leaky += 1;
831 }
832 FlakyResult::Pass => {
833 self.passed += 1;
834 self.leaky += 1;
835 if last_status.is_slow {
836 self.passed_slow += 1;
837 }
838 self.flaky += 1;
839 }
840 }
841 } else {
842 self.passed += 1;
843 self.leaky += 1;
844 if last_status.is_slow {
845 self.passed_slow += 1;
846 }
847 }
848 }
849 ExecutionResultDescription::Leak {
850 result: LeakTimeoutResult::Fail,
851 } => {
852 self.failed += 1;
853 self.leaky_failed += 1;
854 if last_status.is_slow {
855 self.failed_slow += 1;
856 }
857 }
858 ExecutionResultDescription::Fail { .. } => {
859 self.failed += 1;
860 if last_status.is_slow {
861 self.failed_slow += 1;
862 }
863 }
864 ExecutionResultDescription::Timeout {
865 result: SlowTimeoutResult::Pass,
866 } => {
867 let is_flaky = run_statuses.len() > 1;
868 if is_flaky {
869 match run_statuses.flaky_result() {
870 FlakyResult::Fail => {
871 self.failed += 1;
872 if last_status.is_slow {
875 self.failed_slow += 1;
876 }
877 }
878 FlakyResult::Pass => {
879 self.passed += 1;
880 self.passed_timed_out += 1;
881 self.flaky += 1;
882 }
883 }
884 } else {
885 self.passed += 1;
886 self.passed_timed_out += 1;
887 }
888 }
889 ExecutionResultDescription::Timeout {
890 result: SlowTimeoutResult::Fail,
891 } => {
892 self.failed_timed_out += 1;
893 }
894 ExecutionResultDescription::ExecFail => self.exec_failed += 1,
895 }
896 }
897}
898
899#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
901#[serde(tag = "outcome", rename_all = "kebab-case")]
902#[cfg_attr(test, derive(test_strategy::Arbitrary))]
903pub enum FinalRunStats {
904 Success,
906
907 NoTestsRun,
909
910 Cancelled {
912 reason: Option<CancelReason>,
917
918 kind: RunStatsFailureKind,
920 },
921
922 Failed {
924 kind: RunStatsFailureKind,
926 },
927}
928
929#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
931#[serde(rename_all = "kebab-case")]
932#[cfg_attr(test, derive(test_strategy::Arbitrary))]
933pub struct StressRunStats {
934 pub completed: StressIndex,
936
937 pub success_count: u32,
939
940 pub failed_count: u32,
942
943 pub last_final_stats: FinalRunStats,
945}
946
947impl StressRunStats {
948 pub fn summarize_final(&self) -> StressFinalRunStats {
950 if self.failed_count > 0 {
951 StressFinalRunStats::Failed
952 } else if matches!(self.last_final_stats, FinalRunStats::Cancelled { .. }) {
953 StressFinalRunStats::Cancelled
954 } else if matches!(self.last_final_stats, FinalRunStats::NoTestsRun) {
955 StressFinalRunStats::NoTestsRun
956 } else {
957 StressFinalRunStats::Success
958 }
959 }
960}
961
962#[derive(Clone, Copy, Debug, PartialEq, Eq)]
964pub enum StressFinalRunStats {
965 Success,
967
968 NoTestsRun,
970
971 Cancelled,
973
974 Failed,
976}
977
978#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
983#[cfg_attr(test, derive(test_strategy::Arbitrary))]
984pub enum RunFailureStep {
985 Test,
987
988 SetupScript,
990}
991
992#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
994#[serde(tag = "step", rename_all = "kebab-case")]
995#[cfg_attr(test, derive(test_strategy::Arbitrary))]
996pub enum RunStatsFailureKind {
997 SetupScript,
999
1000 Test {
1002 initial_run_count: usize,
1004
1005 not_run: usize,
1008 },
1009}
1010
1011impl RunStatsFailureKind {
1012 pub fn step(&self) -> RunFailureStep {
1014 match self {
1015 Self::SetupScript => RunFailureStep::SetupScript,
1016 Self::Test { .. } => RunFailureStep::Test,
1017 }
1018 }
1019}
1020
1021#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1026#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1027#[must_use]
1028pub enum RunOutcome {
1029 Success,
1032
1033 NoTestsRun,
1038
1039 Cancelled {
1044 step: RunFailureStep,
1049 },
1050
1051 Failed {
1054 step: RunFailureStep,
1058 },
1059}
1060
1061impl RunOutcome {
1062 pub fn from_final_stats(stats: FinalRunStats) -> Self {
1065 match stats {
1066 FinalRunStats::Success => Self::Success,
1067 FinalRunStats::NoTestsRun => Self::NoTestsRun,
1068 FinalRunStats::Cancelled { reason: _, kind } => Self::Cancelled { step: kind.step() },
1069 FinalRunStats::Failed { kind } => Self::Failed { step: kind.step() },
1070 }
1071 }
1072
1073 pub(crate) fn combine(self, other: Self) -> Self {
1081 match (self, other) {
1082 (Self::Failed { step: a }, Self::Failed { step: b }) => Self::Failed { step: a.max(b) },
1083 (Self::Failed { step }, Self::Success | Self::NoTestsRun | Self::Cancelled { .. })
1084 | (Self::Success | Self::NoTestsRun | Self::Cancelled { .. }, Self::Failed { step }) => {
1085 Self::Failed { step }
1086 }
1087 (Self::Cancelled { step: a }, Self::Cancelled { step: b }) => {
1088 Self::Cancelled { step: a.max(b) }
1089 }
1090 (Self::Cancelled { step }, Self::Success | Self::NoTestsRun)
1091 | (Self::Success | Self::NoTestsRun, Self::Cancelled { step }) => {
1092 Self::Cancelled { step }
1093 }
1094 (Self::Success, Self::Success | Self::NoTestsRun)
1095 | (Self::NoTestsRun, Self::Success) => Self::Success,
1096 (Self::NoTestsRun, Self::NoTestsRun) => Self::NoTestsRun,
1097 }
1098 }
1099}
1100
1101#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1106#[derive(Serialize)]
1107#[serde(
1108 rename_all = "kebab-case",
1109 bound(serialize = "S: SerializableOutputSpec")
1110)]
1111#[cfg_attr(
1112 test,
1113 derive(test_strategy::Arbitrary),
1114 arbitrary(bound(S: ArbitraryOutputSpec))
1115)]
1116pub struct ExecutionStatuses<S: OutputSpec> {
1117 #[cfg_attr(test, strategy(proptest::collection::vec(proptest::arbitrary::any::<ExecuteStatus<S>>(), 1..=3)))]
1119 statuses: Vec<ExecuteStatus<S>>,
1120
1121 #[serde(default)]
1123 flaky_result: FlakyResult,
1124}
1125
1126impl<'de, S: SerializableOutputSpec> Deserialize<'de> for ExecutionStatuses<S> {
1127 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1128 #[derive(Deserialize)]
1131 #[serde(
1132 rename_all = "kebab-case",
1133 bound(deserialize = "S: SerializableOutputSpec")
1134 )]
1135 struct Helper<S: OutputSpec> {
1136 statuses: Vec<ExecuteStatus<S>>,
1137 #[serde(default)]
1138 flaky_result: FlakyResult,
1139 }
1140
1141 let helper = Helper::<S>::deserialize(deserializer)?;
1142 if helper.statuses.is_empty() {
1143 return Err(serde::de::Error::custom("expected non-empty statuses"));
1144 }
1145 Ok(Self {
1146 statuses: helper.statuses,
1147 flaky_result: helper.flaky_result,
1148 })
1149 }
1150}
1151
1152#[expect(clippy::len_without_is_empty)] impl<S: OutputSpec> ExecutionStatuses<S> {
1154 pub(crate) fn new(statuses: Vec<ExecuteStatus<S>>, flaky_result: FlakyResult) -> Self {
1155 debug_assert!(!statuses.is_empty(), "ExecutionStatuses must be non-empty");
1156 Self {
1157 statuses,
1158 flaky_result,
1159 }
1160 }
1161
1162 pub fn flaky_result(&self) -> FlakyResult {
1164 self.flaky_result
1165 }
1166
1167 pub fn last_status(&self) -> &ExecuteStatus<S> {
1171 self.statuses
1172 .last()
1173 .expect("execution statuses is non-empty")
1174 }
1175
1176 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus<S>> + '_ {
1178 self.statuses.iter()
1179 }
1180
1181 pub fn len(&self) -> usize {
1183 self.statuses.len()
1184 }
1185
1186 pub fn describe(&self) -> ExecutionDescription<'_, S> {
1188 let last_status = self.last_status();
1189 if last_status.result.is_success() {
1190 if self.statuses.len() > 1 {
1191 ExecutionDescription::Flaky {
1192 last_status,
1193 prior_statuses: &self.statuses[..self.statuses.len() - 1],
1194 result: self.flaky_result,
1195 }
1196 } else {
1197 ExecutionDescription::Success {
1198 single_status: last_status,
1199 }
1200 }
1201 } else {
1202 let first_status = self
1203 .statuses
1204 .first()
1205 .expect("execution statuses is non-empty");
1206 let retries = &self.statuses[1..];
1207 ExecutionDescription::Failure {
1208 first_status,
1209 last_status,
1210 retries,
1211 }
1212 }
1213 }
1214}
1215
1216impl<S: OutputSpec> IntoIterator for ExecutionStatuses<S> {
1217 type Item = ExecuteStatus<S>;
1218 type IntoIter = std::vec::IntoIter<ExecuteStatus<S>>;
1219
1220 fn into_iter(self) -> Self::IntoIter {
1221 self.statuses.into_iter()
1222 }
1223}
1224
1225#[derive_where::derive_where(Debug; S::ChildOutputDesc)]
1232pub enum ExecutionDescription<'a, S: OutputSpec> {
1233 Success {
1235 single_status: &'a ExecuteStatus<S>,
1237 },
1238
1239 Flaky {
1241 last_status: &'a ExecuteStatus<S>,
1243
1244 prior_statuses: &'a [ExecuteStatus<S>],
1246
1247 result: FlakyResult,
1249 },
1250
1251 Failure {
1253 first_status: &'a ExecuteStatus<S>,
1255
1256 last_status: &'a ExecuteStatus<S>,
1258
1259 retries: &'a [ExecuteStatus<S>],
1263 },
1264}
1265
1266impl<S: OutputSpec> Clone for ExecutionDescription<'_, S> {
1269 fn clone(&self) -> Self {
1270 *self
1271 }
1272}
1273
1274impl<S: OutputSpec> Copy for ExecutionDescription<'_, S> {}
1275
1276impl<'a, S: OutputSpec> ExecutionDescription<'a, S> {
1277 pub fn status_level(&self) -> StatusLevel {
1279 match self {
1280 ExecutionDescription::Success { single_status } => match single_status.result {
1281 ExecutionResultDescription::Leak {
1282 result: LeakTimeoutResult::Pass,
1283 } => StatusLevel::Leak,
1284 ExecutionResultDescription::Pass => StatusLevel::Pass,
1285 ExecutionResultDescription::Timeout {
1286 result: SlowTimeoutResult::Pass,
1287 } => StatusLevel::Slow,
1288 ref other => unreachable!(
1289 "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1290 ),
1291 },
1292 ExecutionDescription::Flaky {
1294 result: FlakyResult::Pass,
1295 ..
1296 } => StatusLevel::Retry,
1297 ExecutionDescription::Flaky {
1298 result: FlakyResult::Fail,
1299 ..
1300 } => StatusLevel::Fail,
1301 ExecutionDescription::Failure { .. } => StatusLevel::Fail,
1302 }
1303 }
1304
1305 pub fn final_status_level(&self) -> FinalStatusLevel {
1307 match self {
1308 ExecutionDescription::Success { single_status, .. } => {
1309 if single_status.is_slow {
1311 FinalStatusLevel::Slow
1312 } else {
1313 match single_status.result {
1314 ExecutionResultDescription::Pass => FinalStatusLevel::Pass,
1315 ExecutionResultDescription::Leak {
1316 result: LeakTimeoutResult::Pass,
1317 } => FinalStatusLevel::Leak,
1318 ExecutionResultDescription::Timeout {
1322 result: SlowTimeoutResult::Pass,
1323 } => FinalStatusLevel::Slow,
1324 ref other => unreachable!(
1325 "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1326 ),
1327 }
1328 }
1329 }
1330 ExecutionDescription::Flaky {
1332 result: FlakyResult::Pass,
1333 ..
1334 } => FinalStatusLevel::Flaky,
1335 ExecutionDescription::Flaky {
1337 result: FlakyResult::Fail,
1338 ..
1339 } => FinalStatusLevel::Fail,
1340 ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
1341 }
1342 }
1343
1344 pub fn is_success_for_output(&self) -> bool {
1362 match self {
1363 ExecutionDescription::Success { .. } => true,
1364 ExecutionDescription::Flaky { .. } => true,
1368 ExecutionDescription::Failure { .. } => false,
1369 }
1370 }
1371
1372 pub fn last_status(&self) -> &'a ExecuteStatus<S> {
1374 match self {
1375 ExecutionDescription::Success {
1376 single_status: last_status,
1377 }
1378 | ExecutionDescription::Flaky { last_status, .. }
1379 | ExecutionDescription::Failure { last_status, .. } => last_status,
1380 }
1381 }
1382}
1383
1384#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1390#[serde(rename_all = "kebab-case")]
1391#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1392pub struct ErrorSummary {
1393 pub short_message: String,
1395
1396 pub description: String,
1398}
1399
1400#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1405#[serde(rename_all = "kebab-case")]
1406#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1407pub struct OutputErrorSlice {
1408 pub slice: String,
1410
1411 pub start: usize,
1413}
1414
1415#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1424#[derive(Serialize, Deserialize)]
1425#[serde(
1426 rename_all = "kebab-case",
1427 bound(
1428 serialize = "S: SerializableOutputSpec",
1429 deserialize = "S: SerializableOutputSpec"
1430 )
1431)]
1432#[cfg_attr(
1433 test,
1434 derive(test_strategy::Arbitrary),
1435 arbitrary(bound(S: ArbitraryOutputSpec))
1436)]
1437pub struct ExecuteStatus<S: OutputSpec> {
1438 pub retry_data: RetryData,
1440 pub output: ChildExecutionOutputDescription<S>,
1442 pub result: ExecutionResultDescription,
1444 #[cfg_attr(
1446 test,
1447 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1448 )]
1449 pub start_time: DateTime<FixedOffset>,
1450 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1452 pub time_taken: Duration,
1453 pub is_slow: bool,
1455 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1457 pub delay_before_start: Duration,
1458 pub error_summary: Option<ErrorSummary>,
1463 pub output_error_slice: Option<OutputErrorSlice>,
1468}
1469
1470#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1479#[derive(Serialize, Deserialize)]
1480#[serde(
1481 rename_all = "kebab-case",
1482 bound(
1483 serialize = "S: SerializableOutputSpec",
1484 deserialize = "S: SerializableOutputSpec"
1485 )
1486)]
1487#[cfg_attr(
1488 test,
1489 derive(test_strategy::Arbitrary),
1490 arbitrary(bound(S: ArbitraryOutputSpec))
1491)]
1492pub struct SetupScriptExecuteStatus<S: OutputSpec> {
1493 pub output: ChildExecutionOutputDescription<S>,
1495
1496 pub result: ExecutionResultDescription,
1498
1499 #[cfg_attr(
1501 test,
1502 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1503 )]
1504 pub start_time: DateTime<FixedOffset>,
1505
1506 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1508 pub time_taken: Duration,
1509
1510 pub is_slow: bool,
1512
1513 pub env_map: Option<SetupScriptEnvMap>,
1518
1519 pub error_summary: Option<ErrorSummary>,
1524}
1525
1526#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1530#[serde(rename_all = "kebab-case")]
1531#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1532pub struct SetupScriptEnvMap {
1533 pub env_map: BTreeMap<String, String>,
1535}
1536
1537#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1546#[derive(Serialize, Deserialize)]
1547#[serde(
1548 tag = "type",
1549 rename_all = "kebab-case",
1550 bound(
1551 serialize = "S: SerializableOutputSpec",
1552 deserialize = "S: SerializableOutputSpec"
1553 )
1554)]
1555#[cfg_attr(
1556 test,
1557 derive(test_strategy::Arbitrary),
1558 arbitrary(bound(S: ArbitraryOutputSpec))
1559)]
1560pub enum ChildExecutionOutputDescription<S: OutputSpec> {
1561 Output {
1563 result: Option<ExecutionResultDescription>,
1567
1568 output: S::ChildOutputDesc,
1570
1571 errors: Option<ErrorList<ChildErrorDescription>>,
1574 },
1575
1576 StartError(ChildStartErrorDescription),
1578}
1579
1580impl<S: OutputSpec> ChildExecutionOutputDescription<S> {
1581 pub fn has_errors(&self) -> bool {
1583 match self {
1584 Self::Output { errors, result, .. } => {
1585 if errors.is_some() {
1586 return true;
1587 }
1588 if let Some(result) = result {
1589 return !result.is_success();
1590 }
1591 false
1592 }
1593 Self::StartError(_) => true,
1594 }
1595 }
1596}
1597
1598#[derive(Clone, Debug)]
1610pub enum ChildOutputDescription {
1611 Split {
1613 stdout: Option<ChildSingleOutput>,
1615 stderr: Option<ChildSingleOutput>,
1617 },
1618
1619 Combined {
1621 output: ChildSingleOutput,
1623 },
1624
1625 NotLoaded,
1631}
1632
1633impl ChildOutputDescription {
1634 pub fn stdout_stderr_len(&self) -> (Option<u64>, Option<u64>) {
1638 match self {
1639 Self::Split { stdout, stderr } => (
1640 stdout.as_ref().map(|s| s.buf().len() as u64),
1641 stderr.as_ref().map(|s| s.buf().len() as u64),
1642 ),
1643 Self::Combined { output } => (Some(output.buf().len() as u64), None),
1644 Self::NotLoaded => {
1645 unreachable!(
1646 "attempted to get output lengths from output that was not loaded \
1647 (this method is only called from the live runner, where NotLoaded \
1648 is never produced)"
1649 );
1650 }
1651 }
1652 }
1653}
1654
1655#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1659#[serde(tag = "kind", rename_all = "kebab-case")]
1660#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1661pub enum ChildStartErrorDescription {
1662 TempPath {
1664 source: SerializableError,
1666 },
1667
1668 Spawn {
1670 source: SerializableError,
1672 },
1673}
1674
1675impl fmt::Display for ChildStartErrorDescription {
1676 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1677 match self {
1678 Self::TempPath { .. } => {
1679 write!(f, "error creating temporary path for setup script")
1680 }
1681 Self::Spawn { .. } => write!(f, "error spawning child process"),
1682 }
1683 }
1684}
1685
1686impl std::error::Error for ChildStartErrorDescription {
1687 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1688 match self {
1689 Self::TempPath { source } | Self::Spawn { source } => Some(source),
1690 }
1691 }
1692}
1693
1694#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1698#[serde(tag = "kind", rename_all = "kebab-case")]
1699#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1700pub enum ChildErrorDescription {
1701 ReadStdout {
1703 source: SerializableError,
1705 },
1706
1707 ReadStderr {
1709 source: SerializableError,
1711 },
1712
1713 ReadCombined {
1715 source: SerializableError,
1717 },
1718
1719 Wait {
1721 source: SerializableError,
1723 },
1724
1725 SetupScriptOutput {
1727 source: SerializableError,
1729 },
1730}
1731
1732impl fmt::Display for ChildErrorDescription {
1733 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1734 match self {
1735 Self::ReadStdout { .. } => write!(f, "error reading standard output"),
1736 Self::ReadStderr { .. } => write!(f, "error reading standard error"),
1737 Self::ReadCombined { .. } => {
1738 write!(f, "error reading combined stream")
1739 }
1740 Self::Wait { .. } => {
1741 write!(f, "error waiting for child process to exit")
1742 }
1743 Self::SetupScriptOutput { .. } => {
1744 write!(f, "error reading setup script output")
1745 }
1746 }
1747 }
1748}
1749
1750impl std::error::Error for ChildErrorDescription {
1751 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1752 match self {
1753 Self::ReadStdout { source }
1754 | Self::ReadStderr { source }
1755 | Self::ReadCombined { source }
1756 | Self::Wait { source }
1757 | Self::SetupScriptOutput { source } => Some(source),
1758 }
1759 }
1760}
1761
1762#[derive(Clone, Debug, PartialEq, Eq)]
1767pub struct SerializableError {
1768 message: String,
1769 source: Option<Box<SerializableError>>,
1770}
1771
1772impl SerializableError {
1773 pub fn new(error: &dyn std::error::Error) -> Self {
1776 let message = error.to_string();
1777 let mut causes = Vec::new();
1778 let mut source = error.source();
1779 while let Some(err) = source {
1780 causes.push(err.to_string());
1781 source = err.source();
1782 }
1783 Self::from_message_and_causes(message, causes)
1784 }
1785
1786 pub fn from_message_and_causes(message: String, causes: Vec<String>) -> Self {
1789 let mut next = None;
1793 for cause in causes.into_iter().rev() {
1794 let error = Self {
1795 message: cause,
1796 source: next.map(Box::new),
1797 };
1798 next = Some(error);
1799 }
1800 Self {
1801 message,
1802 source: next.map(Box::new),
1803 }
1804 }
1805
1806 pub fn message(&self) -> &str {
1808 &self.message
1809 }
1810
1811 pub fn sources(&self) -> SerializableErrorSources<'_> {
1813 SerializableErrorSources {
1814 current: self.source.as_deref(),
1815 }
1816 }
1817}
1818
1819impl fmt::Display for SerializableError {
1820 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1821 f.write_str(&self.message)
1822 }
1823}
1824
1825impl std::error::Error for SerializableError {
1826 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1827 self.source
1828 .as_deref()
1829 .map(|s| s as &(dyn std::error::Error + 'static))
1830 }
1831}
1832
1833#[derive(Debug)]
1835pub struct SerializableErrorSources<'a> {
1836 current: Option<&'a SerializableError>,
1837}
1838
1839impl<'a> Iterator for SerializableErrorSources<'a> {
1840 type Item = &'a SerializableError;
1841
1842 fn next(&mut self) -> Option<Self::Item> {
1843 let current = self.current?;
1844 self.current = current.source.as_deref();
1845 Some(current)
1846 }
1847}
1848
1849mod serializable_error_serde {
1850 use super::*;
1851
1852 #[derive(Serialize, Deserialize)]
1853 struct Ser {
1854 message: String,
1855 #[serde(default)]
1858 causes: Vec<String>,
1859 }
1860
1861 impl Serialize for SerializableError {
1862 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1863 let mut causes = Vec::new();
1864 let mut cause = self.source.as_ref();
1865 while let Some(c) = cause {
1866 causes.push(c.message.clone());
1867 cause = c.source.as_ref();
1868 }
1869
1870 let ser = Ser {
1871 message: self.message.clone(),
1872 causes,
1873 };
1874 ser.serialize(serializer)
1875 }
1876 }
1877
1878 impl<'de> Deserialize<'de> for SerializableError {
1879 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1880 let ser = Ser::deserialize(deserializer)?;
1881 Ok(SerializableError::from_message_and_causes(
1882 ser.message,
1883 ser.causes,
1884 ))
1885 }
1886 }
1887}
1888
1889#[cfg(test)]
1890mod serializable_error_arbitrary {
1891 use super::*;
1892 use proptest::prelude::*;
1893
1894 impl Arbitrary for SerializableError {
1895 type Parameters = ();
1896 type Strategy = BoxedStrategy<Self>;
1897
1898 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1899 (
1900 any::<String>(),
1901 proptest::collection::vec(any::<String>(), 0..3),
1902 )
1903 .prop_map(|(message, causes)| {
1904 SerializableError::from_message_and_causes(message, causes)
1905 })
1906 .boxed()
1907 }
1908 }
1909}
1910
1911impl From<ChildExecutionOutput> for ChildExecutionOutputDescription<LiveSpec> {
1912 fn from(output: ChildExecutionOutput) -> Self {
1913 match output {
1914 ChildExecutionOutput::Output {
1915 result,
1916 output,
1917 errors,
1918 } => Self::Output {
1919 result: result.map(ExecutionResultDescription::from),
1920 output: ChildOutputDescription::from(output),
1921 errors: errors.map(|e| e.map(ChildErrorDescription::from)),
1922 },
1923 ChildExecutionOutput::StartError(error) => {
1924 Self::StartError(ChildStartErrorDescription::from(error))
1925 }
1926 }
1927 }
1928}
1929
1930impl From<ChildOutput> for ChildOutputDescription {
1931 fn from(output: ChildOutput) -> Self {
1932 match output {
1933 ChildOutput::Split(split) => Self::Split {
1934 stdout: split.stdout,
1935 stderr: split.stderr,
1936 },
1937 ChildOutput::Combined { output } => Self::Combined { output },
1938 }
1939 }
1940}
1941
1942impl From<ChildStartError> for ChildStartErrorDescription {
1943 fn from(error: ChildStartError) -> Self {
1944 match error {
1945 ChildStartError::TempPath(e) => Self::TempPath {
1946 source: SerializableError::new(&*e),
1947 },
1948 ChildStartError::Spawn(e) => Self::Spawn {
1949 source: SerializableError::new(&*e),
1950 },
1951 }
1952 }
1953}
1954
1955impl From<ChildError> for ChildErrorDescription {
1956 fn from(error: ChildError) -> Self {
1957 match error {
1958 ChildError::Fd(ChildFdError::ReadStdout(e)) => Self::ReadStdout {
1959 source: SerializableError::new(&*e),
1960 },
1961 ChildError::Fd(ChildFdError::ReadStderr(e)) => Self::ReadStderr {
1962 source: SerializableError::new(&*e),
1963 },
1964 ChildError::Fd(ChildFdError::ReadCombined(e)) => Self::ReadCombined {
1965 source: SerializableError::new(&*e),
1966 },
1967 ChildError::Fd(ChildFdError::Wait(e)) => Self::Wait {
1968 source: SerializableError::new(&*e),
1969 },
1970 ChildError::SetupScriptOutput(e) => Self::SetupScriptOutput {
1971 source: SerializableError::new(&e),
1972 },
1973 }
1974 }
1975}
1976
1977#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
1979#[serde(rename_all = "kebab-case")]
1980#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1981pub struct RetryData {
1982 pub attempt: u32,
1984
1985 pub total_attempts: u32,
1987}
1988
1989impl RetryData {
1990 pub fn is_last_attempt(&self) -> bool {
1992 self.attempt >= self.total_attempts
1993 }
1994}
1995
1996#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1998pub enum ExecutionResult {
1999 Pass,
2001 Leak {
2005 result: LeakTimeoutResult,
2012 },
2013 Fail {
2015 failure_status: FailureStatus,
2017
2018 leaked: bool,
2022 },
2023 ExecFail,
2025 Timeout {
2027 result: SlowTimeoutResult,
2029 },
2030}
2031
2032impl ExecutionResult {
2033 pub fn is_success(self) -> bool {
2035 match self {
2036 ExecutionResult::Pass
2037 | ExecutionResult::Timeout {
2038 result: SlowTimeoutResult::Pass,
2039 }
2040 | ExecutionResult::Leak {
2041 result: LeakTimeoutResult::Pass,
2042 } => true,
2043 ExecutionResult::Leak {
2044 result: LeakTimeoutResult::Fail,
2045 }
2046 | ExecutionResult::Fail { .. }
2047 | ExecutionResult::ExecFail
2048 | ExecutionResult::Timeout {
2049 result: SlowTimeoutResult::Fail,
2050 } => false,
2051 }
2052 }
2053
2054 pub fn as_static_str(&self) -> &'static str {
2056 match self {
2057 ExecutionResult::Pass => "pass",
2058 ExecutionResult::Leak { .. } => "leak",
2059 ExecutionResult::Fail { .. } => "fail",
2060 ExecutionResult::ExecFail => "exec-fail",
2061 ExecutionResult::Timeout { .. } => "timeout",
2062 }
2063 }
2064}
2065
2066#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2068pub enum FailureStatus {
2069 ExitCode(i32),
2071
2072 Abort(AbortStatus),
2074}
2075
2076impl FailureStatus {
2077 pub fn extract(exit_status: ExitStatus) -> Self {
2079 if let Some(abort_status) = AbortStatus::extract(exit_status) {
2080 FailureStatus::Abort(abort_status)
2081 } else {
2082 FailureStatus::ExitCode(
2083 exit_status
2084 .code()
2085 .expect("if abort_status is None, then code must be present"),
2086 )
2087 }
2088 }
2089}
2090
2091#[derive(Copy, Clone, Eq, PartialEq)]
2095pub enum AbortStatus {
2096 #[cfg(unix)]
2098 UnixSignal(i32),
2099
2100 #[cfg(windows)]
2102 WindowsNtStatus(windows_sys::Win32::Foundation::NTSTATUS),
2103
2104 #[cfg(windows)]
2106 JobObject,
2107}
2108
2109impl AbortStatus {
2110 pub fn extract(exit_status: ExitStatus) -> Option<Self> {
2112 cfg_if::cfg_if! {
2113 if #[cfg(unix)] {
2114 use std::os::unix::process::ExitStatusExt;
2116 exit_status.signal().map(AbortStatus::UnixSignal)
2117 } else if #[cfg(windows)] {
2118 exit_status.code().and_then(|code| {
2119 (code < 0).then_some(AbortStatus::WindowsNtStatus(code))
2120 })
2121 } else {
2122 None
2123 }
2124 }
2125 }
2126}
2127
2128impl fmt::Debug for AbortStatus {
2129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2130 match self {
2131 #[cfg(unix)]
2132 AbortStatus::UnixSignal(signal) => write!(f, "UnixSignal({signal})"),
2133 #[cfg(windows)]
2134 AbortStatus::WindowsNtStatus(status) => write!(f, "WindowsNtStatus({status:x})"),
2135 #[cfg(windows)]
2136 AbortStatus::JobObject => write!(f, "JobObject"),
2137 }
2138 }
2139}
2140
2141#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2147#[serde(tag = "kind", rename_all = "kebab-case")]
2148#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2149#[non_exhaustive]
2150pub enum AbortDescription {
2151 UnixSignal {
2153 signal: i32,
2155 #[cfg_attr(
2158 test,
2159 strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
2160 )]
2161 name: Option<SmolStr>,
2162 },
2163
2164 WindowsNtStatus {
2166 code: i32,
2168 #[cfg_attr(
2170 test,
2171 strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
2172 )]
2173 message: Option<SmolStr>,
2174 },
2175
2176 WindowsJobObject,
2178}
2179
2180impl fmt::Display for AbortDescription {
2181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2182 match self {
2183 Self::UnixSignal { signal, name } => {
2184 write!(f, "aborted with signal {signal}")?;
2185 if let Some(name) = name {
2186 write!(f, " (SIG{name})")?;
2187 }
2188 Ok(())
2189 }
2190 Self::WindowsNtStatus { code, message } => {
2191 write!(f, "aborted with code {code:#010x}")?;
2192 if let Some(message) = message {
2193 write!(f, ": {message}")?;
2194 }
2195 Ok(())
2196 }
2197 Self::WindowsJobObject => {
2198 write!(f, "terminated via job object")
2199 }
2200 }
2201 }
2202}
2203
2204impl From<AbortStatus> for AbortDescription {
2205 fn from(status: AbortStatus) -> Self {
2206 cfg_if::cfg_if! {
2207 if #[cfg(unix)] {
2208 match status {
2209 AbortStatus::UnixSignal(signal) => Self::UnixSignal {
2210 signal,
2211 name: crate::helpers::signal_str(signal).map(SmolStr::new_static),
2212 },
2213 }
2214 } else if #[cfg(windows)] {
2215 match status {
2216 AbortStatus::WindowsNtStatus(code) => Self::WindowsNtStatus {
2217 code,
2218 message: crate::helpers::windows_nt_status_message(code),
2219 },
2220 AbortStatus::JobObject => Self::WindowsJobObject,
2221 }
2222 } else {
2223 match status {}
2224 }
2225 }
2226 }
2227}
2228
2229#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2233#[serde(tag = "kind", rename_all = "kebab-case")]
2234#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2235#[non_exhaustive]
2236pub enum FailureDescription {
2237 ExitCode {
2239 code: i32,
2241 },
2242
2243 Abort {
2250 abort: AbortDescription,
2252 },
2253}
2254
2255impl From<FailureStatus> for FailureDescription {
2256 fn from(status: FailureStatus) -> Self {
2257 match status {
2258 FailureStatus::ExitCode(code) => Self::ExitCode { code },
2259 FailureStatus::Abort(abort) => Self::Abort {
2260 abort: AbortDescription::from(abort),
2261 },
2262 }
2263 }
2264}
2265
2266impl fmt::Display for FailureDescription {
2267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2268 match self {
2269 Self::ExitCode { code } => write!(f, "exited with code {code}"),
2270 Self::Abort { abort } => write!(f, "{abort}"),
2271 }
2272 }
2273}
2274
2275#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2280#[serde(tag = "status", rename_all = "kebab-case")]
2281#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2282#[non_exhaustive]
2283pub enum ExecutionResultDescription {
2284 Pass,
2286
2287 Leak {
2289 result: LeakTimeoutResult,
2291 },
2292
2293 Fail {
2295 failure: FailureDescription,
2297
2298 leaked: bool,
2300 },
2301
2302 ExecFail,
2304
2305 Timeout {
2307 result: SlowTimeoutResult,
2309 },
2310}
2311
2312impl ExecutionResultDescription {
2313 pub fn is_success(&self) -> bool {
2315 match self {
2316 Self::Pass
2317 | Self::Timeout {
2318 result: SlowTimeoutResult::Pass,
2319 }
2320 | Self::Leak {
2321 result: LeakTimeoutResult::Pass,
2322 } => true,
2323 Self::Leak {
2324 result: LeakTimeoutResult::Fail,
2325 }
2326 | Self::Fail { .. }
2327 | Self::ExecFail
2328 | Self::Timeout {
2329 result: SlowTimeoutResult::Fail,
2330 } => false,
2331 }
2332 }
2333
2334 pub fn as_static_str(&self) -> &'static str {
2336 match self {
2337 Self::Pass => "pass",
2338 Self::Leak { .. } => "leak",
2339 Self::Fail { .. } => "fail",
2340 Self::ExecFail => "exec-fail",
2341 Self::Timeout { .. } => "timeout",
2342 }
2343 }
2344
2345 pub fn is_termination_failure(&self) -> bool {
2357 matches!(
2358 self,
2359 Self::Fail {
2360 failure: FailureDescription::Abort {
2361 abort: AbortDescription::UnixSignal {
2362 signal: SIGTERM,
2363 ..
2364 },
2365 },
2366 ..
2367 } | Self::Fail {
2368 failure: FailureDescription::Abort {
2369 abort: AbortDescription::WindowsJobObject,
2370 },
2371 ..
2372 }
2373 )
2374 }
2375}
2376
2377impl From<ExecutionResult> for ExecutionResultDescription {
2378 fn from(result: ExecutionResult) -> Self {
2379 match result {
2380 ExecutionResult::Pass => Self::Pass,
2381 ExecutionResult::Leak { result } => Self::Leak { result },
2382 ExecutionResult::Fail {
2383 failure_status,
2384 leaked,
2385 } => Self::Fail {
2386 failure: FailureDescription::from(failure_status),
2387 leaked,
2388 },
2389 ExecutionResult::ExecFail => Self::ExecFail,
2390 ExecutionResult::Timeout { result } => Self::Timeout { result },
2391 }
2392 }
2393}
2394
2395#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
2398#[serde(rename_all = "kebab-case")]
2399#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2400pub enum CancelReason {
2401 SetupScriptFailure,
2403
2404 TestFailure,
2406
2407 ReportError,
2409
2410 TestFailureImmediate,
2412
2413 GlobalTimeout,
2415
2416 Signal,
2418
2419 Interrupt,
2421
2422 SecondSignal,
2424}
2425
2426impl CancelReason {
2427 pub(crate) fn to_static_str(self) -> &'static str {
2428 match self {
2429 CancelReason::SetupScriptFailure => "setup script failure",
2430 CancelReason::TestFailure => "test failure",
2431 CancelReason::ReportError => "reporting error",
2432 CancelReason::TestFailureImmediate => "test failure",
2433 CancelReason::GlobalTimeout => "global timeout",
2434 CancelReason::Signal => "signal",
2435 CancelReason::Interrupt => "interrupt",
2436 CancelReason::SecondSignal => "second signal",
2437 }
2438 }
2439}
2440#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2442pub enum UnitKind {
2443 Test,
2445
2446 Script,
2448}
2449
2450impl UnitKind {
2451 pub(crate) const WAITING_ON_TEST_MESSAGE: &str = "waiting on test process";
2452 pub(crate) const WAITING_ON_SCRIPT_MESSAGE: &str = "waiting on script process";
2453
2454 pub(crate) const EXECUTING_TEST_MESSAGE: &str = "executing test";
2455 pub(crate) const EXECUTING_SCRIPT_MESSAGE: &str = "executing script";
2456
2457 pub(crate) fn waiting_on_message(&self) -> &'static str {
2458 match self {
2459 UnitKind::Test => Self::WAITING_ON_TEST_MESSAGE,
2460 UnitKind::Script => Self::WAITING_ON_SCRIPT_MESSAGE,
2461 }
2462 }
2463
2464 pub(crate) fn executing_message(&self) -> &'static str {
2465 match self {
2466 UnitKind::Test => Self::EXECUTING_TEST_MESSAGE,
2467 UnitKind::Script => Self::EXECUTING_SCRIPT_MESSAGE,
2468 }
2469 }
2470}
2471
2472#[derive(Clone, Debug)]
2474pub enum InfoResponse<'a> {
2475 SetupScript(SetupScriptInfoResponse),
2477
2478 Test(TestInfoResponse<'a>),
2480}
2481
2482#[derive(Clone, Debug)]
2484pub struct SetupScriptInfoResponse {
2485 pub stress_index: Option<StressIndex>,
2487
2488 pub script_id: ScriptId,
2490
2491 pub program: String,
2493
2494 pub args: Vec<String>,
2496
2497 pub state: UnitState,
2499
2500 pub output: ChildExecutionOutputDescription<LiveSpec>,
2502}
2503
2504#[derive(Clone, Debug)]
2506pub struct TestInfoResponse<'a> {
2507 pub stress_index: Option<StressIndex>,
2509
2510 pub test_instance: TestInstanceId<'a>,
2512
2513 pub retry_data: RetryData,
2515
2516 pub state: UnitState,
2518
2519 pub output: ChildExecutionOutputDescription<LiveSpec>,
2521}
2522
2523#[derive(Clone, Debug)]
2528pub enum UnitState {
2529 Running {
2531 pid: u32,
2533
2534 time_taken: Duration,
2536
2537 slow_after: Option<Duration>,
2540 },
2541
2542 Exiting {
2545 pid: u32,
2547
2548 time_taken: Duration,
2550
2551 slow_after: Option<Duration>,
2554
2555 tentative_result: Option<ExecutionResultDescription>,
2560
2561 waiting_duration: Duration,
2563
2564 remaining: Duration,
2566 },
2567
2568 Terminating(UnitTerminatingState),
2570
2571 Exited {
2573 result: ExecutionResultDescription,
2575
2576 time_taken: Duration,
2578
2579 slow_after: Option<Duration>,
2582 },
2583
2584 DelayBeforeNextAttempt {
2587 previous_result: ExecutionResultDescription,
2589
2590 previous_slow: bool,
2592
2593 waiting_duration: Duration,
2595
2596 remaining: Duration,
2598 },
2599}
2600
2601impl UnitState {
2602 pub fn has_valid_output(&self) -> bool {
2604 match self {
2605 UnitState::Running { .. }
2606 | UnitState::Exiting { .. }
2607 | UnitState::Terminating(_)
2608 | UnitState::Exited { .. } => true,
2609 UnitState::DelayBeforeNextAttempt { .. } => false,
2610 }
2611 }
2612}
2613
2614#[derive(Clone, Debug)]
2618pub struct UnitTerminatingState {
2619 pub pid: u32,
2621
2622 pub time_taken: Duration,
2624
2625 pub reason: UnitTerminateReason,
2627
2628 pub method: UnitTerminateMethod,
2630
2631 pub waiting_duration: Duration,
2633
2634 pub remaining: Duration,
2636}
2637
2638#[derive(Clone, Copy, Debug)]
2642pub enum UnitTerminateReason {
2643 Timeout,
2645
2646 Signal,
2648
2649 Interrupt,
2651}
2652
2653impl fmt::Display for UnitTerminateReason {
2654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2655 match self {
2656 UnitTerminateReason::Timeout => write!(f, "timeout"),
2657 UnitTerminateReason::Signal => write!(f, "signal"),
2658 UnitTerminateReason::Interrupt => write!(f, "interrupt"),
2659 }
2660 }
2661}
2662
2663#[derive(Clone, Copy, Debug)]
2665pub enum UnitTerminateMethod {
2666 #[cfg(unix)]
2668 Signal(UnitTerminateSignal),
2669
2670 #[cfg(windows)]
2672 JobObject,
2673
2674 #[cfg(windows)]
2682 Wait,
2683
2684 #[cfg(test)]
2686 Fake,
2687}
2688
2689#[cfg(unix)]
2690#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2692pub enum UnitTerminateSignal {
2693 Interrupt,
2695
2696 Term,
2698
2699 Hangup,
2701
2702 Quit,
2704
2705 Kill,
2707}
2708
2709#[cfg(unix)]
2710impl fmt::Display for UnitTerminateSignal {
2711 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2712 match self {
2713 UnitTerminateSignal::Interrupt => write!(f, "SIGINT"),
2714 UnitTerminateSignal::Term => write!(f, "SIGTERM"),
2715 UnitTerminateSignal::Hangup => write!(f, "SIGHUP"),
2716 UnitTerminateSignal::Quit => write!(f, "SIGQUIT"),
2717 UnitTerminateSignal::Kill => write!(f, "SIGKILL"),
2718 }
2719 }
2720}
2721
2722#[cfg(test)]
2723mod tests {
2724 use super::*;
2725 use proptest::prelude::*;
2726 use test_strategy::proptest;
2727
2728 #[derive(Clone, Copy, Debug)]
2729 enum FailureSummary {
2730 Failed,
2731 Cancelled,
2732 }
2733
2734 const FAILURE_SUMMARY_BY_CANCEL_REASON: [(Option<CancelReason>, FailureSummary); 9] = [
2735 (None, FailureSummary::Failed),
2736 (
2737 Some(CancelReason::SetupScriptFailure),
2738 FailureSummary::Failed,
2739 ),
2740 (Some(CancelReason::TestFailure), FailureSummary::Failed),
2741 (Some(CancelReason::ReportError), FailureSummary::Cancelled),
2742 (
2743 Some(CancelReason::TestFailureImmediate),
2744 FailureSummary::Failed,
2745 ),
2746 (Some(CancelReason::GlobalTimeout), FailureSummary::Cancelled),
2747 (Some(CancelReason::Signal), FailureSummary::Cancelled),
2748 (Some(CancelReason::Interrupt), FailureSummary::Cancelled),
2749 (Some(CancelReason::SecondSignal), FailureSummary::Cancelled),
2750 ];
2751
2752 #[proptest]
2755 fn failure_summary_by_cancel_reason_is_complete(cancel_reason: Option<CancelReason>) {
2756 prop_assert!(
2757 FAILURE_SUMMARY_BY_CANCEL_REASON
2758 .iter()
2759 .any(|(reason, _)| *reason == cancel_reason),
2760 "FAILURE_SUMMARY_BY_CANCEL_REASON has a row for {cancel_reason:?}"
2761 );
2762 }
2763
2764 #[track_caller]
2765 fn assert_failure_summary_by_cancel_reason(stats: RunStats, kind: RunStatsFailureKind) {
2766 for (cancel_reason, expected) in FAILURE_SUMMARY_BY_CANCEL_REASON {
2767 let expected = match expected {
2768 FailureSummary::Failed => FinalRunStats::Failed { kind },
2769 FailureSummary::Cancelled => FinalRunStats::Cancelled {
2770 reason: cancel_reason,
2771 kind,
2772 },
2773 };
2774 assert_eq!(
2775 RunStats {
2776 cancel_reason,
2777 ..stats
2778 }
2779 .summarize_final(),
2780 expected,
2781 "summary for cancel reason {cancel_reason:?}"
2782 );
2783 }
2784 }
2785
2786 #[test]
2787 fn summarize_final_failed_test_by_cancel_reason() {
2788 assert_failure_summary_by_cancel_reason(
2789 RunStats {
2790 initial_run_count: 3,
2791 finished_count: 2,
2792 passed: 1,
2793 failed: 1,
2794 ..RunStats::default()
2795 },
2796 RunStatsFailureKind::Test {
2797 initial_run_count: 3,
2798 not_run: 1,
2799 },
2800 );
2801 }
2802
2803 #[test]
2804 fn summarize_final_failed_setup_script_by_cancel_reason() {
2805 assert_failure_summary_by_cancel_reason(
2806 RunStats {
2807 setup_scripts_initial_count: 2,
2808 setup_scripts_finished_count: 1,
2809 setup_scripts_failed: 1,
2810 ..RunStats::default()
2811 },
2812 RunStatsFailureKind::SetupScript,
2813 );
2814 }
2815
2816 #[test]
2817 fn test_is_success() {
2818 assert_eq!(
2819 RunStats::default().summarize_final(),
2820 FinalRunStats::NoTestsRun,
2821 "empty run => no tests run"
2822 );
2823 assert_eq!(
2824 RunStats {
2825 initial_run_count: 42,
2826 finished_count: 42,
2827 ..RunStats::default()
2828 }
2829 .summarize_final(),
2830 FinalRunStats::Success,
2831 "initial run count = final run count => success"
2832 );
2833 assert_eq!(
2834 RunStats {
2835 initial_run_count: 42,
2836 finished_count: 41,
2837 ..RunStats::default()
2838 }
2839 .summarize_final(),
2840 FinalRunStats::Cancelled {
2841 reason: None,
2842 kind: RunStatsFailureKind::Test {
2843 initial_run_count: 42,
2844 not_run: 1
2845 }
2846 },
2847 "initial run count > final run count => cancelled"
2848 );
2849 assert_eq!(
2850 RunStats {
2851 initial_run_count: 42,
2852 finished_count: 42,
2853 failed: 1,
2854 ..RunStats::default()
2855 }
2856 .summarize_final(),
2857 FinalRunStats::Failed {
2858 kind: RunStatsFailureKind::Test {
2859 initial_run_count: 42,
2860 not_run: 0,
2861 },
2862 },
2863 "failed => failure"
2864 );
2865 assert_eq!(
2866 RunStats {
2867 initial_run_count: 42,
2868 finished_count: 42,
2869 exec_failed: 1,
2870 ..RunStats::default()
2871 }
2872 .summarize_final(),
2873 FinalRunStats::Failed {
2874 kind: RunStatsFailureKind::Test {
2875 initial_run_count: 42,
2876 not_run: 0,
2877 },
2878 },
2879 "exec failed => failure"
2880 );
2881 assert_eq!(
2882 RunStats {
2883 initial_run_count: 42,
2884 finished_count: 42,
2885 failed_timed_out: 1,
2886 ..RunStats::default()
2887 }
2888 .summarize_final(),
2889 FinalRunStats::Failed {
2890 kind: RunStatsFailureKind::Test {
2891 initial_run_count: 42,
2892 not_run: 0,
2893 },
2894 },
2895 "timed out => failure {:?} {:?}",
2896 RunStats {
2897 initial_run_count: 42,
2898 finished_count: 42,
2899 failed_timed_out: 1,
2900 ..RunStats::default()
2901 }
2902 .summarize_final(),
2903 FinalRunStats::Failed {
2904 kind: RunStatsFailureKind::Test {
2905 initial_run_count: 42,
2906 not_run: 0,
2907 },
2908 },
2909 );
2910 assert_eq!(
2911 RunStats {
2912 initial_run_count: 42,
2913 finished_count: 42,
2914 skipped: 1,
2915 ..RunStats::default()
2916 }
2917 .summarize_final(),
2918 FinalRunStats::Success,
2919 "skipped => not considered a failure"
2920 );
2921
2922 assert_eq!(
2923 RunStats {
2924 setup_scripts_initial_count: 2,
2925 setup_scripts_finished_count: 1,
2926 ..RunStats::default()
2927 }
2928 .summarize_final(),
2929 FinalRunStats::Cancelled {
2930 reason: None,
2931 kind: RunStatsFailureKind::SetupScript,
2932 },
2933 "setup script failed => failure"
2934 );
2935
2936 assert_eq!(
2937 RunStats {
2938 setup_scripts_initial_count: 2,
2939 setup_scripts_finished_count: 2,
2940 setup_scripts_failed: 1,
2941 ..RunStats::default()
2942 }
2943 .summarize_final(),
2944 FinalRunStats::Failed {
2945 kind: RunStatsFailureKind::SetupScript,
2946 },
2947 "setup script failed => failure"
2948 );
2949 assert_eq!(
2950 RunStats {
2951 setup_scripts_initial_count: 2,
2952 setup_scripts_finished_count: 2,
2953 setup_scripts_exec_failed: 1,
2954 ..RunStats::default()
2955 }
2956 .summarize_final(),
2957 FinalRunStats::Failed {
2958 kind: RunStatsFailureKind::SetupScript,
2959 },
2960 "setup script exec failed => failure"
2961 );
2962 assert_eq!(
2963 RunStats {
2964 setup_scripts_initial_count: 2,
2965 setup_scripts_finished_count: 2,
2966 setup_scripts_timed_out: 1,
2967 ..RunStats::default()
2968 }
2969 .summarize_final(),
2970 FinalRunStats::Failed {
2971 kind: RunStatsFailureKind::SetupScript,
2972 },
2973 "setup script timed out => failure"
2974 );
2975 assert_eq!(
2976 RunStats {
2977 setup_scripts_initial_count: 2,
2978 setup_scripts_finished_count: 2,
2979 setup_scripts_passed: 2,
2980 ..RunStats::default()
2981 }
2982 .summarize_final(),
2983 FinalRunStats::NoTestsRun,
2984 "setup scripts passed => success, but no tests run"
2985 );
2986
2987 }
2990
2991 fn make_execute_status(
2993 result: ExecutionResultDescription,
2994 attempt: u32,
2995 total_attempts: u32,
2996 ) -> ExecuteStatus<LiveSpec> {
2997 make_execute_status_slow(result, attempt, total_attempts, false)
2998 }
2999
3000 fn make_execute_status_slow(
3003 result: ExecutionResultDescription,
3004 attempt: u32,
3005 total_attempts: u32,
3006 is_slow: bool,
3007 ) -> ExecuteStatus<LiveSpec> {
3008 ExecuteStatus {
3009 retry_data: RetryData {
3010 attempt,
3011 total_attempts,
3012 },
3013 output: ChildExecutionOutputDescription::Output {
3014 result: Some(result.clone()),
3015 output: ChildOutputDescription::Split {
3016 stdout: None,
3017 stderr: None,
3018 },
3019 errors: None,
3020 },
3021 result,
3022 start_time: chrono::Utc::now().into(),
3023 time_taken: Duration::from_millis(100),
3024 is_slow,
3025 delay_before_start: Duration::ZERO,
3026 error_summary: None,
3027 output_error_slice: None,
3028 }
3029 }
3030
3031 #[test]
3032 fn is_success_for_output_by_variant() {
3033 let pass_status = make_execute_status(ExecutionResultDescription::Pass, 1, 1);
3035 let success_statuses = ExecutionStatuses::new(vec![pass_status], FlakyResult::Pass);
3036 let describe = success_statuses.describe();
3037 assert!(
3038 matches!(describe, ExecutionDescription::Success { .. }),
3039 "single pass is Success"
3040 );
3041 assert!(
3042 describe.is_success_for_output(),
3043 "Success: output is success output"
3044 );
3045
3046 let fail_status = make_execute_status(
3048 ExecutionResultDescription::Fail {
3049 failure: FailureDescription::ExitCode { code: 1 },
3050 leaked: false,
3051 },
3052 1,
3053 2,
3054 );
3055 let pass_status = make_execute_status(ExecutionResultDescription::Pass, 2, 2);
3056 let flaky_pass_statuses =
3057 ExecutionStatuses::new(vec![fail_status, pass_status], FlakyResult::Pass);
3058 let describe = flaky_pass_statuses.describe();
3059 assert!(
3060 matches!(
3061 describe,
3062 ExecutionDescription::Flaky {
3063 result: FlakyResult::Pass,
3064 ..
3065 }
3066 ),
3067 "fail then pass with FlakyResult::Pass is Flaky Pass"
3068 );
3069 assert!(
3070 describe.is_success_for_output(),
3071 "Flaky pass: output is success output"
3072 );
3073
3074 let fail_status = make_execute_status(
3076 ExecutionResultDescription::Fail {
3077 failure: FailureDescription::ExitCode { code: 1 },
3078 leaked: false,
3079 },
3080 1,
3081 2,
3082 );
3083 let pass_status = make_execute_status(ExecutionResultDescription::Pass, 2, 2);
3084 let flaky_fail_statuses =
3085 ExecutionStatuses::new(vec![fail_status, pass_status], FlakyResult::Fail);
3086 let describe = flaky_fail_statuses.describe();
3087 assert!(
3088 matches!(
3089 describe,
3090 ExecutionDescription::Flaky {
3091 result: FlakyResult::Fail,
3092 ..
3093 }
3094 ),
3095 "fail then pass with FlakyResult::Fail is Flaky Fail"
3096 );
3097 assert!(
3098 describe.is_success_for_output(),
3099 "Flaky fail: output is still success output (last attempt passed)"
3100 );
3101
3102 let fail_status = make_execute_status(
3104 ExecutionResultDescription::Fail {
3105 failure: FailureDescription::ExitCode { code: 1 },
3106 leaked: false,
3107 },
3108 1,
3109 1,
3110 );
3111 let failure_statuses = ExecutionStatuses::new(vec![fail_status], FlakyResult::Pass);
3112 let describe = failure_statuses.describe();
3113 assert!(
3114 matches!(describe, ExecutionDescription::Failure { .. }),
3115 "single fail is Failure"
3116 );
3117 assert!(
3118 !describe.is_success_for_output(),
3119 "Failure: output is not success output"
3120 );
3121
3122 let fail1 = make_execute_status(
3124 ExecutionResultDescription::Fail {
3125 failure: FailureDescription::ExitCode { code: 1 },
3126 leaked: false,
3127 },
3128 1,
3129 2,
3130 );
3131 let fail2 = make_execute_status(
3132 ExecutionResultDescription::Fail {
3133 failure: FailureDescription::ExitCode { code: 1 },
3134 leaked: false,
3135 },
3136 2,
3137 2,
3138 );
3139 let failure_retry_statuses = ExecutionStatuses::new(vec![fail1, fail2], FlakyResult::Pass);
3140 let describe = failure_retry_statuses.describe();
3141 assert!(
3142 matches!(describe, ExecutionDescription::Failure { .. }),
3143 "all-fail with retries is Failure"
3144 );
3145 assert!(
3146 !describe.is_success_for_output(),
3147 "Failure with retries: output is not success output"
3148 );
3149 }
3150
3151 #[test]
3152 fn abort_description_serialization() {
3153 let unix_with_name = AbortDescription::UnixSignal {
3155 signal: 15,
3156 name: Some("TERM".into()),
3157 };
3158 let json = serde_json::to_string_pretty(&unix_with_name).unwrap();
3159 insta::assert_snapshot!("abort_unix_signal_with_name", json);
3160 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3161 assert_eq!(unix_with_name, roundtrip);
3162
3163 let unix_no_name = AbortDescription::UnixSignal {
3165 signal: 42,
3166 name: None,
3167 };
3168 let json = serde_json::to_string_pretty(&unix_no_name).unwrap();
3169 insta::assert_snapshot!("abort_unix_signal_no_name", json);
3170 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3171 assert_eq!(unix_no_name, roundtrip);
3172
3173 let windows_nt = AbortDescription::WindowsNtStatus {
3175 code: -1073741510_i32,
3176 message: Some("The application terminated as a result of a CTRL+C.".into()),
3177 };
3178 let json = serde_json::to_string_pretty(&windows_nt).unwrap();
3179 insta::assert_snapshot!("abort_windows_nt_status", json);
3180 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3181 assert_eq!(windows_nt, roundtrip);
3182
3183 let windows_nt_no_msg = AbortDescription::WindowsNtStatus {
3185 code: -1073741819_i32,
3186 message: None,
3187 };
3188 let json = serde_json::to_string_pretty(&windows_nt_no_msg).unwrap();
3189 insta::assert_snapshot!("abort_windows_nt_status_no_message", json);
3190 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3191 assert_eq!(windows_nt_no_msg, roundtrip);
3192
3193 let job = AbortDescription::WindowsJobObject;
3195 let json = serde_json::to_string_pretty(&job).unwrap();
3196 insta::assert_snapshot!("abort_windows_job_object", json);
3197 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3198 assert_eq!(job, roundtrip);
3199 }
3200
3201 #[test]
3202 fn abort_description_cross_platform_deserialization() {
3203 let unix_json = r#"{"kind":"unix-signal","signal":11,"name":"SEGV"}"#;
3206 let unix_desc: AbortDescription = serde_json::from_str(unix_json).unwrap();
3207 assert_eq!(
3208 unix_desc,
3209 AbortDescription::UnixSignal {
3210 signal: 11,
3211 name: Some("SEGV".into()),
3212 }
3213 );
3214
3215 let windows_json = r#"{"kind":"windows-nt-status","code":-1073741510,"message":"CTRL+C"}"#;
3216 let windows_desc: AbortDescription = serde_json::from_str(windows_json).unwrap();
3217 assert_eq!(
3218 windows_desc,
3219 AbortDescription::WindowsNtStatus {
3220 code: -1073741510,
3221 message: Some("CTRL+C".into()),
3222 }
3223 );
3224
3225 let job_json = r#"{"kind":"windows-job-object"}"#;
3226 let job_desc: AbortDescription = serde_json::from_str(job_json).unwrap();
3227 assert_eq!(job_desc, AbortDescription::WindowsJobObject);
3228 }
3229
3230 #[test]
3231 fn abort_description_display() {
3232 let unix = AbortDescription::UnixSignal {
3234 signal: 15,
3235 name: Some("TERM".into()),
3236 };
3237 assert_eq!(unix.to_string(), "aborted with signal 15 (SIGTERM)");
3238
3239 let unix_no_name = AbortDescription::UnixSignal {
3241 signal: 42,
3242 name: None,
3243 };
3244 assert_eq!(unix_no_name.to_string(), "aborted with signal 42");
3245
3246 let windows = AbortDescription::WindowsNtStatus {
3248 code: -1073741510,
3249 message: Some("CTRL+C exit".into()),
3250 };
3251 assert_eq!(
3252 windows.to_string(),
3253 "aborted with code 0xc000013a: CTRL+C exit"
3254 );
3255
3256 let windows_no_msg = AbortDescription::WindowsNtStatus {
3258 code: -1073741510,
3259 message: None,
3260 };
3261 assert_eq!(windows_no_msg.to_string(), "aborted with code 0xc000013a");
3262
3263 let job = AbortDescription::WindowsJobObject;
3265 assert_eq!(job.to_string(), "terminated via job object");
3266 }
3267
3268 #[cfg(unix)]
3269 #[test]
3270 fn abort_description_from_abort_status() {
3271 let status = AbortStatus::UnixSignal(15);
3273 let description = AbortDescription::from(status);
3274
3275 assert_eq!(
3276 description,
3277 AbortDescription::UnixSignal {
3278 signal: 15,
3279 name: Some("TERM".into()),
3280 }
3281 );
3282
3283 let unknown_status = AbortStatus::UnixSignal(42);
3285 let unknown_description = AbortDescription::from(unknown_status);
3286 assert_eq!(
3287 unknown_description,
3288 AbortDescription::UnixSignal {
3289 signal: 42,
3290 name: None,
3291 }
3292 );
3293 }
3294
3295 #[test]
3296 fn execution_result_description_serialization() {
3297 let pass = ExecutionResultDescription::Pass;
3301 let json = serde_json::to_string_pretty(&pass).unwrap();
3302 insta::assert_snapshot!("pass", json);
3303 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3304 assert_eq!(pass, roundtrip);
3305
3306 let leak_pass = ExecutionResultDescription::Leak {
3308 result: LeakTimeoutResult::Pass,
3309 };
3310 let json = serde_json::to_string_pretty(&leak_pass).unwrap();
3311 insta::assert_snapshot!("leak_pass", json);
3312 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3313 assert_eq!(leak_pass, roundtrip);
3314
3315 let leak_fail = ExecutionResultDescription::Leak {
3317 result: LeakTimeoutResult::Fail,
3318 };
3319 let json = serde_json::to_string_pretty(&leak_fail).unwrap();
3320 insta::assert_snapshot!("leak_fail", json);
3321 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3322 assert_eq!(leak_fail, roundtrip);
3323
3324 let fail_exit_code = ExecutionResultDescription::Fail {
3326 failure: FailureDescription::ExitCode { code: 101 },
3327 leaked: false,
3328 };
3329 let json = serde_json::to_string_pretty(&fail_exit_code).unwrap();
3330 insta::assert_snapshot!("fail_exit_code", json);
3331 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3332 assert_eq!(fail_exit_code, roundtrip);
3333
3334 let fail_exit_code_leaked = ExecutionResultDescription::Fail {
3336 failure: FailureDescription::ExitCode { code: 1 },
3337 leaked: true,
3338 };
3339 let json = serde_json::to_string_pretty(&fail_exit_code_leaked).unwrap();
3340 insta::assert_snapshot!("fail_exit_code_leaked", json);
3341 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3342 assert_eq!(fail_exit_code_leaked, roundtrip);
3343
3344 let fail_unix_signal = ExecutionResultDescription::Fail {
3346 failure: FailureDescription::Abort {
3347 abort: AbortDescription::UnixSignal {
3348 signal: 11,
3349 name: Some("SEGV".into()),
3350 },
3351 },
3352 leaked: false,
3353 };
3354 let json = serde_json::to_string_pretty(&fail_unix_signal).unwrap();
3355 insta::assert_snapshot!("fail_unix_signal", json);
3356 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3357 assert_eq!(fail_unix_signal, roundtrip);
3358
3359 let fail_unix_signal_unknown = ExecutionResultDescription::Fail {
3361 failure: FailureDescription::Abort {
3362 abort: AbortDescription::UnixSignal {
3363 signal: 42,
3364 name: None,
3365 },
3366 },
3367 leaked: true,
3368 };
3369 let json = serde_json::to_string_pretty(&fail_unix_signal_unknown).unwrap();
3370 insta::assert_snapshot!("fail_unix_signal_unknown_leaked", json);
3371 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3372 assert_eq!(fail_unix_signal_unknown, roundtrip);
3373
3374 let fail_windows_nt = ExecutionResultDescription::Fail {
3376 failure: FailureDescription::Abort {
3377 abort: AbortDescription::WindowsNtStatus {
3378 code: -1073741510,
3379 message: Some("The application terminated as a result of a CTRL+C.".into()),
3380 },
3381 },
3382 leaked: false,
3383 };
3384 let json = serde_json::to_string_pretty(&fail_windows_nt).unwrap();
3385 insta::assert_snapshot!("fail_windows_nt_status", json);
3386 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3387 assert_eq!(fail_windows_nt, roundtrip);
3388
3389 let fail_windows_nt_no_msg = ExecutionResultDescription::Fail {
3391 failure: FailureDescription::Abort {
3392 abort: AbortDescription::WindowsNtStatus {
3393 code: -1073741819,
3394 message: None,
3395 },
3396 },
3397 leaked: false,
3398 };
3399 let json = serde_json::to_string_pretty(&fail_windows_nt_no_msg).unwrap();
3400 insta::assert_snapshot!("fail_windows_nt_status_no_message", json);
3401 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3402 assert_eq!(fail_windows_nt_no_msg, roundtrip);
3403
3404 let fail_job_object = ExecutionResultDescription::Fail {
3406 failure: FailureDescription::Abort {
3407 abort: AbortDescription::WindowsJobObject,
3408 },
3409 leaked: false,
3410 };
3411 let json = serde_json::to_string_pretty(&fail_job_object).unwrap();
3412 insta::assert_snapshot!("fail_windows_job_object", json);
3413 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3414 assert_eq!(fail_job_object, roundtrip);
3415
3416 let exec_fail = ExecutionResultDescription::ExecFail;
3418 let json = serde_json::to_string_pretty(&exec_fail).unwrap();
3419 insta::assert_snapshot!("exec_fail", json);
3420 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3421 assert_eq!(exec_fail, roundtrip);
3422
3423 let timeout_pass = ExecutionResultDescription::Timeout {
3425 result: SlowTimeoutResult::Pass,
3426 };
3427 let json = serde_json::to_string_pretty(&timeout_pass).unwrap();
3428 insta::assert_snapshot!("timeout_pass", json);
3429 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3430 assert_eq!(timeout_pass, roundtrip);
3431
3432 let timeout_fail = ExecutionResultDescription::Timeout {
3434 result: SlowTimeoutResult::Fail,
3435 };
3436 let json = serde_json::to_string_pretty(&timeout_fail).unwrap();
3437 insta::assert_snapshot!("timeout_fail", json);
3438 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3439 assert_eq!(timeout_fail, roundtrip);
3440 }
3441
3442 fn make_flaky_statuses(
3447 pass_result: ExecutionResultDescription,
3448 flaky_result: FlakyResult,
3449 is_slow: bool,
3450 ) -> ExecutionStatuses<LiveSpec> {
3451 let fail = make_execute_status(
3452 ExecutionResultDescription::Fail {
3453 failure: FailureDescription::ExitCode { code: 1 },
3454 leaked: false,
3455 },
3456 1,
3457 2,
3458 );
3459 let pass = make_execute_status_slow(pass_result, 2, 2, is_slow);
3460 ExecutionStatuses::new(vec![fail, pass], flaky_result)
3461 }
3462
3463 fn run_on_test_finished(statuses: &ExecutionStatuses<LiveSpec>) -> RunStats {
3465 let mut stats = RunStats {
3466 initial_run_count: 1,
3467 ..RunStats::default()
3468 };
3469 stats.on_test_finished(statuses);
3470 stats
3471 }
3472
3473 #[test]
3474 fn on_test_finished_pass_flaky() {
3475 let stats = run_on_test_finished(&make_flaky_statuses(
3477 ExecutionResultDescription::Pass,
3478 FlakyResult::Fail,
3479 false,
3480 ));
3481 assert_eq!(stats.finished_count, 1);
3482 assert_eq!(stats.failed, 1);
3483 assert_eq!(stats.failed_slow, 0, "not slow");
3484 assert_eq!(stats.passed, 0);
3485 assert_eq!(stats.flaky, 0);
3486
3487 let stats = run_on_test_finished(&make_flaky_statuses(
3489 ExecutionResultDescription::Pass,
3490 FlakyResult::Fail,
3491 true,
3492 ));
3493 assert_eq!(stats.failed, 1);
3494 assert_eq!(stats.failed_slow, 1);
3495 assert_eq!(stats.passed, 0);
3496 assert_eq!(stats.flaky, 0);
3497
3498 let stats = run_on_test_finished(&make_flaky_statuses(
3500 ExecutionResultDescription::Pass,
3501 FlakyResult::Pass,
3502 true,
3503 ));
3504 assert_eq!(stats.passed, 1);
3505 assert_eq!(stats.passed_slow, 1);
3506 assert_eq!(stats.flaky, 1);
3507 assert_eq!(stats.failed, 0);
3508 }
3509
3510 #[test]
3511 fn on_test_finished_leak_pass_flaky() {
3512 let stats = run_on_test_finished(&make_flaky_statuses(
3514 ExecutionResultDescription::Leak {
3515 result: LeakTimeoutResult::Pass,
3516 },
3517 FlakyResult::Fail,
3518 false,
3519 ));
3520 assert_eq!(stats.failed, 1);
3521 assert_eq!(stats.failed_slow, 0, "not slow");
3522 assert_eq!(stats.leaky, 1, "leak still tracked");
3523 assert_eq!(stats.passed, 0);
3524 assert_eq!(stats.flaky, 0);
3525
3526 let stats = run_on_test_finished(&make_flaky_statuses(
3528 ExecutionResultDescription::Leak {
3529 result: LeakTimeoutResult::Pass,
3530 },
3531 FlakyResult::Fail,
3532 true,
3533 ));
3534 assert_eq!(stats.failed, 1);
3535 assert_eq!(stats.failed_slow, 1);
3536 assert_eq!(stats.leaky, 1);
3537 assert_eq!(stats.passed, 0);
3538 assert_eq!(stats.flaky, 0);
3539
3540 let stats = run_on_test_finished(&make_flaky_statuses(
3542 ExecutionResultDescription::Leak {
3543 result: LeakTimeoutResult::Pass,
3544 },
3545 FlakyResult::Pass,
3546 true,
3547 ));
3548 assert_eq!(stats.passed, 1);
3549 assert_eq!(stats.passed_slow, 1);
3550 assert_eq!(stats.leaky, 1);
3551 assert_eq!(stats.flaky, 1);
3552 assert_eq!(stats.failed, 0);
3553 }
3554
3555 #[test]
3556 fn on_test_finished_timeout_pass_flaky() {
3557 let stats = run_on_test_finished(&make_flaky_statuses(
3560 ExecutionResultDescription::Timeout {
3561 result: SlowTimeoutResult::Pass,
3562 },
3563 FlakyResult::Fail,
3564 true,
3565 ));
3566 assert_eq!(stats.failed, 1);
3567 assert_eq!(stats.failed_slow, 1);
3568 assert_eq!(stats.passed, 0);
3569 assert_eq!(stats.passed_timed_out, 0);
3570 assert_eq!(stats.flaky, 0);
3571
3572 let stats = run_on_test_finished(&make_flaky_statuses(
3574 ExecutionResultDescription::Timeout {
3575 result: SlowTimeoutResult::Pass,
3576 },
3577 FlakyResult::Pass,
3578 false,
3579 ));
3580 assert_eq!(stats.passed, 1);
3581 assert_eq!(stats.passed_timed_out, 1);
3582 assert_eq!(stats.flaky, 1);
3583 assert_eq!(stats.failed, 0);
3584 }
3585
3586 #[test]
3587 fn on_test_finished_non_flaky() {
3588 let pass = make_execute_status_slow(ExecutionResultDescription::Pass, 1, 1, true);
3590 let stats = run_on_test_finished(&ExecutionStatuses::new(vec![pass], FlakyResult::Pass));
3591 assert_eq!(stats.passed, 1);
3592 assert_eq!(stats.passed_slow, 1);
3593 assert_eq!(stats.flaky, 0);
3594 assert_eq!(stats.failed, 0);
3595
3596 let fail = make_execute_status_slow(
3598 ExecutionResultDescription::Fail {
3599 failure: FailureDescription::ExitCode { code: 1 },
3600 leaked: false,
3601 },
3602 1,
3603 1,
3604 true,
3605 );
3606 let stats = run_on_test_finished(&ExecutionStatuses::new(vec![fail], FlakyResult::Pass));
3607 assert_eq!(stats.failed, 1);
3608 assert_eq!(stats.failed_slow, 1);
3609 assert_eq!(stats.passed, 0);
3610 assert_eq!(stats.flaky, 0);
3611 }
3612
3613 const ALL_RUN_OUTCOMES: [RunOutcome; 6] = [
3617 RunOutcome::NoTestsRun,
3618 RunOutcome::Success,
3619 RunOutcome::Cancelled {
3620 step: RunFailureStep::Test,
3621 },
3622 RunOutcome::Cancelled {
3623 step: RunFailureStep::SetupScript,
3624 },
3625 RunOutcome::Failed {
3626 step: RunFailureStep::Test,
3627 },
3628 RunOutcome::Failed {
3629 step: RunFailureStep::SetupScript,
3630 },
3631 ];
3632
3633 #[proptest]
3636 fn all_run_outcomes_is_complete(outcome: RunOutcome) {
3637 prop_assert!(
3638 ALL_RUN_OUTCOMES.contains(&outcome),
3639 "ALL_RUN_OUTCOMES contains {outcome:?}"
3640 );
3641 }
3642
3643 #[test]
3644 fn run_outcome_combine_is_commutative() {
3645 for a in ALL_RUN_OUTCOMES {
3646 for b in ALL_RUN_OUTCOMES {
3647 assert_eq!(
3648 a.combine(b),
3649 b.combine(a),
3650 "combine is commutative for {a:?} and {b:?}"
3651 );
3652 }
3653 }
3654 }
3655
3656 #[test]
3657 fn run_outcome_combine_is_associative() {
3658 for a in ALL_RUN_OUTCOMES {
3659 for b in ALL_RUN_OUTCOMES {
3660 for c in ALL_RUN_OUTCOMES {
3661 assert_eq!(
3662 a.combine(b).combine(c),
3663 a.combine(b.combine(c)),
3664 "combine is associative for {a:?}, {b:?}, and {c:?}"
3665 );
3666 }
3667 }
3668 }
3669 }
3670
3671 #[test]
3672 fn run_outcome_combine_is_idempotent() {
3673 for a in ALL_RUN_OUTCOMES {
3674 assert_eq!(a.combine(a), a, "combine is idempotent for {a:?}");
3675 }
3676 }
3677
3678 #[test]
3679 fn run_outcome_combine_matches_oracle() {
3680 for a in ALL_RUN_OUTCOMES {
3681 for b in ALL_RUN_OUTCOMES {
3682 let expected = if outcome_rank(a) >= outcome_rank(b) {
3683 a
3684 } else {
3685 b
3686 };
3687 assert_eq!(
3688 a.combine(b),
3689 expected,
3690 "combine returns the higher-ranked of {a:?} and {b:?}"
3691 );
3692 }
3693 }
3694 }
3695
3696 fn outcome_rank(outcome: RunOutcome) -> (u8, u8) {
3699 match outcome {
3700 RunOutcome::NoTestsRun => (0, 0),
3701 RunOutcome::Success => (1, 0),
3702 RunOutcome::Cancelled { step } => (2, step_rank(step)),
3703 RunOutcome::Failed { step } => (3, step_rank(step)),
3704 }
3705 }
3706
3707 fn step_rank(step: RunFailureStep) -> u8 {
3708 match step {
3709 RunFailureStep::Test => 0,
3710 RunFailureStep::SetupScript => 1,
3711 }
3712 }
3713
3714 #[test]
3715 fn stress_progress_time_remaining() {
3716 let total = Duration::from_secs(30);
3717 let smallest_step = Duration::from_nanos(1);
3718 let progress_at = |elapsed: Duration| StressProgress::Time {
3719 total,
3720 elapsed,
3721 completed: 3,
3722 };
3723
3724 assert_eq!(
3725 progress_at(Duration::ZERO).remaining(),
3726 Some(StressRemaining::Time(
3727 NonZeroDuration::new(total).expect("total is non-zero")
3728 )),
3729 "no time elapsed => the whole duration remains"
3730 );
3731 assert_eq!(
3732 progress_at(total - smallest_step).remaining(),
3733 Some(StressRemaining::Time(
3734 NonZeroDuration::new(smallest_step).expect("smallest step is non-zero")
3735 )),
3736 "elapsed just below total => the difference remains"
3737 );
3738 assert_eq!(
3739 progress_at(total).remaining(),
3740 None,
3741 "elapsed equal to total => nothing remains"
3742 );
3743 assert_eq!(
3744 progress_at(total + smallest_step).remaining(),
3745 None,
3746 "elapsed above total => nothing remains"
3747 );
3748 }
3749}