1use super::{FinalStatusLevel, StatusLevel, TestOutputDisplay};
10use crate::{
11 config::{
12 elements::{LeakTimeoutResult, SlowTimeoutResult},
13 scripts::ScriptId,
14 },
15 errors::{ChildError, ChildFdError, ChildStartError, ErrorList},
16 list::{OwnedTestInstanceId, TestInstanceId, TestList},
17 runner::{StressCondition, StressCount},
18 test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
19};
20use chrono::{DateTime, FixedOffset};
21use nextest_metadata::MismatchReason;
22use quick_junit::ReportUuid;
23use serde::{Deserialize, Serialize};
24use smol_str::SmolStr;
25use std::{
26 collections::BTreeMap, ffi::c_int, fmt, num::NonZero, process::ExitStatus, time::Duration,
27};
28
29pub const SIGTERM: c_int = 15;
34
35#[derive(Clone, Debug)]
37pub enum ReporterEvent<'a> {
38 Tick,
40
41 Test(Box<TestEvent<'a>>),
43}
44#[derive(Clone, Debug)]
49pub struct TestEvent<'a> {
50 pub timestamp: DateTime<FixedOffset>,
52
53 pub elapsed: Duration,
55
56 pub kind: TestEventKind<'a>,
58}
59
60#[derive(Clone, Debug)]
64pub enum TestEventKind<'a> {
65 RunStarted {
67 test_list: &'a TestList<'a>,
71
72 run_id: ReportUuid,
74
75 profile_name: String,
77
78 cli_args: Vec<String>,
80
81 stress_condition: Option<StressCondition>,
83 },
84
85 StressSubRunStarted {
87 progress: StressProgress,
89 },
90
91 SetupScriptStarted {
93 stress_index: Option<StressIndex>,
95
96 index: usize,
98
99 total: usize,
101
102 script_id: ScriptId,
104
105 program: String,
107
108 args: Vec<String>,
110
111 no_capture: bool,
113 },
114
115 SetupScriptSlow {
117 stress_index: Option<StressIndex>,
119
120 script_id: ScriptId,
122
123 program: String,
125
126 args: Vec<String>,
128
129 elapsed: Duration,
131
132 will_terminate: bool,
134 },
135
136 SetupScriptFinished {
138 stress_index: Option<StressIndex>,
140
141 index: usize,
143
144 total: usize,
146
147 script_id: ScriptId,
149
150 program: String,
152
153 args: Vec<String>,
155
156 junit_store_success_output: bool,
158
159 junit_store_failure_output: bool,
161
162 no_capture: bool,
164
165 run_status: SetupScriptExecuteStatus<ChildSingleOutput>,
167 },
168
169 TestStarted {
174 stress_index: Option<StressIndex>,
176
177 test_instance: TestInstanceId<'a>,
179
180 current_stats: RunStats,
182
183 running: usize,
185
186 command_line: Vec<String>,
188 },
189
190 TestSlow {
192 stress_index: Option<StressIndex>,
194
195 test_instance: TestInstanceId<'a>,
197
198 retry_data: RetryData,
200
201 elapsed: Duration,
203
204 will_terminate: bool,
206 },
207
208 TestAttemptFailedWillRetry {
212 stress_index: Option<StressIndex>,
214
215 test_instance: TestInstanceId<'a>,
217
218 run_status: ExecuteStatus<ChildSingleOutput>,
220
221 delay_before_next_attempt: Duration,
223
224 failure_output: TestOutputDisplay,
226
227 running: usize,
229 },
230
231 TestRetryStarted {
233 stress_index: Option<StressIndex>,
235
236 test_instance: TestInstanceId<'a>,
238
239 retry_data: RetryData,
241
242 running: usize,
244
245 command_line: Vec<String>,
247 },
248
249 TestFinished {
251 stress_index: Option<StressIndex>,
253
254 test_instance: TestInstanceId<'a>,
256
257 success_output: TestOutputDisplay,
259
260 failure_output: TestOutputDisplay,
262
263 junit_store_success_output: bool,
265
266 junit_store_failure_output: bool,
268
269 run_statuses: ExecutionStatuses<ChildSingleOutput>,
271
272 current_stats: RunStats,
274
275 running: usize,
277 },
278
279 TestSkipped {
281 stress_index: Option<StressIndex>,
283
284 test_instance: TestInstanceId<'a>,
286
287 reason: MismatchReason,
289 },
290
291 InfoStarted {
293 total: usize,
296
297 run_stats: RunStats,
299 },
300
301 InfoResponse {
303 index: usize,
305
306 total: usize,
308
309 response: InfoResponse<'a>,
311 },
312
313 InfoFinished {
315 missing: usize,
318 },
319
320 InputEnter {
323 current_stats: RunStats,
325
326 running: usize,
328 },
329
330 RunBeginCancel {
332 setup_scripts_running: usize,
334
335 current_stats: RunStats,
339
340 running: usize,
342 },
343
344 RunBeginKill {
346 setup_scripts_running: usize,
348
349 current_stats: RunStats,
353
354 running: usize,
356 },
357
358 RunPaused {
360 setup_scripts_running: usize,
362
363 running: usize,
365 },
366
367 RunContinued {
369 setup_scripts_running: usize,
371
372 running: usize,
374 },
375
376 StressSubRunFinished {
378 progress: StressProgress,
380
381 sub_elapsed: Duration,
383
384 sub_stats: RunStats,
386 },
387
388 RunFinished {
390 run_id: ReportUuid,
392
393 start_time: DateTime<FixedOffset>,
395
396 elapsed: Duration,
398
399 run_stats: RunFinishedStats,
401
402 outstanding_not_seen: Option<TestsNotSeen>,
407 },
408}
409
410#[derive(Clone, Debug)]
412pub struct TestsNotSeen {
413 pub not_seen: Vec<OwnedTestInstanceId>,
420
421 pub total_not_seen: usize,
423}
424
425#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
427#[serde(tag = "progress-type", rename_all = "kebab-case")]
428#[cfg_attr(test, derive(test_strategy::Arbitrary))]
429pub enum StressProgress {
430 Count {
432 total: StressCount,
434
435 elapsed: Duration,
437
438 completed: u32,
440 },
441
442 Time {
444 total: Duration,
446
447 elapsed: Duration,
449
450 completed: u32,
452 },
453}
454
455impl StressProgress {
456 pub fn remaining(&self) -> Option<StressRemaining> {
459 match self {
460 Self::Count {
461 total: StressCount::Count { count },
462 elapsed: _,
463 completed,
464 } => count
465 .get()
466 .checked_sub(*completed)
467 .and_then(|remaining| NonZero::try_from(remaining).ok())
468 .map(StressRemaining::Count),
469 Self::Count {
470 total: StressCount::Infinite,
471 ..
472 } => Some(StressRemaining::Infinite),
473 Self::Time {
474 total,
475 elapsed,
476 completed: _,
477 } => total.checked_sub(*elapsed).map(StressRemaining::Time),
478 }
479 }
480
481 pub fn unique_id(&self, run_id: ReportUuid) -> String {
483 let stress_current = match self {
484 Self::Count { completed, .. } | Self::Time { completed, .. } => *completed,
485 };
486 format!("{}:@stress-{}", run_id, stress_current)
487 }
488}
489
490#[derive(Clone, Debug)]
492pub enum StressRemaining {
493 Count(NonZero<u32>),
495
496 Infinite,
498
499 Time(Duration),
501}
502
503#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
505#[serde(rename_all = "kebab-case")]
506#[cfg_attr(test, derive(test_strategy::Arbitrary))]
507pub struct StressIndex {
508 pub current: u32,
510
511 pub total: Option<NonZero<u32>>,
513}
514
515#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
517#[serde(tag = "type", rename_all = "kebab-case")]
518#[cfg_attr(test, derive(test_strategy::Arbitrary))]
519pub enum RunFinishedStats {
520 Single(RunStats),
522
523 Stress(StressRunStats),
525}
526
527impl RunFinishedStats {
528 pub fn final_stats(&self) -> FinalRunStats {
531 match self {
532 Self::Single(stats) => stats.summarize_final(),
533 Self::Stress(stats) => stats.last_final_stats,
534 }
535 }
536}
537
538#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
540#[serde(rename_all = "kebab-case")]
541#[cfg_attr(test, derive(test_strategy::Arbitrary))]
542pub struct RunStats {
543 pub initial_run_count: usize,
547
548 pub finished_count: usize,
550
551 pub setup_scripts_initial_count: usize,
555
556 pub setup_scripts_finished_count: usize,
558
559 pub setup_scripts_passed: usize,
561
562 pub setup_scripts_failed: usize,
564
565 pub setup_scripts_exec_failed: usize,
567
568 pub setup_scripts_timed_out: usize,
570
571 pub passed: usize,
573
574 pub passed_slow: usize,
576
577 pub passed_timed_out: usize,
579
580 pub flaky: usize,
582
583 pub failed: usize,
585
586 pub failed_slow: usize,
588
589 pub failed_timed_out: usize,
591
592 pub leaky: usize,
594
595 pub leaky_failed: usize,
598
599 pub exec_failed: usize,
601
602 pub skipped: usize,
604
605 pub cancel_reason: Option<CancelReason>,
607}
608
609impl RunStats {
610 pub fn has_failures(&self) -> bool {
612 self.failed_setup_script_count() > 0 || self.failed_count() > 0
613 }
614
615 pub fn failed_setup_script_count(&self) -> usize {
617 self.setup_scripts_failed + self.setup_scripts_exec_failed + self.setup_scripts_timed_out
618 }
619
620 pub fn failed_count(&self) -> usize {
622 self.failed + self.exec_failed + self.failed_timed_out
623 }
624
625 pub fn summarize_final(&self) -> FinalRunStats {
627 if self.failed_setup_script_count() > 0 {
630 if self.cancel_reason > Some(CancelReason::TestFailure) {
633 FinalRunStats::Cancelled {
634 reason: self.cancel_reason,
635 kind: RunStatsFailureKind::SetupScript,
636 }
637 } else {
638 FinalRunStats::Failed {
639 kind: RunStatsFailureKind::SetupScript,
640 }
641 }
642 } else if self.setup_scripts_initial_count > self.setup_scripts_finished_count {
643 FinalRunStats::Cancelled {
644 reason: self.cancel_reason,
645 kind: RunStatsFailureKind::SetupScript,
646 }
647 } else if self.failed_count() > 0 {
648 let kind = RunStatsFailureKind::Test {
649 initial_run_count: self.initial_run_count,
650 not_run: self.initial_run_count.saturating_sub(self.finished_count),
651 };
652
653 if self.cancel_reason > Some(CancelReason::TestFailure) {
656 FinalRunStats::Cancelled {
657 reason: self.cancel_reason,
658 kind,
659 }
660 } else {
661 FinalRunStats::Failed { kind }
662 }
663 } else if self.initial_run_count > self.finished_count {
664 FinalRunStats::Cancelled {
665 reason: self.cancel_reason,
666 kind: RunStatsFailureKind::Test {
667 initial_run_count: self.initial_run_count,
668 not_run: self.initial_run_count.saturating_sub(self.finished_count),
669 },
670 }
671 } else if self.finished_count == 0 {
672 FinalRunStats::NoTestsRun
673 } else {
674 FinalRunStats::Success
675 }
676 }
677
678 pub(crate) fn on_setup_script_finished(
679 &mut self,
680 status: &SetupScriptExecuteStatus<ChildSingleOutput>,
681 ) {
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<ChildSingleOutput>) {
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(Clone, Debug, PartialEq, Eq, Serialize)]
875#[serde(rename_all = "kebab-case")]
876#[cfg_attr(
877 test,
878 derive(test_strategy::Arbitrary),
879 arbitrary(bound(O: proptest::arbitrary::Arbitrary + std::fmt::Debug + 'static))
880)]
881pub struct ExecutionStatuses<O> {
882 #[cfg_attr(test, strategy(proptest::collection::vec(proptest::arbitrary::any::<ExecuteStatus<O>>(), 1..=3)))]
884 statuses: Vec<ExecuteStatus<O>>,
885}
886
887impl<'de, O: Deserialize<'de>> Deserialize<'de> for ExecutionStatuses<O> {
888 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
889 #[derive(Deserialize)]
891 #[serde(rename_all = "kebab-case")]
892 struct Helper<O> {
893 statuses: Vec<ExecuteStatus<O>>,
894 }
895
896 let helper = Helper::<O>::deserialize(deserializer)?;
897 if helper.statuses.is_empty() {
898 return Err(serde::de::Error::custom("expected non-empty statuses"));
899 }
900 Ok(Self {
901 statuses: helper.statuses,
902 })
903 }
904}
905
906#[expect(clippy::len_without_is_empty)] impl<O> ExecutionStatuses<O> {
908 pub(crate) fn new(statuses: Vec<ExecuteStatus<O>>) -> Self {
909 debug_assert!(!statuses.is_empty(), "ExecutionStatuses must be non-empty");
910 Self { statuses }
911 }
912
913 pub fn last_status(&self) -> &ExecuteStatus<O> {
917 self.statuses
918 .last()
919 .expect("execution statuses is non-empty")
920 }
921
922 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus<O>> + '_ {
924 self.statuses.iter()
925 }
926
927 pub fn len(&self) -> usize {
929 self.statuses.len()
930 }
931
932 pub fn describe(&self) -> ExecutionDescription<'_, O> {
934 let last_status = self.last_status();
935 if last_status.result.is_success() {
936 if self.statuses.len() > 1 {
937 ExecutionDescription::Flaky {
938 last_status,
939 prior_statuses: &self.statuses[..self.statuses.len() - 1],
940 }
941 } else {
942 ExecutionDescription::Success {
943 single_status: last_status,
944 }
945 }
946 } else {
947 let first_status = self
948 .statuses
949 .first()
950 .expect("execution statuses is non-empty");
951 let retries = &self.statuses[1..];
952 ExecutionDescription::Failure {
953 first_status,
954 last_status,
955 retries,
956 }
957 }
958 }
959}
960
961impl<O> IntoIterator for ExecutionStatuses<O> {
962 type Item = ExecuteStatus<O>;
963 type IntoIter = std::vec::IntoIter<ExecuteStatus<O>>;
964
965 fn into_iter(self) -> Self::IntoIter {
966 self.statuses.into_iter()
967 }
968}
969
970#[derive(Debug)]
979pub enum ExecutionDescription<'a, O> {
980 Success {
982 single_status: &'a ExecuteStatus<O>,
984 },
985
986 Flaky {
988 last_status: &'a ExecuteStatus<O>,
990
991 prior_statuses: &'a [ExecuteStatus<O>],
993 },
994
995 Failure {
997 first_status: &'a ExecuteStatus<O>,
999
1000 last_status: &'a ExecuteStatus<O>,
1002
1003 retries: &'a [ExecuteStatus<O>],
1007 },
1008}
1009
1010impl<O> Clone for ExecutionDescription<'_, O> {
1013 fn clone(&self) -> Self {
1014 *self
1015 }
1016}
1017
1018impl<O> Copy for ExecutionDescription<'_, O> {}
1019
1020impl<'a, O> ExecutionDescription<'a, O> {
1021 pub fn status_level(&self) -> StatusLevel {
1023 match self {
1024 ExecutionDescription::Success { single_status } => match single_status.result {
1025 ExecutionResultDescription::Leak {
1026 result: LeakTimeoutResult::Pass,
1027 } => StatusLevel::Leak,
1028 ExecutionResultDescription::Pass => StatusLevel::Pass,
1029 ExecutionResultDescription::Timeout {
1030 result: SlowTimeoutResult::Pass,
1031 } => StatusLevel::Slow,
1032 ref other => unreachable!(
1033 "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1034 ),
1035 },
1036 ExecutionDescription::Flaky { .. } => StatusLevel::Retry,
1038 ExecutionDescription::Failure { .. } => StatusLevel::Fail,
1039 }
1040 }
1041
1042 pub fn final_status_level(&self) -> FinalStatusLevel {
1044 match self {
1045 ExecutionDescription::Success { single_status, .. } => {
1046 if single_status.is_slow {
1048 FinalStatusLevel::Slow
1049 } else {
1050 match single_status.result {
1051 ExecutionResultDescription::Pass => FinalStatusLevel::Pass,
1052 ExecutionResultDescription::Leak {
1053 result: LeakTimeoutResult::Pass,
1054 } => FinalStatusLevel::Leak,
1055 ExecutionResultDescription::Timeout {
1059 result: SlowTimeoutResult::Pass,
1060 } => FinalStatusLevel::Slow,
1061 ref other => unreachable!(
1062 "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1063 ),
1064 }
1065 }
1066 }
1067 ExecutionDescription::Flaky { .. } => FinalStatusLevel::Flaky,
1069 ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
1070 }
1071 }
1072
1073 pub fn last_status(&self) -> &'a ExecuteStatus<O> {
1075 match self {
1076 ExecutionDescription::Success {
1077 single_status: last_status,
1078 }
1079 | ExecutionDescription::Flaky { last_status, .. }
1080 | ExecutionDescription::Failure { last_status, .. } => last_status,
1081 }
1082 }
1083}
1084
1085#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1091#[serde(rename_all = "kebab-case")]
1092#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1093pub struct ErrorSummary {
1094 pub short_message: String,
1096
1097 pub description: String,
1099}
1100
1101#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1106#[serde(rename_all = "kebab-case")]
1107#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1108pub struct OutputErrorSlice {
1109 pub slice: String,
1111
1112 pub start: usize,
1114}
1115
1116#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1126#[serde(rename_all = "kebab-case")]
1127#[cfg_attr(
1128 test,
1129 derive(test_strategy::Arbitrary),
1130 arbitrary(bound(O: proptest::arbitrary::Arbitrary + std::fmt::Debug + 'static))
1131)]
1132pub struct ExecuteStatus<O> {
1133 pub retry_data: RetryData,
1135 pub output: ChildExecutionOutputDescription<O>,
1137 pub result: ExecutionResultDescription,
1139 #[cfg_attr(
1141 test,
1142 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1143 )]
1144 pub start_time: DateTime<FixedOffset>,
1145 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1147 pub time_taken: Duration,
1148 pub is_slow: bool,
1150 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1152 pub delay_before_start: Duration,
1153 pub error_summary: Option<ErrorSummary>,
1158 pub output_error_slice: Option<OutputErrorSlice>,
1163}
1164
1165#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1173#[serde(rename_all = "kebab-case")]
1174#[cfg_attr(
1175 test,
1176 derive(test_strategy::Arbitrary),
1177 arbitrary(bound(O: proptest::arbitrary::Arbitrary + std::fmt::Debug + 'static))
1178)]
1179pub struct SetupScriptExecuteStatus<O> {
1180 pub output: ChildExecutionOutputDescription<O>,
1182
1183 pub result: ExecutionResultDescription,
1185
1186 #[cfg_attr(
1188 test,
1189 strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1190 )]
1191 pub start_time: DateTime<FixedOffset>,
1192
1193 #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1195 pub time_taken: Duration,
1196
1197 pub is_slow: bool,
1199
1200 pub env_map: Option<SetupScriptEnvMap>,
1205
1206 pub error_summary: Option<ErrorSummary>,
1211}
1212
1213#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1217#[serde(rename_all = "kebab-case")]
1218#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1219pub struct SetupScriptEnvMap {
1220 pub env_map: BTreeMap<String, String>,
1222}
1223
1224#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1238#[serde(tag = "type", rename_all = "kebab-case")]
1239#[cfg_attr(
1240 test,
1241 derive(test_strategy::Arbitrary),
1242 arbitrary(bound(O: proptest::arbitrary::Arbitrary + std::fmt::Debug + 'static))
1243)]
1244pub enum ChildExecutionOutputDescription<O> {
1245 Output {
1247 result: Option<ExecutionResultDescription>,
1251
1252 output: ChildOutputDescription<O>,
1254
1255 errors: Option<ErrorList<ChildErrorDescription>>,
1258 },
1259
1260 StartError(ChildStartErrorDescription),
1262}
1263
1264impl<O> ChildExecutionOutputDescription<O> {
1265 pub fn has_errors(&self) -> bool {
1267 match self {
1268 Self::Output { errors, result, .. } => {
1269 if errors.is_some() {
1270 return true;
1271 }
1272 if let Some(result) = result {
1273 return !result.is_success();
1274 }
1275 false
1276 }
1277 Self::StartError(_) => true,
1278 }
1279 }
1280}
1281
1282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1288#[serde(tag = "kind", rename_all = "kebab-case")]
1289#[cfg_attr(
1290 test,
1291 derive(test_strategy::Arbitrary),
1292 arbitrary(bound(O: proptest::arbitrary::Arbitrary + std::fmt::Debug + 'static))
1293)]
1294pub enum ChildOutputDescription<O> {
1295 Split {
1297 stdout: Option<O>,
1299 stderr: Option<O>,
1301 },
1302
1303 Combined {
1305 output: O,
1307 },
1308}
1309
1310impl ChildOutputDescription<ChildSingleOutput> {
1311 pub fn stdout_stderr_len(&self) -> (Option<u64>, Option<u64>) {
1315 match self {
1316 Self::Split { stdout, stderr } => (
1317 stdout.as_ref().map(|s| s.buf.len() as u64),
1318 stderr.as_ref().map(|s| s.buf.len() as u64),
1319 ),
1320 Self::Combined { output } => (Some(output.buf.len() as u64), None),
1321 }
1322 }
1323}
1324
1325#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1329#[serde(tag = "kind", rename_all = "kebab-case")]
1330#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1331pub enum ChildStartErrorDescription {
1332 TempPath {
1334 source: IoErrorDescription,
1336 },
1337
1338 Spawn {
1340 source: IoErrorDescription,
1342 },
1343}
1344
1345impl fmt::Display for ChildStartErrorDescription {
1346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1347 match self {
1348 Self::TempPath { .. } => {
1349 write!(f, "error creating temporary path for setup script")
1350 }
1351 Self::Spawn { .. } => write!(f, "error spawning child process"),
1352 }
1353 }
1354}
1355
1356impl std::error::Error for ChildStartErrorDescription {
1357 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1358 match self {
1359 Self::TempPath { source } | Self::Spawn { source } => Some(source),
1360 }
1361 }
1362}
1363
1364#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1368#[serde(tag = "kind", rename_all = "kebab-case")]
1369#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1370pub enum ChildErrorDescription {
1371 ReadStdout {
1373 source: IoErrorDescription,
1375 },
1376
1377 ReadStderr {
1379 source: IoErrorDescription,
1381 },
1382
1383 ReadCombined {
1385 source: IoErrorDescription,
1387 },
1388
1389 Wait {
1391 source: IoErrorDescription,
1393 },
1394
1395 SetupScriptOutput {
1397 source: IoErrorDescription,
1399 },
1400}
1401
1402impl fmt::Display for ChildErrorDescription {
1403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1404 match self {
1405 Self::ReadStdout { .. } => write!(f, "error reading standard output"),
1406 Self::ReadStderr { .. } => write!(f, "error reading standard error"),
1407 Self::ReadCombined { .. } => {
1408 write!(f, "error reading combined stream")
1409 }
1410 Self::Wait { .. } => {
1411 write!(f, "error waiting for child process to exit")
1412 }
1413 Self::SetupScriptOutput { .. } => {
1414 write!(f, "error reading setup script output")
1415 }
1416 }
1417 }
1418}
1419
1420impl std::error::Error for ChildErrorDescription {
1421 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1422 match self {
1423 Self::ReadStdout { source }
1424 | Self::ReadStderr { source }
1425 | Self::ReadCombined { source }
1426 | Self::Wait { source }
1427 | Self::SetupScriptOutput { source } => Some(source),
1428 }
1429 }
1430}
1431
1432#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1436#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1437pub struct IoErrorDescription {
1438 message: String,
1439}
1440
1441impl fmt::Display for IoErrorDescription {
1442 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1443 write!(f, "{}", self.message)
1444 }
1445}
1446
1447impl std::error::Error for IoErrorDescription {}
1448
1449impl From<ChildExecutionOutput> for ChildExecutionOutputDescription<ChildSingleOutput> {
1450 fn from(output: ChildExecutionOutput) -> Self {
1451 match output {
1452 ChildExecutionOutput::Output {
1453 result,
1454 output,
1455 errors,
1456 } => Self::Output {
1457 result: result.map(ExecutionResultDescription::from),
1458 output: ChildOutputDescription::from(output),
1459 errors: errors.map(|e| e.map(ChildErrorDescription::from)),
1460 },
1461 ChildExecutionOutput::StartError(error) => {
1462 Self::StartError(ChildStartErrorDescription::from(error))
1463 }
1464 }
1465 }
1466}
1467
1468impl From<ChildOutput> for ChildOutputDescription<ChildSingleOutput> {
1469 fn from(output: ChildOutput) -> Self {
1470 match output {
1471 ChildOutput::Split(split) => Self::Split {
1472 stdout: split.stdout,
1473 stderr: split.stderr,
1474 },
1475 ChildOutput::Combined { output } => Self::Combined { output },
1476 }
1477 }
1478}
1479
1480impl From<ChildStartError> for ChildStartErrorDescription {
1481 fn from(error: ChildStartError) -> Self {
1482 match error {
1483 ChildStartError::TempPath(e) => Self::TempPath {
1484 source: IoErrorDescription {
1485 message: e.to_string(),
1486 },
1487 },
1488 ChildStartError::Spawn(e) => Self::Spawn {
1489 source: IoErrorDescription {
1490 message: e.to_string(),
1491 },
1492 },
1493 }
1494 }
1495}
1496
1497impl From<ChildError> for ChildErrorDescription {
1498 fn from(error: ChildError) -> Self {
1499 match error {
1500 ChildError::Fd(ChildFdError::ReadStdout(e)) => Self::ReadStdout {
1501 source: IoErrorDescription {
1502 message: e.to_string(),
1503 },
1504 },
1505 ChildError::Fd(ChildFdError::ReadStderr(e)) => Self::ReadStderr {
1506 source: IoErrorDescription {
1507 message: e.to_string(),
1508 },
1509 },
1510 ChildError::Fd(ChildFdError::ReadCombined(e)) => Self::ReadCombined {
1511 source: IoErrorDescription {
1512 message: e.to_string(),
1513 },
1514 },
1515 ChildError::Fd(ChildFdError::Wait(e)) => Self::Wait {
1516 source: IoErrorDescription {
1517 message: e.to_string(),
1518 },
1519 },
1520 ChildError::SetupScriptOutput(e) => Self::SetupScriptOutput {
1521 source: IoErrorDescription {
1522 message: e.to_string(),
1523 },
1524 },
1525 }
1526 }
1527}
1528
1529#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
1531#[serde(rename_all = "kebab-case")]
1532#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1533pub struct RetryData {
1534 pub attempt: u32,
1536
1537 pub total_attempts: u32,
1539}
1540
1541impl RetryData {
1542 pub fn is_last_attempt(&self) -> bool {
1544 self.attempt >= self.total_attempts
1545 }
1546}
1547
1548#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1550pub enum ExecutionResult {
1551 Pass,
1553 Leak {
1557 result: LeakTimeoutResult,
1564 },
1565 Fail {
1567 failure_status: FailureStatus,
1569
1570 leaked: bool,
1574 },
1575 ExecFail,
1577 Timeout {
1579 result: SlowTimeoutResult,
1581 },
1582}
1583
1584impl ExecutionResult {
1585 pub fn is_success(self) -> bool {
1587 match self {
1588 ExecutionResult::Pass
1589 | ExecutionResult::Timeout {
1590 result: SlowTimeoutResult::Pass,
1591 }
1592 | ExecutionResult::Leak {
1593 result: LeakTimeoutResult::Pass,
1594 } => true,
1595 ExecutionResult::Leak {
1596 result: LeakTimeoutResult::Fail,
1597 }
1598 | ExecutionResult::Fail { .. }
1599 | ExecutionResult::ExecFail
1600 | ExecutionResult::Timeout {
1601 result: SlowTimeoutResult::Fail,
1602 } => false,
1603 }
1604 }
1605
1606 pub fn as_static_str(&self) -> &'static str {
1608 match self {
1609 ExecutionResult::Pass => "pass",
1610 ExecutionResult::Leak { .. } => "leak",
1611 ExecutionResult::Fail { .. } => "fail",
1612 ExecutionResult::ExecFail => "exec-fail",
1613 ExecutionResult::Timeout { .. } => "timeout",
1614 }
1615 }
1616}
1617
1618#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1620pub enum FailureStatus {
1621 ExitCode(i32),
1623
1624 Abort(AbortStatus),
1626}
1627
1628impl FailureStatus {
1629 pub fn extract(exit_status: ExitStatus) -> Self {
1631 if let Some(abort_status) = AbortStatus::extract(exit_status) {
1632 FailureStatus::Abort(abort_status)
1633 } else {
1634 FailureStatus::ExitCode(
1635 exit_status
1636 .code()
1637 .expect("if abort_status is None, then code must be present"),
1638 )
1639 }
1640 }
1641}
1642
1643#[derive(Copy, Clone, Eq, PartialEq)]
1647pub enum AbortStatus {
1648 #[cfg(unix)]
1650 UnixSignal(i32),
1651
1652 #[cfg(windows)]
1654 WindowsNtStatus(windows_sys::Win32::Foundation::NTSTATUS),
1655
1656 #[cfg(windows)]
1658 JobObject,
1659}
1660
1661impl AbortStatus {
1662 pub fn extract(exit_status: ExitStatus) -> Option<Self> {
1664 cfg_if::cfg_if! {
1665 if #[cfg(unix)] {
1666 use std::os::unix::process::ExitStatusExt;
1668 exit_status.signal().map(AbortStatus::UnixSignal)
1669 } else if #[cfg(windows)] {
1670 exit_status.code().and_then(|code| {
1671 (code < 0).then_some(AbortStatus::WindowsNtStatus(code))
1672 })
1673 } else {
1674 None
1675 }
1676 }
1677 }
1678}
1679
1680impl fmt::Debug for AbortStatus {
1681 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1682 match self {
1683 #[cfg(unix)]
1684 AbortStatus::UnixSignal(signal) => write!(f, "UnixSignal({signal})"),
1685 #[cfg(windows)]
1686 AbortStatus::WindowsNtStatus(status) => write!(f, "WindowsNtStatus({status:x})"),
1687 #[cfg(windows)]
1688 AbortStatus::JobObject => write!(f, "JobObject"),
1689 }
1690 }
1691}
1692
1693#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1699#[serde(tag = "kind", rename_all = "kebab-case")]
1700#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1701#[non_exhaustive]
1702pub enum AbortDescription {
1703 UnixSignal {
1705 signal: i32,
1707 #[cfg_attr(
1710 test,
1711 strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
1712 )]
1713 name: Option<SmolStr>,
1714 },
1715
1716 WindowsNtStatus {
1718 code: i32,
1720 #[cfg_attr(
1722 test,
1723 strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
1724 )]
1725 message: Option<SmolStr>,
1726 },
1727
1728 WindowsJobObject,
1730}
1731
1732impl fmt::Display for AbortDescription {
1733 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1734 match self {
1735 Self::UnixSignal { signal, name } => {
1736 write!(f, "aborted with signal {signal}")?;
1737 if let Some(name) = name {
1738 write!(f, " (SIG{name})")?;
1739 }
1740 Ok(())
1741 }
1742 Self::WindowsNtStatus { code, message } => {
1743 write!(f, "aborted with code {code:#010x}")?;
1744 if let Some(message) = message {
1745 write!(f, ": {message}")?;
1746 }
1747 Ok(())
1748 }
1749 Self::WindowsJobObject => {
1750 write!(f, "terminated via job object")
1751 }
1752 }
1753 }
1754}
1755
1756impl From<AbortStatus> for AbortDescription {
1757 fn from(status: AbortStatus) -> Self {
1758 cfg_if::cfg_if! {
1759 if #[cfg(unix)] {
1760 match status {
1761 AbortStatus::UnixSignal(signal) => Self::UnixSignal {
1762 signal,
1763 name: crate::helpers::signal_str(signal).map(SmolStr::new_static),
1764 },
1765 }
1766 } else if #[cfg(windows)] {
1767 match status {
1768 AbortStatus::WindowsNtStatus(code) => Self::WindowsNtStatus {
1769 code,
1770 message: crate::helpers::windows_nt_status_message(code),
1771 },
1772 AbortStatus::JobObject => Self::WindowsJobObject,
1773 }
1774 } else {
1775 match status {}
1776 }
1777 }
1778 }
1779}
1780
1781#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1785#[serde(tag = "kind", rename_all = "kebab-case")]
1786#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1787#[non_exhaustive]
1788pub enum FailureDescription {
1789 ExitCode {
1791 code: i32,
1793 },
1794
1795 Abort {
1802 abort: AbortDescription,
1804 },
1805}
1806
1807impl From<FailureStatus> for FailureDescription {
1808 fn from(status: FailureStatus) -> Self {
1809 match status {
1810 FailureStatus::ExitCode(code) => Self::ExitCode { code },
1811 FailureStatus::Abort(abort) => Self::Abort {
1812 abort: AbortDescription::from(abort),
1813 },
1814 }
1815 }
1816}
1817
1818impl fmt::Display for FailureDescription {
1819 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1820 match self {
1821 Self::ExitCode { code } => write!(f, "exited with code {code}"),
1822 Self::Abort { abort } => write!(f, "{abort}"),
1823 }
1824 }
1825}
1826
1827#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1832#[serde(tag = "status", rename_all = "kebab-case")]
1833#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1834#[non_exhaustive]
1835pub enum ExecutionResultDescription {
1836 Pass,
1838
1839 Leak {
1841 result: LeakTimeoutResult,
1843 },
1844
1845 Fail {
1847 failure: FailureDescription,
1849
1850 leaked: bool,
1852 },
1853
1854 ExecFail,
1856
1857 Timeout {
1859 result: SlowTimeoutResult,
1861 },
1862}
1863
1864impl ExecutionResultDescription {
1865 pub fn is_success(&self) -> bool {
1867 match self {
1868 Self::Pass
1869 | Self::Timeout {
1870 result: SlowTimeoutResult::Pass,
1871 }
1872 | Self::Leak {
1873 result: LeakTimeoutResult::Pass,
1874 } => true,
1875 Self::Leak {
1876 result: LeakTimeoutResult::Fail,
1877 }
1878 | Self::Fail { .. }
1879 | Self::ExecFail
1880 | Self::Timeout {
1881 result: SlowTimeoutResult::Fail,
1882 } => false,
1883 }
1884 }
1885
1886 pub fn as_static_str(&self) -> &'static str {
1888 match self {
1889 Self::Pass => "pass",
1890 Self::Leak { .. } => "leak",
1891 Self::Fail { .. } => "fail",
1892 Self::ExecFail => "exec-fail",
1893 Self::Timeout { .. } => "timeout",
1894 }
1895 }
1896
1897 pub fn is_termination_failure(&self) -> bool {
1909 matches!(
1910 self,
1911 Self::Fail {
1912 failure: FailureDescription::Abort {
1913 abort: AbortDescription::UnixSignal {
1914 signal: SIGTERM,
1915 ..
1916 },
1917 },
1918 ..
1919 } | Self::Fail {
1920 failure: FailureDescription::Abort {
1921 abort: AbortDescription::WindowsJobObject,
1922 },
1923 ..
1924 }
1925 )
1926 }
1927}
1928
1929impl From<ExecutionResult> for ExecutionResultDescription {
1930 fn from(result: ExecutionResult) -> Self {
1931 match result {
1932 ExecutionResult::Pass => Self::Pass,
1933 ExecutionResult::Leak { result } => Self::Leak { result },
1934 ExecutionResult::Fail {
1935 failure_status,
1936 leaked,
1937 } => Self::Fail {
1938 failure: FailureDescription::from(failure_status),
1939 leaked,
1940 },
1941 ExecutionResult::ExecFail => Self::ExecFail,
1942 ExecutionResult::Timeout { result } => Self::Timeout { result },
1943 }
1944 }
1945}
1946
1947#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
1950#[serde(rename_all = "kebab-case")]
1951#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1952pub enum CancelReason {
1953 SetupScriptFailure,
1955
1956 TestFailure,
1958
1959 ReportError,
1961
1962 GlobalTimeout,
1964
1965 TestFailureImmediate,
1967
1968 Signal,
1970
1971 Interrupt,
1973
1974 SecondSignal,
1976}
1977
1978impl CancelReason {
1979 pub(crate) fn to_static_str(self) -> &'static str {
1980 match self {
1981 CancelReason::SetupScriptFailure => "setup script failure",
1982 CancelReason::TestFailure => "test failure",
1983 CancelReason::ReportError => "reporting error",
1984 CancelReason::GlobalTimeout => "global timeout",
1985 CancelReason::TestFailureImmediate => "test failure",
1986 CancelReason::Signal => "signal",
1987 CancelReason::Interrupt => "interrupt",
1988 CancelReason::SecondSignal => "second signal",
1989 }
1990 }
1991}
1992#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1994pub enum UnitKind {
1995 Test,
1997
1998 Script,
2000}
2001
2002impl UnitKind {
2003 pub(crate) const WAITING_ON_TEST_MESSAGE: &str = "waiting on test process";
2004 pub(crate) const WAITING_ON_SCRIPT_MESSAGE: &str = "waiting on script process";
2005
2006 pub(crate) const EXECUTING_TEST_MESSAGE: &str = "executing test";
2007 pub(crate) const EXECUTING_SCRIPT_MESSAGE: &str = "executing script";
2008
2009 pub(crate) fn waiting_on_message(&self) -> &'static str {
2010 match self {
2011 UnitKind::Test => Self::WAITING_ON_TEST_MESSAGE,
2012 UnitKind::Script => Self::WAITING_ON_SCRIPT_MESSAGE,
2013 }
2014 }
2015
2016 pub(crate) fn executing_message(&self) -> &'static str {
2017 match self {
2018 UnitKind::Test => Self::EXECUTING_TEST_MESSAGE,
2019 UnitKind::Script => Self::EXECUTING_SCRIPT_MESSAGE,
2020 }
2021 }
2022}
2023
2024#[derive(Clone, Debug)]
2026pub enum InfoResponse<'a> {
2027 SetupScript(SetupScriptInfoResponse),
2029
2030 Test(TestInfoResponse<'a>),
2032}
2033
2034#[derive(Clone, Debug)]
2036pub struct SetupScriptInfoResponse {
2037 pub stress_index: Option<StressIndex>,
2039
2040 pub script_id: ScriptId,
2042
2043 pub program: String,
2045
2046 pub args: Vec<String>,
2048
2049 pub state: UnitState,
2051
2052 pub output: ChildExecutionOutputDescription<ChildSingleOutput>,
2054}
2055
2056#[derive(Clone, Debug)]
2058pub struct TestInfoResponse<'a> {
2059 pub stress_index: Option<StressIndex>,
2061
2062 pub test_instance: TestInstanceId<'a>,
2064
2065 pub retry_data: RetryData,
2067
2068 pub state: UnitState,
2070
2071 pub output: ChildExecutionOutputDescription<ChildSingleOutput>,
2073}
2074
2075#[derive(Clone, Debug)]
2080pub enum UnitState {
2081 Running {
2083 pid: u32,
2085
2086 time_taken: Duration,
2088
2089 slow_after: Option<Duration>,
2092 },
2093
2094 Exiting {
2097 pid: u32,
2099
2100 time_taken: Duration,
2102
2103 slow_after: Option<Duration>,
2106
2107 tentative_result: Option<ExecutionResult>,
2112
2113 waiting_duration: Duration,
2115
2116 remaining: Duration,
2118 },
2119
2120 Terminating(UnitTerminatingState),
2122
2123 Exited {
2125 result: ExecutionResult,
2127
2128 time_taken: Duration,
2130
2131 slow_after: Option<Duration>,
2134 },
2135
2136 DelayBeforeNextAttempt {
2139 previous_result: ExecutionResult,
2141
2142 previous_slow: bool,
2144
2145 waiting_duration: Duration,
2147
2148 remaining: Duration,
2150 },
2151}
2152
2153impl UnitState {
2154 pub fn has_valid_output(&self) -> bool {
2156 match self {
2157 UnitState::Running { .. }
2158 | UnitState::Exiting { .. }
2159 | UnitState::Terminating(_)
2160 | UnitState::Exited { .. } => true,
2161 UnitState::DelayBeforeNextAttempt { .. } => false,
2162 }
2163 }
2164}
2165
2166#[derive(Clone, Debug)]
2170pub struct UnitTerminatingState {
2171 pub pid: u32,
2173
2174 pub time_taken: Duration,
2176
2177 pub reason: UnitTerminateReason,
2179
2180 pub method: UnitTerminateMethod,
2182
2183 pub waiting_duration: Duration,
2185
2186 pub remaining: Duration,
2188}
2189
2190#[derive(Clone, Copy, Debug)]
2194pub enum UnitTerminateReason {
2195 Timeout,
2197
2198 Signal,
2200
2201 Interrupt,
2203}
2204
2205impl fmt::Display for UnitTerminateReason {
2206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2207 match self {
2208 UnitTerminateReason::Timeout => write!(f, "timeout"),
2209 UnitTerminateReason::Signal => write!(f, "signal"),
2210 UnitTerminateReason::Interrupt => write!(f, "interrupt"),
2211 }
2212 }
2213}
2214
2215#[derive(Clone, Copy, Debug)]
2217pub enum UnitTerminateMethod {
2218 #[cfg(unix)]
2220 Signal(UnitTerminateSignal),
2221
2222 #[cfg(windows)]
2224 JobObject,
2225
2226 #[cfg(windows)]
2234 Wait,
2235
2236 #[cfg(test)]
2238 Fake,
2239}
2240
2241#[cfg(unix)]
2242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2244pub enum UnitTerminateSignal {
2245 Interrupt,
2247
2248 Term,
2250
2251 Hangup,
2253
2254 Quit,
2256
2257 Kill,
2259}
2260
2261#[cfg(unix)]
2262impl fmt::Display for UnitTerminateSignal {
2263 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2264 match self {
2265 UnitTerminateSignal::Interrupt => write!(f, "SIGINT"),
2266 UnitTerminateSignal::Term => write!(f, "SIGTERM"),
2267 UnitTerminateSignal::Hangup => write!(f, "SIGHUP"),
2268 UnitTerminateSignal::Quit => write!(f, "SIGQUIT"),
2269 UnitTerminateSignal::Kill => write!(f, "SIGKILL"),
2270 }
2271 }
2272}
2273
2274#[cfg(test)]
2275mod tests {
2276 use super::*;
2277
2278 #[test]
2279 fn test_is_success() {
2280 assert_eq!(
2281 RunStats::default().summarize_final(),
2282 FinalRunStats::NoTestsRun,
2283 "empty run => no tests run"
2284 );
2285 assert_eq!(
2286 RunStats {
2287 initial_run_count: 42,
2288 finished_count: 42,
2289 ..RunStats::default()
2290 }
2291 .summarize_final(),
2292 FinalRunStats::Success,
2293 "initial run count = final run count => success"
2294 );
2295 assert_eq!(
2296 RunStats {
2297 initial_run_count: 42,
2298 finished_count: 41,
2299 ..RunStats::default()
2300 }
2301 .summarize_final(),
2302 FinalRunStats::Cancelled {
2303 reason: None,
2304 kind: RunStatsFailureKind::Test {
2305 initial_run_count: 42,
2306 not_run: 1
2307 }
2308 },
2309 "initial run count > final run count => cancelled"
2310 );
2311 assert_eq!(
2312 RunStats {
2313 initial_run_count: 42,
2314 finished_count: 42,
2315 failed: 1,
2316 ..RunStats::default()
2317 }
2318 .summarize_final(),
2319 FinalRunStats::Failed {
2320 kind: RunStatsFailureKind::Test {
2321 initial_run_count: 42,
2322 not_run: 0,
2323 },
2324 },
2325 "failed => failure"
2326 );
2327 assert_eq!(
2328 RunStats {
2329 initial_run_count: 42,
2330 finished_count: 42,
2331 exec_failed: 1,
2332 ..RunStats::default()
2333 }
2334 .summarize_final(),
2335 FinalRunStats::Failed {
2336 kind: RunStatsFailureKind::Test {
2337 initial_run_count: 42,
2338 not_run: 0,
2339 },
2340 },
2341 "exec failed => failure"
2342 );
2343 assert_eq!(
2344 RunStats {
2345 initial_run_count: 42,
2346 finished_count: 42,
2347 failed_timed_out: 1,
2348 ..RunStats::default()
2349 }
2350 .summarize_final(),
2351 FinalRunStats::Failed {
2352 kind: RunStatsFailureKind::Test {
2353 initial_run_count: 42,
2354 not_run: 0,
2355 },
2356 },
2357 "timed out => failure {:?} {:?}",
2358 RunStats {
2359 initial_run_count: 42,
2360 finished_count: 42,
2361 failed_timed_out: 1,
2362 ..RunStats::default()
2363 }
2364 .summarize_final(),
2365 FinalRunStats::Failed {
2366 kind: RunStatsFailureKind::Test {
2367 initial_run_count: 42,
2368 not_run: 0,
2369 },
2370 },
2371 );
2372 assert_eq!(
2373 RunStats {
2374 initial_run_count: 42,
2375 finished_count: 42,
2376 skipped: 1,
2377 ..RunStats::default()
2378 }
2379 .summarize_final(),
2380 FinalRunStats::Success,
2381 "skipped => not considered a failure"
2382 );
2383
2384 assert_eq!(
2385 RunStats {
2386 setup_scripts_initial_count: 2,
2387 setup_scripts_finished_count: 1,
2388 ..RunStats::default()
2389 }
2390 .summarize_final(),
2391 FinalRunStats::Cancelled {
2392 reason: None,
2393 kind: RunStatsFailureKind::SetupScript,
2394 },
2395 "setup script failed => failure"
2396 );
2397
2398 assert_eq!(
2399 RunStats {
2400 setup_scripts_initial_count: 2,
2401 setup_scripts_finished_count: 2,
2402 setup_scripts_failed: 1,
2403 ..RunStats::default()
2404 }
2405 .summarize_final(),
2406 FinalRunStats::Failed {
2407 kind: RunStatsFailureKind::SetupScript,
2408 },
2409 "setup script failed => failure"
2410 );
2411 assert_eq!(
2412 RunStats {
2413 setup_scripts_initial_count: 2,
2414 setup_scripts_finished_count: 2,
2415 setup_scripts_exec_failed: 1,
2416 ..RunStats::default()
2417 }
2418 .summarize_final(),
2419 FinalRunStats::Failed {
2420 kind: RunStatsFailureKind::SetupScript,
2421 },
2422 "setup script exec failed => failure"
2423 );
2424 assert_eq!(
2425 RunStats {
2426 setup_scripts_initial_count: 2,
2427 setup_scripts_finished_count: 2,
2428 setup_scripts_timed_out: 1,
2429 ..RunStats::default()
2430 }
2431 .summarize_final(),
2432 FinalRunStats::Failed {
2433 kind: RunStatsFailureKind::SetupScript,
2434 },
2435 "setup script timed out => failure"
2436 );
2437 assert_eq!(
2438 RunStats {
2439 setup_scripts_initial_count: 2,
2440 setup_scripts_finished_count: 2,
2441 setup_scripts_passed: 2,
2442 ..RunStats::default()
2443 }
2444 .summarize_final(),
2445 FinalRunStats::NoTestsRun,
2446 "setup scripts passed => success, but no tests run"
2447 );
2448 }
2449
2450 #[test]
2451 fn abort_description_serialization() {
2452 let unix_with_name = AbortDescription::UnixSignal {
2454 signal: 15,
2455 name: Some("TERM".into()),
2456 };
2457 let json = serde_json::to_string_pretty(&unix_with_name).unwrap();
2458 insta::assert_snapshot!("abort_unix_signal_with_name", json);
2459 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2460 assert_eq!(unix_with_name, roundtrip);
2461
2462 let unix_no_name = AbortDescription::UnixSignal {
2464 signal: 42,
2465 name: None,
2466 };
2467 let json = serde_json::to_string_pretty(&unix_no_name).unwrap();
2468 insta::assert_snapshot!("abort_unix_signal_no_name", json);
2469 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2470 assert_eq!(unix_no_name, roundtrip);
2471
2472 let windows_nt = AbortDescription::WindowsNtStatus {
2474 code: -1073741510_i32,
2475 message: Some("The application terminated as a result of a CTRL+C.".into()),
2476 };
2477 let json = serde_json::to_string_pretty(&windows_nt).unwrap();
2478 insta::assert_snapshot!("abort_windows_nt_status", json);
2479 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2480 assert_eq!(windows_nt, roundtrip);
2481
2482 let windows_nt_no_msg = AbortDescription::WindowsNtStatus {
2484 code: -1073741819_i32,
2485 message: None,
2486 };
2487 let json = serde_json::to_string_pretty(&windows_nt_no_msg).unwrap();
2488 insta::assert_snapshot!("abort_windows_nt_status_no_message", json);
2489 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2490 assert_eq!(windows_nt_no_msg, roundtrip);
2491
2492 let job = AbortDescription::WindowsJobObject;
2494 let json = serde_json::to_string_pretty(&job).unwrap();
2495 insta::assert_snapshot!("abort_windows_job_object", json);
2496 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2497 assert_eq!(job, roundtrip);
2498 }
2499
2500 #[test]
2501 fn abort_description_cross_platform_deserialization() {
2502 let unix_json = r#"{"kind":"unix-signal","signal":11,"name":"SEGV"}"#;
2505 let unix_desc: AbortDescription = serde_json::from_str(unix_json).unwrap();
2506 assert_eq!(
2507 unix_desc,
2508 AbortDescription::UnixSignal {
2509 signal: 11,
2510 name: Some("SEGV".into()),
2511 }
2512 );
2513
2514 let windows_json = r#"{"kind":"windows-nt-status","code":-1073741510,"message":"CTRL+C"}"#;
2515 let windows_desc: AbortDescription = serde_json::from_str(windows_json).unwrap();
2516 assert_eq!(
2517 windows_desc,
2518 AbortDescription::WindowsNtStatus {
2519 code: -1073741510,
2520 message: Some("CTRL+C".into()),
2521 }
2522 );
2523
2524 let job_json = r#"{"kind":"windows-job-object"}"#;
2525 let job_desc: AbortDescription = serde_json::from_str(job_json).unwrap();
2526 assert_eq!(job_desc, AbortDescription::WindowsJobObject);
2527 }
2528
2529 #[test]
2530 fn abort_description_display() {
2531 let unix = AbortDescription::UnixSignal {
2533 signal: 15,
2534 name: Some("TERM".into()),
2535 };
2536 assert_eq!(unix.to_string(), "aborted with signal 15 (SIGTERM)");
2537
2538 let unix_no_name = AbortDescription::UnixSignal {
2540 signal: 42,
2541 name: None,
2542 };
2543 assert_eq!(unix_no_name.to_string(), "aborted with signal 42");
2544
2545 let windows = AbortDescription::WindowsNtStatus {
2547 code: -1073741510,
2548 message: Some("CTRL+C exit".into()),
2549 };
2550 assert_eq!(
2551 windows.to_string(),
2552 "aborted with code 0xc000013a: CTRL+C exit"
2553 );
2554
2555 let windows_no_msg = AbortDescription::WindowsNtStatus {
2557 code: -1073741510,
2558 message: None,
2559 };
2560 assert_eq!(windows_no_msg.to_string(), "aborted with code 0xc000013a");
2561
2562 let job = AbortDescription::WindowsJobObject;
2564 assert_eq!(job.to_string(), "terminated via job object");
2565 }
2566
2567 #[cfg(unix)]
2568 #[test]
2569 fn abort_description_from_abort_status() {
2570 let status = AbortStatus::UnixSignal(15);
2572 let description = AbortDescription::from(status);
2573
2574 assert_eq!(
2575 description,
2576 AbortDescription::UnixSignal {
2577 signal: 15,
2578 name: Some("TERM".into()),
2579 }
2580 );
2581
2582 let unknown_status = AbortStatus::UnixSignal(42);
2584 let unknown_description = AbortDescription::from(unknown_status);
2585 assert_eq!(
2586 unknown_description,
2587 AbortDescription::UnixSignal {
2588 signal: 42,
2589 name: None,
2590 }
2591 );
2592 }
2593
2594 #[test]
2595 fn execution_result_description_serialization() {
2596 let pass = ExecutionResultDescription::Pass;
2600 let json = serde_json::to_string_pretty(&pass).unwrap();
2601 insta::assert_snapshot!("pass", json);
2602 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2603 assert_eq!(pass, roundtrip);
2604
2605 let leak_pass = ExecutionResultDescription::Leak {
2607 result: LeakTimeoutResult::Pass,
2608 };
2609 let json = serde_json::to_string_pretty(&leak_pass).unwrap();
2610 insta::assert_snapshot!("leak_pass", json);
2611 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2612 assert_eq!(leak_pass, roundtrip);
2613
2614 let leak_fail = ExecutionResultDescription::Leak {
2616 result: LeakTimeoutResult::Fail,
2617 };
2618 let json = serde_json::to_string_pretty(&leak_fail).unwrap();
2619 insta::assert_snapshot!("leak_fail", json);
2620 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2621 assert_eq!(leak_fail, roundtrip);
2622
2623 let fail_exit_code = ExecutionResultDescription::Fail {
2625 failure: FailureDescription::ExitCode { code: 101 },
2626 leaked: false,
2627 };
2628 let json = serde_json::to_string_pretty(&fail_exit_code).unwrap();
2629 insta::assert_snapshot!("fail_exit_code", json);
2630 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2631 assert_eq!(fail_exit_code, roundtrip);
2632
2633 let fail_exit_code_leaked = ExecutionResultDescription::Fail {
2635 failure: FailureDescription::ExitCode { code: 1 },
2636 leaked: true,
2637 };
2638 let json = serde_json::to_string_pretty(&fail_exit_code_leaked).unwrap();
2639 insta::assert_snapshot!("fail_exit_code_leaked", json);
2640 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2641 assert_eq!(fail_exit_code_leaked, roundtrip);
2642
2643 let fail_unix_signal = ExecutionResultDescription::Fail {
2645 failure: FailureDescription::Abort {
2646 abort: AbortDescription::UnixSignal {
2647 signal: 11,
2648 name: Some("SEGV".into()),
2649 },
2650 },
2651 leaked: false,
2652 };
2653 let json = serde_json::to_string_pretty(&fail_unix_signal).unwrap();
2654 insta::assert_snapshot!("fail_unix_signal", json);
2655 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2656 assert_eq!(fail_unix_signal, roundtrip);
2657
2658 let fail_unix_signal_unknown = ExecutionResultDescription::Fail {
2660 failure: FailureDescription::Abort {
2661 abort: AbortDescription::UnixSignal {
2662 signal: 42,
2663 name: None,
2664 },
2665 },
2666 leaked: true,
2667 };
2668 let json = serde_json::to_string_pretty(&fail_unix_signal_unknown).unwrap();
2669 insta::assert_snapshot!("fail_unix_signal_unknown_leaked", json);
2670 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2671 assert_eq!(fail_unix_signal_unknown, roundtrip);
2672
2673 let fail_windows_nt = ExecutionResultDescription::Fail {
2675 failure: FailureDescription::Abort {
2676 abort: AbortDescription::WindowsNtStatus {
2677 code: -1073741510,
2678 message: Some("The application terminated as a result of a CTRL+C.".into()),
2679 },
2680 },
2681 leaked: false,
2682 };
2683 let json = serde_json::to_string_pretty(&fail_windows_nt).unwrap();
2684 insta::assert_snapshot!("fail_windows_nt_status", json);
2685 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2686 assert_eq!(fail_windows_nt, roundtrip);
2687
2688 let fail_windows_nt_no_msg = ExecutionResultDescription::Fail {
2690 failure: FailureDescription::Abort {
2691 abort: AbortDescription::WindowsNtStatus {
2692 code: -1073741819,
2693 message: None,
2694 },
2695 },
2696 leaked: false,
2697 };
2698 let json = serde_json::to_string_pretty(&fail_windows_nt_no_msg).unwrap();
2699 insta::assert_snapshot!("fail_windows_nt_status_no_message", json);
2700 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2701 assert_eq!(fail_windows_nt_no_msg, roundtrip);
2702
2703 let fail_job_object = ExecutionResultDescription::Fail {
2705 failure: FailureDescription::Abort {
2706 abort: AbortDescription::WindowsJobObject,
2707 },
2708 leaked: false,
2709 };
2710 let json = serde_json::to_string_pretty(&fail_job_object).unwrap();
2711 insta::assert_snapshot!("fail_windows_job_object", json);
2712 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2713 assert_eq!(fail_job_object, roundtrip);
2714
2715 let exec_fail = ExecutionResultDescription::ExecFail;
2717 let json = serde_json::to_string_pretty(&exec_fail).unwrap();
2718 insta::assert_snapshot!("exec_fail", json);
2719 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2720 assert_eq!(exec_fail, roundtrip);
2721
2722 let timeout_pass = ExecutionResultDescription::Timeout {
2724 result: SlowTimeoutResult::Pass,
2725 };
2726 let json = serde_json::to_string_pretty(&timeout_pass).unwrap();
2727 insta::assert_snapshot!("timeout_pass", json);
2728 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2729 assert_eq!(timeout_pass, roundtrip);
2730
2731 let timeout_fail = ExecutionResultDescription::Timeout {
2733 result: SlowTimeoutResult::Fail,
2734 };
2735 let json = serde_json::to_string_pretty(&timeout_fail).unwrap();
2736 insta::assert_snapshot!("timeout_fail", json);
2737 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2738 assert_eq!(timeout_fail, roundtrip);
2739 }
2740}