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::{TestInstanceId, TestList},
17 runner::{StressCondition, StressCount},
18 test_output::{ChildExecutionOutput, ChildOutput},
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: &'a [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: &'a [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: &'a [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,
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,
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,
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}
403
404#[derive(Clone, Debug)]
406pub enum StressProgress {
407 Count {
409 total: StressCount,
411
412 elapsed: Duration,
414
415 completed: u32,
417 },
418
419 Time {
421 total: Duration,
423
424 elapsed: Duration,
426
427 completed: u32,
429 },
430}
431
432impl StressProgress {
433 pub fn remaining(&self) -> Option<StressRemaining> {
436 match self {
437 Self::Count {
438 total: StressCount::Count(total),
439 elapsed: _,
440 completed,
441 } => total
442 .get()
443 .checked_sub(*completed)
444 .and_then(|remaining| NonZero::try_from(remaining).ok())
445 .map(StressRemaining::Count),
446 Self::Count {
447 total: StressCount::Infinite,
448 ..
449 } => Some(StressRemaining::Infinite),
450 Self::Time {
451 total,
452 elapsed,
453 completed: _,
454 } => total.checked_sub(*elapsed).map(StressRemaining::Time),
455 }
456 }
457
458 pub fn unique_id(&self, run_id: ReportUuid) -> String {
460 let stress_current = match self {
461 Self::Count { completed, .. } | Self::Time { completed, .. } => *completed,
462 };
463 format!("{}:@stress-{}", run_id, stress_current)
464 }
465}
466
467#[derive(Clone, Debug)]
469pub enum StressRemaining {
470 Count(NonZero<u32>),
472
473 Infinite,
475
476 Time(Duration),
478}
479
480#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
482pub struct StressIndex {
483 pub current: u32,
485
486 pub total: Option<NonZero<u32>>,
488}
489
490#[derive(Clone, Debug)]
492pub enum RunFinishedStats {
493 Single(RunStats),
495
496 Stress(StressRunStats),
498}
499
500impl RunFinishedStats {
501 pub fn final_stats(&self) -> FinalRunStats {
504 match self {
505 Self::Single(stats) => stats.summarize_final(),
506 Self::Stress(stats) => stats.last_final_stats,
507 }
508 }
509}
510
511#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
513pub struct RunStats {
514 pub initial_run_count: usize,
518
519 pub finished_count: usize,
521
522 pub setup_scripts_initial_count: usize,
526
527 pub setup_scripts_finished_count: usize,
529
530 pub setup_scripts_passed: usize,
532
533 pub setup_scripts_failed: usize,
535
536 pub setup_scripts_exec_failed: usize,
538
539 pub setup_scripts_timed_out: usize,
541
542 pub passed: usize,
544
545 pub passed_slow: usize,
547
548 pub passed_timed_out: usize,
550
551 pub flaky: usize,
553
554 pub failed: usize,
556
557 pub failed_slow: usize,
559
560 pub failed_timed_out: usize,
562
563 pub leaky: usize,
565
566 pub leaky_failed: usize,
569
570 pub exec_failed: usize,
572
573 pub skipped: usize,
575
576 pub cancel_reason: Option<CancelReason>,
578}
579
580impl RunStats {
581 pub fn has_failures(&self) -> bool {
583 self.failed_setup_script_count() > 0 || self.failed_count() > 0
584 }
585
586 pub fn failed_setup_script_count(&self) -> usize {
588 self.setup_scripts_failed + self.setup_scripts_exec_failed + self.setup_scripts_timed_out
589 }
590
591 pub fn failed_count(&self) -> usize {
593 self.failed + self.exec_failed + self.failed_timed_out
594 }
595
596 pub fn summarize_final(&self) -> FinalRunStats {
598 if self.failed_setup_script_count() > 0 {
601 if self.cancel_reason > Some(CancelReason::TestFailure) {
604 FinalRunStats::Cancelled {
605 reason: self.cancel_reason,
606 kind: RunStatsFailureKind::SetupScript,
607 }
608 } else {
609 FinalRunStats::Failed(RunStatsFailureKind::SetupScript)
610 }
611 } else if self.setup_scripts_initial_count > self.setup_scripts_finished_count {
612 FinalRunStats::Cancelled {
613 reason: self.cancel_reason,
614 kind: RunStatsFailureKind::SetupScript,
615 }
616 } else if self.failed_count() > 0 {
617 let kind = RunStatsFailureKind::Test {
618 initial_run_count: self.initial_run_count,
619 not_run: self.initial_run_count.saturating_sub(self.finished_count),
620 };
621
622 if self.cancel_reason > Some(CancelReason::TestFailure) {
625 FinalRunStats::Cancelled {
626 reason: self.cancel_reason,
627 kind,
628 }
629 } else {
630 FinalRunStats::Failed(kind)
631 }
632 } else if self.initial_run_count > self.finished_count {
633 FinalRunStats::Cancelled {
634 reason: self.cancel_reason,
635 kind: RunStatsFailureKind::Test {
636 initial_run_count: self.initial_run_count,
637 not_run: self.initial_run_count.saturating_sub(self.finished_count),
638 },
639 }
640 } else if self.finished_count == 0 {
641 FinalRunStats::NoTestsRun
642 } else {
643 FinalRunStats::Success
644 }
645 }
646
647 pub(crate) fn on_setup_script_finished(&mut self, status: &SetupScriptExecuteStatus) {
648 self.setup_scripts_finished_count += 1;
649
650 match status.result {
651 ExecutionResultDescription::Pass
652 | ExecutionResultDescription::Leak {
653 result: LeakTimeoutResult::Pass,
654 } => {
655 self.setup_scripts_passed += 1;
656 }
657 ExecutionResultDescription::Fail { .. }
658 | ExecutionResultDescription::Leak {
659 result: LeakTimeoutResult::Fail,
660 } => {
661 self.setup_scripts_failed += 1;
662 }
663 ExecutionResultDescription::ExecFail => {
664 self.setup_scripts_exec_failed += 1;
665 }
666 ExecutionResultDescription::Timeout { .. } => {
668 self.setup_scripts_timed_out += 1;
669 }
670 }
671 }
672
673 pub(crate) fn on_test_finished(&mut self, run_statuses: &ExecutionStatuses) {
674 self.finished_count += 1;
675 let last_status = run_statuses.last_status();
684 match last_status.result {
685 ExecutionResultDescription::Pass => {
686 self.passed += 1;
687 if last_status.is_slow {
688 self.passed_slow += 1;
689 }
690 if run_statuses.len() > 1 {
691 self.flaky += 1;
692 }
693 }
694 ExecutionResultDescription::Leak {
695 result: LeakTimeoutResult::Pass,
696 } => {
697 self.passed += 1;
698 self.leaky += 1;
699 if last_status.is_slow {
700 self.passed_slow += 1;
701 }
702 if run_statuses.len() > 1 {
703 self.flaky += 1;
704 }
705 }
706 ExecutionResultDescription::Leak {
707 result: LeakTimeoutResult::Fail,
708 } => {
709 self.failed += 1;
710 self.leaky_failed += 1;
711 if last_status.is_slow {
712 self.failed_slow += 1;
713 }
714 }
715 ExecutionResultDescription::Fail { .. } => {
716 self.failed += 1;
717 if last_status.is_slow {
718 self.failed_slow += 1;
719 }
720 }
721 ExecutionResultDescription::Timeout {
722 result: SlowTimeoutResult::Pass,
723 } => {
724 self.passed += 1;
725 self.passed_timed_out += 1;
726 if run_statuses.len() > 1 {
727 self.flaky += 1;
728 }
729 }
730 ExecutionResultDescription::Timeout {
731 result: SlowTimeoutResult::Fail,
732 } => {
733 self.failed_timed_out += 1;
734 }
735 ExecutionResultDescription::ExecFail => self.exec_failed += 1,
736 }
737 }
738}
739
740#[derive(Copy, Clone, Debug, Eq, PartialEq)]
742pub enum FinalRunStats {
743 Success,
745
746 NoTestsRun,
748
749 Cancelled {
751 reason: Option<CancelReason>,
756
757 kind: RunStatsFailureKind,
759 },
760
761 Failed(RunStatsFailureKind),
763}
764
765#[derive(Clone, Debug)]
767pub struct StressRunStats {
768 pub completed: StressIndex,
770
771 pub success_count: u32,
773
774 pub failed_count: u32,
776
777 pub last_final_stats: FinalRunStats,
779}
780
781impl StressRunStats {
782 pub fn summarize_final(&self) -> StressFinalRunStats {
784 if self.failed_count > 0 {
785 StressFinalRunStats::Failed
786 } else if matches!(self.last_final_stats, FinalRunStats::Cancelled { .. }) {
787 StressFinalRunStats::Cancelled
788 } else if matches!(self.last_final_stats, FinalRunStats::NoTestsRun) {
789 StressFinalRunStats::NoTestsRun
790 } else {
791 StressFinalRunStats::Success
792 }
793 }
794}
795
796pub enum StressFinalRunStats {
798 Success,
800
801 NoTestsRun,
803
804 Cancelled,
806
807 Failed,
809}
810
811#[derive(Copy, Clone, Debug, Eq, PartialEq)]
813pub enum RunStatsFailureKind {
814 SetupScript,
816
817 Test {
819 initial_run_count: usize,
821
822 not_run: usize,
825 },
826}
827
828#[derive(Clone, Debug)]
830pub struct ExecutionStatuses {
831 statuses: Vec<ExecuteStatus>,
833}
834
835#[expect(clippy::len_without_is_empty)] impl ExecutionStatuses {
837 pub(crate) fn new(statuses: Vec<ExecuteStatus>) -> Self {
838 Self { statuses }
839 }
840
841 pub fn last_status(&self) -> &ExecuteStatus {
845 self.statuses
846 .last()
847 .expect("execution statuses is non-empty")
848 }
849
850 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus> + '_ {
852 self.statuses.iter()
853 }
854
855 pub fn len(&self) -> usize {
857 self.statuses.len()
858 }
859
860 pub fn describe(&self) -> ExecutionDescription<'_> {
862 let last_status = self.last_status();
863 if last_status.result.is_success() {
864 if self.statuses.len() > 1 {
865 ExecutionDescription::Flaky {
866 last_status,
867 prior_statuses: &self.statuses[..self.statuses.len() - 1],
868 }
869 } else {
870 ExecutionDescription::Success {
871 single_status: last_status,
872 }
873 }
874 } else {
875 let first_status = self
876 .statuses
877 .first()
878 .expect("execution statuses is non-empty");
879 let retries = &self.statuses[1..];
880 ExecutionDescription::Failure {
881 first_status,
882 last_status,
883 retries,
884 }
885 }
886 }
887}
888
889#[derive(Copy, Clone, Debug)]
893pub enum ExecutionDescription<'a> {
894 Success {
896 single_status: &'a ExecuteStatus,
898 },
899
900 Flaky {
902 last_status: &'a ExecuteStatus,
904
905 prior_statuses: &'a [ExecuteStatus],
907 },
908
909 Failure {
911 first_status: &'a ExecuteStatus,
913
914 last_status: &'a ExecuteStatus,
916
917 retries: &'a [ExecuteStatus],
921 },
922}
923
924impl<'a> ExecutionDescription<'a> {
925 pub fn status_level(&self) -> StatusLevel {
927 match self {
928 ExecutionDescription::Success { single_status } => match single_status.result {
929 ExecutionResultDescription::Leak {
930 result: LeakTimeoutResult::Pass,
931 } => StatusLevel::Leak,
932 ExecutionResultDescription::Pass => StatusLevel::Pass,
933 ExecutionResultDescription::Timeout {
934 result: SlowTimeoutResult::Pass,
935 } => StatusLevel::Slow,
936 ref other => unreachable!(
937 "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
938 ),
939 },
940 ExecutionDescription::Flaky { .. } => StatusLevel::Retry,
942 ExecutionDescription::Failure { .. } => StatusLevel::Fail,
943 }
944 }
945
946 pub fn final_status_level(&self) -> FinalStatusLevel {
948 match self {
949 ExecutionDescription::Success { single_status, .. } => {
950 if single_status.is_slow {
952 FinalStatusLevel::Slow
953 } else {
954 match single_status.result {
955 ExecutionResultDescription::Pass => FinalStatusLevel::Pass,
956 ExecutionResultDescription::Leak {
957 result: LeakTimeoutResult::Pass,
958 } => FinalStatusLevel::Leak,
959 ExecutionResultDescription::Timeout {
963 result: SlowTimeoutResult::Pass,
964 } => FinalStatusLevel::Slow,
965 ref other => unreachable!(
966 "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
967 ),
968 }
969 }
970 }
971 ExecutionDescription::Flaky { .. } => FinalStatusLevel::Flaky,
973 ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
974 }
975 }
976
977 pub fn last_status(&self) -> &'a ExecuteStatus {
979 match self {
980 ExecutionDescription::Success {
981 single_status: last_status,
982 }
983 | ExecutionDescription::Flaky { last_status, .. }
984 | ExecutionDescription::Failure { last_status, .. } => last_status,
985 }
986 }
987}
988
989#[derive(Clone, Debug)]
995pub struct ErrorSummary {
996 pub short_message: String,
998
999 pub description: String,
1001}
1002
1003#[derive(Clone, Debug)]
1008pub struct OutputErrorSlice {
1009 pub slice: String,
1011
1012 pub start: usize,
1014}
1015
1016#[derive(Clone, Debug)]
1022pub struct ExecuteStatus {
1023 pub retry_data: RetryData,
1025 pub output: ChildExecutionOutputDescription,
1027 pub result: ExecutionResultDescription,
1029 pub start_time: DateTime<FixedOffset>,
1031 pub time_taken: Duration,
1033 pub is_slow: bool,
1035 pub delay_before_start: Duration,
1037 pub error_summary: Option<ErrorSummary>,
1042 pub output_error_slice: Option<OutputErrorSlice>,
1047}
1048
1049#[derive(Clone, Debug)]
1055pub struct SetupScriptExecuteStatus {
1056 pub output: ChildExecutionOutputDescription,
1058
1059 pub result: ExecutionResultDescription,
1061
1062 pub start_time: DateTime<FixedOffset>,
1064
1065 pub time_taken: Duration,
1067
1068 pub is_slow: bool,
1070
1071 pub env_map: Option<SetupScriptEnvMap>,
1076
1077 pub error_summary: Option<ErrorSummary>,
1082}
1083
1084#[derive(Clone, Debug)]
1088pub struct SetupScriptEnvMap {
1089 pub env_map: BTreeMap<String, String>,
1091}
1092
1093#[derive(Clone, Debug)]
1101pub enum ChildExecutionOutputDescription {
1102 Output {
1104 result: Option<ExecutionResultDescription>,
1108
1109 output: ChildOutput,
1111
1112 errors: Option<ErrorList<ChildErrorDescription>>,
1115 },
1116
1117 StartError(ChildStartErrorDescription),
1119}
1120
1121impl ChildExecutionOutputDescription {
1122 pub fn has_errors(&self) -> bool {
1124 match self {
1125 Self::Output { errors, result, .. } => {
1126 if errors.is_some() {
1127 return true;
1128 }
1129 if let Some(result) = result {
1130 return !result.is_success();
1131 }
1132 false
1133 }
1134 Self::StartError(_) => true,
1135 }
1136 }
1137}
1138
1139#[derive(Clone, Debug)]
1143pub enum ChildStartErrorDescription {
1144 TempPath {
1146 source: IoErrorDescription,
1148 },
1149
1150 Spawn {
1152 source: IoErrorDescription,
1154 },
1155}
1156
1157impl fmt::Display for ChildStartErrorDescription {
1158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1159 match self {
1160 Self::TempPath { .. } => {
1161 write!(f, "error creating temporary path for setup script")
1162 }
1163 Self::Spawn { .. } => write!(f, "error spawning child process"),
1164 }
1165 }
1166}
1167
1168impl std::error::Error for ChildStartErrorDescription {
1169 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1170 match self {
1171 Self::TempPath { source } | Self::Spawn { source } => Some(source),
1172 }
1173 }
1174}
1175
1176#[derive(Clone, Debug)]
1180pub enum ChildErrorDescription {
1181 ReadStdout {
1183 source: IoErrorDescription,
1185 },
1186
1187 ReadStderr {
1189 source: IoErrorDescription,
1191 },
1192
1193 ReadCombined {
1195 source: IoErrorDescription,
1197 },
1198
1199 Wait {
1201 source: IoErrorDescription,
1203 },
1204
1205 SetupScriptOutput {
1207 source: IoErrorDescription,
1209 },
1210}
1211
1212impl fmt::Display for ChildErrorDescription {
1213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1214 match self {
1215 Self::ReadStdout { .. } => write!(f, "error reading standard output"),
1216 Self::ReadStderr { .. } => write!(f, "error reading standard error"),
1217 Self::ReadCombined { .. } => {
1218 write!(f, "error reading combined stream")
1219 }
1220 Self::Wait { .. } => {
1221 write!(f, "error waiting for child process to exit")
1222 }
1223 Self::SetupScriptOutput { .. } => {
1224 write!(f, "error reading setup script output")
1225 }
1226 }
1227 }
1228}
1229
1230impl std::error::Error for ChildErrorDescription {
1231 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1232 match self {
1233 Self::ReadStdout { source }
1234 | Self::ReadStderr { source }
1235 | Self::ReadCombined { source }
1236 | Self::Wait { source }
1237 | Self::SetupScriptOutput { source } => Some(source),
1238 }
1239 }
1240}
1241
1242#[derive(Clone, Debug)]
1246pub struct IoErrorDescription {
1247 message: String,
1248}
1249
1250impl fmt::Display for IoErrorDescription {
1251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1252 write!(f, "{}", self.message)
1253 }
1254}
1255
1256impl std::error::Error for IoErrorDescription {}
1257
1258impl From<ChildExecutionOutput> for ChildExecutionOutputDescription {
1259 fn from(output: ChildExecutionOutput) -> Self {
1260 match output {
1261 ChildExecutionOutput::Output {
1262 result,
1263 output,
1264 errors,
1265 } => Self::Output {
1266 result: result.map(ExecutionResultDescription::from),
1267 output,
1268 errors: errors.map(|e| e.map(ChildErrorDescription::from)),
1269 },
1270 ChildExecutionOutput::StartError(error) => {
1271 Self::StartError(ChildStartErrorDescription::from(error))
1272 }
1273 }
1274 }
1275}
1276
1277impl From<ChildStartError> for ChildStartErrorDescription {
1278 fn from(error: ChildStartError) -> Self {
1279 match error {
1280 ChildStartError::TempPath(e) => Self::TempPath {
1281 source: IoErrorDescription {
1282 message: e.to_string(),
1283 },
1284 },
1285 ChildStartError::Spawn(e) => Self::Spawn {
1286 source: IoErrorDescription {
1287 message: e.to_string(),
1288 },
1289 },
1290 }
1291 }
1292}
1293
1294impl From<ChildError> for ChildErrorDescription {
1295 fn from(error: ChildError) -> Self {
1296 match error {
1297 ChildError::Fd(ChildFdError::ReadStdout(e)) => Self::ReadStdout {
1298 source: IoErrorDescription {
1299 message: e.to_string(),
1300 },
1301 },
1302 ChildError::Fd(ChildFdError::ReadStderr(e)) => Self::ReadStderr {
1303 source: IoErrorDescription {
1304 message: e.to_string(),
1305 },
1306 },
1307 ChildError::Fd(ChildFdError::ReadCombined(e)) => Self::ReadCombined {
1308 source: IoErrorDescription {
1309 message: e.to_string(),
1310 },
1311 },
1312 ChildError::Fd(ChildFdError::Wait(e)) => Self::Wait {
1313 source: IoErrorDescription {
1314 message: e.to_string(),
1315 },
1316 },
1317 ChildError::SetupScriptOutput(e) => Self::SetupScriptOutput {
1318 source: IoErrorDescription {
1319 message: e.to_string(),
1320 },
1321 },
1322 }
1323 }
1324}
1325
1326#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
1328pub struct RetryData {
1329 pub attempt: u32,
1331
1332 pub total_attempts: u32,
1334}
1335
1336impl RetryData {
1337 pub fn is_last_attempt(&self) -> bool {
1339 self.attempt >= self.total_attempts
1340 }
1341}
1342
1343#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1345pub enum ExecutionResult {
1346 Pass,
1348 Leak {
1352 result: LeakTimeoutResult,
1359 },
1360 Fail {
1362 failure_status: FailureStatus,
1364
1365 leaked: bool,
1369 },
1370 ExecFail,
1372 Timeout {
1374 result: SlowTimeoutResult,
1376 },
1377}
1378
1379impl ExecutionResult {
1380 pub fn is_success(self) -> bool {
1382 match self {
1383 ExecutionResult::Pass
1384 | ExecutionResult::Timeout {
1385 result: SlowTimeoutResult::Pass,
1386 }
1387 | ExecutionResult::Leak {
1388 result: LeakTimeoutResult::Pass,
1389 } => true,
1390 ExecutionResult::Leak {
1391 result: LeakTimeoutResult::Fail,
1392 }
1393 | ExecutionResult::Fail { .. }
1394 | ExecutionResult::ExecFail
1395 | ExecutionResult::Timeout {
1396 result: SlowTimeoutResult::Fail,
1397 } => false,
1398 }
1399 }
1400
1401 pub fn as_static_str(&self) -> &'static str {
1403 match self {
1404 ExecutionResult::Pass => "pass",
1405 ExecutionResult::Leak { .. } => "leak",
1406 ExecutionResult::Fail { .. } => "fail",
1407 ExecutionResult::ExecFail => "exec-fail",
1408 ExecutionResult::Timeout { .. } => "timeout",
1409 }
1410 }
1411}
1412
1413#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1415pub enum FailureStatus {
1416 ExitCode(i32),
1418
1419 Abort(AbortStatus),
1421}
1422
1423impl FailureStatus {
1424 pub fn extract(exit_status: ExitStatus) -> Self {
1426 if let Some(abort_status) = AbortStatus::extract(exit_status) {
1427 FailureStatus::Abort(abort_status)
1428 } else {
1429 FailureStatus::ExitCode(
1430 exit_status
1431 .code()
1432 .expect("if abort_status is None, then code must be present"),
1433 )
1434 }
1435 }
1436}
1437
1438#[derive(Copy, Clone, Eq, PartialEq)]
1442pub enum AbortStatus {
1443 #[cfg(unix)]
1445 UnixSignal(i32),
1446
1447 #[cfg(windows)]
1449 WindowsNtStatus(windows_sys::Win32::Foundation::NTSTATUS),
1450
1451 #[cfg(windows)]
1453 JobObject,
1454}
1455
1456impl AbortStatus {
1457 pub fn extract(exit_status: ExitStatus) -> Option<Self> {
1459 cfg_if::cfg_if! {
1460 if #[cfg(unix)] {
1461 use std::os::unix::process::ExitStatusExt;
1463 exit_status.signal().map(AbortStatus::UnixSignal)
1464 } else if #[cfg(windows)] {
1465 exit_status.code().and_then(|code| {
1466 (code < 0).then_some(AbortStatus::WindowsNtStatus(code))
1467 })
1468 } else {
1469 None
1470 }
1471 }
1472 }
1473}
1474
1475impl fmt::Debug for AbortStatus {
1476 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1477 match self {
1478 #[cfg(unix)]
1479 AbortStatus::UnixSignal(signal) => write!(f, "UnixSignal({signal})"),
1480 #[cfg(windows)]
1481 AbortStatus::WindowsNtStatus(status) => write!(f, "WindowsNtStatus({status:x})"),
1482 #[cfg(windows)]
1483 AbortStatus::JobObject => write!(f, "JobObject"),
1484 }
1485 }
1486}
1487
1488#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1494#[serde(tag = "kind", rename_all = "kebab-case")]
1495#[non_exhaustive]
1496pub enum AbortDescription {
1497 UnixSignal {
1499 signal: i32,
1501 name: Option<SmolStr>,
1504 },
1505
1506 WindowsNtStatus {
1508 code: i32,
1510 message: Option<SmolStr>,
1512 },
1513
1514 WindowsJobObject,
1516}
1517
1518impl fmt::Display for AbortDescription {
1519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1520 match self {
1521 Self::UnixSignal { signal, name } => {
1522 write!(f, "aborted with signal {signal}")?;
1523 if let Some(name) = name {
1524 write!(f, " (SIG{name})")?;
1525 }
1526 Ok(())
1527 }
1528 Self::WindowsNtStatus { code, message } => {
1529 write!(f, "aborted with code {code:#010x}")?;
1530 if let Some(message) = message {
1531 write!(f, ": {message}")?;
1532 }
1533 Ok(())
1534 }
1535 Self::WindowsJobObject => {
1536 write!(f, "terminated via job object")
1537 }
1538 }
1539 }
1540}
1541
1542impl From<AbortStatus> for AbortDescription {
1543 fn from(status: AbortStatus) -> Self {
1544 cfg_if::cfg_if! {
1545 if #[cfg(unix)] {
1546 match status {
1547 AbortStatus::UnixSignal(signal) => Self::UnixSignal {
1548 signal,
1549 name: crate::helpers::signal_str(signal).map(SmolStr::new_static),
1550 },
1551 }
1552 } else if #[cfg(windows)] {
1553 match status {
1554 AbortStatus::WindowsNtStatus(code) => Self::WindowsNtStatus {
1555 code,
1556 message: crate::helpers::windows_nt_status_message(code),
1557 },
1558 AbortStatus::JobObject => Self::WindowsJobObject,
1559 }
1560 } else {
1561 match status {}
1562 }
1563 }
1564 }
1565}
1566
1567#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1571#[serde(tag = "kind", rename_all = "kebab-case")]
1572#[non_exhaustive]
1573pub enum FailureDescription {
1574 ExitCode {
1576 code: i32,
1578 },
1579
1580 Abort {
1587 abort: AbortDescription,
1589 },
1590}
1591
1592impl From<FailureStatus> for FailureDescription {
1593 fn from(status: FailureStatus) -> Self {
1594 match status {
1595 FailureStatus::ExitCode(code) => Self::ExitCode { code },
1596 FailureStatus::Abort(abort) => Self::Abort {
1597 abort: AbortDescription::from(abort),
1598 },
1599 }
1600 }
1601}
1602
1603impl fmt::Display for FailureDescription {
1604 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1605 match self {
1606 Self::ExitCode { code } => write!(f, "exited with code {code}"),
1607 Self::Abort { abort } => write!(f, "{abort}"),
1608 }
1609 }
1610}
1611
1612#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1617#[serde(tag = "status", rename_all = "kebab-case")]
1618#[non_exhaustive]
1619pub enum ExecutionResultDescription {
1620 Pass,
1622
1623 Leak {
1625 result: LeakTimeoutResult,
1627 },
1628
1629 Fail {
1631 failure: FailureDescription,
1633
1634 leaked: bool,
1636 },
1637
1638 ExecFail,
1640
1641 Timeout {
1643 result: SlowTimeoutResult,
1645 },
1646}
1647
1648impl ExecutionResultDescription {
1649 pub fn is_success(&self) -> bool {
1651 match self {
1652 Self::Pass
1653 | Self::Timeout {
1654 result: SlowTimeoutResult::Pass,
1655 }
1656 | Self::Leak {
1657 result: LeakTimeoutResult::Pass,
1658 } => true,
1659 Self::Leak {
1660 result: LeakTimeoutResult::Fail,
1661 }
1662 | Self::Fail { .. }
1663 | Self::ExecFail
1664 | Self::Timeout {
1665 result: SlowTimeoutResult::Fail,
1666 } => false,
1667 }
1668 }
1669
1670 pub fn as_static_str(&self) -> &'static str {
1672 match self {
1673 Self::Pass => "pass",
1674 Self::Leak { .. } => "leak",
1675 Self::Fail { .. } => "fail",
1676 Self::ExecFail => "exec-fail",
1677 Self::Timeout { .. } => "timeout",
1678 }
1679 }
1680
1681 pub fn is_termination_failure(&self) -> bool {
1693 matches!(
1694 self,
1695 Self::Fail {
1696 failure: FailureDescription::Abort {
1697 abort: AbortDescription::UnixSignal {
1698 signal: SIGTERM,
1699 ..
1700 },
1701 },
1702 ..
1703 } | Self::Fail {
1704 failure: FailureDescription::Abort {
1705 abort: AbortDescription::WindowsJobObject,
1706 },
1707 ..
1708 }
1709 )
1710 }
1711}
1712
1713impl From<ExecutionResult> for ExecutionResultDescription {
1714 fn from(result: ExecutionResult) -> Self {
1715 match result {
1716 ExecutionResult::Pass => Self::Pass,
1717 ExecutionResult::Leak { result } => Self::Leak { result },
1718 ExecutionResult::Fail {
1719 failure_status,
1720 leaked,
1721 } => Self::Fail {
1722 failure: FailureDescription::from(failure_status),
1723 leaked,
1724 },
1725 ExecutionResult::ExecFail => Self::ExecFail,
1726 ExecutionResult::Timeout { result } => Self::Timeout { result },
1727 }
1728 }
1729}
1730
1731#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
1734#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1735pub enum CancelReason {
1736 SetupScriptFailure,
1738
1739 TestFailure,
1741
1742 ReportError,
1744
1745 GlobalTimeout,
1747
1748 TestFailureImmediate,
1750
1751 Signal,
1753
1754 Interrupt,
1756
1757 SecondSignal,
1759}
1760
1761impl CancelReason {
1762 pub(crate) fn to_static_str(self) -> &'static str {
1763 match self {
1764 CancelReason::SetupScriptFailure => "setup script failure",
1765 CancelReason::TestFailure => "test failure",
1766 CancelReason::ReportError => "reporting error",
1767 CancelReason::GlobalTimeout => "global timeout",
1768 CancelReason::TestFailureImmediate => "test failure",
1769 CancelReason::Signal => "signal",
1770 CancelReason::Interrupt => "interrupt",
1771 CancelReason::SecondSignal => "second signal",
1772 }
1773 }
1774}
1775#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1777pub enum UnitKind {
1778 Test,
1780
1781 Script,
1783}
1784
1785impl UnitKind {
1786 pub(crate) const WAITING_ON_TEST_MESSAGE: &str = "waiting on test process";
1787 pub(crate) const WAITING_ON_SCRIPT_MESSAGE: &str = "waiting on script process";
1788
1789 pub(crate) const EXECUTING_TEST_MESSAGE: &str = "executing test";
1790 pub(crate) const EXECUTING_SCRIPT_MESSAGE: &str = "executing script";
1791
1792 pub(crate) fn waiting_on_message(&self) -> &'static str {
1793 match self {
1794 UnitKind::Test => Self::WAITING_ON_TEST_MESSAGE,
1795 UnitKind::Script => Self::WAITING_ON_SCRIPT_MESSAGE,
1796 }
1797 }
1798
1799 pub(crate) fn executing_message(&self) -> &'static str {
1800 match self {
1801 UnitKind::Test => Self::EXECUTING_TEST_MESSAGE,
1802 UnitKind::Script => Self::EXECUTING_SCRIPT_MESSAGE,
1803 }
1804 }
1805}
1806
1807#[derive(Clone, Debug)]
1809pub enum InfoResponse<'a> {
1810 SetupScript(SetupScriptInfoResponse<'a>),
1812
1813 Test(TestInfoResponse<'a>),
1815}
1816
1817#[derive(Clone, Debug)]
1819pub struct SetupScriptInfoResponse<'a> {
1820 pub stress_index: Option<StressIndex>,
1822
1823 pub script_id: ScriptId,
1825
1826 pub program: String,
1828
1829 pub args: &'a [String],
1831
1832 pub state: UnitState,
1834
1835 pub output: ChildExecutionOutputDescription,
1837}
1838
1839#[derive(Clone, Debug)]
1841pub struct TestInfoResponse<'a> {
1842 pub stress_index: Option<StressIndex>,
1844
1845 pub test_instance: TestInstanceId<'a>,
1847
1848 pub retry_data: RetryData,
1850
1851 pub state: UnitState,
1853
1854 pub output: ChildExecutionOutputDescription,
1856}
1857
1858#[derive(Clone, Debug)]
1863pub enum UnitState {
1864 Running {
1866 pid: u32,
1868
1869 time_taken: Duration,
1871
1872 slow_after: Option<Duration>,
1875 },
1876
1877 Exiting {
1880 pid: u32,
1882
1883 time_taken: Duration,
1885
1886 slow_after: Option<Duration>,
1889
1890 tentative_result: Option<ExecutionResult>,
1895
1896 waiting_duration: Duration,
1898
1899 remaining: Duration,
1901 },
1902
1903 Terminating(UnitTerminatingState),
1905
1906 Exited {
1908 result: ExecutionResult,
1910
1911 time_taken: Duration,
1913
1914 slow_after: Option<Duration>,
1917 },
1918
1919 DelayBeforeNextAttempt {
1922 previous_result: ExecutionResult,
1924
1925 previous_slow: bool,
1927
1928 waiting_duration: Duration,
1930
1931 remaining: Duration,
1933 },
1934}
1935
1936impl UnitState {
1937 pub fn has_valid_output(&self) -> bool {
1939 match self {
1940 UnitState::Running { .. }
1941 | UnitState::Exiting { .. }
1942 | UnitState::Terminating(_)
1943 | UnitState::Exited { .. } => true,
1944 UnitState::DelayBeforeNextAttempt { .. } => false,
1945 }
1946 }
1947}
1948
1949#[derive(Clone, Debug)]
1953pub struct UnitTerminatingState {
1954 pub pid: u32,
1956
1957 pub time_taken: Duration,
1959
1960 pub reason: UnitTerminateReason,
1962
1963 pub method: UnitTerminateMethod,
1965
1966 pub waiting_duration: Duration,
1968
1969 pub remaining: Duration,
1971}
1972
1973#[derive(Clone, Copy, Debug)]
1977pub enum UnitTerminateReason {
1978 Timeout,
1980
1981 Signal,
1983
1984 Interrupt,
1986}
1987
1988impl fmt::Display for UnitTerminateReason {
1989 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1990 match self {
1991 UnitTerminateReason::Timeout => write!(f, "timeout"),
1992 UnitTerminateReason::Signal => write!(f, "signal"),
1993 UnitTerminateReason::Interrupt => write!(f, "interrupt"),
1994 }
1995 }
1996}
1997
1998#[derive(Clone, Copy, Debug)]
2000pub enum UnitTerminateMethod {
2001 #[cfg(unix)]
2003 Signal(UnitTerminateSignal),
2004
2005 #[cfg(windows)]
2007 JobObject,
2008
2009 #[cfg(windows)]
2017 Wait,
2018
2019 #[cfg(test)]
2021 Fake,
2022}
2023
2024#[cfg(unix)]
2025#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2027pub enum UnitTerminateSignal {
2028 Interrupt,
2030
2031 Term,
2033
2034 Hangup,
2036
2037 Quit,
2039
2040 Kill,
2042}
2043
2044#[cfg(unix)]
2045impl fmt::Display for UnitTerminateSignal {
2046 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2047 match self {
2048 UnitTerminateSignal::Interrupt => write!(f, "SIGINT"),
2049 UnitTerminateSignal::Term => write!(f, "SIGTERM"),
2050 UnitTerminateSignal::Hangup => write!(f, "SIGHUP"),
2051 UnitTerminateSignal::Quit => write!(f, "SIGQUIT"),
2052 UnitTerminateSignal::Kill => write!(f, "SIGKILL"),
2053 }
2054 }
2055}
2056
2057#[cfg(test)]
2058mod tests {
2059 use super::*;
2060
2061 #[test]
2062 fn test_is_success() {
2063 assert_eq!(
2064 RunStats::default().summarize_final(),
2065 FinalRunStats::NoTestsRun,
2066 "empty run => no tests run"
2067 );
2068 assert_eq!(
2069 RunStats {
2070 initial_run_count: 42,
2071 finished_count: 42,
2072 ..RunStats::default()
2073 }
2074 .summarize_final(),
2075 FinalRunStats::Success,
2076 "initial run count = final run count => success"
2077 );
2078 assert_eq!(
2079 RunStats {
2080 initial_run_count: 42,
2081 finished_count: 41,
2082 ..RunStats::default()
2083 }
2084 .summarize_final(),
2085 FinalRunStats::Cancelled {
2086 reason: None,
2087 kind: RunStatsFailureKind::Test {
2088 initial_run_count: 42,
2089 not_run: 1
2090 }
2091 },
2092 "initial run count > final run count => cancelled"
2093 );
2094 assert_eq!(
2095 RunStats {
2096 initial_run_count: 42,
2097 finished_count: 42,
2098 failed: 1,
2099 ..RunStats::default()
2100 }
2101 .summarize_final(),
2102 FinalRunStats::Failed(RunStatsFailureKind::Test {
2103 initial_run_count: 42,
2104 not_run: 0
2105 }),
2106 "failed => failure"
2107 );
2108 assert_eq!(
2109 RunStats {
2110 initial_run_count: 42,
2111 finished_count: 42,
2112 exec_failed: 1,
2113 ..RunStats::default()
2114 }
2115 .summarize_final(),
2116 FinalRunStats::Failed(RunStatsFailureKind::Test {
2117 initial_run_count: 42,
2118 not_run: 0
2119 }),
2120 "exec failed => failure"
2121 );
2122 assert_eq!(
2123 RunStats {
2124 initial_run_count: 42,
2125 finished_count: 42,
2126 failed_timed_out: 1,
2127 ..RunStats::default()
2128 }
2129 .summarize_final(),
2130 FinalRunStats::Failed(RunStatsFailureKind::Test {
2131 initial_run_count: 42,
2132 not_run: 0
2133 }),
2134 "timed out => failure {:?} {:?}",
2135 RunStats {
2136 initial_run_count: 42,
2137 finished_count: 42,
2138 failed_timed_out: 1,
2139 ..RunStats::default()
2140 }
2141 .summarize_final(),
2142 FinalRunStats::Failed(RunStatsFailureKind::Test {
2143 initial_run_count: 42,
2144 not_run: 0
2145 }),
2146 );
2147 assert_eq!(
2148 RunStats {
2149 initial_run_count: 42,
2150 finished_count: 42,
2151 skipped: 1,
2152 ..RunStats::default()
2153 }
2154 .summarize_final(),
2155 FinalRunStats::Success,
2156 "skipped => not considered a failure"
2157 );
2158
2159 assert_eq!(
2160 RunStats {
2161 setup_scripts_initial_count: 2,
2162 setup_scripts_finished_count: 1,
2163 ..RunStats::default()
2164 }
2165 .summarize_final(),
2166 FinalRunStats::Cancelled {
2167 reason: None,
2168 kind: RunStatsFailureKind::SetupScript,
2169 },
2170 "setup script failed => failure"
2171 );
2172
2173 assert_eq!(
2174 RunStats {
2175 setup_scripts_initial_count: 2,
2176 setup_scripts_finished_count: 2,
2177 setup_scripts_failed: 1,
2178 ..RunStats::default()
2179 }
2180 .summarize_final(),
2181 FinalRunStats::Failed(RunStatsFailureKind::SetupScript),
2182 "setup script failed => failure"
2183 );
2184 assert_eq!(
2185 RunStats {
2186 setup_scripts_initial_count: 2,
2187 setup_scripts_finished_count: 2,
2188 setup_scripts_exec_failed: 1,
2189 ..RunStats::default()
2190 }
2191 .summarize_final(),
2192 FinalRunStats::Failed(RunStatsFailureKind::SetupScript),
2193 "setup script exec failed => failure"
2194 );
2195 assert_eq!(
2196 RunStats {
2197 setup_scripts_initial_count: 2,
2198 setup_scripts_finished_count: 2,
2199 setup_scripts_timed_out: 1,
2200 ..RunStats::default()
2201 }
2202 .summarize_final(),
2203 FinalRunStats::Failed(RunStatsFailureKind::SetupScript),
2204 "setup script timed out => failure"
2205 );
2206 assert_eq!(
2207 RunStats {
2208 setup_scripts_initial_count: 2,
2209 setup_scripts_finished_count: 2,
2210 setup_scripts_passed: 2,
2211 ..RunStats::default()
2212 }
2213 .summarize_final(),
2214 FinalRunStats::NoTestsRun,
2215 "setup scripts passed => success, but no tests run"
2216 );
2217 }
2218
2219 #[test]
2220 fn abort_description_serialization() {
2221 let unix_with_name = AbortDescription::UnixSignal {
2223 signal: 15,
2224 name: Some("TERM".into()),
2225 };
2226 let json = serde_json::to_string_pretty(&unix_with_name).unwrap();
2227 insta::assert_snapshot!("abort_unix_signal_with_name", json);
2228 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2229 assert_eq!(unix_with_name, roundtrip);
2230
2231 let unix_no_name = AbortDescription::UnixSignal {
2233 signal: 42,
2234 name: None,
2235 };
2236 let json = serde_json::to_string_pretty(&unix_no_name).unwrap();
2237 insta::assert_snapshot!("abort_unix_signal_no_name", json);
2238 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2239 assert_eq!(unix_no_name, roundtrip);
2240
2241 let windows_nt = AbortDescription::WindowsNtStatus {
2243 code: -1073741510_i32,
2244 message: Some("The application terminated as a result of a CTRL+C.".into()),
2245 };
2246 let json = serde_json::to_string_pretty(&windows_nt).unwrap();
2247 insta::assert_snapshot!("abort_windows_nt_status", json);
2248 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2249 assert_eq!(windows_nt, roundtrip);
2250
2251 let windows_nt_no_msg = AbortDescription::WindowsNtStatus {
2253 code: -1073741819_i32,
2254 message: None,
2255 };
2256 let json = serde_json::to_string_pretty(&windows_nt_no_msg).unwrap();
2257 insta::assert_snapshot!("abort_windows_nt_status_no_message", json);
2258 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2259 assert_eq!(windows_nt_no_msg, roundtrip);
2260
2261 let job = AbortDescription::WindowsJobObject;
2263 let json = serde_json::to_string_pretty(&job).unwrap();
2264 insta::assert_snapshot!("abort_windows_job_object", json);
2265 let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2266 assert_eq!(job, roundtrip);
2267 }
2268
2269 #[test]
2270 fn abort_description_cross_platform_deserialization() {
2271 let unix_json = r#"{"kind":"unix-signal","signal":11,"name":"SEGV"}"#;
2274 let unix_desc: AbortDescription = serde_json::from_str(unix_json).unwrap();
2275 assert_eq!(
2276 unix_desc,
2277 AbortDescription::UnixSignal {
2278 signal: 11,
2279 name: Some("SEGV".into()),
2280 }
2281 );
2282
2283 let windows_json = r#"{"kind":"windows-nt-status","code":-1073741510,"message":"CTRL+C"}"#;
2284 let windows_desc: AbortDescription = serde_json::from_str(windows_json).unwrap();
2285 assert_eq!(
2286 windows_desc,
2287 AbortDescription::WindowsNtStatus {
2288 code: -1073741510,
2289 message: Some("CTRL+C".into()),
2290 }
2291 );
2292
2293 let job_json = r#"{"kind":"windows-job-object"}"#;
2294 let job_desc: AbortDescription = serde_json::from_str(job_json).unwrap();
2295 assert_eq!(job_desc, AbortDescription::WindowsJobObject);
2296 }
2297
2298 #[test]
2299 fn abort_description_display() {
2300 let unix = AbortDescription::UnixSignal {
2302 signal: 15,
2303 name: Some("TERM".into()),
2304 };
2305 assert_eq!(unix.to_string(), "aborted with signal 15 (SIGTERM)");
2306
2307 let unix_no_name = AbortDescription::UnixSignal {
2309 signal: 42,
2310 name: None,
2311 };
2312 assert_eq!(unix_no_name.to_string(), "aborted with signal 42");
2313
2314 let windows = AbortDescription::WindowsNtStatus {
2316 code: -1073741510,
2317 message: Some("CTRL+C exit".into()),
2318 };
2319 assert_eq!(
2320 windows.to_string(),
2321 "aborted with code 0xc000013a: CTRL+C exit"
2322 );
2323
2324 let windows_no_msg = AbortDescription::WindowsNtStatus {
2326 code: -1073741510,
2327 message: None,
2328 };
2329 assert_eq!(windows_no_msg.to_string(), "aborted with code 0xc000013a");
2330
2331 let job = AbortDescription::WindowsJobObject;
2333 assert_eq!(job.to_string(), "terminated via job object");
2334 }
2335
2336 #[cfg(unix)]
2337 #[test]
2338 fn abort_description_from_abort_status() {
2339 let status = AbortStatus::UnixSignal(15);
2341 let description = AbortDescription::from(status);
2342
2343 assert_eq!(
2344 description,
2345 AbortDescription::UnixSignal {
2346 signal: 15,
2347 name: Some("TERM".into()),
2348 }
2349 );
2350
2351 let unknown_status = AbortStatus::UnixSignal(42);
2353 let unknown_description = AbortDescription::from(unknown_status);
2354 assert_eq!(
2355 unknown_description,
2356 AbortDescription::UnixSignal {
2357 signal: 42,
2358 name: None,
2359 }
2360 );
2361 }
2362
2363 #[test]
2364 fn execution_result_description_serialization() {
2365 let pass = ExecutionResultDescription::Pass;
2369 let json = serde_json::to_string_pretty(&pass).unwrap();
2370 insta::assert_snapshot!("pass", json);
2371 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2372 assert_eq!(pass, roundtrip);
2373
2374 let leak_pass = ExecutionResultDescription::Leak {
2376 result: LeakTimeoutResult::Pass,
2377 };
2378 let json = serde_json::to_string_pretty(&leak_pass).unwrap();
2379 insta::assert_snapshot!("leak_pass", json);
2380 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2381 assert_eq!(leak_pass, roundtrip);
2382
2383 let leak_fail = ExecutionResultDescription::Leak {
2385 result: LeakTimeoutResult::Fail,
2386 };
2387 let json = serde_json::to_string_pretty(&leak_fail).unwrap();
2388 insta::assert_snapshot!("leak_fail", json);
2389 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2390 assert_eq!(leak_fail, roundtrip);
2391
2392 let fail_exit_code = ExecutionResultDescription::Fail {
2394 failure: FailureDescription::ExitCode { code: 101 },
2395 leaked: false,
2396 };
2397 let json = serde_json::to_string_pretty(&fail_exit_code).unwrap();
2398 insta::assert_snapshot!("fail_exit_code", json);
2399 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2400 assert_eq!(fail_exit_code, roundtrip);
2401
2402 let fail_exit_code_leaked = ExecutionResultDescription::Fail {
2404 failure: FailureDescription::ExitCode { code: 1 },
2405 leaked: true,
2406 };
2407 let json = serde_json::to_string_pretty(&fail_exit_code_leaked).unwrap();
2408 insta::assert_snapshot!("fail_exit_code_leaked", json);
2409 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2410 assert_eq!(fail_exit_code_leaked, roundtrip);
2411
2412 let fail_unix_signal = ExecutionResultDescription::Fail {
2414 failure: FailureDescription::Abort {
2415 abort: AbortDescription::UnixSignal {
2416 signal: 11,
2417 name: Some("SEGV".into()),
2418 },
2419 },
2420 leaked: false,
2421 };
2422 let json = serde_json::to_string_pretty(&fail_unix_signal).unwrap();
2423 insta::assert_snapshot!("fail_unix_signal", json);
2424 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2425 assert_eq!(fail_unix_signal, roundtrip);
2426
2427 let fail_unix_signal_unknown = ExecutionResultDescription::Fail {
2429 failure: FailureDescription::Abort {
2430 abort: AbortDescription::UnixSignal {
2431 signal: 42,
2432 name: None,
2433 },
2434 },
2435 leaked: true,
2436 };
2437 let json = serde_json::to_string_pretty(&fail_unix_signal_unknown).unwrap();
2438 insta::assert_snapshot!("fail_unix_signal_unknown_leaked", json);
2439 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2440 assert_eq!(fail_unix_signal_unknown, roundtrip);
2441
2442 let fail_windows_nt = ExecutionResultDescription::Fail {
2444 failure: FailureDescription::Abort {
2445 abort: AbortDescription::WindowsNtStatus {
2446 code: -1073741510,
2447 message: Some("The application terminated as a result of a CTRL+C.".into()),
2448 },
2449 },
2450 leaked: false,
2451 };
2452 let json = serde_json::to_string_pretty(&fail_windows_nt).unwrap();
2453 insta::assert_snapshot!("fail_windows_nt_status", json);
2454 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2455 assert_eq!(fail_windows_nt, roundtrip);
2456
2457 let fail_windows_nt_no_msg = ExecutionResultDescription::Fail {
2459 failure: FailureDescription::Abort {
2460 abort: AbortDescription::WindowsNtStatus {
2461 code: -1073741819,
2462 message: None,
2463 },
2464 },
2465 leaked: false,
2466 };
2467 let json = serde_json::to_string_pretty(&fail_windows_nt_no_msg).unwrap();
2468 insta::assert_snapshot!("fail_windows_nt_status_no_message", json);
2469 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2470 assert_eq!(fail_windows_nt_no_msg, roundtrip);
2471
2472 let fail_job_object = ExecutionResultDescription::Fail {
2474 failure: FailureDescription::Abort {
2475 abort: AbortDescription::WindowsJobObject,
2476 },
2477 leaked: false,
2478 };
2479 let json = serde_json::to_string_pretty(&fail_job_object).unwrap();
2480 insta::assert_snapshot!("fail_windows_job_object", json);
2481 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2482 assert_eq!(fail_job_object, roundtrip);
2483
2484 let exec_fail = ExecutionResultDescription::ExecFail;
2486 let json = serde_json::to_string_pretty(&exec_fail).unwrap();
2487 insta::assert_snapshot!("exec_fail", json);
2488 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2489 assert_eq!(exec_fail, roundtrip);
2490
2491 let timeout_pass = ExecutionResultDescription::Timeout {
2493 result: SlowTimeoutResult::Pass,
2494 };
2495 let json = serde_json::to_string_pretty(&timeout_pass).unwrap();
2496 insta::assert_snapshot!("timeout_pass", json);
2497 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2498 assert_eq!(timeout_pass, roundtrip);
2499
2500 let timeout_fail = ExecutionResultDescription::Timeout {
2502 result: SlowTimeoutResult::Fail,
2503 };
2504 let json = serde_json::to_string_pretty(&timeout_fail).unwrap();
2505 insta::assert_snapshot!("timeout_fail", json);
2506 let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2507 assert_eq!(timeout_fail, roundtrip);
2508 }
2509}