1use super::{FinalStatusLevel, StatusLevel, TestOutputDisplay};
10#[cfg(test)]
11use crate::output_spec::ArbitraryOutputSpec;
12use crate::{
13 config::{
14 elements::{LeakTimeoutResult, SlowTimeoutResult},
15 scripts::ScriptId,
16 },
17 errors::{ChildError, ChildFdError, ChildStartError, ErrorList},
18 list::{OwnedTestInstanceId, TestInstanceId, TestList},
19 output_spec::{LiveSpec, OutputSpec, SerializableOutputSpec},
20 runner::{StressCondition, StressCount},
21 test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
22};
23use chrono::{DateTime, FixedOffset};
24use nextest_metadata::MismatchReason;
25use quick_junit::ReportUuid;
26use serde::{Deserialize, Serialize};
27use smol_str::SmolStr;
28use std::{
29 collections::BTreeMap, ffi::c_int, fmt, num::NonZero, process::ExitStatus, time::Duration,
30};
31
32pub const SIGTERM: c_int = 15;
37
38#[derive(Clone, Debug)]
40pub enum ReporterEvent<'a> {
41 Tick,
43
44 Test(Box<TestEvent<'a>>),
46}
47#[derive(Clone, Debug)]
52pub struct TestEvent<'a> {
53 pub timestamp: DateTime<FixedOffset>,
55
56 pub elapsed: Duration,
58
59 pub kind: TestEventKind<'a>,
61}
62
63#[derive(Clone, Debug)]
67pub enum TestEventKind<'a> {
68 RunStarted {
70 test_list: &'a TestList<'a>,
74
75 run_id: ReportUuid,
77
78 profile_name: String,
80
81 cli_args: Vec<String>,
83
84 stress_condition: Option<StressCondition>,
86 },
87
88 StressSubRunStarted {
90 progress: StressProgress,
92 },
93
94 SetupScriptStarted {
96 stress_index: Option<StressIndex>,
98
99 index: usize,
101
102 total: usize,
104
105 script_id: ScriptId,
107
108 program: String,
110
111 args: Vec<String>,
113
114 no_capture: bool,
116 },
117
118 SetupScriptSlow {
120 stress_index: Option<StressIndex>,
122
123 script_id: ScriptId,
125
126 program: String,
128
129 args: Vec<String>,
131
132 elapsed: Duration,
134
135 will_terminate: bool,
137 },
138
139 SetupScriptFinished {
141 stress_index: Option<StressIndex>,
143
144 index: usize,
146
147 total: usize,
149
150 script_id: ScriptId,
152
153 program: String,
155
156 args: Vec<String>,
158
159 junit_store_success_output: bool,
161
162 junit_store_failure_output: bool,
164
165 no_capture: bool,
167
168 run_status: SetupScriptExecuteStatus<LiveSpec>,
170 },
171
172 TestStarted {
177 stress_index: Option<StressIndex>,
179
180 test_instance: TestInstanceId<'a>,
182
183 current_stats: RunStats,
185
186 running: usize,
188
189 command_line: Vec<String>,
191 },
192
193 TestSlow {
195 stress_index: Option<StressIndex>,
197
198 test_instance: TestInstanceId<'a>,
200
201 retry_data: RetryData,
203
204 elapsed: Duration,
206
207 will_terminate: bool,
209 },
210
211 TestAttemptFailedWillRetry {
215 stress_index: Option<StressIndex>,
217
218 test_instance: TestInstanceId<'a>,
220
221 run_status: ExecuteStatus<LiveSpec>,
223
224 delay_before_next_attempt: Duration,
226
227 failure_output: TestOutputDisplay,
229
230 running: usize,
232 },
233
234 TestRetryStarted {
236 stress_index: Option<StressIndex>,
238
239 test_instance: TestInstanceId<'a>,
241
242 retry_data: RetryData,
244
245 running: usize,
247
248 command_line: Vec<String>,
250 },
251
252 TestFinished {
254 stress_index: Option<StressIndex>,
256
257 test_instance: TestInstanceId<'a>,
259
260 success_output: TestOutputDisplay,
262
263 failure_output: TestOutputDisplay,
265
266 junit_store_success_output: bool,
268
269 junit_store_failure_output: bool,
271
272 run_statuses: ExecutionStatuses<LiveSpec>,
274
275 current_stats: RunStats,
277
278 running: usize,
280 },
281
282 TestSkipped {
284 stress_index: Option<StressIndex>,
286
287 test_instance: TestInstanceId<'a>,
289
290 reason: MismatchReason,
292 },
293
294 InfoStarted {
296 total: usize,
299
300 run_stats: RunStats,
302 },
303
304 InfoResponse {
306 index: usize,
308
309 total: usize,
311
312 response: InfoResponse<'a>,
314 },
315
316 InfoFinished {
318 missing: usize,
321 },
322
323 InputEnter {
326 current_stats: RunStats,
328
329 running: usize,
331 },
332
333 RunBeginCancel {
335 setup_scripts_running: usize,
337
338 current_stats: RunStats,
342
343 running: usize,
345 },
346
347 RunBeginKill {
349 setup_scripts_running: usize,
351
352 current_stats: RunStats,
356
357 running: usize,
359 },
360
361 RunPaused {
363 setup_scripts_running: usize,
365
366 running: usize,
368 },
369
370 RunContinued {
372 setup_scripts_running: usize,
374
375 running: usize,
377 },
378
379 StressSubRunFinished {
381 progress: StressProgress,
383
384 sub_elapsed: Duration,
386
387 sub_stats: RunStats,
389 },
390
391 RunFinished {
393 run_id: ReportUuid,
395
396 start_time: DateTime<FixedOffset>,
398
399 elapsed: Duration,
401
402 run_stats: RunFinishedStats,
404
405 outstanding_not_seen: Option<TestsNotSeen>,
410 },
411}
412
413#[derive(Clone, Debug)]
415pub struct TestsNotSeen {
416 pub not_seen: Vec<OwnedTestInstanceId>,
423
424 pub total_not_seen: usize,
426}
427
428#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
430#[serde(tag = "progress-type", rename_all = "kebab-case")]
431#[cfg_attr(test, derive(test_strategy::Arbitrary))]
432pub enum StressProgress {
433 Count {
435 total: StressCount,
437
438 elapsed: Duration,
440
441 completed: u32,
443 },
444
445 Time {
447 total: Duration,
449
450 elapsed: Duration,
452
453 completed: u32,
455 },
456}
457
458impl StressProgress {
459 pub fn remaining(&self) -> Option<StressRemaining> {
462 match self {
463 Self::Count {
464 total: StressCount::Count { count },
465 elapsed: _,
466 completed,
467 } => count
468 .get()
469 .checked_sub(*completed)
470 .and_then(|remaining| NonZero::try_from(remaining).ok())
471 .map(StressRemaining::Count),
472 Self::Count {
473 total: StressCount::Infinite,
474 ..
475 } => Some(StressRemaining::Infinite),
476 Self::Time {
477 total,
478 elapsed,
479 completed: _,
480 } => total.checked_sub(*elapsed).map(StressRemaining::Time),
481 }
482 }
483
484 pub fn unique_id(&self, run_id: ReportUuid) -> String {
486 let stress_current = match self {
487 Self::Count { completed, .. } | Self::Time { completed, .. } => *completed,
488 };
489 format!("{}:@stress-{}", run_id, stress_current)
490 }
491}
492
493#[derive(Clone, Debug)]
495pub enum StressRemaining {
496 Count(NonZero<u32>),
498
499 Infinite,
501
502 Time(Duration),
504}
505
506#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
508#[serde(rename_all = "kebab-case")]
509#[cfg_attr(test, derive(test_strategy::Arbitrary))]
510pub struct StressIndex {
511 pub current: u32,
513
514 pub total: Option<NonZero<u32>>,
516}
517
518#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
520#[serde(tag = "type", rename_all = "kebab-case")]
521#[cfg_attr(test, derive(test_strategy::Arbitrary))]
522pub enum RunFinishedStats {
523 Single(RunStats),
525
526 Stress(StressRunStats),
528}
529
530impl RunFinishedStats {
531 pub fn final_stats(&self) -> FinalRunStats {
534 match self {
535 Self::Single(stats) => stats.summarize_final(),
536 Self::Stress(stats) => stats.last_final_stats,
537 }
538 }
539}
540
541#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
543#[serde(rename_all = "kebab-case")]
544#[cfg_attr(test, derive(test_strategy::Arbitrary))]
545pub struct RunStats {
546 pub initial_run_count: usize,
550
551 pub finished_count: usize,
553
554 pub setup_scripts_initial_count: usize,
558
559 pub setup_scripts_finished_count: usize,
561
562 pub setup_scripts_passed: usize,
564
565 pub setup_scripts_failed: usize,
567
568 pub setup_scripts_exec_failed: usize,
570
571 pub setup_scripts_timed_out: usize,
573
574 pub passed: usize,
576
577 pub passed_slow: usize,
579
580 pub passed_timed_out: usize,
582
583 pub flaky: usize,
585
586 pub failed: usize,
588
589 pub failed_slow: usize,
591
592 pub failed_timed_out: usize,
594
595 pub leaky: usize,
597
598 pub leaky_failed: usize,
601
602 pub exec_failed: usize,
604
605 pub skipped: usize,
607
608 pub cancel_reason: Option<CancelReason>,
610}
611
612impl RunStats {
613 pub fn has_failures(&self) -> bool {
615 self.failed_setup_script_count() > 0 || self.failed_count() > 0
616 }
617
618 pub fn failed_setup_script_count(&self) -> usize {
620 self.setup_scripts_failed + self.setup_scripts_exec_failed + self.setup_scripts_timed_out
621 }
622
623 pub fn failed_count(&self) -> usize {
625 self.failed + self.exec_failed + self.failed_timed_out
626 }
627
628 pub fn summarize_final(&self) -> FinalRunStats {
630 if self.failed_setup_script_count() > 0 {
633 if self.cancel_reason > Some(CancelReason::TestFailure) {
636 FinalRunStats::Cancelled {
637 reason: self.cancel_reason,
638 kind: RunStatsFailureKind::SetupScript,
639 }
640 } else {
641 FinalRunStats::Failed {
642 kind: RunStatsFailureKind::SetupScript,
643 }
644 }
645 } else if self.setup_scripts_initial_count > self.setup_scripts_finished_count {
646 FinalRunStats::Cancelled {
647 reason: self.cancel_reason,
648 kind: RunStatsFailureKind::SetupScript,
649 }
650 } else if self.failed_count() > 0 {
651 let kind = RunStatsFailureKind::Test {
652 initial_run_count: self.initial_run_count,
653 not_run: self.initial_run_count.saturating_sub(self.finished_count),
654 };
655
656 if self.cancel_reason > Some(CancelReason::TestFailure) {
659 FinalRunStats::Cancelled {
660 reason: self.cancel_reason,
661 kind,
662 }
663 } else {
664 FinalRunStats::Failed { kind }
665 }
666 } else if self.initial_run_count > self.finished_count {
667 FinalRunStats::Cancelled {
668 reason: self.cancel_reason,
669 kind: RunStatsFailureKind::Test {
670 initial_run_count: self.initial_run_count,
671 not_run: self.initial_run_count.saturating_sub(self.finished_count),
672 },
673 }
674 } else if self.finished_count == 0 {
675 FinalRunStats::NoTestsRun
676 } else {
677 FinalRunStats::Success
678 }
679 }
680
681 pub(crate) fn on_setup_script_finished(&mut self, status: &SetupScriptExecuteStatus<LiveSpec>) {
682 self.setup_scripts_finished_count += 1;
683
684 match status.result {
685 ExecutionResultDescription::Pass
686 | ExecutionResultDescription::Leak {
687 result: LeakTimeoutResult::Pass,
688 } => {
689 self.setup_scripts_passed += 1;
690 }
691 ExecutionResultDescription::Fail { .. }
692 | ExecutionResultDescription::Leak {
693 result: LeakTimeoutResult::Fail,
694 } => {
695 self.setup_scripts_failed += 1;
696 }
697 ExecutionResultDescription::ExecFail => {
698 self.setup_scripts_exec_failed += 1;
699 }
700 ExecutionResultDescription::Timeout { .. } => {
702 self.setup_scripts_timed_out += 1;
703 }
704 }
705 }
706
707 pub(crate) fn on_test_finished(&mut self, run_statuses: &ExecutionStatuses<LiveSpec>) {
708 self.finished_count += 1;
709 let last_status = run_statuses.last_status();
718 match last_status.result {
719 ExecutionResultDescription::Pass => {
720 self.passed += 1;
721 if last_status.is_slow {
722 self.passed_slow += 1;
723 }
724 if run_statuses.len() > 1 {
725 self.flaky += 1;
726 }
727 }
728 ExecutionResultDescription::Leak {
729 result: LeakTimeoutResult::Pass,
730 } => {
731 self.passed += 1;
732 self.leaky += 1;
733 if last_status.is_slow {
734 self.passed_slow += 1;
735 }
736 if run_statuses.len() > 1 {
737 self.flaky += 1;
738 }
739 }
740 ExecutionResultDescription::Leak {
741 result: LeakTimeoutResult::Fail,
742 } => {
743 self.failed += 1;
744 self.leaky_failed += 1;
745 if last_status.is_slow {
746 self.failed_slow += 1;
747 }
748 }
749 ExecutionResultDescription::Fail { .. } => {
750 self.failed += 1;
751 if last_status.is_slow {
752 self.failed_slow += 1;
753 }
754 }
755 ExecutionResultDescription::Timeout {
756 result: SlowTimeoutResult::Pass,
757 } => {
758 self.passed += 1;
759 self.passed_timed_out += 1;
760 if run_statuses.len() > 1 {
761 self.flaky += 1;
762 }
763 }
764 ExecutionResultDescription::Timeout {
765 result: SlowTimeoutResult::Fail,
766 } => {
767 self.failed_timed_out += 1;
768 }
769 ExecutionResultDescription::ExecFail => self.exec_failed += 1,
770 }
771 }
772}
773
774#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
776#[serde(tag = "outcome", rename_all = "kebab-case")]
777#[cfg_attr(test, derive(test_strategy::Arbitrary))]
778pub enum FinalRunStats {
779 Success,
781
782 NoTestsRun,
784
785 Cancelled {
787 reason: Option<CancelReason>,
792
793 kind: RunStatsFailureKind,
795 },
796
797 Failed {
799 kind: RunStatsFailureKind,
801 },
802}
803
804#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
806#[serde(rename_all = "kebab-case")]
807#[cfg_attr(test, derive(test_strategy::Arbitrary))]
808pub struct StressRunStats {
809 pub completed: StressIndex,
811
812 pub success_count: u32,
814
815 pub failed_count: u32,
817
818 pub last_final_stats: FinalRunStats,
820}
821
822impl StressRunStats {
823 pub fn summarize_final(&self) -> StressFinalRunStats {
825 if self.failed_count > 0 {
826 StressFinalRunStats::Failed
827 } else if matches!(self.last_final_stats, FinalRunStats::Cancelled { .. }) {
828 StressFinalRunStats::Cancelled
829 } else if matches!(self.last_final_stats, FinalRunStats::NoTestsRun) {
830 StressFinalRunStats::NoTestsRun
831 } else {
832 StressFinalRunStats::Success
833 }
834 }
835}
836
837pub enum StressFinalRunStats {
839 Success,
841
842 NoTestsRun,
844
845 Cancelled,
847
848 Failed,
850}
851
852#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
854#[serde(tag = "step", rename_all = "kebab-case")]
855#[cfg_attr(test, derive(test_strategy::Arbitrary))]
856pub enum RunStatsFailureKind {
857 SetupScript,
859
860 Test {
862 initial_run_count: usize,
864
865 not_run: usize,
868 },
869}
870
871#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
876#[derive(Serialize)]
877#[serde(
878 rename_all = "kebab-case",
879 bound(serialize = "S: SerializableOutputSpec")
880)]
881#[cfg_attr(
882 test,
883 derive(test_strategy::Arbitrary),
884 arbitrary(bound(S: ArbitraryOutputSpec))
885)]
886pub struct ExecutionStatuses<S: OutputSpec> {
887 #[cfg_attr(test, strategy(proptest::collection::vec(proptest::arbitrary::any::<ExecuteStatus<S>>(), 1..=3)))]
889 statuses: Vec<ExecuteStatus<S>>,
890}
891
892impl<'de, S: SerializableOutputSpec> Deserialize<'de> for ExecutionStatuses<S> {
893 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
894 #[derive(Deserialize)]
897 #[serde(
898 rename_all = "kebab-case",
899 bound(deserialize = "S: SerializableOutputSpec")
900 )]
901 struct Helper<S: OutputSpec> {
902 statuses: Vec<ExecuteStatus<S>>,
903 }
904
905 let helper = Helper::<S>::deserialize(deserializer)?;
906 if helper.statuses.is_empty() {
907 return Err(serde::de::Error::custom("expected non-empty statuses"));
908 }
909 Ok(Self {
910 statuses: helper.statuses,
911 })
912 }
913}
914
915#[expect(clippy::len_without_is_empty)] impl<S: OutputSpec> ExecutionStatuses<S> {
917 pub(crate) fn new(statuses: Vec<ExecuteStatus<S>>) -> Self {
918 debug_assert!(!statuses.is_empty(), "ExecutionStatuses must be non-empty");
919 Self { statuses }
920 }
921
922 pub fn last_status(&self) -> &ExecuteStatus<S> {
926 self.statuses
927 .last()
928 .expect("execution statuses is non-empty")
929 }
930
931 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus<S>> + '_ {
933 self.statuses.iter()
934 }
935
936 pub fn len(&self) -> usize {
938 self.statuses.len()
939 }
940
941 pub fn describe(&self) -> ExecutionDescription<'_, S> {
943 let last_status = self.last_status();
944 if last_status.result.is_success() {
945 if self.statuses.len() > 1 {
946 ExecutionDescription::Flaky {
947 last_status,
948 prior_statuses: &self.statuses[..self.statuses.len() - 1],
949 }
950 } else {
951 ExecutionDescription::Success {
952 single_status: last_status,
953 }
954 }
955 } else {
956 let first_status = self
957 .statuses
958 .first()
959 .expect("execution statuses is non-empty");
960 let retries = &self.statuses[1..];
961 ExecutionDescription::Failure {
962 first_status,
963 last_status,
964 retries,
965 }
966 }
967 }
968}
969
970impl<S: OutputSpec> IntoIterator for ExecutionStatuses<S> {
971 type Item = ExecuteStatus<S>;
972 type IntoIter = std::vec::IntoIter<ExecuteStatus<S>>;
973
974 fn into_iter(self) -> Self::IntoIter {
975 self.statuses.into_iter()
976 }
977}
978
979#[derive_where::derive_where(Debug; S::ChildOutputDesc)]
986pub enum ExecutionDescription<'a, S: OutputSpec> {
987 Success {
989 single_status: &'a ExecuteStatus<S>,
991 },
992
993 Flaky {
995 last_status: &'a ExecuteStatus<S>,
997
998 prior_statuses: &'a [ExecuteStatus<S>],
1000 },
1001
1002 Failure {
1004 first_status: &'a ExecuteStatus<S>,
1006
1007 last_status: &'a ExecuteStatus<S>,
1009
1010 retries: &'a [ExecuteStatus<S>],
1014 },
1015}
1016
1017impl<S: OutputSpec> Clone for ExecutionDescription<'_, S> {
1020 fn clone(&self) -> Self {
1021 *self
1022 }
1023}
1024
1025impl<S: OutputSpec> Copy for ExecutionDescription<'_, S> {}
1026
1027impl<'a, S: OutputSpec> ExecutionDescription<'a, S> {
1028 pub fn status_level(&self) -> StatusLevel {
1030 match self {
1031 ExecutionDescription::Success { single_status } => match single_status.result {
1032 ExecutionResultDescription::Leak {
1033 result: LeakTimeoutResult::Pass,
1034 } => StatusLevel::Leak,
1035 ExecutionResultDescription::Pass => StatusLevel::Pass,
1036 ExecutionResultDescription::Timeout {
1037 result: SlowTimeoutResult::Pass,
1038 } => StatusLevel::Slow,
1039 ref other => unreachable!(
1040 "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1041 ),
1042 },
1043 ExecutionDescription::Flaky { .. } => StatusLevel::Retry,
1045 ExecutionDescription::Failure { .. } => StatusLevel::Fail,
1046 }
1047 }
1048
1049 pub fn final_status_level(&self) -> FinalStatusLevel {
1051 match self {
1052 ExecutionDescription::Success { single_status, .. } => {
1053 if single_status.is_slow {
1055 FinalStatusLevel::Slow
1056 } else {
1057 match single_status.result {
1058 ExecutionResultDescription::Pass => FinalStatusLevel::Pass,
1059 ExecutionResultDescription::Leak {
1060 result: LeakTimeoutResult::Pass,
1061 } => FinalStatusLevel::Leak,
1062 ExecutionResultDescription::Timeout {
1066 result: SlowTimeoutResult::Pass,
1067 } => FinalStatusLevel::Slow,
1068 ref other => unreachable!(
1069 "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1070 ),
1071 }
1072 }
1073 }
1074 ExecutionDescription::Flaky { .. } => FinalStatusLevel::Flaky,
1076 ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
1077 }
1078 }
1079
1080 pub fn last_status(&self) -> &'a ExecuteStatus<S> {
1082 match self {
1083 ExecutionDescription::Success {
1084 single_status: last_status,
1085 }
1086 | ExecutionDescription::Flaky { last_status, .. }
1087 | ExecutionDescription::Failure { last_status, .. } => last_status,
1088 }
1089 }
1090}
1091
1092#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1098#[serde(rename_all = "kebab-case")]
1099#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1100pub struct ErrorSummary {
1101 pub short_message: String,
1103
1104 pub description: String,
1106}
1107
1108#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1113#[serde(rename_all = "kebab-case")]
1114#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1115pub struct OutputErrorSlice {
1116 pub slice: String,
1118
1119 pub start: usize,
1121}
1122
1123#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1132#[derive(Serialize, Deserialize)]
1133#[serde(
1134 rename_all = "kebab-case",
1135 bound(
1136 serialize = "S: SerializableOutputSpec",
1137 deserialize = "S: SerializableOutputSpec"
1138 )
1139)]
1140#[cfg_attr(
1141 test,
1142 derive(test_strategy::Arbitrary),
1143 arbitrary(bound(S: ArbitraryOutputSpec))
1144)]
1145pub struct ExecuteStatus<S: OutputSpec> {
1146 pub retry_data: RetryData,
1148 pub output: ChildExecutionOutputDescription<S>,
1150 pub result: ExecutionResultDescription,
1152 #[cfg_attr(
1154 test,
1155 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1156 )]
1157 pub start_time: DateTime<FixedOffset>,
1158 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1160 pub time_taken: Duration,
1161 pub is_slow: bool,
1163 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1165 pub delay_before_start: Duration,
1166 pub error_summary: Option<ErrorSummary>,
1171 pub output_error_slice: Option<OutputErrorSlice>,
1176}
1177
1178#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1187#[derive(Serialize, Deserialize)]
1188#[serde(
1189 rename_all = "kebab-case",
1190 bound(
1191 serialize = "S: SerializableOutputSpec",
1192 deserialize = "S: SerializableOutputSpec"
1193 )
1194)]
1195#[cfg_attr(
1196 test,
1197 derive(test_strategy::Arbitrary),
1198 arbitrary(bound(S: ArbitraryOutputSpec))
1199)]
1200pub struct SetupScriptExecuteStatus<S: OutputSpec> {
1201 pub output: ChildExecutionOutputDescription<S>,
1203
1204 pub result: ExecutionResultDescription,
1206
1207 #[cfg_attr(
1209 test,
1210 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1211 )]
1212 pub start_time: DateTime<FixedOffset>,
1213
1214 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1216 pub time_taken: Duration,
1217
1218 pub is_slow: bool,
1220
1221 pub env_map: Option<SetupScriptEnvMap>,
1226
1227 pub error_summary: Option<ErrorSummary>,
1232}
1233
1234#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1238#[serde(rename_all = "kebab-case")]
1239#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1240pub struct SetupScriptEnvMap {
1241 pub env_map: BTreeMap<String, String>,
1243}
1244
1245#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1254#[derive(Serialize, Deserialize)]
1255#[serde(
1256 tag = "type",
1257 rename_all = "kebab-case",
1258 bound(
1259 serialize = "S: SerializableOutputSpec",
1260 deserialize = "S: SerializableOutputSpec"
1261 )
1262)]
1263#[cfg_attr(
1264 test,
1265 derive(test_strategy::Arbitrary),
1266 arbitrary(bound(S: ArbitraryOutputSpec))
1267)]
1268pub enum ChildExecutionOutputDescription<S: OutputSpec> {
1269 Output {
1271 result: Option<ExecutionResultDescription>,
1275
1276 output: S::ChildOutputDesc,
1278
1279 errors: Option<ErrorList<ChildErrorDescription>>,
1282 },
1283
1284 StartError(ChildStartErrorDescription),
1286}
1287
1288impl<S: OutputSpec> ChildExecutionOutputDescription<S> {
1289 pub fn has_errors(&self) -> bool {
1291 match self {
1292 Self::Output { errors, result, .. } => {
1293 if errors.is_some() {
1294 return true;
1295 }
1296 if let Some(result) = result {
1297 return !result.is_success();
1298 }
1299 false
1300 }
1301 Self::StartError(_) => true,
1302 }
1303 }
1304}
1305
1306#[derive(Clone, Debug)]
1318pub enum ChildOutputDescription {
1319 Split {
1321 stdout: Option<ChildSingleOutput>,
1323 stderr: Option<ChildSingleOutput>,
1325 },
1326
1327 Combined {
1329 output: ChildSingleOutput,
1331 },
1332
1333 NotLoaded,
1339}
1340
1341impl ChildOutputDescription {
1342 pub fn stdout_stderr_len(&self) -> (Option<u64>, Option<u64>) {
1346 match self {
1347 Self::Split { stdout, stderr } => (
1348 stdout.as_ref().map(|s| s.buf.len() as u64),
1349 stderr.as_ref().map(|s| s.buf.len() as u64),
1350 ),
1351 Self::Combined { output } => (Some(output.buf.len() as u64), None),
1352 Self::NotLoaded => {
1353 unreachable!(
1354 "attempted to get output lengths from output that was not loaded \
1355 (this method is only called from the live runner, where NotLoaded \
1356 is never produced)"
1357 );
1358 }
1359 }
1360 }
1361}
1362
1363#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1367#[serde(tag = "kind", rename_all = "kebab-case")]
1368#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1369pub enum ChildStartErrorDescription {
1370 TempPath {
1372 source: IoErrorDescription,
1374 },
1375
1376 Spawn {
1378 source: IoErrorDescription,
1380 },
1381}
1382
1383impl fmt::Display for ChildStartErrorDescription {
1384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1385 match self {
1386 Self::TempPath { .. } => {
1387 write!(f, "error creating temporary path for setup script")
1388 }
1389 Self::Spawn { .. } => write!(f, "error spawning child process"),
1390 }
1391 }
1392}
1393
1394impl std::error::Error for ChildStartErrorDescription {
1395 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1396 match self {
1397 Self::TempPath { source } | Self::Spawn { source } => Some(source),
1398 }
1399 }
1400}
1401
1402#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1406#[serde(tag = "kind", rename_all = "kebab-case")]
1407#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1408pub enum ChildErrorDescription {
1409 ReadStdout {
1411 source: IoErrorDescription,
1413 },
1414
1415 ReadStderr {
1417 source: IoErrorDescription,
1419 },
1420
1421 ReadCombined {
1423 source: IoErrorDescription,
1425 },
1426
1427 Wait {
1429 source: IoErrorDescription,
1431 },
1432
1433 SetupScriptOutput {
1435 source: IoErrorDescription,
1437 },
1438}
1439
1440impl fmt::Display for ChildErrorDescription {
1441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1442 match self {
1443 Self::ReadStdout { .. } => write!(f, "error reading standard output"),
1444 Self::ReadStderr { .. } => write!(f, "error reading standard error"),
1445 Self::ReadCombined { .. } => {
1446 write!(f, "error reading combined stream")
1447 }
1448 Self::Wait { .. } => {
1449 write!(f, "error waiting for child process to exit")
1450 }
1451 Self::SetupScriptOutput { .. } => {
1452 write!(f, "error reading setup script output")
1453 }
1454 }
1455 }
1456}
1457
1458impl std::error::Error for ChildErrorDescription {
1459 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1460 match self {
1461 Self::ReadStdout { source }
1462 | Self::ReadStderr { source }
1463 | Self::ReadCombined { source }
1464 | Self::Wait { source }
1465 | Self::SetupScriptOutput { source } => Some(source),
1466 }
1467 }
1468}
1469
1470#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1474#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1475pub struct IoErrorDescription {
1476 message: String,
1477}
1478
1479impl fmt::Display for IoErrorDescription {
1480 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1481 write!(f, "{}", self.message)
1482 }
1483}
1484
1485impl std::error::Error for IoErrorDescription {}
1486
1487impl From<ChildExecutionOutput> for ChildExecutionOutputDescription<LiveSpec> {
1488 fn from(output: ChildExecutionOutput) -> Self {
1489 match output {
1490 ChildExecutionOutput::Output {
1491 result,
1492 output,
1493 errors,
1494 } => Self::Output {
1495 result: result.map(ExecutionResultDescription::from),
1496 output: ChildOutputDescription::from(output),
1497 errors: errors.map(|e| e.map(ChildErrorDescription::from)),
1498 },
1499 ChildExecutionOutput::StartError(error) => {
1500 Self::StartError(ChildStartErrorDescription::from(error))
1501 }
1502 }
1503 }
1504}
1505
1506impl From<ChildOutput> for ChildOutputDescription {
1507 fn from(output: ChildOutput) -> Self {
1508 match output {
1509 ChildOutput::Split(split) => Self::Split {
1510 stdout: split.stdout,
1511 stderr: split.stderr,
1512 },
1513 ChildOutput::Combined { output } => Self::Combined { output },
1514 }
1515 }
1516}
1517
1518impl From<ChildStartError> for ChildStartErrorDescription {
1519 fn from(error: ChildStartError) -> Self {
1520 match error {
1521 ChildStartError::TempPath(e) => Self::TempPath {
1522 source: IoErrorDescription {
1523 message: e.to_string(),
1524 },
1525 },
1526 ChildStartError::Spawn(e) => Self::Spawn {
1527 source: IoErrorDescription {
1528 message: e.to_string(),
1529 },
1530 },
1531 }
1532 }
1533}
1534
1535impl From<ChildError> for ChildErrorDescription {
1536 fn from(error: ChildError) -> Self {
1537 match error {
1538 ChildError::Fd(ChildFdError::ReadStdout(e)) => Self::ReadStdout {
1539 source: IoErrorDescription {
1540 message: e.to_string(),
1541 },
1542 },
1543 ChildError::Fd(ChildFdError::ReadStderr(e)) => Self::ReadStderr {
1544 source: IoErrorDescription {
1545 message: e.to_string(),
1546 },
1547 },
1548 ChildError::Fd(ChildFdError::ReadCombined(e)) => Self::ReadCombined {
1549 source: IoErrorDescription {
1550 message: e.to_string(),
1551 },
1552 },
1553 ChildError::Fd(ChildFdError::Wait(e)) => Self::Wait {
1554 source: IoErrorDescription {
1555 message: e.to_string(),
1556 },
1557 },
1558 ChildError::SetupScriptOutput(e) => Self::SetupScriptOutput {
1559 source: IoErrorDescription {
1560 message: e.to_string(),
1561 },
1562 },
1563 }
1564 }
1565}
1566
1567#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
1569#[serde(rename_all = "kebab-case")]
1570#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1571pub struct RetryData {
1572 pub attempt: u32,
1574
1575 pub total_attempts: u32,
1577}
1578
1579impl RetryData {
1580 pub fn is_last_attempt(&self) -> bool {
1582 self.attempt >= self.total_attempts
1583 }
1584}
1585
1586#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1588pub enum ExecutionResult {
1589 Pass,
1591 Leak {
1595 result: LeakTimeoutResult,
1602 },
1603 Fail {
1605 failure_status: FailureStatus,
1607
1608 leaked: bool,
1612 },
1613 ExecFail,
1615 Timeout {
1617 result: SlowTimeoutResult,
1619 },
1620}
1621
1622impl ExecutionResult {
1623 pub fn is_success(self) -> bool {
1625 match self {
1626 ExecutionResult::Pass
1627 | ExecutionResult::Timeout {
1628 result: SlowTimeoutResult::Pass,
1629 }
1630 | ExecutionResult::Leak {
1631 result: LeakTimeoutResult::Pass,
1632 } => true,
1633 ExecutionResult::Leak {
1634 result: LeakTimeoutResult::Fail,
1635 }
1636 | ExecutionResult::Fail { .. }
1637 | ExecutionResult::ExecFail
1638 | ExecutionResult::Timeout {
1639 result: SlowTimeoutResult::Fail,
1640 } => false,
1641 }
1642 }
1643
1644 pub fn as_static_str(&self) -> &'static str {
1646 match self {
1647 ExecutionResult::Pass => "pass",
1648 ExecutionResult::Leak { .. } => "leak",
1649 ExecutionResult::Fail { .. } => "fail",
1650 ExecutionResult::ExecFail => "exec-fail",
1651 ExecutionResult::Timeout { .. } => "timeout",
1652 }
1653 }
1654}
1655
1656#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1658pub enum FailureStatus {
1659 ExitCode(i32),
1661
1662 Abort(AbortStatus),
1664}
1665
1666impl FailureStatus {
1667 pub fn extract(exit_status: ExitStatus) -> Self {
1669 if let Some(abort_status) = AbortStatus::extract(exit_status) {
1670 FailureStatus::Abort(abort_status)
1671 } else {
1672 FailureStatus::ExitCode(
1673 exit_status
1674 .code()
1675 .expect("if abort_status is None, then code must be present"),
1676 )
1677 }
1678 }
1679}
1680
1681#[derive(Copy, Clone, Eq, PartialEq)]
1685pub enum AbortStatus {
1686 #[cfg(unix)]
1688 UnixSignal(i32),
1689
1690 #[cfg(windows)]
1692 WindowsNtStatus(windows_sys::Win32::Foundation::NTSTATUS),
1693
1694 #[cfg(windows)]
1696 JobObject,
1697}
1698
1699impl AbortStatus {
1700 pub fn extract(exit_status: ExitStatus) -> Option<Self> {
1702 cfg_if::cfg_if! {
1703 if #[cfg(unix)] {
1704 use std::os::unix::process::ExitStatusExt;
1706 exit_status.signal().map(AbortStatus::UnixSignal)
1707 } else if #[cfg(windows)] {
1708 exit_status.code().and_then(|code| {
1709 (code < 0).then_some(AbortStatus::WindowsNtStatus(code))
1710 })
1711 } else {
1712 None
1713 }
1714 }
1715 }
1716}
1717
1718impl fmt::Debug for AbortStatus {
1719 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1720 match self {
1721 #[cfg(unix)]
1722 AbortStatus::UnixSignal(signal) => write!(f, "UnixSignal({signal})"),
1723 #[cfg(windows)]
1724 AbortStatus::WindowsNtStatus(status) => write!(f, "WindowsNtStatus({status:x})"),
1725 #[cfg(windows)]
1726 AbortStatus::JobObject => write!(f, "JobObject"),
1727 }
1728 }
1729}
1730
1731#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1737#[serde(tag = "kind", rename_all = "kebab-case")]
1738#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1739#[non_exhaustive]
1740pub enum AbortDescription {
1741 UnixSignal {
1743 signal: i32,
1745 #[cfg_attr(
1748 test,
1749 strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
1750 )]
1751 name: Option<SmolStr>,
1752 },
1753
1754 WindowsNtStatus {
1756 code: i32,
1758 #[cfg_attr(
1760 test,
1761 strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
1762 )]
1763 message: Option<SmolStr>,
1764 },
1765
1766 WindowsJobObject,
1768}
1769
1770impl fmt::Display for AbortDescription {
1771 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1772 match self {
1773 Self::UnixSignal { signal, name } => {
1774 write!(f, "aborted with signal {signal}")?;
1775 if let Some(name) = name {
1776 write!(f, " (SIG{name})")?;
1777 }
1778 Ok(())
1779 }
1780 Self::WindowsNtStatus { code, message } => {
1781 write!(f, "aborted with code {code:#010x}")?;
1782 if let Some(message) = message {
1783 write!(f, ": {message}")?;
1784 }
1785 Ok(())
1786 }
1787 Self::WindowsJobObject => {
1788 write!(f, "terminated via job object")
1789 }
1790 }
1791 }
1792}
1793
1794impl From<AbortStatus> for AbortDescription {
1795 fn from(status: AbortStatus) -> Self {
1796 cfg_if::cfg_if! {
1797 if #[cfg(unix)] {
1798 match status {
1799 AbortStatus::UnixSignal(signal) => Self::UnixSignal {
1800 signal,
1801 name: crate::helpers::signal_str(signal).map(SmolStr::new_static),
1802 },
1803 }
1804 } else if #[cfg(windows)] {
1805 match status {
1806 AbortStatus::WindowsNtStatus(code) => Self::WindowsNtStatus {
1807 code,
1808 message: crate::helpers::windows_nt_status_message(code),
1809 },
1810 AbortStatus::JobObject => Self::WindowsJobObject,
1811 }
1812 } else {
1813 match status {}
1814 }
1815 }
1816 }
1817}
1818
1819#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1823#[serde(tag = "kind", rename_all = "kebab-case")]
1824#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1825#[non_exhaustive]
1826pub enum FailureDescription {
1827 ExitCode {
1829 code: i32,
1831 },
1832
1833 Abort {
1840 abort: AbortDescription,
1842 },
1843}
1844
1845impl From<FailureStatus> for FailureDescription {
1846 fn from(status: FailureStatus) -> Self {
1847 match status {
1848 FailureStatus::ExitCode(code) => Self::ExitCode { code },
1849 FailureStatus::Abort(abort) => Self::Abort {
1850 abort: AbortDescription::from(abort),
1851 },
1852 }
1853 }
1854}
1855
1856impl fmt::Display for FailureDescription {
1857 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1858 match self {
1859 Self::ExitCode { code } => write!(f, "exited with code {code}"),
1860 Self::Abort { abort } => write!(f, "{abort}"),
1861 }
1862 }
1863}
1864
1865#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1870#[serde(tag = "status", rename_all = "kebab-case")]
1871#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1872#[non_exhaustive]
1873pub enum ExecutionResultDescription {
1874 Pass,
1876
1877 Leak {
1879 result: LeakTimeoutResult,
1881 },
1882
1883 Fail {
1885 failure: FailureDescription,
1887
1888 leaked: bool,
1890 },
1891
1892 ExecFail,
1894
1895 Timeout {
1897 result: SlowTimeoutResult,
1899 },
1900}
1901
1902impl ExecutionResultDescription {
1903 pub fn is_success(&self) -> bool {
1905 match self {
1906 Self::Pass
1907 | Self::Timeout {
1908 result: SlowTimeoutResult::Pass,
1909 }
1910 | Self::Leak {
1911 result: LeakTimeoutResult::Pass,
1912 } => true,
1913 Self::Leak {
1914 result: LeakTimeoutResult::Fail,
1915 }
1916 | Self::Fail { .. }
1917 | Self::ExecFail
1918 | Self::Timeout {
1919 result: SlowTimeoutResult::Fail,
1920 } => false,
1921 }
1922 }
1923
1924 pub fn as_static_str(&self) -> &'static str {
1926 match self {
1927 Self::Pass => "pass",
1928 Self::Leak { .. } => "leak",
1929 Self::Fail { .. } => "fail",
1930 Self::ExecFail => "exec-fail",
1931 Self::Timeout { .. } => "timeout",
1932 }
1933 }
1934
1935 pub fn is_termination_failure(&self) -> bool {
1947 matches!(
1948 self,
1949 Self::Fail {
1950 failure: FailureDescription::Abort {
1951 abort: AbortDescription::UnixSignal {
1952 signal: SIGTERM,
1953 ..
1954 },
1955 },
1956 ..
1957 } | Self::Fail {
1958 failure: FailureDescription::Abort {
1959 abort: AbortDescription::WindowsJobObject,
1960 },
1961 ..
1962 }
1963 )
1964 }
1965}
1966
1967impl From<ExecutionResult> for ExecutionResultDescription {
1968 fn from(result: ExecutionResult) -> Self {
1969 match result {
1970 ExecutionResult::Pass => Self::Pass,
1971 ExecutionResult::Leak { result } => Self::Leak { result },
1972 ExecutionResult::Fail {
1973 failure_status,
1974 leaked,
1975 } => Self::Fail {
1976 failure: FailureDescription::from(failure_status),
1977 leaked,
1978 },
1979 ExecutionResult::ExecFail => Self::ExecFail,
1980 ExecutionResult::Timeout { result } => Self::Timeout { result },
1981 }
1982 }
1983}
1984
1985#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
1988#[serde(rename_all = "kebab-case")]
1989#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1990pub enum CancelReason {
1991 SetupScriptFailure,
1993
1994 TestFailure,
1996
1997 ReportError,
1999
2000 GlobalTimeout,
2002
2003 TestFailureImmediate,
2005
2006 Signal,
2008
2009 Interrupt,
2011
2012 SecondSignal,
2014}
2015
2016impl CancelReason {
2017 pub(crate) fn to_static_str(self) -> &'static str {
2018 match self {
2019 CancelReason::SetupScriptFailure => "setup script failure",
2020 CancelReason::TestFailure => "test failure",
2021 CancelReason::ReportError => "reporting error",
2022 CancelReason::GlobalTimeout => "global timeout",
2023 CancelReason::TestFailureImmediate => "test failure",
2024 CancelReason::Signal => "signal",
2025 CancelReason::Interrupt => "interrupt",
2026 CancelReason::SecondSignal => "second signal",
2027 }
2028 }
2029}
2030#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2032pub enum UnitKind {
2033 Test,
2035
2036 Script,
2038}
2039
2040impl UnitKind {
2041 pub(crate) const WAITING_ON_TEST_MESSAGE: &str = "waiting on test process";
2042 pub(crate) const WAITING_ON_SCRIPT_MESSAGE: &str = "waiting on script process";
2043
2044 pub(crate) const EXECUTING_TEST_MESSAGE: &str = "executing test";
2045 pub(crate) const EXECUTING_SCRIPT_MESSAGE: &str = "executing script";
2046
2047 pub(crate) fn waiting_on_message(&self) -> &'static str {
2048 match self {
2049 UnitKind::Test => Self::WAITING_ON_TEST_MESSAGE,
2050 UnitKind::Script => Self::WAITING_ON_SCRIPT_MESSAGE,
2051 }
2052 }
2053
2054 pub(crate) fn executing_message(&self) -> &'static str {
2055 match self {
2056 UnitKind::Test => Self::EXECUTING_TEST_MESSAGE,
2057 UnitKind::Script => Self::EXECUTING_SCRIPT_MESSAGE,
2058 }
2059 }
2060}
2061
2062#[derive(Clone, Debug)]
2064pub enum InfoResponse<'a> {
2065 SetupScript(SetupScriptInfoResponse),
2067
2068 Test(TestInfoResponse<'a>),
2070}
2071
2072#[derive(Clone, Debug)]
2074pub struct SetupScriptInfoResponse {
2075 pub stress_index: Option<StressIndex>,
2077
2078 pub script_id: ScriptId,
2080
2081 pub program: String,
2083
2084 pub args: Vec<String>,
2086
2087 pub state: UnitState,
2089
2090 pub output: ChildExecutionOutputDescription<LiveSpec>,
2092}
2093
2094#[derive(Clone, Debug)]
2096pub struct TestInfoResponse<'a> {
2097 pub stress_index: Option<StressIndex>,
2099
2100 pub test_instance: TestInstanceId<'a>,
2102
2103 pub retry_data: RetryData,
2105
2106 pub state: UnitState,
2108
2109 pub output: ChildExecutionOutputDescription<LiveSpec>,
2111}
2112
2113#[derive(Clone, Debug)]
2118pub enum UnitState {
2119 Running {
2121 pid: u32,
2123
2124 time_taken: Duration,
2126
2127 slow_after: Option<Duration>,
2130 },
2131
2132 Exiting {
2135 pid: u32,
2137
2138 time_taken: Duration,
2140
2141 slow_after: Option<Duration>,
2144
2145 tentative_result: Option<ExecutionResult>,
2150
2151 waiting_duration: Duration,
2153
2154 remaining: Duration,
2156 },
2157
2158 Terminating(UnitTerminatingState),
2160
2161 Exited {
2163 result: ExecutionResult,
2165
2166 time_taken: Duration,
2168
2169 slow_after: Option<Duration>,
2172 },
2173
2174 DelayBeforeNextAttempt {
2177 previous_result: ExecutionResult,
2179
2180 previous_slow: bool,
2182
2183 waiting_duration: Duration,
2185
2186 remaining: Duration,
2188 },
2189}
2190
2191impl UnitState {
2192 pub fn has_valid_output(&self) -> bool {
2194 match self {
2195 UnitState::Running { .. }
2196 | UnitState::Exiting { .. }
2197 | UnitState::Terminating(_)
2198 | UnitState::Exited { .. } => true,
2199 UnitState::DelayBeforeNextAttempt { .. } => false,
2200 }
2201 }
2202}
2203
2204#[derive(Clone, Debug)]
2208pub struct UnitTerminatingState {
2209 pub pid: u32,
2211
2212 pub time_taken: Duration,
2214
2215 pub reason: UnitTerminateReason,
2217
2218 pub method: UnitTerminateMethod,
2220
2221 pub waiting_duration: Duration,
2223
2224 pub remaining: Duration,
2226}
2227
2228#[derive(Clone, Copy, Debug)]
2232pub enum UnitTerminateReason {
2233 Timeout,
2235
2236 Signal,
2238
2239 Interrupt,
2241}
2242
2243impl fmt::Display for UnitTerminateReason {
2244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2245 match self {
2246 UnitTerminateReason::Timeout => write!(f, "timeout"),
2247 UnitTerminateReason::Signal => write!(f, "signal"),
2248 UnitTerminateReason::Interrupt => write!(f, "interrupt"),
2249 }
2250 }
2251}
2252
2253#[derive(Clone, Copy, Debug)]
2255pub enum UnitTerminateMethod {
2256 #[cfg(unix)]
2258 Signal(UnitTerminateSignal),
2259
2260 #[cfg(windows)]
2262 JobObject,
2263
2264 #[cfg(windows)]
2272 Wait,
2273
2274 #[cfg(test)]
2276 Fake,
2277}
2278
2279#[cfg(unix)]
2280#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2282pub enum UnitTerminateSignal {
2283 Interrupt,
2285
2286 Term,
2288
2289 Hangup,
2291
2292 Quit,
2294
2295 Kill,
2297}
2298
2299#[cfg(unix)]
2300impl fmt::Display for UnitTerminateSignal {
2301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2302 match self {
2303 UnitTerminateSignal::Interrupt => write!(f, "SIGINT"),
2304 UnitTerminateSignal::Term => write!(f, "SIGTERM"),
2305 UnitTerminateSignal::Hangup => write!(f, "SIGHUP"),
2306 UnitTerminateSignal::Quit => write!(f, "SIGQUIT"),
2307 UnitTerminateSignal::Kill => write!(f, "SIGKILL"),
2308 }
2309 }
2310}
2311
2312#[cfg(test)]
2313mod tests {
2314 use super::*;
2315
2316 #[test]
2317 fn test_is_success() {
2318 assert_eq!(
2319 RunStats::default().summarize_final(),
2320 FinalRunStats::NoTestsRun,
2321 "empty run => no tests run"
2322 );
2323 assert_eq!(
2324 RunStats {
2325 initial_run_count: 42,
2326 finished_count: 42,
2327 ..RunStats::default()
2328 }
2329 .summarize_final(),
2330 FinalRunStats::Success,
2331 "initial run count = final run count => success"
2332 );
2333 assert_eq!(
2334 RunStats {
2335 initial_run_count: 42,
2336 finished_count: 41,
2337 ..RunStats::default()
2338 }
2339 .summarize_final(),
2340 FinalRunStats::Cancelled {
2341 reason: None,
2342 kind: RunStatsFailureKind::Test {
2343 initial_run_count: 42,
2344 not_run: 1
2345 }
2346 },
2347 "initial run count > final run count => cancelled"
2348 );
2349 assert_eq!(
2350 RunStats {
2351 initial_run_count: 42,
2352 finished_count: 42,
2353 failed: 1,
2354 ..RunStats::default()
2355 }
2356 .summarize_final(),
2357 FinalRunStats::Failed {
2358 kind: RunStatsFailureKind::Test {
2359 initial_run_count: 42,
2360 not_run: 0,
2361 },
2362 },
2363 "failed => failure"
2364 );
2365 assert_eq!(
2366 RunStats {
2367 initial_run_count: 42,
2368 finished_count: 42,
2369 exec_failed: 1,
2370 ..RunStats::default()
2371 }
2372 .summarize_final(),
2373 FinalRunStats::Failed {
2374 kind: RunStatsFailureKind::Test {
2375 initial_run_count: 42,
2376 not_run: 0,
2377 },
2378 },
2379 "exec failed => failure"
2380 );
2381 assert_eq!(
2382 RunStats {
2383 initial_run_count: 42,
2384 finished_count: 42,
2385 failed_timed_out: 1,
2386 ..RunStats::default()
2387 }
2388 .summarize_final(),
2389 FinalRunStats::Failed {
2390 kind: RunStatsFailureKind::Test {
2391 initial_run_count: 42,
2392 not_run: 0,
2393 },
2394 },
2395 "timed out => failure {:?} {:?}",
2396 RunStats {
2397 initial_run_count: 42,
2398 finished_count: 42,
2399 failed_timed_out: 1,
2400 ..RunStats::default()
2401 }
2402 .summarize_final(),
2403 FinalRunStats::Failed {
2404 kind: RunStatsFailureKind::Test {
2405 initial_run_count: 42,
2406 not_run: 0,
2407 },
2408 },
2409 );
2410 assert_eq!(
2411 RunStats {
2412 initial_run_count: 42,
2413 finished_count: 42,
2414 skipped: 1,
2415 ..RunStats::default()
2416 }
2417 .summarize_final(),
2418 FinalRunStats::Success,
2419 "skipped => not considered a failure"
2420 );
2421
2422 assert_eq!(
2423 RunStats {
2424 setup_scripts_initial_count: 2,
2425 setup_scripts_finished_count: 1,
2426 ..RunStats::default()
2427 }
2428 .summarize_final(),
2429 FinalRunStats::Cancelled {
2430 reason: None,
2431 kind: RunStatsFailureKind::SetupScript,
2432 },
2433 "setup script failed => failure"
2434 );
2435
2436 assert_eq!(
2437 RunStats {
2438 setup_scripts_initial_count: 2,
2439 setup_scripts_finished_count: 2,
2440 setup_scripts_failed: 1,
2441 ..RunStats::default()
2442 }
2443 .summarize_final(),
2444 FinalRunStats::Failed {
2445 kind: RunStatsFailureKind::SetupScript,
2446 },
2447 "setup script failed => failure"
2448 );
2449 assert_eq!(
2450 RunStats {
2451 setup_scripts_initial_count: 2,
2452 setup_scripts_finished_count: 2,
2453 setup_scripts_exec_failed: 1,
2454 ..RunStats::default()
2455 }
2456 .summarize_final(),
2457 FinalRunStats::Failed {
2458 kind: RunStatsFailureKind::SetupScript,
2459 },
2460 "setup script exec failed => failure"
2461 );
2462 assert_eq!(
2463 RunStats {
2464 setup_scripts_initial_count: 2,
2465 setup_scripts_finished_count: 2,
2466 setup_scripts_timed_out: 1,
2467 ..RunStats::default()
2468 }
2469 .summarize_final(),
2470 FinalRunStats::Failed {
2471 kind: RunStatsFailureKind::SetupScript,
2472 },
2473 "setup script timed out => failure"
2474 );
2475 assert_eq!(
2476 RunStats {
2477 setup_scripts_initial_count: 2,
2478 setup_scripts_finished_count: 2,
2479 setup_scripts_passed: 2,
2480 ..RunStats::default()
2481 }
2482 .summarize_final(),
2483 FinalRunStats::NoTestsRun,
2484 "setup scripts passed => success, but no tests run"
2485 );
2486 }
2487
2488 #[test]
2489 fn abort_description_serialization() {
2490 let unix_with_name = AbortDescription::UnixSignal {
2492 signal: 15,
2493 name: Some("TERM".into()),
2494 };
2495 let json = serde_json::to_string_pretty(&unix_with_name).unwrap();
2496 insta::assert_snapshot!("abort_unix_signal_with_name", json);
2497 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2498 assert_eq!(unix_with_name, roundtrip);
2499
2500 let unix_no_name = AbortDescription::UnixSignal {
2502 signal: 42,
2503 name: None,
2504 };
2505 let json = serde_json::to_string_pretty(&unix_no_name).unwrap();
2506 insta::assert_snapshot!("abort_unix_signal_no_name", json);
2507 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2508 assert_eq!(unix_no_name, roundtrip);
2509
2510 let windows_nt = AbortDescription::WindowsNtStatus {
2512 code: -1073741510_i32,
2513 message: Some("The application terminated as a result of a CTRL+C.".into()),
2514 };
2515 let json = serde_json::to_string_pretty(&windows_nt).unwrap();
2516 insta::assert_snapshot!("abort_windows_nt_status", json);
2517 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2518 assert_eq!(windows_nt, roundtrip);
2519
2520 let windows_nt_no_msg = AbortDescription::WindowsNtStatus {
2522 code: -1073741819_i32,
2523 message: None,
2524 };
2525 let json = serde_json::to_string_pretty(&windows_nt_no_msg).unwrap();
2526 insta::assert_snapshot!("abort_windows_nt_status_no_message", json);
2527 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2528 assert_eq!(windows_nt_no_msg, roundtrip);
2529
2530 let job = AbortDescription::WindowsJobObject;
2532 let json = serde_json::to_string_pretty(&job).unwrap();
2533 insta::assert_snapshot!("abort_windows_job_object", json);
2534 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2535 assert_eq!(job, roundtrip);
2536 }
2537
2538 #[test]
2539 fn abort_description_cross_platform_deserialization() {
2540 let unix_json = r#"{"kind":"unix-signal","signal":11,"name":"SEGV"}"#;
2543 let unix_desc: AbortDescription = serde_json::from_str(unix_json).unwrap();
2544 assert_eq!(
2545 unix_desc,
2546 AbortDescription::UnixSignal {
2547 signal: 11,
2548 name: Some("SEGV".into()),
2549 }
2550 );
2551
2552 let windows_json = r#"{"kind":"windows-nt-status","code":-1073741510,"message":"CTRL+C"}"#;
2553 let windows_desc: AbortDescription = serde_json::from_str(windows_json).unwrap();
2554 assert_eq!(
2555 windows_desc,
2556 AbortDescription::WindowsNtStatus {
2557 code: -1073741510,
2558 message: Some("CTRL+C".into()),
2559 }
2560 );
2561
2562 let job_json = r#"{"kind":"windows-job-object"}"#;
2563 let job_desc: AbortDescription = serde_json::from_str(job_json).unwrap();
2564 assert_eq!(job_desc, AbortDescription::WindowsJobObject);
2565 }
2566
2567 #[test]
2568 fn abort_description_display() {
2569 let unix = AbortDescription::UnixSignal {
2571 signal: 15,
2572 name: Some("TERM".into()),
2573 };
2574 assert_eq!(unix.to_string(), "aborted with signal 15 (SIGTERM)");
2575
2576 let unix_no_name = AbortDescription::UnixSignal {
2578 signal: 42,
2579 name: None,
2580 };
2581 assert_eq!(unix_no_name.to_string(), "aborted with signal 42");
2582
2583 let windows = AbortDescription::WindowsNtStatus {
2585 code: -1073741510,
2586 message: Some("CTRL+C exit".into()),
2587 };
2588 assert_eq!(
2589 windows.to_string(),
2590 "aborted with code 0xc000013a: CTRL+C exit"
2591 );
2592
2593 let windows_no_msg = AbortDescription::WindowsNtStatus {
2595 code: -1073741510,
2596 message: None,
2597 };
2598 assert_eq!(windows_no_msg.to_string(), "aborted with code 0xc000013a");
2599
2600 let job = AbortDescription::WindowsJobObject;
2602 assert_eq!(job.to_string(), "terminated via job object");
2603 }
2604
2605 #[cfg(unix)]
2606 #[test]
2607 fn abort_description_from_abort_status() {
2608 let status = AbortStatus::UnixSignal(15);
2610 let description = AbortDescription::from(status);
2611
2612 assert_eq!(
2613 description,
2614 AbortDescription::UnixSignal {
2615 signal: 15,
2616 name: Some("TERM".into()),
2617 }
2618 );
2619
2620 let unknown_status = AbortStatus::UnixSignal(42);
2622 let unknown_description = AbortDescription::from(unknown_status);
2623 assert_eq!(
2624 unknown_description,
2625 AbortDescription::UnixSignal {
2626 signal: 42,
2627 name: None,
2628 }
2629 );
2630 }
2631
2632 #[test]
2633 fn execution_result_description_serialization() {
2634 let pass = ExecutionResultDescription::Pass;
2638 let json = serde_json::to_string_pretty(&pass).unwrap();
2639 insta::assert_snapshot!("pass", json);
2640 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2641 assert_eq!(pass, roundtrip);
2642
2643 let leak_pass = ExecutionResultDescription::Leak {
2645 result: LeakTimeoutResult::Pass,
2646 };
2647 let json = serde_json::to_string_pretty(&leak_pass).unwrap();
2648 insta::assert_snapshot!("leak_pass", json);
2649 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2650 assert_eq!(leak_pass, roundtrip);
2651
2652 let leak_fail = ExecutionResultDescription::Leak {
2654 result: LeakTimeoutResult::Fail,
2655 };
2656 let json = serde_json::to_string_pretty(&leak_fail).unwrap();
2657 insta::assert_snapshot!("leak_fail", json);
2658 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2659 assert_eq!(leak_fail, roundtrip);
2660
2661 let fail_exit_code = ExecutionResultDescription::Fail {
2663 failure: FailureDescription::ExitCode { code: 101 },
2664 leaked: false,
2665 };
2666 let json = serde_json::to_string_pretty(&fail_exit_code).unwrap();
2667 insta::assert_snapshot!("fail_exit_code", json);
2668 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2669 assert_eq!(fail_exit_code, roundtrip);
2670
2671 let fail_exit_code_leaked = ExecutionResultDescription::Fail {
2673 failure: FailureDescription::ExitCode { code: 1 },
2674 leaked: true,
2675 };
2676 let json = serde_json::to_string_pretty(&fail_exit_code_leaked).unwrap();
2677 insta::assert_snapshot!("fail_exit_code_leaked", json);
2678 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2679 assert_eq!(fail_exit_code_leaked, roundtrip);
2680
2681 let fail_unix_signal = ExecutionResultDescription::Fail {
2683 failure: FailureDescription::Abort {
2684 abort: AbortDescription::UnixSignal {
2685 signal: 11,
2686 name: Some("SEGV".into()),
2687 },
2688 },
2689 leaked: false,
2690 };
2691 let json = serde_json::to_string_pretty(&fail_unix_signal).unwrap();
2692 insta::assert_snapshot!("fail_unix_signal", json);
2693 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2694 assert_eq!(fail_unix_signal, roundtrip);
2695
2696 let fail_unix_signal_unknown = ExecutionResultDescription::Fail {
2698 failure: FailureDescription::Abort {
2699 abort: AbortDescription::UnixSignal {
2700 signal: 42,
2701 name: None,
2702 },
2703 },
2704 leaked: true,
2705 };
2706 let json = serde_json::to_string_pretty(&fail_unix_signal_unknown).unwrap();
2707 insta::assert_snapshot!("fail_unix_signal_unknown_leaked", json);
2708 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2709 assert_eq!(fail_unix_signal_unknown, roundtrip);
2710
2711 let fail_windows_nt = ExecutionResultDescription::Fail {
2713 failure: FailureDescription::Abort {
2714 abort: AbortDescription::WindowsNtStatus {
2715 code: -1073741510,
2716 message: Some("The application terminated as a result of a CTRL+C.".into()),
2717 },
2718 },
2719 leaked: false,
2720 };
2721 let json = serde_json::to_string_pretty(&fail_windows_nt).unwrap();
2722 insta::assert_snapshot!("fail_windows_nt_status", json);
2723 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2724 assert_eq!(fail_windows_nt, roundtrip);
2725
2726 let fail_windows_nt_no_msg = ExecutionResultDescription::Fail {
2728 failure: FailureDescription::Abort {
2729 abort: AbortDescription::WindowsNtStatus {
2730 code: -1073741819,
2731 message: None,
2732 },
2733 },
2734 leaked: false,
2735 };
2736 let json = serde_json::to_string_pretty(&fail_windows_nt_no_msg).unwrap();
2737 insta::assert_snapshot!("fail_windows_nt_status_no_message", json);
2738 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2739 assert_eq!(fail_windows_nt_no_msg, roundtrip);
2740
2741 let fail_job_object = ExecutionResultDescription::Fail {
2743 failure: FailureDescription::Abort {
2744 abort: AbortDescription::WindowsJobObject,
2745 },
2746 leaked: false,
2747 };
2748 let json = serde_json::to_string_pretty(&fail_job_object).unwrap();
2749 insta::assert_snapshot!("fail_windows_job_object", json);
2750 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2751 assert_eq!(fail_job_object, roundtrip);
2752
2753 let exec_fail = ExecutionResultDescription::ExecFail;
2755 let json = serde_json::to_string_pretty(&exec_fail).unwrap();
2756 insta::assert_snapshot!("exec_fail", json);
2757 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2758 assert_eq!(exec_fail, roundtrip);
2759
2760 let timeout_pass = ExecutionResultDescription::Timeout {
2762 result: SlowTimeoutResult::Pass,
2763 };
2764 let json = serde_json::to_string_pretty(&timeout_pass).unwrap();
2765 insta::assert_snapshot!("timeout_pass", json);
2766 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2767 assert_eq!(timeout_pass, roundtrip);
2768
2769 let timeout_fail = ExecutionResultDescription::Timeout {
2771 result: SlowTimeoutResult::Fail,
2772 };
2773 let json = serde_json::to_string_pretty(&timeout_fail).unwrap();
2774 insta::assert_snapshot!("timeout_fail", json);
2775 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2776 assert_eq!(timeout_fail, roundtrip);
2777 }
2778}