nextest_runner/reporter/
events.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Events for the reporter.
5//!
6//! These types form the interface between the test runner and the test
7//! reporter. The root structure for all events is [`TestEvent`].
8
9use 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
29/// The signal number for SIGTERM.
30///
31/// This is 15 on all platforms. We define it here rather than using `SIGTERM` because
32/// `SIGTERM` is not available on Windows, but the value is platform-independent.
33pub const SIGTERM: c_int = 15;
34
35/// A reporter event.
36#[derive(Clone, Debug)]
37pub enum ReporterEvent<'a> {
38    /// A periodic tick.
39    Tick,
40
41    /// A test event.
42    Test(Box<TestEvent<'a>>),
43}
44/// A test event.
45///
46/// Events are produced by a [`TestRunner`](crate::runner::TestRunner) and
47/// consumed by a [`Reporter`](crate::reporter::Reporter).
48#[derive(Clone, Debug)]
49pub struct TestEvent<'a> {
50    /// The time at which the event was generated, including the offset from UTC.
51    pub timestamp: DateTime<FixedOffset>,
52
53    /// The amount of time elapsed since the start of the test run.
54    pub elapsed: Duration,
55
56    /// The kind of test event this is.
57    pub kind: TestEventKind<'a>,
58}
59
60/// The kind of test event this is.
61///
62/// Forms part of [`TestEvent`].
63#[derive(Clone, Debug)]
64pub enum TestEventKind<'a> {
65    /// The test run started.
66    RunStarted {
67        /// The list of tests that will be run.
68        ///
69        /// The methods on the test list indicate the number of tests that will be run.
70        test_list: &'a TestList<'a>,
71
72        /// The UUID for this run.
73        run_id: ReportUuid,
74
75        /// The nextest profile chosen for this run.
76        profile_name: String,
77
78        /// The command-line arguments for the process.
79        cli_args: Vec<String>,
80
81        /// The stress condition for this run, if any.
82        stress_condition: Option<StressCondition>,
83    },
84
85    /// When running stress tests serially, a sub-run started.
86    StressSubRunStarted {
87        /// The amount of progress completed so far.
88        progress: StressProgress,
89    },
90
91    /// A setup script started.
92    SetupScriptStarted {
93        /// If a stress test is being run, the stress index, starting from 0.
94        stress_index: Option<StressIndex>,
95
96        /// The setup script index.
97        index: usize,
98
99        /// The total number of setup scripts.
100        total: usize,
101
102        /// The script ID.
103        script_id: ScriptId,
104
105        /// The program to run.
106        program: String,
107
108        /// The arguments to the program.
109        args: &'a [String],
110
111        /// True if some output from the setup script is being passed through.
112        no_capture: bool,
113    },
114
115    /// A setup script was slow.
116    SetupScriptSlow {
117        /// If a stress test is being run, the stress index, starting from 0.
118        stress_index: Option<StressIndex>,
119
120        /// The script ID.
121        script_id: ScriptId,
122
123        /// The program to run.
124        program: String,
125
126        /// The arguments to the program.
127        args: &'a [String],
128
129        /// The amount of time elapsed since the start of execution.
130        elapsed: Duration,
131
132        /// True if the script has hit its timeout and is about to be terminated.
133        will_terminate: bool,
134    },
135
136    /// A setup script completed execution.
137    SetupScriptFinished {
138        /// If a stress test is being run, the stress index, starting from 0.
139        stress_index: Option<StressIndex>,
140
141        /// The setup script index.
142        index: usize,
143
144        /// The total number of setup scripts.
145        total: usize,
146
147        /// The script ID.
148        script_id: ScriptId,
149
150        /// The program to run.
151        program: String,
152
153        /// The arguments to the program.
154        args: &'a [String],
155
156        /// Whether the JUnit report should store success output for this script.
157        junit_store_success_output: bool,
158
159        /// Whether the JUnit report should store failure output for this script.
160        junit_store_failure_output: bool,
161
162        /// True if some output from the setup script was passed through.
163        no_capture: bool,
164
165        /// The execution status of the setup script.
166        run_status: SetupScriptExecuteStatus,
167    },
168
169    // TODO: add events for BinaryStarted and BinaryFinished? May want a slightly different way to
170    // do things, maybe a couple of reporter traits (one for the run as a whole and one for each
171    // binary).
172    /// A test started running.
173    TestStarted {
174        /// If a stress test is being run, the stress index, starting from 0.
175        stress_index: Option<StressIndex>,
176
177        /// The test instance that was started.
178        test_instance: TestInstanceId<'a>,
179
180        /// Current run statistics so far.
181        current_stats: RunStats,
182
183        /// The number of tests currently running, including this one.
184        running: usize,
185
186        /// The command line that will be used to run this test.
187        command_line: Vec<String>,
188    },
189
190    /// A test was slower than a configured soft timeout.
191    TestSlow {
192        /// If a stress test is being run, the stress index, starting from 0.
193        stress_index: Option<StressIndex>,
194
195        /// The test instance that was slow.
196        test_instance: TestInstanceId<'a>,
197
198        /// Retry data.
199        retry_data: RetryData,
200
201        /// The amount of time that has elapsed since the beginning of the test.
202        elapsed: Duration,
203
204        /// True if the test has hit its timeout and is about to be terminated.
205        will_terminate: bool,
206    },
207
208    /// A test attempt failed and will be retried in the future.
209    ///
210    /// This event does not occur on the final run of a failing test.
211    TestAttemptFailedWillRetry {
212        /// If a stress test is being run, the stress index, starting from 0.
213        stress_index: Option<StressIndex>,
214
215        /// The test instance that is being retried.
216        test_instance: TestInstanceId<'a>,
217
218        /// The status of this attempt to run the test. Will never be success.
219        run_status: ExecuteStatus,
220
221        /// The delay before the next attempt to run the test.
222        delay_before_next_attempt: Duration,
223
224        /// Whether failure outputs are printed out.
225        failure_output: TestOutputDisplay,
226
227        /// The current number of running tests.
228        running: usize,
229    },
230
231    /// A retry has started.
232    TestRetryStarted {
233        /// If a stress test is being run, the stress index, starting from 0.
234        stress_index: Option<StressIndex>,
235
236        /// The test instance that is being retried.
237        test_instance: TestInstanceId<'a>,
238
239        /// Data related to retries.
240        retry_data: RetryData,
241
242        /// The current number of running tests.
243        running: usize,
244
245        /// The command line that will be used to run this test.
246        command_line: Vec<String>,
247    },
248
249    /// A test finished running.
250    TestFinished {
251        /// If a stress test is being run, the stress index, starting from 0.
252        stress_index: Option<StressIndex>,
253
254        /// The test instance that finished running.
255        test_instance: TestInstanceId<'a>,
256
257        /// Test setting for success output.
258        success_output: TestOutputDisplay,
259
260        /// Test setting for failure output.
261        failure_output: TestOutputDisplay,
262
263        /// Whether the JUnit report should store success output for this test.
264        junit_store_success_output: bool,
265
266        /// Whether the JUnit report should store failure output for this test.
267        junit_store_failure_output: bool,
268
269        /// Information about all the runs for this test.
270        run_statuses: ExecutionStatuses,
271
272        /// Current statistics for number of tests so far.
273        current_stats: RunStats,
274
275        /// The number of tests that are currently running, excluding this one.
276        running: usize,
277    },
278
279    /// A test was skipped.
280    TestSkipped {
281        /// If a stress test is being run, the stress index, starting from 0.
282        stress_index: Option<StressIndex>,
283
284        /// The test instance that was skipped.
285        test_instance: TestInstanceId<'a>,
286
287        /// The reason this test was skipped.
288        reason: MismatchReason,
289    },
290
291    /// An information request was received.
292    InfoStarted {
293        /// The number of tasks currently running. This is the same as the
294        /// number of expected responses.
295        total: usize,
296
297        /// Statistics for the run.
298        run_stats: RunStats,
299    },
300
301    /// Information about a script or test was received.
302    InfoResponse {
303        /// The index of the response, starting from 0.
304        index: usize,
305
306        /// The total number of responses expected.
307        total: usize,
308
309        /// The response itself.
310        response: InfoResponse<'a>,
311    },
312
313    /// An information request was completed.
314    InfoFinished {
315        /// The number of responses that were not received. In most cases, this
316        /// is 0.
317        missing: usize,
318    },
319
320    /// `Enter` was pressed. Either a newline or a progress bar snapshot needs
321    /// to be printed.
322    InputEnter {
323        /// Current statistics for number of tests so far.
324        current_stats: RunStats,
325
326        /// The number of tests running.
327        running: usize,
328    },
329
330    /// A cancellation notice was received.
331    RunBeginCancel {
332        /// The number of setup scripts still running.
333        setup_scripts_running: usize,
334
335        /// Current statistics for number of tests so far.
336        ///
337        /// `current_stats.cancel_reason` is set to `Some`.
338        current_stats: RunStats,
339
340        /// The number of tests still running.
341        running: usize,
342    },
343
344    /// A forcible kill was requested due to receiving a signal.
345    RunBeginKill {
346        /// The number of setup scripts still running.
347        setup_scripts_running: usize,
348
349        /// Current statistics for number of tests so far.
350        ///
351        /// `current_stats.cancel_reason` is set to `Some`.
352        current_stats: RunStats,
353
354        /// The number of tests still running.
355        running: usize,
356    },
357
358    /// A SIGTSTP event was received and the run was paused.
359    RunPaused {
360        /// The number of setup scripts running.
361        setup_scripts_running: usize,
362
363        /// The number of tests currently running.
364        running: usize,
365    },
366
367    /// A SIGCONT event was received and the run is being continued.
368    RunContinued {
369        /// The number of setup scripts that will be started up again.
370        setup_scripts_running: usize,
371
372        /// The number of tests that will be started up again.
373        running: usize,
374    },
375
376    /// When running stress tests serially, a sub-run finished.
377    StressSubRunFinished {
378        /// The amount of progress completed so far.
379        progress: StressProgress,
380
381        /// The amount of time it took for this sub-run to complete.
382        sub_elapsed: Duration,
383
384        /// Statistics for the sub-run.
385        sub_stats: RunStats,
386    },
387
388    /// The test run finished.
389    RunFinished {
390        /// The unique ID for this run.
391        run_id: ReportUuid,
392
393        /// The time at which the run was started.
394        start_time: DateTime<FixedOffset>,
395
396        /// The amount of time it took for the tests to run.
397        elapsed: Duration,
398
399        /// Statistics for the run, or overall statistics for stress tests.
400        run_stats: RunFinishedStats,
401    },
402}
403
404/// Progress for a stress test.
405#[derive(Clone, Debug)]
406pub enum StressProgress {
407    /// This is a count-based stress run.
408    Count {
409        /// The total number of stress runs.
410        total: StressCount,
411
412        /// The total time that has elapsed across all stress runs so far.
413        elapsed: Duration,
414
415        /// The number of stress runs that have been completed.
416        completed: u32,
417    },
418
419    /// This is a time-based stress run.
420    Time {
421        /// The total time for the stress run.
422        total: Duration,
423
424        /// The total time that has elapsed across all stress runs so far.
425        elapsed: Duration,
426
427        /// The number of stress runs that have been completed.
428        completed: u32,
429    },
430}
431
432impl StressProgress {
433    /// Returns the remaining amount of work if the progress indicates there's
434    /// still more to do, otherwise `None`.
435    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    /// Returns a unique ID for this stress sub-run, consisting of the run ID and stress index.
459    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/// For a stress test, the amount of time or number of stress runs remaining.
468#[derive(Clone, Debug)]
469pub enum StressRemaining {
470    /// The number of stress runs remaining, guaranteed to be non-zero.
471    Count(NonZero<u32>),
472
473    /// Infinite number of stress runs remaining.
474    Infinite,
475
476    /// The amount of time remaining.
477    Time(Duration),
478}
479
480/// The index of the current stress run.
481#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
482pub struct StressIndex {
483    /// The 0-indexed index.
484    pub current: u32,
485
486    /// The total number of stress runs, if that is available.
487    pub total: Option<NonZero<u32>>,
488}
489
490/// Statistics for a completed test run or stress run.
491#[derive(Clone, Debug)]
492pub enum RunFinishedStats {
493    /// A single test run was completed.
494    Single(RunStats),
495
496    /// A stress run was completed.
497    Stress(StressRunStats),
498}
499
500impl RunFinishedStats {
501    /// For a single run, returns a summary of statistics as an enum. For a
502    /// stress run, returns a summary for the last sub-run.
503    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/// Statistics for a test run.
512#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
513pub struct RunStats {
514    /// The total number of tests that were expected to be run at the beginning.
515    ///
516    /// If the test run is cancelled, this will be more than `finished_count` at the end.
517    pub initial_run_count: usize,
518
519    /// The total number of tests that finished running.
520    pub finished_count: usize,
521
522    /// The total number of setup scripts that were expected to be run at the beginning.
523    ///
524    /// If the test run is cancelled, this will be more than `finished_count` at the end.
525    pub setup_scripts_initial_count: usize,
526
527    /// The total number of setup scripts that finished running.
528    pub setup_scripts_finished_count: usize,
529
530    /// The number of setup scripts that passed.
531    pub setup_scripts_passed: usize,
532
533    /// The number of setup scripts that failed.
534    pub setup_scripts_failed: usize,
535
536    /// The number of setup scripts that encountered an execution failure.
537    pub setup_scripts_exec_failed: usize,
538
539    /// The number of setup scripts that timed out.
540    pub setup_scripts_timed_out: usize,
541
542    /// The number of tests that passed. Includes `passed_slow`, `passed_timed_out`, `flaky` and `leaky`.
543    pub passed: usize,
544
545    /// The number of slow tests that passed.
546    pub passed_slow: usize,
547
548    /// The number of timed out tests that passed.
549    pub passed_timed_out: usize,
550
551    /// The number of tests that passed on retry.
552    pub flaky: usize,
553
554    /// The number of tests that failed. Includes `leaky_failed`.
555    pub failed: usize,
556
557    /// The number of failed tests that were slow.
558    pub failed_slow: usize,
559
560    /// The number of timed out tests that failed.
561    pub failed_timed_out: usize,
562
563    /// The number of tests that passed but leaked handles.
564    pub leaky: usize,
565
566    /// The number of tests that otherwise passed, but leaked handles and were
567    /// treated as failed as a result.
568    pub leaky_failed: usize,
569
570    /// The number of tests that encountered an execution failure.
571    pub exec_failed: usize,
572
573    /// The number of tests that were skipped.
574    pub skipped: usize,
575
576    /// If the run is cancelled, the reason the cancellation is happening.
577    pub cancel_reason: Option<CancelReason>,
578}
579
580impl RunStats {
581    /// Returns true if there are any failures recorded in the stats.
582    pub fn has_failures(&self) -> bool {
583        self.failed_setup_script_count() > 0 || self.failed_count() > 0
584    }
585
586    /// Returns count of setup scripts that did not pass.
587    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    /// Returns count of tests that did not pass.
592    pub fn failed_count(&self) -> usize {
593        self.failed + self.exec_failed + self.failed_timed_out
594    }
595
596    /// Summarizes the stats as an enum at the end of a test run.
597    pub fn summarize_final(&self) -> FinalRunStats {
598        // Check for failures first. The order of setup scripts vs tests should
599        // not be important, though we don't assert that here.
600        if self.failed_setup_script_count() > 0 {
601            // Is this related to a cancellation other than one directly caused
602            // by the failure?
603            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            // Is this related to a cancellation other than one directly caused
623            // by the failure?
624            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            // Timed out setup scripts are always treated as failures.
667            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        // run_statuses is guaranteed to have at least one element.
676        // * If the last element is success, treat it as success (and possibly flaky).
677        // * If the last element is a failure, use it to determine fail/exec fail.
678        // Note that this is different from what Maven Surefire does (use the first failure):
679        // https://maven.apache.org/surefire/maven-surefire-plugin/examples/rerun-failing-tests.html
680        //
681        // This is not likely to matter much in practice since failures are likely to be of the
682        // same type.
683        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/// A type summarizing the possible outcomes of a test run.
741#[derive(Copy, Clone, Debug, Eq, PartialEq)]
742pub enum FinalRunStats {
743    /// The test run was successful, or is successful so far.
744    Success,
745
746    /// The test run was successful, or is successful so far, but no tests were selected to run.
747    NoTestsRun,
748
749    /// The test run was cancelled.
750    Cancelled {
751        /// The reason for cancellation, if available.
752        ///
753        /// This should generally be available, but may be None if some tests
754        /// that were selected to run were not executed.
755        reason: Option<CancelReason>,
756
757        /// The kind of failure that occurred.
758        kind: RunStatsFailureKind,
759    },
760
761    /// At least one test failed.
762    Failed(RunStatsFailureKind),
763}
764
765/// Statistics for a stress run.
766#[derive(Clone, Debug)]
767pub struct StressRunStats {
768    /// The number of stress runs completed.
769    pub completed: StressIndex,
770
771    /// The number of stress runs that succeeded.
772    pub success_count: u32,
773
774    /// The number of stress runs that failed.
775    pub failed_count: u32,
776
777    /// The last stress run's `FinalRunStats`.
778    pub last_final_stats: FinalRunStats,
779}
780
781impl StressRunStats {
782    /// Summarizes the stats as an enum at the end of a test run.
783    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
796/// A summary of final statistics for a stress run.
797pub enum StressFinalRunStats {
798    /// The stress run was successful.
799    Success,
800
801    /// No tests were run.
802    NoTestsRun,
803
804    /// The stress run was cancelled.
805    Cancelled,
806
807    /// At least one stress run failed.
808    Failed,
809}
810
811/// A type summarizing the step at which a test run failed.
812#[derive(Copy, Clone, Debug, Eq, PartialEq)]
813pub enum RunStatsFailureKind {
814    /// The run was interrupted during setup script execution.
815    SetupScript,
816
817    /// The run was interrupted during test execution.
818    Test {
819        /// The total number of tests scheduled.
820        initial_run_count: usize,
821
822        /// The number of tests not run, or for a currently-executing test the number queued up to
823        /// run.
824        not_run: usize,
825    },
826}
827
828/// Information about executions of a test, including retries.
829#[derive(Clone, Debug)]
830pub struct ExecutionStatuses {
831    /// This is guaranteed to be non-empty.
832    statuses: Vec<ExecuteStatus>,
833}
834
835#[expect(clippy::len_without_is_empty)] // RunStatuses is never empty
836impl ExecutionStatuses {
837    pub(crate) fn new(statuses: Vec<ExecuteStatus>) -> Self {
838        Self { statuses }
839    }
840
841    /// Returns the last execution status.
842    ///
843    /// This status is typically used as the final result.
844    pub fn last_status(&self) -> &ExecuteStatus {
845        self.statuses
846            .last()
847            .expect("execution statuses is non-empty")
848    }
849
850    /// Iterates over all the statuses.
851    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus> + '_ {
852        self.statuses.iter()
853    }
854
855    /// Returns the number of times the test was executed.
856    pub fn len(&self) -> usize {
857        self.statuses.len()
858    }
859
860    /// Returns a description of self.
861    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/// A description of test executions obtained from `ExecuteStatuses`.
890///
891/// This can be used to quickly determine whether a test passed, failed or was flaky.
892#[derive(Copy, Clone, Debug)]
893pub enum ExecutionDescription<'a> {
894    /// The test was run once and was successful.
895    Success {
896        /// The status of the test.
897        single_status: &'a ExecuteStatus,
898    },
899
900    /// The test was run more than once. The final result was successful.
901    Flaky {
902        /// The last, successful status.
903        last_status: &'a ExecuteStatus,
904
905        /// Previous statuses, none of which are successes.
906        prior_statuses: &'a [ExecuteStatus],
907    },
908
909    /// The test was run once, or possibly multiple times. All runs failed.
910    Failure {
911        /// The first, failing status.
912        first_status: &'a ExecuteStatus,
913
914        /// The last, failing status. Same as the first status if no retries were performed.
915        last_status: &'a ExecuteStatus,
916
917        /// Any retries that were performed. All of these runs failed.
918        ///
919        /// May be empty.
920        retries: &'a [ExecuteStatus],
921    },
922}
923
924impl<'a> ExecutionDescription<'a> {
925    /// Returns the status level for this `ExecutionDescription`.
926    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            // A flaky test implies that we print out retry information for it.
941            ExecutionDescription::Flaky { .. } => StatusLevel::Retry,
942            ExecutionDescription::Failure { .. } => StatusLevel::Fail,
943        }
944    }
945
946    /// Returns the final status level for this `ExecutionDescription`.
947    pub fn final_status_level(&self) -> FinalStatusLevel {
948        match self {
949            ExecutionDescription::Success { single_status, .. } => {
950                // Slow is higher priority than leaky, so return slow first here.
951                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                        // Timeout with Pass should return Slow, but this case
960                        // shouldn't be reached because is_slow is true for
961                        // timeout scenarios. Handle it for completeness.
962                        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            // A flaky test implies that we print out retry information for it.
972            ExecutionDescription::Flaky { .. } => FinalStatusLevel::Flaky,
973            ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
974        }
975    }
976
977    /// Returns the last run status.
978    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/// Pre-computed error summary for display.
990///
991/// This contains the formatted error messages, pre-computed from the execution
992/// output and result. Useful for record-replay scenarios where the rendering
993/// is done on the server.
994#[derive(Clone, Debug)]
995pub struct ErrorSummary {
996    /// A short summary of the error, suitable for display in a single line.
997    pub short_message: String,
998
999    /// A full description of the error chain, suitable for detailed display.
1000    pub description: String,
1001}
1002
1003/// Pre-computed output error slice for display.
1004///
1005/// This contains an error message heuristically extracted from test output,
1006/// such as a panic message or error string.
1007#[derive(Clone, Debug)]
1008pub struct OutputErrorSlice {
1009    /// The extracted error slice as a string.
1010    pub slice: String,
1011
1012    /// The byte offset in the original output where this slice starts.
1013    pub start: usize,
1014}
1015
1016/// Information about a single execution of a test.
1017///
1018/// This is the external-facing type used by reporters. The `result` field uses
1019/// [`ExecutionResultDescription`], a platform-independent type that can be
1020/// serialized and deserialized across platforms.
1021#[derive(Clone, Debug)]
1022pub struct ExecuteStatus {
1023    /// Retry-related data.
1024    pub retry_data: RetryData,
1025    /// The stdout and stderr output for this test.
1026    pub output: ChildExecutionOutputDescription,
1027    /// The execution result for this test: pass, fail or execution error.
1028    pub result: ExecutionResultDescription,
1029    /// The time at which the test started.
1030    pub start_time: DateTime<FixedOffset>,
1031    /// The time it took for the test to run.
1032    pub time_taken: Duration,
1033    /// Whether this test counts as slow.
1034    pub is_slow: bool,
1035    /// The delay will be non-zero if this is a retry and delay was specified.
1036    pub delay_before_start: Duration,
1037    /// Pre-computed error summary, if available.
1038    ///
1039    /// This is computed from the execution output and result, and can be used
1040    /// for display without needing to re-compute the error chain.
1041    pub error_summary: Option<ErrorSummary>,
1042    /// Pre-computed output error slice, if available.
1043    ///
1044    /// This is a heuristically extracted error message from the test output,
1045    /// such as a panic message or error string.
1046    pub output_error_slice: Option<OutputErrorSlice>,
1047}
1048
1049/// Information about the execution of a setup script.
1050///
1051/// This is the external-facing type used by reporters. The `result` field uses
1052/// [`ExecutionResultDescription`], a platform-independent type that can be
1053/// serialized and deserialized across platforms.
1054#[derive(Clone, Debug)]
1055pub struct SetupScriptExecuteStatus {
1056    /// Output for this setup script.
1057    pub output: ChildExecutionOutputDescription,
1058
1059    /// The execution result for this setup script: pass, fail or execution error.
1060    pub result: ExecutionResultDescription,
1061
1062    /// The time at which the script started.
1063    pub start_time: DateTime<FixedOffset>,
1064
1065    /// The time it took for the script to run.
1066    pub time_taken: Duration,
1067
1068    /// Whether this script counts as slow.
1069    pub is_slow: bool,
1070
1071    /// The map of environment variables that were set by this script.
1072    ///
1073    /// `None` if an error occurred while running the script or reading the
1074    /// environment map.
1075    pub env_map: Option<SetupScriptEnvMap>,
1076
1077    /// Pre-computed error summary, if available.
1078    ///
1079    /// This is computed from the execution output and result, and can be used
1080    /// for display without needing to re-compute the error chain.
1081    pub error_summary: Option<ErrorSummary>,
1082}
1083
1084/// A map of environment variables set by a setup script.
1085///
1086/// Part of [`SetupScriptExecuteStatus`].
1087#[derive(Clone, Debug)]
1088pub struct SetupScriptEnvMap {
1089    /// The map of environment variables set by the script.
1090    pub env_map: BTreeMap<String, String>,
1091}
1092
1093// ---
1094// Child execution output description types
1095// ---
1096
1097/// The result of executing a child process, in a serializable format.
1098///
1099/// This is the external-facing counterpart to [`ChildExecutionOutput`].
1100#[derive(Clone, Debug)]
1101pub enum ChildExecutionOutputDescription {
1102    /// The process was run and the output was captured.
1103    Output {
1104        /// If the process has finished executing, the final state it is in.
1105        ///
1106        /// `None` means execution is currently in progress.
1107        result: Option<ExecutionResultDescription>,
1108
1109        /// The captured output.
1110        output: ChildOutput,
1111
1112        /// Errors that occurred while waiting on the child process or parsing
1113        /// its output.
1114        errors: Option<ErrorList<ChildErrorDescription>>,
1115    },
1116
1117    /// There was a failure to start the process.
1118    StartError(ChildStartErrorDescription),
1119}
1120
1121impl ChildExecutionOutputDescription {
1122    /// Returns true if there are any errors in this output.
1123    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/// A serializable description of an error that occurred while starting a child process.
1140///
1141/// This is the external-facing counterpart to [`ChildStartError`].
1142#[derive(Clone, Debug)]
1143pub enum ChildStartErrorDescription {
1144    /// An error occurred while creating a temporary path for a setup script.
1145    TempPath {
1146        /// The source error.
1147        source: IoErrorDescription,
1148    },
1149
1150    /// An error occurred while spawning the child process.
1151    Spawn {
1152        /// The source error.
1153        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/// A serializable description of an error that occurred while managing a child process.
1177///
1178/// This is the external-facing counterpart to [`ChildError`].
1179#[derive(Clone, Debug)]
1180pub enum ChildErrorDescription {
1181    /// An error occurred while reading standard output.
1182    ReadStdout {
1183        /// The source error.
1184        source: IoErrorDescription,
1185    },
1186
1187    /// An error occurred while reading standard error.
1188    ReadStderr {
1189        /// The source error.
1190        source: IoErrorDescription,
1191    },
1192
1193    /// An error occurred while reading combined output.
1194    ReadCombined {
1195        /// The source error.
1196        source: IoErrorDescription,
1197    },
1198
1199    /// An error occurred while waiting for the child process to exit.
1200    Wait {
1201        /// The source error.
1202        source: IoErrorDescription,
1203    },
1204
1205    /// An error occurred while reading the output of a setup script.
1206    SetupScriptOutput {
1207        /// The source error.
1208        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/// A serializable description of an I/O error.
1243///
1244/// This captures the error message from an [`std::io::Error`].
1245#[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/// Data related to retries for a test.
1327#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
1328pub struct RetryData {
1329    /// The current attempt. In the range `[1, total_attempts]`.
1330    pub attempt: u32,
1331
1332    /// The total number of times this test can be run. Equal to `1 + retries`.
1333    pub total_attempts: u32,
1334}
1335
1336impl RetryData {
1337    /// Returns true if there are no more attempts after this.
1338    pub fn is_last_attempt(&self) -> bool {
1339        self.attempt >= self.total_attempts
1340    }
1341}
1342
1343/// Whether a test passed, failed or an error occurred while executing the test.
1344#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1345pub enum ExecutionResult {
1346    /// The test passed.
1347    Pass,
1348    /// The test passed but leaked handles. This usually indicates that
1349    /// a subprocess that inherit standard IO was created, but it didn't shut down when
1350    /// the test failed.
1351    Leak {
1352        /// Whether this leak was treated as a failure.
1353        ///
1354        /// Note the difference between `Fail { leaked: true }` and `Leak {
1355        /// failed: true }`. In the former case, the test failed and also leaked
1356        /// handles. In the latter case, the test passed but leaked handles, and
1357        /// configuration indicated that this is a failure.
1358        result: LeakTimeoutResult,
1359    },
1360    /// The test failed.
1361    Fail {
1362        /// The abort status of the test, if any (for example, the signal on Unix).
1363        failure_status: FailureStatus,
1364
1365        /// Whether a test leaked handles. If set to true, this usually indicates that
1366        /// a subprocess that inherit standard IO was created, but it didn't shut down when
1367        /// the test failed.
1368        leaked: bool,
1369    },
1370    /// An error occurred while executing the test.
1371    ExecFail,
1372    /// The test was terminated due to a timeout.
1373    Timeout {
1374        /// Whether this timeout was treated as a failure.
1375        result: SlowTimeoutResult,
1376    },
1377}
1378
1379impl ExecutionResult {
1380    /// Returns true if the test was successful.
1381    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    /// Returns a static string representation of the result.
1402    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/// Failure status: either an exit code or an abort status.
1414#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1415pub enum FailureStatus {
1416    /// The test exited with a non-zero exit code.
1417    ExitCode(i32),
1418
1419    /// The test aborted.
1420    Abort(AbortStatus),
1421}
1422
1423impl FailureStatus {
1424    /// Extract the failure status from an `ExitStatus`.
1425    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/// A regular exit code or Windows NT abort status for a test.
1439///
1440/// Returned as part of the [`ExecutionResult::Fail`] variant.
1441#[derive(Copy, Clone, Eq, PartialEq)]
1442pub enum AbortStatus {
1443    /// The test was aborted due to a signal on Unix.
1444    #[cfg(unix)]
1445    UnixSignal(i32),
1446
1447    /// The test was determined to have aborted because the high bit was set on Windows.
1448    #[cfg(windows)]
1449    WindowsNtStatus(windows_sys::Win32::Foundation::NTSTATUS),
1450
1451    /// The test was terminated via job object on Windows.
1452    #[cfg(windows)]
1453    JobObject,
1454}
1455
1456impl AbortStatus {
1457    /// Extract the abort status from an [`ExitStatus`].
1458    pub fn extract(exit_status: ExitStatus) -> Option<Self> {
1459        cfg_if::cfg_if! {
1460            if #[cfg(unix)] {
1461                // On Unix, extract the signal if it's found.
1462                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/// A platform-independent description of an abort status.
1489///
1490/// This type can be serialized on one platform and deserialized on another,
1491/// containing all information needed for display without requiring
1492/// platform-specific lookups.
1493#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1494#[serde(tag = "kind", rename_all = "kebab-case")]
1495#[non_exhaustive]
1496pub enum AbortDescription {
1497    /// The process was aborted by a Unix signal.
1498    UnixSignal {
1499        /// The signal number.
1500        signal: i32,
1501        /// The signal name without the "SIG" prefix (e.g., "TERM", "SEGV"),
1502        /// if known.
1503        name: Option<SmolStr>,
1504    },
1505
1506    /// The process was aborted with a Windows NT status code.
1507    WindowsNtStatus {
1508        /// The NTSTATUS code.
1509        code: i32,
1510        /// The human-readable message from the Win32 error code, if available.
1511        message: Option<SmolStr>,
1512    },
1513
1514    /// The process was terminated via a Windows job object.
1515    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/// A platform-independent description of a test failure status.
1568///
1569/// This is the platform-independent counterpart to [`FailureStatus`].
1570#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1571#[serde(tag = "kind", rename_all = "kebab-case")]
1572#[non_exhaustive]
1573pub enum FailureDescription {
1574    /// The test exited with a non-zero exit code.
1575    ExitCode {
1576        /// The exit code.
1577        code: i32,
1578    },
1579
1580    /// The test was aborted (e.g., by a signal on Unix or NT status on Windows).
1581    ///
1582    /// Note: this is a struct variant rather than a newtype variant to ensure
1583    /// proper JSON nesting. Both `FailureDescription` and `AbortDescription`
1584    /// use `#[serde(tag = "kind")]`, and if this were a newtype variant, serde
1585    /// would flatten the inner type causing duplicate `"kind"` fields.
1586    Abort {
1587        /// The abort description.
1588        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/// A platform-independent description of a test execution result.
1613///
1614/// This is the platform-independent counterpart to [`ExecutionResult`], used
1615/// in external-facing types like [`ExecuteStatus`].
1616#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1617#[serde(tag = "status", rename_all = "kebab-case")]
1618#[non_exhaustive]
1619pub enum ExecutionResultDescription {
1620    /// The test passed.
1621    Pass,
1622
1623    /// The test passed but leaked handles.
1624    Leak {
1625        /// Whether this leak was treated as a failure.
1626        result: LeakTimeoutResult,
1627    },
1628
1629    /// The test failed.
1630    Fail {
1631        /// The failure status.
1632        failure: FailureDescription,
1633
1634        /// Whether the test leaked handles.
1635        leaked: bool,
1636    },
1637
1638    /// An error occurred while executing the test.
1639    ExecFail,
1640
1641    /// The test was terminated due to a timeout.
1642    Timeout {
1643        /// Whether this timeout was treated as a failure.
1644        result: SlowTimeoutResult,
1645    },
1646}
1647
1648impl ExecutionResultDescription {
1649    /// Returns true if the test was successful.
1650    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    /// Returns a static string representation of the result.
1671    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    /// Returns true if this result represents a test that was terminated by nextest
1682    /// (as opposed to failing naturally).
1683    ///
1684    /// This is used to suppress output spam when running under
1685    /// TestFailureImmediate.
1686    ///
1687    /// TODO: This is a heuristic that checks if the test was terminated by
1688    /// SIGTERM (Unix) or job object (Windows). In an edge case, a test could
1689    /// send SIGTERM to itself, which would incorrectly be detected as a
1690    /// nextest-initiated termination. A more robust solution would track which
1691    /// tests were explicitly sent termination signals by nextest.
1692    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// Note: the order here matters -- it indicates severity of cancellation
1732/// The reason why a test run is being cancelled.
1733#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
1734#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1735pub enum CancelReason {
1736    /// A setup script failed.
1737    SetupScriptFailure,
1738
1739    /// A test failed and --no-fail-fast wasn't specified.
1740    TestFailure,
1741
1742    /// An error occurred while reporting results.
1743    ReportError,
1744
1745    /// The global timeout was exceeded.
1746    GlobalTimeout,
1747
1748    /// A test failed and fail-fast with immediate termination was specified.
1749    TestFailureImmediate,
1750
1751    /// A termination signal (on Unix, SIGTERM or SIGHUP) was received.
1752    Signal,
1753
1754    /// An interrupt (on Unix, Ctrl-C) was received.
1755    Interrupt,
1756
1757    /// A second signal was received, and the run is being forcibly killed.
1758    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/// The kind of unit of work that nextest is executing.
1776#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1777pub enum UnitKind {
1778    /// A test.
1779    Test,
1780
1781    /// A script (e.g. a setup script).
1782    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/// A response to an information request.
1808#[derive(Clone, Debug)]
1809pub enum InfoResponse<'a> {
1810    /// A setup script's response.
1811    SetupScript(SetupScriptInfoResponse<'a>),
1812
1813    /// A test's response.
1814    Test(TestInfoResponse<'a>),
1815}
1816
1817/// A setup script's response to an information request.
1818#[derive(Clone, Debug)]
1819pub struct SetupScriptInfoResponse<'a> {
1820    /// The stress index of the setup script.
1821    pub stress_index: Option<StressIndex>,
1822
1823    /// The identifier of the setup script instance.
1824    pub script_id: ScriptId,
1825
1826    /// The program to run.
1827    pub program: String,
1828
1829    /// The list of arguments to the program.
1830    pub args: &'a [String],
1831
1832    /// The state of the setup script.
1833    pub state: UnitState,
1834
1835    /// Output obtained from the setup script.
1836    pub output: ChildExecutionOutputDescription,
1837}
1838
1839/// A test's response to an information request.
1840#[derive(Clone, Debug)]
1841pub struct TestInfoResponse<'a> {
1842    /// The stress index of the test.
1843    pub stress_index: Option<StressIndex>,
1844
1845    /// The test instance that the information is about.
1846    pub test_instance: TestInstanceId<'a>,
1847
1848    /// Information about retries.
1849    pub retry_data: RetryData,
1850
1851    /// The state of the test.
1852    pub state: UnitState,
1853
1854    /// Output obtained from the test.
1855    pub output: ChildExecutionOutputDescription,
1856}
1857
1858/// The current state of a test or script process: running, exiting, or
1859/// terminating.
1860///
1861/// Part of information response requests.
1862#[derive(Clone, Debug)]
1863pub enum UnitState {
1864    /// The unit is currently running.
1865    Running {
1866        /// The process ID.
1867        pid: u32,
1868
1869        /// The amount of time the unit has been running.
1870        time_taken: Duration,
1871
1872        /// `Some` if the test is marked as slow, along with the duration after
1873        /// which it was marked as slow.
1874        slow_after: Option<Duration>,
1875    },
1876
1877    /// The test has finished running, and is currently in the process of
1878    /// exiting.
1879    Exiting {
1880        /// The process ID.
1881        pid: u32,
1882
1883        /// The amount of time the unit ran for.
1884        time_taken: Duration,
1885
1886        /// `Some` if the unit is marked as slow, along with the duration after
1887        /// which it was marked as slow.
1888        slow_after: Option<Duration>,
1889
1890        /// The tentative execution result before leaked status is determined.
1891        ///
1892        /// None means that the exit status could not be read, and should be
1893        /// treated as a failure.
1894        tentative_result: Option<ExecutionResult>,
1895
1896        /// How long has been spent waiting for the process to exit.
1897        waiting_duration: Duration,
1898
1899        /// How much longer nextest will wait until the test is marked leaky.
1900        remaining: Duration,
1901    },
1902
1903    /// The child process is being terminated by nextest.
1904    Terminating(UnitTerminatingState),
1905
1906    /// The unit has finished running and the process has exited.
1907    Exited {
1908        /// The result of executing the unit.
1909        result: ExecutionResult,
1910
1911        /// The amount of time the unit ran for.
1912        time_taken: Duration,
1913
1914        /// `Some` if the unit is marked as slow, along with the duration after
1915        /// which it was marked as slow.
1916        slow_after: Option<Duration>,
1917    },
1918
1919    /// A delay is being waited out before the next attempt of the test is
1920    /// started. (Only relevant for tests.)
1921    DelayBeforeNextAttempt {
1922        /// The previous execution result.
1923        previous_result: ExecutionResult,
1924
1925        /// Whether the previous attempt was marked as slow.
1926        previous_slow: bool,
1927
1928        /// How long has been spent waiting so far.
1929        waiting_duration: Duration,
1930
1931        /// How much longer nextest will wait until retrying the test.
1932        remaining: Duration,
1933    },
1934}
1935
1936impl UnitState {
1937    /// Returns true if the state has a valid output attached to it.
1938    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/// The current terminating state of a test or script process.
1950///
1951/// Part of [`UnitState::Terminating`].
1952#[derive(Clone, Debug)]
1953pub struct UnitTerminatingState {
1954    /// The process ID.
1955    pub pid: u32,
1956
1957    /// The amount of time the unit ran for.
1958    pub time_taken: Duration,
1959
1960    /// The reason for the termination.
1961    pub reason: UnitTerminateReason,
1962
1963    /// The method by which the process is being terminated.
1964    pub method: UnitTerminateMethod,
1965
1966    /// How long has been spent waiting for the process to exit.
1967    pub waiting_duration: Duration,
1968
1969    /// How much longer nextest will wait until a kill command is sent to the process.
1970    pub remaining: Duration,
1971}
1972
1973/// The reason for a script or test being forcibly terminated by nextest.
1974///
1975/// Part of information response requests.
1976#[derive(Clone, Copy, Debug)]
1977pub enum UnitTerminateReason {
1978    /// The unit is being terminated due to a test timeout being hit.
1979    Timeout,
1980
1981    /// The unit is being terminated due to nextest receiving a signal.
1982    Signal,
1983
1984    /// The unit is being terminated due to an interrupt (i.e. Ctrl-C).
1985    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/// The way in which a script or test is being forcibly terminated by nextest.
1999#[derive(Clone, Copy, Debug)]
2000pub enum UnitTerminateMethod {
2001    /// The unit is being terminated by sending a signal.
2002    #[cfg(unix)]
2003    Signal(UnitTerminateSignal),
2004
2005    /// The unit is being terminated by terminating the Windows job object.
2006    #[cfg(windows)]
2007    JobObject,
2008
2009    /// The unit is being waited on to exit. A termination signal will be sent
2010    /// if it doesn't exit within the grace period.
2011    ///
2012    /// On Windows, this occurs when nextest receives Ctrl-C. In that case, it
2013    /// is assumed that tests will also receive Ctrl-C and exit on their own. If
2014    /// tests do not exit within the grace period configured for them, their
2015    /// corresponding job objects will be terminated.
2016    #[cfg(windows)]
2017    Wait,
2018
2019    /// A fake method used for testing.
2020    #[cfg(test)]
2021    Fake,
2022}
2023
2024#[cfg(unix)]
2025/// The signal that is or was sent to terminate a script or test.
2026#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2027pub enum UnitTerminateSignal {
2028    /// The unit is being terminated by sending a SIGINT.
2029    Interrupt,
2030
2031    /// The unit is being terminated by sending a SIGTERM signal.
2032    Term,
2033
2034    /// The unit is being terminated by sending a SIGHUP signal.
2035    Hangup,
2036
2037    /// The unit is being terminated by sending a SIGQUIT signal.
2038    Quit,
2039
2040    /// The unit is being terminated by sending a SIGKILL signal.
2041    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        // Unix signal with name.
2222        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        // Unix signal without name.
2232        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        // Windows NT status (0xC000013A is STATUS_CONTROL_C_EXIT).
2242        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        // Windows NT status without message.
2252        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        // Windows job object.
2262        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        // Cross-platform deserialization: these JSON strings could come from any
2272        // platform. Verify they deserialize correctly regardless of current platform.
2273        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        // Unix signal with name.
2301        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        // Unix signal without a name.
2308        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        // Windows NT status with message.
2315        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        // Windows NT status without message.
2325        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        // Windows job object.
2332        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        // Test conversion from AbortStatus to AbortDescription on Unix.
2340        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        // Unknown signal.
2352        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        // Test all variants of ExecutionResultDescription for serialization roundtrips.
2366
2367        // Pass.
2368        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        // Leak with pass result.
2375        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        // Leak with fail result.
2384        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        // Fail with exit code, no leak.
2393        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        // Fail with exit code and leak.
2403        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        // Fail with Unix signal abort.
2413        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        // Fail with Unix signal abort (no name) and leak.
2428        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        // Fail with Windows NT status abort.
2443        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        // Fail with Windows NT status abort (no message).
2458        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        // Fail with Windows job object abort.
2473        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        // ExecFail.
2485        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        // Timeout with pass result.
2492        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        // Timeout with fail result.
2501        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}