Skip to main content

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};
10#[cfg(test)]
11use crate::output_spec::ArbitraryOutputSpec;
12use crate::{
13    config::{
14        elements::{
15            FlakyResult, JunitFlakyFailStatus, LeakTimeoutResult, ReportSkipPolicy,
16            SlowTimeoutResult, TestGroup,
17        },
18        scripts::ScriptId,
19    },
20    errors::{ChildError, ChildFdError, ChildStartError, ErrorList},
21    list::{OwnedTestInstanceId, TestInstanceId, TestList},
22    output_spec::{LiveSpec, OutputSpec, SerializableOutputSpec},
23    runner::{StressCondition, StressCount},
24    test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
25};
26use chrono::{DateTime, FixedOffset};
27use nextest_metadata::MismatchReason;
28use quick_junit::ReportUuid;
29use serde::{Deserialize, Serialize};
30use smol_str::SmolStr;
31use std::{
32    collections::BTreeMap, ffi::c_int, fmt, num::NonZero, process::ExitStatus, time::Duration,
33};
34
35/// The signal number for SIGTERM.
36///
37/// This is 15 on all platforms. We define it here rather than using `SIGTERM` because
38/// `SIGTERM` is not available on Windows, but the value is platform-independent.
39pub const SIGTERM: c_int = 15;
40
41/// A reporter event.
42#[derive(Clone, Debug)]
43pub enum ReporterEvent<'a> {
44    /// A periodic tick.
45    Tick,
46
47    /// A test event.
48    Test(Box<TestEvent<'a>>),
49}
50/// A test event.
51///
52/// Events are produced by a [`TestRunner`](crate::runner::TestRunner) and
53/// consumed by a [`Reporter`](crate::reporter::Reporter).
54#[derive(Clone, Debug)]
55pub struct TestEvent<'a> {
56    /// The time at which the event was generated, including the offset from UTC.
57    pub timestamp: DateTime<FixedOffset>,
58
59    /// The amount of time elapsed since the start of the test run.
60    pub elapsed: Duration,
61
62    /// The kind of test event this is.
63    pub kind: TestEventKind<'a>,
64}
65
66/// Scheduling information about a test's slot and group assignment.
67///
68/// This information is assigned by the `future_queue` scheduler and remains
69/// constant across all retry attempts of the same test.
70#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "kebab-case")]
72pub struct TestSlotAssignment {
73    /// The global slot number assigned to this test. Compact: starts from 0
74    /// and is always the smallest available number at assignment time.
75    pub global_slot: u64,
76
77    /// The slot number within this test's group, if the test is in a custom
78    /// group. `None` for tests in the global group.
79    pub group_slot: Option<u64>,
80
81    /// The test group this test belongs to.
82    pub test_group: TestGroup,
83}
84
85/// The kind of test event this is.
86///
87/// Forms part of [`TestEvent`].
88#[derive(Clone, Debug)]
89pub enum TestEventKind<'a> {
90    /// The test run started.
91    RunStarted {
92        /// The list of tests that will be run.
93        ///
94        /// The methods on the test list indicate the number of tests that will be run.
95        test_list: &'a TestList<'a>,
96
97        /// The UUID for this run.
98        run_id: ReportUuid,
99
100        /// The nextest profile chosen for this run.
101        profile_name: String,
102
103        /// The command-line arguments for the process.
104        cli_args: Vec<String>,
105
106        /// The stress condition for this run, if any.
107        stress_condition: Option<StressCondition>,
108    },
109
110    /// When running stress tests serially, a sub-run started.
111    StressSubRunStarted {
112        /// The amount of progress completed so far.
113        progress: StressProgress,
114    },
115
116    /// A setup script started.
117    SetupScriptStarted {
118        /// If a stress test is being run, the stress index, starting from 0.
119        stress_index: Option<StressIndex>,
120
121        /// The setup script index.
122        index: usize,
123
124        /// The total number of setup scripts.
125        total: usize,
126
127        /// The script ID.
128        script_id: ScriptId,
129
130        /// The program to run.
131        program: String,
132
133        /// The arguments to the program.
134        args: Vec<String>,
135
136        /// True if some output from the setup script is being passed through.
137        no_capture: bool,
138    },
139
140    /// A setup script was slow.
141    SetupScriptSlow {
142        /// If a stress test is being run, the stress index, starting from 0.
143        stress_index: Option<StressIndex>,
144
145        /// The script ID.
146        script_id: ScriptId,
147
148        /// The program to run.
149        program: String,
150
151        /// The arguments to the program.
152        args: Vec<String>,
153
154        /// The amount of time elapsed since the start of execution.
155        elapsed: Duration,
156
157        /// True if the script has hit its timeout and is about to be terminated.
158        will_terminate: bool,
159    },
160
161    /// A setup script completed execution.
162    SetupScriptFinished {
163        /// If a stress test is being run, the stress index, starting from 0.
164        stress_index: Option<StressIndex>,
165
166        /// The setup script index.
167        index: usize,
168
169        /// The total number of setup scripts.
170        total: usize,
171
172        /// The script ID.
173        script_id: ScriptId,
174
175        /// The program to run.
176        program: String,
177
178        /// The arguments to the program.
179        args: Vec<String>,
180
181        /// Whether the JUnit report should store success output for this script.
182        junit_store_success_output: bool,
183
184        /// Whether the JUnit report should store failure output for this script.
185        junit_store_failure_output: bool,
186
187        /// True if some output from the setup script was passed through.
188        no_capture: bool,
189
190        /// The execution status of the setup script.
191        run_status: SetupScriptExecuteStatus<LiveSpec>,
192    },
193
194    // TODO: add events for BinaryStarted and BinaryFinished? May want a slightly different way to
195    // do things, maybe a couple of reporter traits (one for the run as a whole and one for each
196    // binary).
197    /// A test started running.
198    TestStarted {
199        /// If a stress test is being run, the stress index, starting from 0.
200        stress_index: Option<StressIndex>,
201
202        /// The test instance that was started.
203        test_instance: TestInstanceId<'a>,
204
205        /// Scheduling information (slot and group assignment).
206        slot_assignment: TestSlotAssignment,
207
208        /// Current run statistics so far.
209        current_stats: RunStats,
210
211        /// The number of tests currently running, including this one.
212        running: usize,
213
214        /// The command line that will be used to run this test.
215        command_line: Vec<String>,
216    },
217
218    /// A test was slower than a configured soft timeout.
219    TestSlow {
220        /// If a stress test is being run, the stress index, starting from 0.
221        stress_index: Option<StressIndex>,
222
223        /// The test instance that was slow.
224        test_instance: TestInstanceId<'a>,
225
226        /// Retry data.
227        retry_data: RetryData,
228
229        /// The amount of time that has elapsed since the beginning of the test.
230        elapsed: Duration,
231
232        /// True if the test has hit its timeout and is about to be terminated.
233        will_terminate: bool,
234    },
235
236    /// A test attempt failed and will be retried in the future.
237    ///
238    /// This event does not occur on the final run of a failing test.
239    TestAttemptFailedWillRetry {
240        /// If a stress test is being run, the stress index, starting from 0.
241        stress_index: Option<StressIndex>,
242
243        /// The test instance that is being retried.
244        test_instance: TestInstanceId<'a>,
245
246        /// The status of this attempt to run the test. Will never be success.
247        run_status: ExecuteStatus<LiveSpec>,
248
249        /// The delay before the next attempt to run the test.
250        delay_before_next_attempt: Duration,
251
252        /// Whether failure outputs are printed out.
253        failure_output: TestOutputDisplay,
254
255        /// The current number of running tests.
256        running: usize,
257    },
258
259    /// A retry has started.
260    TestRetryStarted {
261        /// If a stress test is being run, the stress index, starting from 0.
262        stress_index: Option<StressIndex>,
263
264        /// The test instance that is being retried.
265        test_instance: TestInstanceId<'a>,
266
267        /// Scheduling information (slot and group assignment). Same as the
268        /// initial `TestStarted` event for this test.
269        slot_assignment: TestSlotAssignment,
270
271        /// Data related to retries.
272        retry_data: RetryData,
273
274        /// The current number of running tests.
275        running: usize,
276
277        /// The command line that will be used to run this test.
278        command_line: Vec<String>,
279    },
280
281    /// A test finished running.
282    TestFinished {
283        /// If a stress test is being run, the stress index, starting from 0.
284        stress_index: Option<StressIndex>,
285
286        /// The test instance that finished running.
287        test_instance: TestInstanceId<'a>,
288
289        /// Test setting for success output.
290        success_output: TestOutputDisplay,
291
292        /// Test setting for failure output.
293        failure_output: TestOutputDisplay,
294
295        /// Whether the JUnit report should store success output for this test.
296        junit_store_success_output: bool,
297
298        /// Whether the JUnit report should store failure output for this test.
299        junit_store_failure_output: bool,
300
301        /// How flaky-fail tests should be reported in JUnit.
302        junit_flaky_fail_status: JunitFlakyFailStatus,
303
304        /// Information about all the runs for this test.
305        run_statuses: ExecutionStatuses<LiveSpec>,
306
307        /// Current statistics for number of tests so far.
308        current_stats: RunStats,
309
310        /// The number of tests that are currently running, excluding this one.
311        running: usize,
312    },
313
314    /// A test was skipped.
315    TestSkipped {
316        /// If a stress test is being run, the stress index, starting from 0.
317        stress_index: Option<StressIndex>,
318
319        /// The test instance that was skipped.
320        test_instance: TestInstanceId<'a>,
321
322        /// The reason this test was skipped.
323        reason: MismatchReason,
324
325        /// The per-test resolved policy controlling which skipped tests are
326        /// emitted in machine-readable reports such as JUnit.
327        junit_report_skipped: ReportSkipPolicy,
328    },
329
330    /// An information request was received.
331    InfoStarted {
332        /// The number of tasks currently running. This is the same as the
333        /// number of expected responses.
334        total: usize,
335
336        /// Statistics for the run.
337        run_stats: RunStats,
338    },
339
340    /// Information about a script or test was received.
341    InfoResponse {
342        /// The index of the response, starting from 0.
343        index: usize,
344
345        /// The total number of responses expected.
346        total: usize,
347
348        /// The response itself.
349        response: InfoResponse<'a>,
350    },
351
352    /// An information request was completed.
353    InfoFinished {
354        /// The number of responses that were not received. In most cases, this
355        /// is 0.
356        missing: usize,
357    },
358
359    /// `Enter` was pressed. Either a newline or a progress bar snapshot needs
360    /// to be printed.
361    InputEnter {
362        /// Current statistics for number of tests so far.
363        current_stats: RunStats,
364
365        /// The number of tests running.
366        running: usize,
367    },
368
369    /// A cancellation notice was received.
370    RunBeginCancel {
371        /// The number of setup scripts still running.
372        setup_scripts_running: usize,
373
374        /// Current statistics for number of tests so far.
375        ///
376        /// `current_stats.cancel_reason` is set to `Some`.
377        current_stats: RunStats,
378
379        /// The number of tests still running.
380        running: usize,
381    },
382
383    /// A forcible kill was requested due to receiving a signal.
384    RunBeginKill {
385        /// The number of setup scripts still running.
386        setup_scripts_running: usize,
387
388        /// Current statistics for number of tests so far.
389        ///
390        /// `current_stats.cancel_reason` is set to `Some`.
391        current_stats: RunStats,
392
393        /// The number of tests still running.
394        running: usize,
395    },
396
397    /// A SIGTSTP event was received and the run was paused.
398    RunPaused {
399        /// The number of setup scripts running.
400        setup_scripts_running: usize,
401
402        /// The number of tests currently running.
403        running: usize,
404    },
405
406    /// A SIGCONT event was received and the run is being continued.
407    RunContinued {
408        /// The number of setup scripts that will be started up again.
409        setup_scripts_running: usize,
410
411        /// The number of tests that will be started up again.
412        running: usize,
413    },
414
415    /// When running stress tests serially, a sub-run finished.
416    StressSubRunFinished {
417        /// The amount of progress completed so far.
418        progress: StressProgress,
419
420        /// The amount of time it took for this sub-run to complete.
421        sub_elapsed: Duration,
422
423        /// Statistics for the sub-run.
424        sub_stats: RunStats,
425    },
426
427    /// The test run finished.
428    RunFinished {
429        /// The unique ID for this run.
430        run_id: ReportUuid,
431
432        /// The time at which the run was started.
433        start_time: DateTime<FixedOffset>,
434
435        /// The amount of time it took for the tests to run.
436        elapsed: Duration,
437
438        /// Statistics for the run, or overall statistics for stress tests.
439        run_stats: RunFinishedStats,
440
441        /// Tests that were expected to run but were not seen during this run.
442        ///
443        /// This is only set for reruns when some tests from the outstanding set
444        /// did not produce any events.
445        outstanding_not_seen: Option<TestsNotSeen>,
446    },
447}
448
449/// Tests that were expected to run but were not seen during a rerun.
450#[derive(Clone, Debug)]
451pub struct TestsNotSeen {
452    /// A sample of test instance IDs that were not seen, up to a reasonable
453    /// limit.
454    ///
455    /// This uses [`OwnedTestInstanceId`] rather than [`TestInstanceId`]
456    /// because the tests may not be present in the current test list (they
457    /// come from the expected outstanding set from a prior run).
458    pub not_seen: Vec<OwnedTestInstanceId>,
459
460    /// The total number of tests not seen (may exceed `not_seen.len()`).
461    pub total_not_seen: usize,
462}
463
464/// Progress for a stress test.
465#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
466#[serde(tag = "progress-type", rename_all = "kebab-case")]
467#[cfg_attr(test, derive(test_strategy::Arbitrary))]
468pub enum StressProgress {
469    /// This is a count-based stress run.
470    Count {
471        /// The total number of stress runs.
472        total: StressCount,
473
474        /// The total time that has elapsed across all stress runs so far.
475        elapsed: Duration,
476
477        /// The number of stress runs that have been completed.
478        completed: u32,
479    },
480
481    /// This is a time-based stress run.
482    Time {
483        /// The total time for the stress run.
484        total: Duration,
485
486        /// The total time that has elapsed across all stress runs so far.
487        elapsed: Duration,
488
489        /// The number of stress runs that have been completed.
490        completed: u32,
491    },
492}
493
494impl StressProgress {
495    /// Returns the remaining amount of work if the progress indicates there's
496    /// still more to do, otherwise `None`.
497    pub fn remaining(&self) -> Option<StressRemaining> {
498        match self {
499            Self::Count {
500                total: StressCount::Count { count },
501                elapsed: _,
502                completed,
503            } => count
504                .get()
505                .checked_sub(*completed)
506                .and_then(|remaining| NonZero::try_from(remaining).ok())
507                .map(StressRemaining::Count),
508            Self::Count {
509                total: StressCount::Infinite,
510                ..
511            } => Some(StressRemaining::Infinite),
512            Self::Time {
513                total,
514                elapsed,
515                completed: _,
516            } => total
517                .checked_sub(*elapsed)
518                .and_then(NonZeroDuration::new)
519                .map(StressRemaining::Time),
520        }
521    }
522
523    /// Returns a unique ID for this stress sub-run, consisting of the run ID and stress index.
524    pub fn unique_id(&self, run_id: ReportUuid) -> String {
525        let stress_current = match self {
526            Self::Count { completed, .. } | Self::Time { completed, .. } => *completed,
527        };
528        format!("{}:@stress-{}", run_id, stress_current)
529    }
530}
531
532/// For a stress test, the amount of time or number of stress runs remaining.
533#[derive(Clone, Debug, PartialEq, Eq)]
534pub enum StressRemaining {
535    /// The number of stress runs remaining, guaranteed to be non-zero.
536    Count(NonZero<u32>),
537
538    /// Infinite number of stress runs remaining.
539    Infinite,
540
541    /// The amount of time remaining.
542    Time(NonZeroDuration),
543}
544
545/// A [`Duration`] that is guaranteed to be non-zero.
546#[derive(Clone, Copy, Debug, PartialEq, Eq)]
547pub struct NonZeroDuration(Duration);
548
549impl NonZeroDuration {
550    /// Creates a `NonZeroDuration` from a [`Duration`].
551    ///
552    /// Returns `None` if the duration is zero.
553    pub fn new(duration: Duration) -> Option<Self> {
554        if duration.is_zero() {
555            None
556        } else {
557            Some(Self(duration))
558        }
559    }
560
561    /// Returns the underlying [`Duration`].
562    pub fn get(self) -> Duration {
563        self.0
564    }
565}
566
567/// The index of the current stress run.
568#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
569#[serde(rename_all = "kebab-case")]
570#[cfg_attr(test, derive(test_strategy::Arbitrary))]
571pub struct StressIndex {
572    /// The 0-indexed index.
573    pub current: u32,
574
575    /// The total number of stress runs, if that is available.
576    pub total: Option<NonZero<u32>>,
577}
578
579impl StressIndex {
580    /// Returns the total as a plain `u32`, if available.
581    pub fn total_get(&self) -> Option<u32> {
582        self.total.map(|t| t.get())
583    }
584}
585
586/// Statistics for a completed test run or stress run.
587#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
588#[serde(tag = "type", rename_all = "kebab-case")]
589#[cfg_attr(test, derive(test_strategy::Arbitrary))]
590pub enum RunFinishedStats {
591    /// A single test run was completed.
592    Single(RunStats),
593
594    /// A stress run was completed.
595    Stress(StressRunStats),
596}
597
598impl RunFinishedStats {
599    /// For a single run, returns a summary of statistics as an enum. For a
600    /// stress run, returns a summary for the last sub-run.
601    pub fn final_stats(&self) -> FinalRunStats {
602        match self {
603            Self::Single(stats) => stats.summarize_final(),
604            Self::Stress(stats) => stats.last_final_stats,
605        }
606    }
607}
608
609/// Statistics for a test run.
610#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
611#[serde(rename_all = "kebab-case")]
612#[cfg_attr(test, derive(test_strategy::Arbitrary))]
613pub struct RunStats {
614    /// The total number of tests that were expected to be run at the beginning.
615    ///
616    /// If the test run is cancelled, this will be more than `finished_count` at the end.
617    pub initial_run_count: usize,
618
619    /// The total number of tests that finished running.
620    pub finished_count: usize,
621
622    /// The total number of setup scripts that were expected to be run at the beginning.
623    ///
624    /// If the test run is cancelled, this will be more than `finished_count` at the end.
625    pub setup_scripts_initial_count: usize,
626
627    /// The total number of setup scripts that finished running.
628    pub setup_scripts_finished_count: usize,
629
630    /// The number of setup scripts that passed.
631    pub setup_scripts_passed: usize,
632
633    /// The number of setup scripts that failed.
634    pub setup_scripts_failed: usize,
635
636    /// The number of setup scripts that encountered an execution failure.
637    pub setup_scripts_exec_failed: usize,
638
639    /// The number of setup scripts that timed out.
640    pub setup_scripts_timed_out: usize,
641
642    /// The number of tests that passed. Includes `passed_slow`, `passed_timed_out`, `flaky`, and
643    /// `leaky`.
644    pub passed: usize,
645
646    /// The number of slow tests that passed.
647    pub passed_slow: usize,
648
649    /// The number of timed out tests that passed.
650    pub passed_timed_out: usize,
651
652    /// The number of tests that passed on retry.
653    pub flaky: usize,
654
655    /// The number of tests that failed. Includes `leaky_failed` and tests that
656    /// were flaky but treated as failed due to `flaky-result = "fail"` configuration.
657    pub failed: usize,
658
659    /// The number of failed tests that were slow.
660    pub failed_slow: usize,
661
662    /// The number of timed out tests that failed.
663    pub failed_timed_out: usize,
664
665    /// The number of tests that passed but leaked handles.
666    pub leaky: usize,
667
668    /// The number of tests that otherwise passed, but leaked handles and were
669    /// treated as failed as a result.
670    ///
671    /// Included in `failed`.
672    pub leaky_failed: usize,
673
674    /// The number of tests that encountered an execution failure.
675    pub exec_failed: usize,
676
677    /// The number of tests that were skipped.
678    pub skipped: usize,
679
680    /// If the run is cancelled, the reason the cancellation is happening.
681    pub cancel_reason: Option<CancelReason>,
682}
683
684impl RunStats {
685    /// Returns true if there are any failures recorded in the stats.
686    pub fn has_failures(&self) -> bool {
687        self.failed_setup_script_count() > 0 || self.failed_count() > 0
688    }
689
690    /// Returns count of setup scripts that did not pass.
691    pub fn failed_setup_script_count(&self) -> usize {
692        self.setup_scripts_failed + self.setup_scripts_exec_failed + self.setup_scripts_timed_out
693    }
694
695    /// Returns count of tests that did not pass.
696    pub fn failed_count(&self) -> usize {
697        self.failed + self.exec_failed + self.failed_timed_out
698    }
699
700    /// Summarizes the stats as an enum at the end of a test run.
701    pub fn summarize_final(&self) -> FinalRunStats {
702        // Check for failures first. The order of setup scripts vs tests should
703        // not be important, though we don't assert that here.
704        if self.failed_setup_script_count() > 0 {
705            self.summarize_failure(RunStatsFailureKind::SetupScript)
706        } else if self.setup_scripts_initial_count > self.setup_scripts_finished_count {
707            FinalRunStats::Cancelled {
708                reason: self.cancel_reason,
709                kind: RunStatsFailureKind::SetupScript,
710            }
711        } else if self.failed_count() > 0 {
712            self.summarize_failure(RunStatsFailureKind::Test {
713                initial_run_count: self.initial_run_count,
714                not_run: self.initial_run_count.saturating_sub(self.finished_count),
715            })
716        } else if self.initial_run_count > self.finished_count {
717            FinalRunStats::Cancelled {
718                reason: self.cancel_reason,
719                kind: RunStatsFailureKind::Test {
720                    initial_run_count: self.initial_run_count,
721                    not_run: self.initial_run_count.saturating_sub(self.finished_count),
722                },
723            }
724        } else if self.finished_count == 0 {
725            FinalRunStats::NoTestsRun
726        } else {
727            FinalRunStats::Success
728        }
729    }
730
731    fn summarize_failure(&self, kind: RunStatsFailureKind) -> FinalRunStats {
732        match self.cancel_reason {
733            None
734            | Some(
735                CancelReason::SetupScriptFailure
736                | CancelReason::TestFailure
737                | CancelReason::TestFailureImmediate,
738            ) => FinalRunStats::Failed { kind },
739            Some(
740                CancelReason::ReportError
741                | CancelReason::GlobalTimeout
742                | CancelReason::Signal
743                | CancelReason::Interrupt
744                | CancelReason::SecondSignal,
745            ) => FinalRunStats::Cancelled {
746                reason: self.cancel_reason,
747                kind,
748            },
749        }
750    }
751
752    pub(crate) fn on_setup_script_finished(&mut self, status: &SetupScriptExecuteStatus<LiveSpec>) {
753        self.setup_scripts_finished_count += 1;
754
755        match status.result {
756            ExecutionResultDescription::Pass
757            | ExecutionResultDescription::Leak {
758                result: LeakTimeoutResult::Pass,
759            } => {
760                self.setup_scripts_passed += 1;
761            }
762            ExecutionResultDescription::Fail { .. }
763            | ExecutionResultDescription::Leak {
764                result: LeakTimeoutResult::Fail,
765            } => {
766                self.setup_scripts_failed += 1;
767            }
768            ExecutionResultDescription::ExecFail => {
769                self.setup_scripts_exec_failed += 1;
770            }
771            // Timed out setup scripts are always treated as failures.
772            ExecutionResultDescription::Timeout { .. } => {
773                self.setup_scripts_timed_out += 1;
774            }
775        }
776    }
777
778    pub(crate) fn on_test_finished(&mut self, run_statuses: &ExecutionStatuses<LiveSpec>) {
779        self.finished_count += 1;
780        // run_statuses is guaranteed to have at least one element.
781        // * If the last element is success, treat it as success (and possibly flaky).
782        // * If the last element is a failure, use it to determine fail/exec fail.
783        // Note that this is different from what Maven Surefire does (use the first failure):
784        // https://maven.apache.org/surefire/maven-surefire-plugin/examples/rerun-failing-tests.html
785        //
786        // This is not likely to matter much in practice since failures are likely to be of the
787        // same type.
788        let last_status = run_statuses.last_status();
789        match last_status.result {
790            ExecutionResultDescription::Pass => {
791                // The test is flaky if there were multiple attempts. How
792                // it's counted depends on flaky_result — match
793                // exhaustively so the compiler catches new variants.
794                let is_flaky = run_statuses.len() > 1;
795                if is_flaky {
796                    match run_statuses.flaky_result() {
797                        FlakyResult::Fail => {
798                            self.failed += 1;
799                            if last_status.is_slow {
800                                self.failed_slow += 1;
801                            }
802                        }
803                        FlakyResult::Pass => {
804                            self.passed += 1;
805                            if last_status.is_slow {
806                                self.passed_slow += 1;
807                            }
808                            self.flaky += 1;
809                        }
810                    }
811                } else {
812                    self.passed += 1;
813                    if last_status.is_slow {
814                        self.passed_slow += 1;
815                    }
816                }
817            }
818            ExecutionResultDescription::Leak {
819                result: LeakTimeoutResult::Pass,
820            } => {
821                let is_flaky = run_statuses.len() > 1;
822                if is_flaky {
823                    match run_statuses.flaky_result() {
824                        FlakyResult::Fail => {
825                            self.failed += 1;
826                            if last_status.is_slow {
827                                self.failed_slow += 1;
828                            }
829                            // Still count as leaky since the leak was detected.
830                            self.leaky += 1;
831                        }
832                        FlakyResult::Pass => {
833                            self.passed += 1;
834                            self.leaky += 1;
835                            if last_status.is_slow {
836                                self.passed_slow += 1;
837                            }
838                            self.flaky += 1;
839                        }
840                    }
841                } else {
842                    self.passed += 1;
843                    self.leaky += 1;
844                    if last_status.is_slow {
845                        self.passed_slow += 1;
846                    }
847                }
848            }
849            ExecutionResultDescription::Leak {
850                result: LeakTimeoutResult::Fail,
851            } => {
852                self.failed += 1;
853                self.leaky_failed += 1;
854                if last_status.is_slow {
855                    self.failed_slow += 1;
856                }
857            }
858            ExecutionResultDescription::Fail { .. } => {
859                self.failed += 1;
860                if last_status.is_slow {
861                    self.failed_slow += 1;
862                }
863            }
864            ExecutionResultDescription::Timeout {
865                result: SlowTimeoutResult::Pass,
866            } => {
867                let is_flaky = run_statuses.len() > 1;
868                if is_flaky {
869                    match run_statuses.flaky_result() {
870                        FlakyResult::Fail => {
871                            self.failed += 1;
872                            // Track as failed_slow since the overall result
873                            // is failure.
874                            if last_status.is_slow {
875                                self.failed_slow += 1;
876                            }
877                        }
878                        FlakyResult::Pass => {
879                            self.passed += 1;
880                            self.passed_timed_out += 1;
881                            self.flaky += 1;
882                        }
883                    }
884                } else {
885                    self.passed += 1;
886                    self.passed_timed_out += 1;
887                }
888            }
889            ExecutionResultDescription::Timeout {
890                result: SlowTimeoutResult::Fail,
891            } => {
892                self.failed_timed_out += 1;
893            }
894            ExecutionResultDescription::ExecFail => self.exec_failed += 1,
895        }
896    }
897}
898
899/// A type summarizing the possible outcomes of a test run.
900#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
901#[serde(tag = "outcome", rename_all = "kebab-case")]
902#[cfg_attr(test, derive(test_strategy::Arbitrary))]
903pub enum FinalRunStats {
904    /// The test run was successful, or is successful so far.
905    Success,
906
907    /// The test run was successful, or is successful so far, but no tests were selected to run.
908    NoTestsRun,
909
910    /// The test run was cancelled.
911    Cancelled {
912        /// The reason for cancellation, if available.
913        ///
914        /// This should generally be available, but may be None if some tests
915        /// that were selected to run were not executed.
916        reason: Option<CancelReason>,
917
918        /// The kind of failure that occurred.
919        kind: RunStatsFailureKind,
920    },
921
922    /// At least one test failed.
923    Failed {
924        /// The kind of failure that occurred.
925        kind: RunStatsFailureKind,
926    },
927}
928
929/// Statistics for a stress run.
930#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
931#[serde(rename_all = "kebab-case")]
932#[cfg_attr(test, derive(test_strategy::Arbitrary))]
933pub struct StressRunStats {
934    /// The number of stress runs completed.
935    pub completed: StressIndex,
936
937    /// The number of stress runs that succeeded.
938    pub success_count: u32,
939
940    /// The number of stress runs that failed.
941    pub failed_count: u32,
942
943    /// The last stress run's `FinalRunStats`.
944    pub last_final_stats: FinalRunStats,
945}
946
947impl StressRunStats {
948    /// Summarizes the stats as an enum at the end of a test run.
949    pub fn summarize_final(&self) -> StressFinalRunStats {
950        if self.failed_count > 0 {
951            StressFinalRunStats::Failed
952        } else if matches!(self.last_final_stats, FinalRunStats::Cancelled { .. }) {
953            StressFinalRunStats::Cancelled
954        } else if matches!(self.last_final_stats, FinalRunStats::NoTestsRun) {
955            StressFinalRunStats::NoTestsRun
956        } else {
957            StressFinalRunStats::Success
958        }
959    }
960}
961
962/// A summary of final statistics for a stress run.
963#[derive(Clone, Copy, Debug, PartialEq, Eq)]
964pub enum StressFinalRunStats {
965    /// The stress run was successful.
966    Success,
967
968    /// No tests were run.
969    NoTestsRun,
970
971    /// The stress run was cancelled.
972    Cancelled,
973
974    /// At least one stress run failed.
975    Failed,
976}
977
978/// The step at which a run failed, without per-run details.
979///
980/// Later variants take precedence over earlier ones when combining steps across
981/// stress sub-runs.
982#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
983#[cfg_attr(test, derive(test_strategy::Arbitrary))]
984pub enum RunFailureStep {
985    /// A test failed.
986    Test,
987
988    /// A setup script failed.
989    SetupScript,
990}
991
992/// A type summarizing the step at which a test run failed.
993#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
994#[serde(tag = "step", rename_all = "kebab-case")]
995#[cfg_attr(test, derive(test_strategy::Arbitrary))]
996pub enum RunStatsFailureKind {
997    /// The run was interrupted during setup script execution.
998    SetupScript,
999
1000    /// The run was interrupted during test execution.
1001    Test {
1002        /// The total number of tests scheduled.
1003        initial_run_count: usize,
1004
1005        /// The number of tests not run, or for a currently-executing test the number queued up to
1006        /// run.
1007        not_run: usize,
1008    },
1009}
1010
1011impl RunStatsFailureKind {
1012    /// Returns the step at which the run failed.
1013    pub fn step(&self) -> RunFailureStep {
1014        match self {
1015            Self::SetupScript => RunFailureStep::SetupScript,
1016            Self::Test { .. } => RunFailureStep::Test,
1017        }
1018    }
1019}
1020
1021/// The final outcome of a run.
1022///
1023/// For standard runs this is a reduced form of [`FinalRunStats`]. For stress
1024/// runs it accounts for every sub-run.
1025#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1026#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1027#[must_use]
1028pub enum RunOutcome {
1029    /// * For standard runs: the run was successful.
1030    /// * For stress runs: all sub-runs were successful.
1031    Success,
1032
1033    /// * For standard runs: no tests were run, setup scripts did not fail,
1034    ///   and the test run was not cancelled.
1035    /// * For stress runs: no sub-run ran any tests, setup scripts did not
1036    ///   fail, and none were cancelled.
1037    NoTestsRun,
1038
1039    /// * For standard runs: the run was cancelled.
1040    /// * For stress runs: no sub-run failed, and at least one was cancelled.
1041    ///   (Currently, stress runs occur serially, so only the last sub-run
1042    ///   can be cancelled.)
1043    Cancelled {
1044        /// * For standard runs: the step the run was cancelled at.
1045        /// * For stress runs: the highest-precedence step across cancelled
1046        ///   sub-runs (see [`RunFailureStep`]; currently, stress runs occur
1047        ///   serially, so this always refers to the last sub-run).
1048        step: RunFailureStep,
1049    },
1050
1051    /// * For standard runs: the run failed.
1052    /// * For stress runs: at least one sub-run failed.
1053    Failed {
1054        /// * For standard runs: the step the run failed at.
1055        /// * For stress runs: the highest-precedence step across failed
1056        ///   sub-runs (see [`RunFailureStep`]).
1057        step: RunFailureStep,
1058    },
1059}
1060
1061impl RunOutcome {
1062    /// Creates a [`RunOutcome`] from the [`FinalRunStats`] for a standard run
1063    /// or stress sub-run.
1064    pub fn from_final_stats(stats: FinalRunStats) -> Self {
1065        match stats {
1066            FinalRunStats::Success => Self::Success,
1067            FinalRunStats::NoTestsRun => Self::NoTestsRun,
1068            FinalRunStats::Cancelled { reason: _, kind } => Self::Cancelled { step: kind.step() },
1069            FinalRunStats::Failed { kind } => Self::Failed { step: kind.step() },
1070        }
1071    }
1072
1073    /// Combines two [`RunOutcome`] values.
1074    ///
1075    /// This is commutative, associative, and idempotent. The rules are:
1076    ///
1077    /// * `NoTestsRun < Success < Cancelled < Failed`.
1078    /// * Within a class (failure or cancellation), `SetupScript` outranks
1079    ///   `Test` (see [`RunFailureStep`]).
1080    pub(crate) fn combine(self, other: Self) -> Self {
1081        match (self, other) {
1082            (Self::Failed { step: a }, Self::Failed { step: b }) => Self::Failed { step: a.max(b) },
1083            (Self::Failed { step }, Self::Success | Self::NoTestsRun | Self::Cancelled { .. })
1084            | (Self::Success | Self::NoTestsRun | Self::Cancelled { .. }, Self::Failed { step }) => {
1085                Self::Failed { step }
1086            }
1087            (Self::Cancelled { step: a }, Self::Cancelled { step: b }) => {
1088                Self::Cancelled { step: a.max(b) }
1089            }
1090            (Self::Cancelled { step }, Self::Success | Self::NoTestsRun)
1091            | (Self::Success | Self::NoTestsRun, Self::Cancelled { step }) => {
1092                Self::Cancelled { step }
1093            }
1094            (Self::Success, Self::Success | Self::NoTestsRun)
1095            | (Self::NoTestsRun, Self::Success) => Self::Success,
1096            (Self::NoTestsRun, Self::NoTestsRun) => Self::NoTestsRun,
1097        }
1098    }
1099}
1100
1101/// Information about executions of a test, including retries.
1102///
1103/// The type parameter `S` specifies how test output is stored (see
1104/// [`OutputSpec`]).
1105#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1106#[derive(Serialize)]
1107#[serde(
1108    rename_all = "kebab-case",
1109    bound(serialize = "S: SerializableOutputSpec")
1110)]
1111#[cfg_attr(
1112    test,
1113    derive(test_strategy::Arbitrary),
1114    arbitrary(bound(S: ArbitraryOutputSpec))
1115)]
1116pub struct ExecutionStatuses<S: OutputSpec> {
1117    /// This is guaranteed to be non-empty.
1118    #[cfg_attr(test, strategy(proptest::collection::vec(proptest::arbitrary::any::<ExecuteStatus<S>>(), 1..=3)))]
1119    statuses: Vec<ExecuteStatus<S>>,
1120
1121    /// Controls whether a flaky test is treated as a pass or a failure.
1122    #[serde(default)]
1123    flaky_result: FlakyResult,
1124}
1125
1126impl<'de, S: SerializableOutputSpec> Deserialize<'de> for ExecutionStatuses<S> {
1127    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1128        // Deserialize as the wrapper struct that matches the Serialize output.
1129        // S is already bound as SerializableOutputSpec on this impl.
1130        #[derive(Deserialize)]
1131        #[serde(
1132            rename_all = "kebab-case",
1133            bound(deserialize = "S: SerializableOutputSpec")
1134        )]
1135        struct Helper<S: OutputSpec> {
1136            statuses: Vec<ExecuteStatus<S>>,
1137            #[serde(default)]
1138            flaky_result: FlakyResult,
1139        }
1140
1141        let helper = Helper::<S>::deserialize(deserializer)?;
1142        if helper.statuses.is_empty() {
1143            return Err(serde::de::Error::custom("expected non-empty statuses"));
1144        }
1145        Ok(Self {
1146            statuses: helper.statuses,
1147            flaky_result: helper.flaky_result,
1148        })
1149    }
1150}
1151
1152#[expect(clippy::len_without_is_empty)] // RunStatuses is never empty
1153impl<S: OutputSpec> ExecutionStatuses<S> {
1154    pub(crate) fn new(statuses: Vec<ExecuteStatus<S>>, flaky_result: FlakyResult) -> Self {
1155        debug_assert!(!statuses.is_empty(), "ExecutionStatuses must be non-empty");
1156        Self {
1157            statuses,
1158            flaky_result,
1159        }
1160    }
1161
1162    /// Returns the configured flaky result for this test.
1163    pub fn flaky_result(&self) -> FlakyResult {
1164        self.flaky_result
1165    }
1166
1167    /// Returns the last execution status.
1168    ///
1169    /// This status is typically used as the final result.
1170    pub fn last_status(&self) -> &ExecuteStatus<S> {
1171        self.statuses
1172            .last()
1173            .expect("execution statuses is non-empty")
1174    }
1175
1176    /// Iterates over all the statuses.
1177    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus<S>> + '_ {
1178        self.statuses.iter()
1179    }
1180
1181    /// Returns the number of times the test was executed.
1182    pub fn len(&self) -> usize {
1183        self.statuses.len()
1184    }
1185
1186    /// Returns a description of self.
1187    pub fn describe(&self) -> ExecutionDescription<'_, S> {
1188        let last_status = self.last_status();
1189        if last_status.result.is_success() {
1190            if self.statuses.len() > 1 {
1191                ExecutionDescription::Flaky {
1192                    last_status,
1193                    prior_statuses: &self.statuses[..self.statuses.len() - 1],
1194                    result: self.flaky_result,
1195                }
1196            } else {
1197                ExecutionDescription::Success {
1198                    single_status: last_status,
1199                }
1200            }
1201        } else {
1202            let first_status = self
1203                .statuses
1204                .first()
1205                .expect("execution statuses is non-empty");
1206            let retries = &self.statuses[1..];
1207            ExecutionDescription::Failure {
1208                first_status,
1209                last_status,
1210                retries,
1211            }
1212        }
1213    }
1214}
1215
1216impl<S: OutputSpec> IntoIterator for ExecutionStatuses<S> {
1217    type Item = ExecuteStatus<S>;
1218    type IntoIter = std::vec::IntoIter<ExecuteStatus<S>>;
1219
1220    fn into_iter(self) -> Self::IntoIter {
1221        self.statuses.into_iter()
1222    }
1223}
1224
1225/// A description of test executions obtained from `ExecuteStatuses`.
1226///
1227/// This can be used to quickly determine whether a test passed, failed or was flaky.
1228///
1229/// The type parameter `S` specifies how test output is stored (see
1230/// [`OutputSpec`]).
1231#[derive_where::derive_where(Debug; S::ChildOutputDesc)]
1232pub enum ExecutionDescription<'a, S: OutputSpec> {
1233    /// The test was run once and was successful.
1234    Success {
1235        /// The status of the test.
1236        single_status: &'a ExecuteStatus<S>,
1237    },
1238
1239    /// The test was run more than once. The final result was successful.
1240    Flaky {
1241        /// The last, successful status.
1242        last_status: &'a ExecuteStatus<S>,
1243
1244        /// Previous statuses, none of which are successes.
1245        prior_statuses: &'a [ExecuteStatus<S>],
1246
1247        /// Controls whether this flaky test is treated as a pass or a failure.
1248        result: FlakyResult,
1249    },
1250
1251    /// The test was run once, or possibly multiple times. All runs failed.
1252    Failure {
1253        /// The first, failing status.
1254        first_status: &'a ExecuteStatus<S>,
1255
1256        /// The last, failing status. Same as the first status if no retries were performed.
1257        last_status: &'a ExecuteStatus<S>,
1258
1259        /// Any retries that were performed. All of these runs failed.
1260        ///
1261        /// May be empty.
1262        retries: &'a [ExecuteStatus<S>],
1263    },
1264}
1265
1266// Manual Copy and Clone implementations to avoid requiring S::ChildOutputDesc:
1267// Copy/Clone, since ExecutionDescription only stores references.
1268impl<S: OutputSpec> Clone for ExecutionDescription<'_, S> {
1269    fn clone(&self) -> Self {
1270        *self
1271    }
1272}
1273
1274impl<S: OutputSpec> Copy for ExecutionDescription<'_, S> {}
1275
1276impl<'a, S: OutputSpec> ExecutionDescription<'a, S> {
1277    /// Returns the status level for this `ExecutionDescription`.
1278    pub fn status_level(&self) -> StatusLevel {
1279        match self {
1280            ExecutionDescription::Success { single_status } => match single_status.result {
1281                ExecutionResultDescription::Leak {
1282                    result: LeakTimeoutResult::Pass,
1283                } => StatusLevel::Leak,
1284                ExecutionResultDescription::Pass => StatusLevel::Pass,
1285                ExecutionResultDescription::Timeout {
1286                    result: SlowTimeoutResult::Pass,
1287                } => StatusLevel::Slow,
1288                ref other => unreachable!(
1289                    "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1290                ),
1291            },
1292            // A flaky test implies that we print out retry information for it.
1293            ExecutionDescription::Flaky {
1294                result: FlakyResult::Pass,
1295                ..
1296            } => StatusLevel::Retry,
1297            ExecutionDescription::Flaky {
1298                result: FlakyResult::Fail,
1299                ..
1300            } => StatusLevel::Fail,
1301            ExecutionDescription::Failure { .. } => StatusLevel::Fail,
1302        }
1303    }
1304
1305    /// Returns the final status level for this `ExecutionDescription`.
1306    pub fn final_status_level(&self) -> FinalStatusLevel {
1307        match self {
1308            ExecutionDescription::Success { single_status, .. } => {
1309                // Slow is higher priority than leaky, so return slow first here.
1310                if single_status.is_slow {
1311                    FinalStatusLevel::Slow
1312                } else {
1313                    match single_status.result {
1314                        ExecutionResultDescription::Pass => FinalStatusLevel::Pass,
1315                        ExecutionResultDescription::Leak {
1316                            result: LeakTimeoutResult::Pass,
1317                        } => FinalStatusLevel::Leak,
1318                        // Timeout with Pass should return Slow, but this case
1319                        // shouldn't be reached because is_slow is true for
1320                        // timeout scenarios. Handle it for completeness.
1321                        ExecutionResultDescription::Timeout {
1322                            result: SlowTimeoutResult::Pass,
1323                        } => FinalStatusLevel::Slow,
1324                        ref other => unreachable!(
1325                            "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1326                        ),
1327                    }
1328                }
1329            }
1330            // A flaky-pass test implies that we print out retry information.
1331            ExecutionDescription::Flaky {
1332                result: FlakyResult::Pass,
1333                ..
1334            } => FinalStatusLevel::Flaky,
1335            // A flaky-fail test is treated as a failure.
1336            ExecutionDescription::Flaky {
1337                result: FlakyResult::Fail,
1338                ..
1339            } => FinalStatusLevel::Fail,
1340            ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
1341        }
1342    }
1343
1344    /// Returns whether this test's output should be treated as success output
1345    /// for display and storage purposes.
1346    ///
1347    /// For flaky tests (both pass and fail variants), the last attempt
1348    /// succeeded, so its output is success output: it contains no panics or
1349    /// errors, and is generally not interesting. The failure information comes
1350    /// from the status line and from prior retry attempts' output (shown via
1351    /// `TestAttemptFailedWillRetry` events, controlled by `failure-output`).
1352    ///
1353    /// This means:
1354    /// - The _visibility_ is controlled by `success-output`.
1355    /// - The _styling_ is pass/green headers, no error extraction.
1356    /// - _JUnit storage_ is controlled by `store-success-output` (default:
1357    ///   `false`).
1358    ///
1359    /// The status line uses failure semantics independently (e.g. `FLKY-FL` in
1360    /// red for flaky-fail tests).
1361    pub fn is_success_for_output(&self) -> bool {
1362        match self {
1363            ExecutionDescription::Success { .. } => true,
1364            // All flaky tests have a successful last attempt — the output
1365            // from that attempt is success output regardless of the overall
1366            // test outcome.
1367            ExecutionDescription::Flaky { .. } => true,
1368            ExecutionDescription::Failure { .. } => false,
1369        }
1370    }
1371
1372    /// Returns the last run status.
1373    pub fn last_status(&self) -> &'a ExecuteStatus<S> {
1374        match self {
1375            ExecutionDescription::Success {
1376                single_status: last_status,
1377            }
1378            | ExecutionDescription::Flaky { last_status, .. }
1379            | ExecutionDescription::Failure { last_status, .. } => last_status,
1380        }
1381    }
1382}
1383
1384/// Pre-computed error summary for display.
1385///
1386/// This contains the formatted error messages, pre-computed from the execution
1387/// output and result. Useful for record-replay scenarios where the rendering
1388/// is done on the server.
1389#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1390#[serde(rename_all = "kebab-case")]
1391#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1392pub struct ErrorSummary {
1393    /// A short summary of the error, suitable for display in a single line.
1394    pub short_message: String,
1395
1396    /// A full description of the error chain, suitable for detailed display.
1397    pub description: String,
1398}
1399
1400/// Pre-computed output error slice for display.
1401///
1402/// This contains an error message heuristically extracted from test output,
1403/// such as a panic message or error string.
1404#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1405#[serde(rename_all = "kebab-case")]
1406#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1407pub struct OutputErrorSlice {
1408    /// The extracted error slice as a string.
1409    pub slice: String,
1410
1411    /// The byte offset in the original output where this slice starts.
1412    pub start: usize,
1413}
1414
1415/// Information about a single execution of a test.
1416///
1417/// This is the external-facing type used by reporters. The `result` field uses
1418/// [`ExecutionResultDescription`], a platform-independent type that can be
1419/// serialized and deserialized across platforms.
1420///
1421/// The type parameter `S` specifies how test output is stored (see
1422/// [`OutputSpec`]).
1423#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1424#[derive(Serialize, Deserialize)]
1425#[serde(
1426    rename_all = "kebab-case",
1427    bound(
1428        serialize = "S: SerializableOutputSpec",
1429        deserialize = "S: SerializableOutputSpec"
1430    )
1431)]
1432#[cfg_attr(
1433    test,
1434    derive(test_strategy::Arbitrary),
1435    arbitrary(bound(S: ArbitraryOutputSpec))
1436)]
1437pub struct ExecuteStatus<S: OutputSpec> {
1438    /// Retry-related data.
1439    pub retry_data: RetryData,
1440    /// The stdout and stderr output for this test.
1441    pub output: ChildExecutionOutputDescription<S>,
1442    /// The execution result for this test: pass, fail or execution error.
1443    pub result: ExecutionResultDescription,
1444    /// The time at which the test started.
1445    #[cfg_attr(
1446        test,
1447        strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1448    )]
1449    pub start_time: DateTime<FixedOffset>,
1450    /// The time it took for the test to run.
1451    #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1452    pub time_taken: Duration,
1453    /// Whether this test counts as slow.
1454    pub is_slow: bool,
1455    /// The delay will be non-zero if this is a retry and delay was specified.
1456    #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1457    pub delay_before_start: Duration,
1458    /// Pre-computed error summary, if available.
1459    ///
1460    /// This is computed from the execution output and result, and can be used
1461    /// for display without needing to re-compute the error chain.
1462    pub error_summary: Option<ErrorSummary>,
1463    /// Pre-computed output error slice, if available.
1464    ///
1465    /// This is a heuristically extracted error message from the test output,
1466    /// such as a panic message or error string.
1467    pub output_error_slice: Option<OutputErrorSlice>,
1468}
1469
1470/// Information about the execution of a setup script.
1471///
1472/// This is the external-facing type used by reporters. The `result` field uses
1473/// [`ExecutionResultDescription`], a platform-independent type that can be
1474/// serialized and deserialized across platforms.
1475///
1476/// The type parameter `S` specifies how test output is stored (see
1477/// [`OutputSpec`]).
1478#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1479#[derive(Serialize, Deserialize)]
1480#[serde(
1481    rename_all = "kebab-case",
1482    bound(
1483        serialize = "S: SerializableOutputSpec",
1484        deserialize = "S: SerializableOutputSpec"
1485    )
1486)]
1487#[cfg_attr(
1488    test,
1489    derive(test_strategy::Arbitrary),
1490    arbitrary(bound(S: ArbitraryOutputSpec))
1491)]
1492pub struct SetupScriptExecuteStatus<S: OutputSpec> {
1493    /// Output for this setup script.
1494    pub output: ChildExecutionOutputDescription<S>,
1495
1496    /// The execution result for this setup script: pass, fail or execution error.
1497    pub result: ExecutionResultDescription,
1498
1499    /// The time at which the script started.
1500    #[cfg_attr(
1501        test,
1502        strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1503    )]
1504    pub start_time: DateTime<FixedOffset>,
1505
1506    /// The time it took for the script to run.
1507    #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1508    pub time_taken: Duration,
1509
1510    /// Whether this script counts as slow.
1511    pub is_slow: bool,
1512
1513    /// The map of environment variables that were set by this script.
1514    ///
1515    /// `None` if an error occurred while running the script or reading the
1516    /// environment map.
1517    pub env_map: Option<SetupScriptEnvMap>,
1518
1519    /// Pre-computed error summary, if available.
1520    ///
1521    /// This is computed from the execution output and result, and can be used
1522    /// for display without needing to re-compute the error chain.
1523    pub error_summary: Option<ErrorSummary>,
1524}
1525
1526/// A map of environment variables set by a setup script.
1527///
1528/// Part of [`SetupScriptExecuteStatus`].
1529#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1530#[serde(rename_all = "kebab-case")]
1531#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1532pub struct SetupScriptEnvMap {
1533    /// The map of environment variables set by the script.
1534    pub env_map: BTreeMap<String, String>,
1535}
1536
1537// ---
1538// Child execution output description types
1539// ---
1540
1541/// The result of executing a child process, generic over output storage.
1542///
1543/// This is the external-facing counterpart to [`ChildExecutionOutput`]. The
1544/// type parameter `S` specifies how output is stored (see [`OutputSpec`]).
1545#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1546#[derive(Serialize, Deserialize)]
1547#[serde(
1548    tag = "type",
1549    rename_all = "kebab-case",
1550    bound(
1551        serialize = "S: SerializableOutputSpec",
1552        deserialize = "S: SerializableOutputSpec"
1553    )
1554)]
1555#[cfg_attr(
1556    test,
1557    derive(test_strategy::Arbitrary),
1558    arbitrary(bound(S: ArbitraryOutputSpec))
1559)]
1560pub enum ChildExecutionOutputDescription<S: OutputSpec> {
1561    /// The process was run and the output was captured.
1562    Output {
1563        /// If the process has finished executing, the final state it is in.
1564        ///
1565        /// `None` means execution is currently in progress.
1566        result: Option<ExecutionResultDescription>,
1567
1568        /// The captured output.
1569        output: S::ChildOutputDesc,
1570
1571        /// Errors that occurred while waiting on the child process or parsing
1572        /// its output.
1573        errors: Option<ErrorList<ChildErrorDescription>>,
1574    },
1575
1576    /// There was a failure to start the process.
1577    StartError(ChildStartErrorDescription),
1578}
1579
1580impl<S: OutputSpec> ChildExecutionOutputDescription<S> {
1581    /// Returns true if there are any errors in this output.
1582    pub fn has_errors(&self) -> bool {
1583        match self {
1584            Self::Output { errors, result, .. } => {
1585                if errors.is_some() {
1586                    return true;
1587                }
1588                if let Some(result) = result {
1589                    return !result.is_success();
1590                }
1591                false
1592            }
1593            Self::StartError(_) => true,
1594        }
1595    }
1596}
1597
1598/// The output of a child process during live execution.
1599///
1600/// This represents either split stdout/stderr or combined output. The `Option`
1601/// wrappers distinguish between "not captured" (`None`) and "captured but
1602/// empty" (`Some` with empty content).
1603///
1604/// The `NotLoaded` variant is used during replay when the display
1605/// configuration indicates that output won't be shown.
1606///
1607/// For the recording counterpart, see
1608/// [`ZipStoreOutputDescription`](crate::record::ZipStoreOutputDescription).
1609#[derive(Clone, Debug)]
1610pub enum ChildOutputDescription {
1611    /// The output was split into stdout and stderr.
1612    Split {
1613        /// Standard output, or `None` if not captured.
1614        stdout: Option<ChildSingleOutput>,
1615        /// Standard error, or `None` if not captured.
1616        stderr: Option<ChildSingleOutput>,
1617    },
1618
1619    /// The output was combined into a single stream.
1620    Combined {
1621        /// The combined output.
1622        output: ChildSingleOutput,
1623    },
1624
1625    /// Output exists but was not loaded.
1626    ///
1627    /// This variant is used during replay when the display configuration
1628    /// indicates that output won't be shown. Code that accesses output
1629    /// bytes must never be reached with this variant.
1630    NotLoaded,
1631}
1632
1633impl ChildOutputDescription {
1634    /// Returns the lengths of stdout and stderr in bytes.
1635    ///
1636    /// Returns `None` for each stream that wasn't captured.
1637    pub fn stdout_stderr_len(&self) -> (Option<u64>, Option<u64>) {
1638        match self {
1639            Self::Split { stdout, stderr } => (
1640                stdout.as_ref().map(|s| s.buf().len() as u64),
1641                stderr.as_ref().map(|s| s.buf().len() as u64),
1642            ),
1643            Self::Combined { output } => (Some(output.buf().len() as u64), None),
1644            Self::NotLoaded => {
1645                unreachable!(
1646                    "attempted to get output lengths from output that was not loaded \
1647                     (this method is only called from the live runner, where NotLoaded \
1648                     is never produced)"
1649                );
1650            }
1651        }
1652    }
1653}
1654
1655/// A serializable description of an error that occurred while starting a child process.
1656///
1657/// This is the external-facing counterpart to [`ChildStartError`].
1658#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1659#[serde(tag = "kind", rename_all = "kebab-case")]
1660#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1661pub enum ChildStartErrorDescription {
1662    /// An error occurred while creating a temporary path for a setup script.
1663    TempPath {
1664        /// The source error.
1665        source: SerializableError,
1666    },
1667
1668    /// An error occurred while spawning the child process.
1669    Spawn {
1670        /// The source error.
1671        source: SerializableError,
1672    },
1673}
1674
1675impl fmt::Display for ChildStartErrorDescription {
1676    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1677        match self {
1678            Self::TempPath { .. } => {
1679                write!(f, "error creating temporary path for setup script")
1680            }
1681            Self::Spawn { .. } => write!(f, "error spawning child process"),
1682        }
1683    }
1684}
1685
1686impl std::error::Error for ChildStartErrorDescription {
1687    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1688        match self {
1689            Self::TempPath { source } | Self::Spawn { source } => Some(source),
1690        }
1691    }
1692}
1693
1694/// A serializable description of an error that occurred while managing a child process.
1695///
1696/// This is the external-facing counterpart to [`ChildError`].
1697#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1698#[serde(tag = "kind", rename_all = "kebab-case")]
1699#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1700pub enum ChildErrorDescription {
1701    /// An error occurred while reading standard output.
1702    ReadStdout {
1703        /// The source error.
1704        source: SerializableError,
1705    },
1706
1707    /// An error occurred while reading standard error.
1708    ReadStderr {
1709        /// The source error.
1710        source: SerializableError,
1711    },
1712
1713    /// An error occurred while reading combined output.
1714    ReadCombined {
1715        /// The source error.
1716        source: SerializableError,
1717    },
1718
1719    /// An error occurred while waiting for the child process to exit.
1720    Wait {
1721        /// The source error.
1722        source: SerializableError,
1723    },
1724
1725    /// An error occurred while reading the output of a setup script.
1726    SetupScriptOutput {
1727        /// The source error.
1728        source: SerializableError,
1729    },
1730}
1731
1732impl fmt::Display for ChildErrorDescription {
1733    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1734        match self {
1735            Self::ReadStdout { .. } => write!(f, "error reading standard output"),
1736            Self::ReadStderr { .. } => write!(f, "error reading standard error"),
1737            Self::ReadCombined { .. } => {
1738                write!(f, "error reading combined stream")
1739            }
1740            Self::Wait { .. } => {
1741                write!(f, "error waiting for child process to exit")
1742            }
1743            Self::SetupScriptOutput { .. } => {
1744                write!(f, "error reading setup script output")
1745            }
1746        }
1747    }
1748}
1749
1750impl std::error::Error for ChildErrorDescription {
1751    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1752        match self {
1753            Self::ReadStdout { source }
1754            | Self::ReadStderr { source }
1755            | Self::ReadCombined { source }
1756            | Self::Wait { source }
1757            | Self::SetupScriptOutput { source } => Some(source),
1758        }
1759    }
1760}
1761
1762/// A serializable representation of an error chain.
1763///
1764/// This captures the error message and the chain of source errors from
1765/// any [`std::error::Error`] implementation.
1766#[derive(Clone, Debug, PartialEq, Eq)]
1767pub struct SerializableError {
1768    message: String,
1769    source: Option<Box<SerializableError>>,
1770}
1771
1772impl SerializableError {
1773    /// Creates a new `SerializableError` from an error, walking the
1774    /// full source chain.
1775    pub fn new(error: &dyn std::error::Error) -> Self {
1776        let message = error.to_string();
1777        let mut causes = Vec::new();
1778        let mut source = error.source();
1779        while let Some(err) = source {
1780            causes.push(err.to_string());
1781            source = err.source();
1782        }
1783        Self::from_message_and_causes(message, causes)
1784    }
1785
1786    /// Creates a new `SerializableError` from a message and a list of
1787    /// causes.
1788    pub fn from_message_and_causes(message: String, causes: Vec<String>) -> Self {
1789        // This builds a singly-linked list from the causes. You rarely
1790        // see them in Rust, but they're required to implement
1791        // Error::source.
1792        let mut next = None;
1793        for cause in causes.into_iter().rev() {
1794            let error = Self {
1795                message: cause,
1796                source: next.map(Box::new),
1797            };
1798            next = Some(error);
1799        }
1800        Self {
1801            message,
1802            source: next.map(Box::new),
1803        }
1804    }
1805
1806    /// Returns the message associated with this error.
1807    pub fn message(&self) -> &str {
1808        &self.message
1809    }
1810
1811    /// Returns the causes of this error as an iterator.
1812    pub fn sources(&self) -> SerializableErrorSources<'_> {
1813        SerializableErrorSources {
1814            current: self.source.as_deref(),
1815        }
1816    }
1817}
1818
1819impl fmt::Display for SerializableError {
1820    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1821        f.write_str(&self.message)
1822    }
1823}
1824
1825impl std::error::Error for SerializableError {
1826    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1827        self.source
1828            .as_deref()
1829            .map(|s| s as &(dyn std::error::Error + 'static))
1830    }
1831}
1832
1833/// The sources of a [`SerializableError`] as an iterator.
1834#[derive(Debug)]
1835pub struct SerializableErrorSources<'a> {
1836    current: Option<&'a SerializableError>,
1837}
1838
1839impl<'a> Iterator for SerializableErrorSources<'a> {
1840    type Item = &'a SerializableError;
1841
1842    fn next(&mut self) -> Option<Self::Item> {
1843        let current = self.current?;
1844        self.current = current.source.as_deref();
1845        Some(current)
1846    }
1847}
1848
1849mod serializable_error_serde {
1850    use super::*;
1851
1852    #[derive(Serialize, Deserialize)]
1853    struct Ser {
1854        message: String,
1855        // For backwards compatibility with IoErrorDescription, which
1856        // didn't have a causes field.
1857        #[serde(default)]
1858        causes: Vec<String>,
1859    }
1860
1861    impl Serialize for SerializableError {
1862        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1863            let mut causes = Vec::new();
1864            let mut cause = self.source.as_ref();
1865            while let Some(c) = cause {
1866                causes.push(c.message.clone());
1867                cause = c.source.as_ref();
1868            }
1869
1870            let ser = Ser {
1871                message: self.message.clone(),
1872                causes,
1873            };
1874            ser.serialize(serializer)
1875        }
1876    }
1877
1878    impl<'de> Deserialize<'de> for SerializableError {
1879        fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1880            let ser = Ser::deserialize(deserializer)?;
1881            Ok(SerializableError::from_message_and_causes(
1882                ser.message,
1883                ser.causes,
1884            ))
1885        }
1886    }
1887}
1888
1889#[cfg(test)]
1890mod serializable_error_arbitrary {
1891    use super::*;
1892    use proptest::prelude::*;
1893
1894    impl Arbitrary for SerializableError {
1895        type Parameters = ();
1896        type Strategy = BoxedStrategy<Self>;
1897
1898        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1899            (
1900                any::<String>(),
1901                proptest::collection::vec(any::<String>(), 0..3),
1902            )
1903                .prop_map(|(message, causes)| {
1904                    SerializableError::from_message_and_causes(message, causes)
1905                })
1906                .boxed()
1907        }
1908    }
1909}
1910
1911impl From<ChildExecutionOutput> for ChildExecutionOutputDescription<LiveSpec> {
1912    fn from(output: ChildExecutionOutput) -> Self {
1913        match output {
1914            ChildExecutionOutput::Output {
1915                result,
1916                output,
1917                errors,
1918            } => Self::Output {
1919                result: result.map(ExecutionResultDescription::from),
1920                output: ChildOutputDescription::from(output),
1921                errors: errors.map(|e| e.map(ChildErrorDescription::from)),
1922            },
1923            ChildExecutionOutput::StartError(error) => {
1924                Self::StartError(ChildStartErrorDescription::from(error))
1925            }
1926        }
1927    }
1928}
1929
1930impl From<ChildOutput> for ChildOutputDescription {
1931    fn from(output: ChildOutput) -> Self {
1932        match output {
1933            ChildOutput::Split(split) => Self::Split {
1934                stdout: split.stdout,
1935                stderr: split.stderr,
1936            },
1937            ChildOutput::Combined { output } => Self::Combined { output },
1938        }
1939    }
1940}
1941
1942impl From<ChildStartError> for ChildStartErrorDescription {
1943    fn from(error: ChildStartError) -> Self {
1944        match error {
1945            ChildStartError::TempPath(e) => Self::TempPath {
1946                source: SerializableError::new(&*e),
1947            },
1948            ChildStartError::Spawn(e) => Self::Spawn {
1949                source: SerializableError::new(&*e),
1950            },
1951        }
1952    }
1953}
1954
1955impl From<ChildError> for ChildErrorDescription {
1956    fn from(error: ChildError) -> Self {
1957        match error {
1958            ChildError::Fd(ChildFdError::ReadStdout(e)) => Self::ReadStdout {
1959                source: SerializableError::new(&*e),
1960            },
1961            ChildError::Fd(ChildFdError::ReadStderr(e)) => Self::ReadStderr {
1962                source: SerializableError::new(&*e),
1963            },
1964            ChildError::Fd(ChildFdError::ReadCombined(e)) => Self::ReadCombined {
1965                source: SerializableError::new(&*e),
1966            },
1967            ChildError::Fd(ChildFdError::Wait(e)) => Self::Wait {
1968                source: SerializableError::new(&*e),
1969            },
1970            ChildError::SetupScriptOutput(e) => Self::SetupScriptOutput {
1971                source: SerializableError::new(&e),
1972            },
1973        }
1974    }
1975}
1976
1977/// Data related to retries for a test.
1978#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
1979#[serde(rename_all = "kebab-case")]
1980#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1981pub struct RetryData {
1982    /// The current attempt. In the range `[1, total_attempts]`.
1983    pub attempt: u32,
1984
1985    /// The total number of times this test can be run. Equal to `1 + retries`.
1986    pub total_attempts: u32,
1987}
1988
1989impl RetryData {
1990    /// Returns true if there are no more attempts after this.
1991    pub fn is_last_attempt(&self) -> bool {
1992        self.attempt >= self.total_attempts
1993    }
1994}
1995
1996/// Whether a test passed, failed or an error occurred while executing the test.
1997#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1998pub enum ExecutionResult {
1999    /// The test passed.
2000    Pass,
2001    /// The test passed but leaked handles. This usually indicates that
2002    /// a subprocess that inherit standard IO was created, but it didn't shut down when
2003    /// the test failed.
2004    Leak {
2005        /// Whether this leak was treated as a failure.
2006        ///
2007        /// Note the difference between `Fail { leaked: true }` and `Leak {
2008        /// failed: true }`. In the former case, the test failed and also leaked
2009        /// handles. In the latter case, the test passed but leaked handles, and
2010        /// configuration indicated that this is a failure.
2011        result: LeakTimeoutResult,
2012    },
2013    /// The test failed.
2014    Fail {
2015        /// The abort status of the test, if any (for example, the signal on Unix).
2016        failure_status: FailureStatus,
2017
2018        /// Whether a test leaked handles. If set to true, this usually indicates that
2019        /// a subprocess that inherit standard IO was created, but it didn't shut down when
2020        /// the test failed.
2021        leaked: bool,
2022    },
2023    /// An error occurred while executing the test.
2024    ExecFail,
2025    /// The test was terminated due to a timeout.
2026    Timeout {
2027        /// Whether this timeout was treated as a failure.
2028        result: SlowTimeoutResult,
2029    },
2030}
2031
2032impl ExecutionResult {
2033    /// Returns true if the test was successful.
2034    pub fn is_success(self) -> bool {
2035        match self {
2036            ExecutionResult::Pass
2037            | ExecutionResult::Timeout {
2038                result: SlowTimeoutResult::Pass,
2039            }
2040            | ExecutionResult::Leak {
2041                result: LeakTimeoutResult::Pass,
2042            } => true,
2043            ExecutionResult::Leak {
2044                result: LeakTimeoutResult::Fail,
2045            }
2046            | ExecutionResult::Fail { .. }
2047            | ExecutionResult::ExecFail
2048            | ExecutionResult::Timeout {
2049                result: SlowTimeoutResult::Fail,
2050            } => false,
2051        }
2052    }
2053
2054    /// Returns a static string representation of the result.
2055    pub fn as_static_str(&self) -> &'static str {
2056        match self {
2057            ExecutionResult::Pass => "pass",
2058            ExecutionResult::Leak { .. } => "leak",
2059            ExecutionResult::Fail { .. } => "fail",
2060            ExecutionResult::ExecFail => "exec-fail",
2061            ExecutionResult::Timeout { .. } => "timeout",
2062        }
2063    }
2064}
2065
2066/// Failure status: either an exit code or an abort status.
2067#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2068pub enum FailureStatus {
2069    /// The test exited with a non-zero exit code.
2070    ExitCode(i32),
2071
2072    /// The test aborted.
2073    Abort(AbortStatus),
2074}
2075
2076impl FailureStatus {
2077    /// Extract the failure status from an `ExitStatus`.
2078    pub fn extract(exit_status: ExitStatus) -> Self {
2079        if let Some(abort_status) = AbortStatus::extract(exit_status) {
2080            FailureStatus::Abort(abort_status)
2081        } else {
2082            FailureStatus::ExitCode(
2083                exit_status
2084                    .code()
2085                    .expect("if abort_status is None, then code must be present"),
2086            )
2087        }
2088    }
2089}
2090
2091/// A regular exit code or Windows NT abort status for a test.
2092///
2093/// Returned as part of the [`ExecutionResult::Fail`] variant.
2094#[derive(Copy, Clone, Eq, PartialEq)]
2095pub enum AbortStatus {
2096    /// The test was aborted due to a signal on Unix.
2097    #[cfg(unix)]
2098    UnixSignal(i32),
2099
2100    /// The test was determined to have aborted because the high bit was set on Windows.
2101    #[cfg(windows)]
2102    WindowsNtStatus(windows_sys::Win32::Foundation::NTSTATUS),
2103
2104    /// The test was terminated via job object on Windows.
2105    #[cfg(windows)]
2106    JobObject,
2107}
2108
2109impl AbortStatus {
2110    /// Extract the abort status from an [`ExitStatus`].
2111    pub fn extract(exit_status: ExitStatus) -> Option<Self> {
2112        cfg_if::cfg_if! {
2113            if #[cfg(unix)] {
2114                // On Unix, extract the signal if it's found.
2115                use std::os::unix::process::ExitStatusExt;
2116                exit_status.signal().map(AbortStatus::UnixSignal)
2117            } else if #[cfg(windows)] {
2118                exit_status.code().and_then(|code| {
2119                    (code < 0).then_some(AbortStatus::WindowsNtStatus(code))
2120                })
2121            } else {
2122                None
2123            }
2124        }
2125    }
2126}
2127
2128impl fmt::Debug for AbortStatus {
2129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2130        match self {
2131            #[cfg(unix)]
2132            AbortStatus::UnixSignal(signal) => write!(f, "UnixSignal({signal})"),
2133            #[cfg(windows)]
2134            AbortStatus::WindowsNtStatus(status) => write!(f, "WindowsNtStatus({status:x})"),
2135            #[cfg(windows)]
2136            AbortStatus::JobObject => write!(f, "JobObject"),
2137        }
2138    }
2139}
2140
2141/// A platform-independent description of an abort status.
2142///
2143/// This type can be serialized on one platform and deserialized on another,
2144/// containing all information needed for display without requiring
2145/// platform-specific lookups.
2146#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2147#[serde(tag = "kind", rename_all = "kebab-case")]
2148#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2149#[non_exhaustive]
2150pub enum AbortDescription {
2151    /// The process was aborted by a Unix signal.
2152    UnixSignal {
2153        /// The signal number.
2154        signal: i32,
2155        /// The signal name without the "SIG" prefix (e.g., "TERM", "SEGV"),
2156        /// if known.
2157        #[cfg_attr(
2158            test,
2159            strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
2160        )]
2161        name: Option<SmolStr>,
2162    },
2163
2164    /// The process was aborted with a Windows NT status code.
2165    WindowsNtStatus {
2166        /// The NTSTATUS code.
2167        code: i32,
2168        /// The human-readable message from the Win32 error code, if available.
2169        #[cfg_attr(
2170            test,
2171            strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
2172        )]
2173        message: Option<SmolStr>,
2174    },
2175
2176    /// The process was terminated via a Windows job object.
2177    WindowsJobObject,
2178}
2179
2180impl fmt::Display for AbortDescription {
2181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2182        match self {
2183            Self::UnixSignal { signal, name } => {
2184                write!(f, "aborted with signal {signal}")?;
2185                if let Some(name) = name {
2186                    write!(f, " (SIG{name})")?;
2187                }
2188                Ok(())
2189            }
2190            Self::WindowsNtStatus { code, message } => {
2191                write!(f, "aborted with code {code:#010x}")?;
2192                if let Some(message) = message {
2193                    write!(f, ": {message}")?;
2194                }
2195                Ok(())
2196            }
2197            Self::WindowsJobObject => {
2198                write!(f, "terminated via job object")
2199            }
2200        }
2201    }
2202}
2203
2204impl From<AbortStatus> for AbortDescription {
2205    fn from(status: AbortStatus) -> Self {
2206        cfg_if::cfg_if! {
2207            if #[cfg(unix)] {
2208                match status {
2209                    AbortStatus::UnixSignal(signal) => Self::UnixSignal {
2210                        signal,
2211                        name: crate::helpers::signal_str(signal).map(SmolStr::new_static),
2212                    },
2213                }
2214            } else if #[cfg(windows)] {
2215                match status {
2216                    AbortStatus::WindowsNtStatus(code) => Self::WindowsNtStatus {
2217                        code,
2218                        message: crate::helpers::windows_nt_status_message(code),
2219                    },
2220                    AbortStatus::JobObject => Self::WindowsJobObject,
2221                }
2222            } else {
2223                match status {}
2224            }
2225        }
2226    }
2227}
2228
2229/// A platform-independent description of a test failure status.
2230///
2231/// This is the platform-independent counterpart to [`FailureStatus`].
2232#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2233#[serde(tag = "kind", rename_all = "kebab-case")]
2234#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2235#[non_exhaustive]
2236pub enum FailureDescription {
2237    /// The test exited with a non-zero exit code.
2238    ExitCode {
2239        /// The exit code.
2240        code: i32,
2241    },
2242
2243    /// The test was aborted (e.g., by a signal on Unix or NT status on Windows).
2244    ///
2245    /// Note: this is a struct variant rather than a newtype variant to ensure
2246    /// proper JSON nesting. Both `FailureDescription` and `AbortDescription`
2247    /// use `#[serde(tag = "kind")]`, and if this were a newtype variant, serde
2248    /// would flatten the inner type causing duplicate `"kind"` fields.
2249    Abort {
2250        /// The abort description.
2251        abort: AbortDescription,
2252    },
2253}
2254
2255impl From<FailureStatus> for FailureDescription {
2256    fn from(status: FailureStatus) -> Self {
2257        match status {
2258            FailureStatus::ExitCode(code) => Self::ExitCode { code },
2259            FailureStatus::Abort(abort) => Self::Abort {
2260                abort: AbortDescription::from(abort),
2261            },
2262        }
2263    }
2264}
2265
2266impl fmt::Display for FailureDescription {
2267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2268        match self {
2269            Self::ExitCode { code } => write!(f, "exited with code {code}"),
2270            Self::Abort { abort } => write!(f, "{abort}"),
2271        }
2272    }
2273}
2274
2275/// A platform-independent description of a test execution result.
2276///
2277/// This is the platform-independent counterpart to [`ExecutionResult`], used
2278/// in external-facing types like [`ExecuteStatus`].
2279#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2280#[serde(tag = "status", rename_all = "kebab-case")]
2281#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2282#[non_exhaustive]
2283pub enum ExecutionResultDescription {
2284    /// The test passed.
2285    Pass,
2286
2287    /// The test passed but leaked handles.
2288    Leak {
2289        /// Whether this leak was treated as a failure.
2290        result: LeakTimeoutResult,
2291    },
2292
2293    /// The test failed.
2294    Fail {
2295        /// The failure status.
2296        failure: FailureDescription,
2297
2298        /// Whether the test leaked handles.
2299        leaked: bool,
2300    },
2301
2302    /// An error occurred while executing the test.
2303    ExecFail,
2304
2305    /// The test was terminated due to a timeout.
2306    Timeout {
2307        /// Whether this timeout was treated as a failure.
2308        result: SlowTimeoutResult,
2309    },
2310}
2311
2312impl ExecutionResultDescription {
2313    /// Returns true if the test was successful.
2314    pub fn is_success(&self) -> bool {
2315        match self {
2316            Self::Pass
2317            | Self::Timeout {
2318                result: SlowTimeoutResult::Pass,
2319            }
2320            | Self::Leak {
2321                result: LeakTimeoutResult::Pass,
2322            } => true,
2323            Self::Leak {
2324                result: LeakTimeoutResult::Fail,
2325            }
2326            | Self::Fail { .. }
2327            | Self::ExecFail
2328            | Self::Timeout {
2329                result: SlowTimeoutResult::Fail,
2330            } => false,
2331        }
2332    }
2333
2334    /// Returns a static string representation of the result.
2335    pub fn as_static_str(&self) -> &'static str {
2336        match self {
2337            Self::Pass => "pass",
2338            Self::Leak { .. } => "leak",
2339            Self::Fail { .. } => "fail",
2340            Self::ExecFail => "exec-fail",
2341            Self::Timeout { .. } => "timeout",
2342        }
2343    }
2344
2345    /// Returns true if this result represents a test that was terminated by nextest
2346    /// (as opposed to failing naturally).
2347    ///
2348    /// This is used to suppress output spam when running under
2349    /// TestFailureImmediate.
2350    ///
2351    /// TODO: This is a heuristic that checks if the test was terminated by
2352    /// SIGTERM (Unix) or job object (Windows). In an edge case, a test could
2353    /// send SIGTERM to itself, which would incorrectly be detected as a
2354    /// nextest-initiated termination. A more robust solution would track which
2355    /// tests were explicitly sent termination signals by nextest.
2356    pub fn is_termination_failure(&self) -> bool {
2357        matches!(
2358            self,
2359            Self::Fail {
2360                failure: FailureDescription::Abort {
2361                    abort: AbortDescription::UnixSignal {
2362                        signal: SIGTERM,
2363                        ..
2364                    },
2365                },
2366                ..
2367            } | Self::Fail {
2368                failure: FailureDescription::Abort {
2369                    abort: AbortDescription::WindowsJobObject,
2370                },
2371                ..
2372            }
2373        )
2374    }
2375}
2376
2377impl From<ExecutionResult> for ExecutionResultDescription {
2378    fn from(result: ExecutionResult) -> Self {
2379        match result {
2380            ExecutionResult::Pass => Self::Pass,
2381            ExecutionResult::Leak { result } => Self::Leak { result },
2382            ExecutionResult::Fail {
2383                failure_status,
2384                leaked,
2385            } => Self::Fail {
2386                failure: FailureDescription::from(failure_status),
2387                leaked,
2388            },
2389            ExecutionResult::ExecFail => Self::ExecFail,
2390            ExecutionResult::Timeout { result } => Self::Timeout { result },
2391        }
2392    }
2393}
2394
2395// Note: the order here matters -- it indicates severity of cancellation
2396/// The reason why a test run is being cancelled.
2397#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
2398#[serde(rename_all = "kebab-case")]
2399#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2400pub enum CancelReason {
2401    /// A setup script failed.
2402    SetupScriptFailure,
2403
2404    /// A test failed and --no-fail-fast wasn't specified.
2405    TestFailure,
2406
2407    /// An error occurred while reporting results.
2408    ReportError,
2409
2410    /// A test failed and fail-fast with immediate termination was specified.
2411    TestFailureImmediate,
2412
2413    /// The global timeout was exceeded.
2414    GlobalTimeout,
2415
2416    /// A termination signal (on Unix, SIGTERM or SIGHUP) was received.
2417    Signal,
2418
2419    /// An interrupt (on Unix, Ctrl-C) was received.
2420    Interrupt,
2421
2422    /// A second signal was received, and the run is being forcibly killed.
2423    SecondSignal,
2424}
2425
2426impl CancelReason {
2427    pub(crate) fn to_static_str(self) -> &'static str {
2428        match self {
2429            CancelReason::SetupScriptFailure => "setup script failure",
2430            CancelReason::TestFailure => "test failure",
2431            CancelReason::ReportError => "reporting error",
2432            CancelReason::TestFailureImmediate => "test failure",
2433            CancelReason::GlobalTimeout => "global timeout",
2434            CancelReason::Signal => "signal",
2435            CancelReason::Interrupt => "interrupt",
2436            CancelReason::SecondSignal => "second signal",
2437        }
2438    }
2439}
2440/// The kind of unit of work that nextest is executing.
2441#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2442pub enum UnitKind {
2443    /// A test.
2444    Test,
2445
2446    /// A script (e.g. a setup script).
2447    Script,
2448}
2449
2450impl UnitKind {
2451    pub(crate) const WAITING_ON_TEST_MESSAGE: &str = "waiting on test process";
2452    pub(crate) const WAITING_ON_SCRIPT_MESSAGE: &str = "waiting on script process";
2453
2454    pub(crate) const EXECUTING_TEST_MESSAGE: &str = "executing test";
2455    pub(crate) const EXECUTING_SCRIPT_MESSAGE: &str = "executing script";
2456
2457    pub(crate) fn waiting_on_message(&self) -> &'static str {
2458        match self {
2459            UnitKind::Test => Self::WAITING_ON_TEST_MESSAGE,
2460            UnitKind::Script => Self::WAITING_ON_SCRIPT_MESSAGE,
2461        }
2462    }
2463
2464    pub(crate) fn executing_message(&self) -> &'static str {
2465        match self {
2466            UnitKind::Test => Self::EXECUTING_TEST_MESSAGE,
2467            UnitKind::Script => Self::EXECUTING_SCRIPT_MESSAGE,
2468        }
2469    }
2470}
2471
2472/// A response to an information request.
2473#[derive(Clone, Debug)]
2474pub enum InfoResponse<'a> {
2475    /// A setup script's response.
2476    SetupScript(SetupScriptInfoResponse),
2477
2478    /// A test's response.
2479    Test(TestInfoResponse<'a>),
2480}
2481
2482/// A setup script's response to an information request.
2483#[derive(Clone, Debug)]
2484pub struct SetupScriptInfoResponse {
2485    /// The stress index of the setup script.
2486    pub stress_index: Option<StressIndex>,
2487
2488    /// The identifier of the setup script instance.
2489    pub script_id: ScriptId,
2490
2491    /// The program to run.
2492    pub program: String,
2493
2494    /// The list of arguments to the program.
2495    pub args: Vec<String>,
2496
2497    /// The state of the setup script.
2498    pub state: UnitState,
2499
2500    /// Output obtained from the setup script.
2501    pub output: ChildExecutionOutputDescription<LiveSpec>,
2502}
2503
2504/// A test's response to an information request.
2505#[derive(Clone, Debug)]
2506pub struct TestInfoResponse<'a> {
2507    /// The stress index of the test.
2508    pub stress_index: Option<StressIndex>,
2509
2510    /// The test instance that the information is about.
2511    pub test_instance: TestInstanceId<'a>,
2512
2513    /// Information about retries.
2514    pub retry_data: RetryData,
2515
2516    /// The state of the test.
2517    pub state: UnitState,
2518
2519    /// Output obtained from the test.
2520    pub output: ChildExecutionOutputDescription<LiveSpec>,
2521}
2522
2523/// The current state of a test or script process: running, exiting, or
2524/// terminating.
2525///
2526/// Part of information response requests.
2527#[derive(Clone, Debug)]
2528pub enum UnitState {
2529    /// The unit is currently running.
2530    Running {
2531        /// The process ID.
2532        pid: u32,
2533
2534        /// The amount of time the unit has been running.
2535        time_taken: Duration,
2536
2537        /// `Some` if the test is marked as slow, along with the duration after
2538        /// which it was marked as slow.
2539        slow_after: Option<Duration>,
2540    },
2541
2542    /// The test has finished running, and is currently in the process of
2543    /// exiting.
2544    Exiting {
2545        /// The process ID.
2546        pid: u32,
2547
2548        /// The amount of time the unit ran for.
2549        time_taken: Duration,
2550
2551        /// `Some` if the unit is marked as slow, along with the duration after
2552        /// which it was marked as slow.
2553        slow_after: Option<Duration>,
2554
2555        /// The tentative execution result before leaked status is determined.
2556        ///
2557        /// None means that the exit status could not be read, and should be
2558        /// treated as a failure.
2559        tentative_result: Option<ExecutionResultDescription>,
2560
2561        /// How long has been spent waiting for the process to exit.
2562        waiting_duration: Duration,
2563
2564        /// How much longer nextest will wait until the test is marked leaky.
2565        remaining: Duration,
2566    },
2567
2568    /// The child process is being terminated by nextest.
2569    Terminating(UnitTerminatingState),
2570
2571    /// The unit has finished running and the process has exited.
2572    Exited {
2573        /// The result of executing the unit.
2574        result: ExecutionResultDescription,
2575
2576        /// The amount of time the unit ran for.
2577        time_taken: Duration,
2578
2579        /// `Some` if the unit is marked as slow, along with the duration after
2580        /// which it was marked as slow.
2581        slow_after: Option<Duration>,
2582    },
2583
2584    /// A delay is being waited out before the next attempt of the test is
2585    /// started. (Only relevant for tests.)
2586    DelayBeforeNextAttempt {
2587        /// The previous execution result.
2588        previous_result: ExecutionResultDescription,
2589
2590        /// Whether the previous attempt was marked as slow.
2591        previous_slow: bool,
2592
2593        /// How long has been spent waiting so far.
2594        waiting_duration: Duration,
2595
2596        /// How much longer nextest will wait until retrying the test.
2597        remaining: Duration,
2598    },
2599}
2600
2601impl UnitState {
2602    /// Returns true if the state has a valid output attached to it.
2603    pub fn has_valid_output(&self) -> bool {
2604        match self {
2605            UnitState::Running { .. }
2606            | UnitState::Exiting { .. }
2607            | UnitState::Terminating(_)
2608            | UnitState::Exited { .. } => true,
2609            UnitState::DelayBeforeNextAttempt { .. } => false,
2610        }
2611    }
2612}
2613
2614/// The current terminating state of a test or script process.
2615///
2616/// Part of [`UnitState::Terminating`].
2617#[derive(Clone, Debug)]
2618pub struct UnitTerminatingState {
2619    /// The process ID.
2620    pub pid: u32,
2621
2622    /// The amount of time the unit ran for.
2623    pub time_taken: Duration,
2624
2625    /// The reason for the termination.
2626    pub reason: UnitTerminateReason,
2627
2628    /// The method by which the process is being terminated.
2629    pub method: UnitTerminateMethod,
2630
2631    /// How long has been spent waiting for the process to exit.
2632    pub waiting_duration: Duration,
2633
2634    /// How much longer nextest will wait until a kill command is sent to the process.
2635    pub remaining: Duration,
2636}
2637
2638/// The reason for a script or test being forcibly terminated by nextest.
2639///
2640/// Part of information response requests.
2641#[derive(Clone, Copy, Debug)]
2642pub enum UnitTerminateReason {
2643    /// The unit is being terminated due to a test timeout being hit.
2644    Timeout,
2645
2646    /// The unit is being terminated due to nextest receiving a signal.
2647    Signal,
2648
2649    /// The unit is being terminated due to an interrupt (i.e. Ctrl-C).
2650    Interrupt,
2651}
2652
2653impl fmt::Display for UnitTerminateReason {
2654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2655        match self {
2656            UnitTerminateReason::Timeout => write!(f, "timeout"),
2657            UnitTerminateReason::Signal => write!(f, "signal"),
2658            UnitTerminateReason::Interrupt => write!(f, "interrupt"),
2659        }
2660    }
2661}
2662
2663/// The way in which a script or test is being forcibly terminated by nextest.
2664#[derive(Clone, Copy, Debug)]
2665pub enum UnitTerminateMethod {
2666    /// The unit is being terminated by sending a signal.
2667    #[cfg(unix)]
2668    Signal(UnitTerminateSignal),
2669
2670    /// The unit is being terminated by terminating the Windows job object.
2671    #[cfg(windows)]
2672    JobObject,
2673
2674    /// The unit is being waited on to exit. A termination signal will be sent
2675    /// if it doesn't exit within the grace period.
2676    ///
2677    /// On Windows, this occurs when nextest receives Ctrl-C. In that case, it
2678    /// is assumed that tests will also receive Ctrl-C and exit on their own. If
2679    /// tests do not exit within the grace period configured for them, their
2680    /// corresponding job objects will be terminated.
2681    #[cfg(windows)]
2682    Wait,
2683
2684    /// A fake method used for testing.
2685    #[cfg(test)]
2686    Fake,
2687}
2688
2689#[cfg(unix)]
2690/// The signal that is or was sent to terminate a script or test.
2691#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2692pub enum UnitTerminateSignal {
2693    /// The unit is being terminated by sending a SIGINT.
2694    Interrupt,
2695
2696    /// The unit is being terminated by sending a SIGTERM signal.
2697    Term,
2698
2699    /// The unit is being terminated by sending a SIGHUP signal.
2700    Hangup,
2701
2702    /// The unit is being terminated by sending a SIGQUIT signal.
2703    Quit,
2704
2705    /// The unit is being terminated by sending a SIGKILL signal.
2706    Kill,
2707}
2708
2709#[cfg(unix)]
2710impl fmt::Display for UnitTerminateSignal {
2711    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2712        match self {
2713            UnitTerminateSignal::Interrupt => write!(f, "SIGINT"),
2714            UnitTerminateSignal::Term => write!(f, "SIGTERM"),
2715            UnitTerminateSignal::Hangup => write!(f, "SIGHUP"),
2716            UnitTerminateSignal::Quit => write!(f, "SIGQUIT"),
2717            UnitTerminateSignal::Kill => write!(f, "SIGKILL"),
2718        }
2719    }
2720}
2721
2722#[cfg(test)]
2723mod tests {
2724    use super::*;
2725    use proptest::prelude::*;
2726    use test_strategy::proptest;
2727
2728    #[derive(Clone, Copy, Debug)]
2729    enum FailureSummary {
2730        Failed,
2731        Cancelled,
2732    }
2733
2734    const FAILURE_SUMMARY_BY_CANCEL_REASON: [(Option<CancelReason>, FailureSummary); 9] = [
2735        (None, FailureSummary::Failed),
2736        (
2737            Some(CancelReason::SetupScriptFailure),
2738            FailureSummary::Failed,
2739        ),
2740        (Some(CancelReason::TestFailure), FailureSummary::Failed),
2741        (Some(CancelReason::ReportError), FailureSummary::Cancelled),
2742        (
2743            Some(CancelReason::TestFailureImmediate),
2744            FailureSummary::Failed,
2745        ),
2746        (Some(CancelReason::GlobalTimeout), FailureSummary::Cancelled),
2747        (Some(CancelReason::Signal), FailureSummary::Cancelled),
2748        (Some(CancelReason::Interrupt), FailureSummary::Cancelled),
2749        (Some(CancelReason::SecondSignal), FailureSummary::Cancelled),
2750    ];
2751
2752    /// Ensure that all possible cancel reasons are covered in
2753    /// `FAILURE_SUMMARY_BY_CANCEL_REASON` above.
2754    #[proptest]
2755    fn failure_summary_by_cancel_reason_is_complete(cancel_reason: Option<CancelReason>) {
2756        prop_assert!(
2757            FAILURE_SUMMARY_BY_CANCEL_REASON
2758                .iter()
2759                .any(|(reason, _)| *reason == cancel_reason),
2760            "FAILURE_SUMMARY_BY_CANCEL_REASON has a row for {cancel_reason:?}"
2761        );
2762    }
2763
2764    #[track_caller]
2765    fn assert_failure_summary_by_cancel_reason(stats: RunStats, kind: RunStatsFailureKind) {
2766        for (cancel_reason, expected) in FAILURE_SUMMARY_BY_CANCEL_REASON {
2767            let expected = match expected {
2768                FailureSummary::Failed => FinalRunStats::Failed { kind },
2769                FailureSummary::Cancelled => FinalRunStats::Cancelled {
2770                    reason: cancel_reason,
2771                    kind,
2772                },
2773            };
2774            assert_eq!(
2775                RunStats {
2776                    cancel_reason,
2777                    ..stats
2778                }
2779                .summarize_final(),
2780                expected,
2781                "summary for cancel reason {cancel_reason:?}"
2782            );
2783        }
2784    }
2785
2786    #[test]
2787    fn summarize_final_failed_test_by_cancel_reason() {
2788        assert_failure_summary_by_cancel_reason(
2789            RunStats {
2790                initial_run_count: 3,
2791                finished_count: 2,
2792                passed: 1,
2793                failed: 1,
2794                ..RunStats::default()
2795            },
2796            RunStatsFailureKind::Test {
2797                initial_run_count: 3,
2798                not_run: 1,
2799            },
2800        );
2801    }
2802
2803    #[test]
2804    fn summarize_final_failed_setup_script_by_cancel_reason() {
2805        assert_failure_summary_by_cancel_reason(
2806            RunStats {
2807                setup_scripts_initial_count: 2,
2808                setup_scripts_finished_count: 1,
2809                setup_scripts_failed: 1,
2810                ..RunStats::default()
2811            },
2812            RunStatsFailureKind::SetupScript,
2813        );
2814    }
2815
2816    #[test]
2817    fn test_is_success() {
2818        assert_eq!(
2819            RunStats::default().summarize_final(),
2820            FinalRunStats::NoTestsRun,
2821            "empty run => no tests run"
2822        );
2823        assert_eq!(
2824            RunStats {
2825                initial_run_count: 42,
2826                finished_count: 42,
2827                ..RunStats::default()
2828            }
2829            .summarize_final(),
2830            FinalRunStats::Success,
2831            "initial run count = final run count => success"
2832        );
2833        assert_eq!(
2834            RunStats {
2835                initial_run_count: 42,
2836                finished_count: 41,
2837                ..RunStats::default()
2838            }
2839            .summarize_final(),
2840            FinalRunStats::Cancelled {
2841                reason: None,
2842                kind: RunStatsFailureKind::Test {
2843                    initial_run_count: 42,
2844                    not_run: 1
2845                }
2846            },
2847            "initial run count > final run count => cancelled"
2848        );
2849        assert_eq!(
2850            RunStats {
2851                initial_run_count: 42,
2852                finished_count: 42,
2853                failed: 1,
2854                ..RunStats::default()
2855            }
2856            .summarize_final(),
2857            FinalRunStats::Failed {
2858                kind: RunStatsFailureKind::Test {
2859                    initial_run_count: 42,
2860                    not_run: 0,
2861                },
2862            },
2863            "failed => failure"
2864        );
2865        assert_eq!(
2866            RunStats {
2867                initial_run_count: 42,
2868                finished_count: 42,
2869                exec_failed: 1,
2870                ..RunStats::default()
2871            }
2872            .summarize_final(),
2873            FinalRunStats::Failed {
2874                kind: RunStatsFailureKind::Test {
2875                    initial_run_count: 42,
2876                    not_run: 0,
2877                },
2878            },
2879            "exec failed => failure"
2880        );
2881        assert_eq!(
2882            RunStats {
2883                initial_run_count: 42,
2884                finished_count: 42,
2885                failed_timed_out: 1,
2886                ..RunStats::default()
2887            }
2888            .summarize_final(),
2889            FinalRunStats::Failed {
2890                kind: RunStatsFailureKind::Test {
2891                    initial_run_count: 42,
2892                    not_run: 0,
2893                },
2894            },
2895            "timed out => failure {:?} {:?}",
2896            RunStats {
2897                initial_run_count: 42,
2898                finished_count: 42,
2899                failed_timed_out: 1,
2900                ..RunStats::default()
2901            }
2902            .summarize_final(),
2903            FinalRunStats::Failed {
2904                kind: RunStatsFailureKind::Test {
2905                    initial_run_count: 42,
2906                    not_run: 0,
2907                },
2908            },
2909        );
2910        assert_eq!(
2911            RunStats {
2912                initial_run_count: 42,
2913                finished_count: 42,
2914                skipped: 1,
2915                ..RunStats::default()
2916            }
2917            .summarize_final(),
2918            FinalRunStats::Success,
2919            "skipped => not considered a failure"
2920        );
2921
2922        assert_eq!(
2923            RunStats {
2924                setup_scripts_initial_count: 2,
2925                setup_scripts_finished_count: 1,
2926                ..RunStats::default()
2927            }
2928            .summarize_final(),
2929            FinalRunStats::Cancelled {
2930                reason: None,
2931                kind: RunStatsFailureKind::SetupScript,
2932            },
2933            "setup script failed => failure"
2934        );
2935
2936        assert_eq!(
2937            RunStats {
2938                setup_scripts_initial_count: 2,
2939                setup_scripts_finished_count: 2,
2940                setup_scripts_failed: 1,
2941                ..RunStats::default()
2942            }
2943            .summarize_final(),
2944            FinalRunStats::Failed {
2945                kind: RunStatsFailureKind::SetupScript,
2946            },
2947            "setup script failed => failure"
2948        );
2949        assert_eq!(
2950            RunStats {
2951                setup_scripts_initial_count: 2,
2952                setup_scripts_finished_count: 2,
2953                setup_scripts_exec_failed: 1,
2954                ..RunStats::default()
2955            }
2956            .summarize_final(),
2957            FinalRunStats::Failed {
2958                kind: RunStatsFailureKind::SetupScript,
2959            },
2960            "setup script exec failed => failure"
2961        );
2962        assert_eq!(
2963            RunStats {
2964                setup_scripts_initial_count: 2,
2965                setup_scripts_finished_count: 2,
2966                setup_scripts_timed_out: 1,
2967                ..RunStats::default()
2968            }
2969            .summarize_final(),
2970            FinalRunStats::Failed {
2971                kind: RunStatsFailureKind::SetupScript,
2972            },
2973            "setup script timed out => failure"
2974        );
2975        assert_eq!(
2976            RunStats {
2977                setup_scripts_initial_count: 2,
2978                setup_scripts_finished_count: 2,
2979                setup_scripts_passed: 2,
2980                ..RunStats::default()
2981            }
2982            .summarize_final(),
2983            FinalRunStats::NoTestsRun,
2984            "setup scripts passed => success, but no tests run"
2985        );
2986
2987        // Flaky tests with flaky-result = "fail" are included in `failed`, so this
2988        // is covered by the general failure tests above.
2989    }
2990
2991    /// Helper to build a minimal `ExecuteStatus<LiveSpec>` for tests.
2992    fn make_execute_status(
2993        result: ExecutionResultDescription,
2994        attempt: u32,
2995        total_attempts: u32,
2996    ) -> ExecuteStatus<LiveSpec> {
2997        make_execute_status_slow(result, attempt, total_attempts, false)
2998    }
2999
3000    /// Helper to build a minimal `ExecuteStatus<LiveSpec>` for tests, with
3001    /// the `is_slow` flag set.
3002    fn make_execute_status_slow(
3003        result: ExecutionResultDescription,
3004        attempt: u32,
3005        total_attempts: u32,
3006        is_slow: bool,
3007    ) -> ExecuteStatus<LiveSpec> {
3008        ExecuteStatus {
3009            retry_data: RetryData {
3010                attempt,
3011                total_attempts,
3012            },
3013            output: ChildExecutionOutputDescription::Output {
3014                result: Some(result.clone()),
3015                output: ChildOutputDescription::Split {
3016                    stdout: None,
3017                    stderr: None,
3018                },
3019                errors: None,
3020            },
3021            result,
3022            start_time: chrono::Utc::now().into(),
3023            time_taken: Duration::from_millis(100),
3024            is_slow,
3025            delay_before_start: Duration::ZERO,
3026            error_summary: None,
3027            output_error_slice: None,
3028        }
3029    }
3030
3031    #[test]
3032    fn is_success_for_output_by_variant() {
3033        // Success: single passing run → true.
3034        let pass_status = make_execute_status(ExecutionResultDescription::Pass, 1, 1);
3035        let success_statuses = ExecutionStatuses::new(vec![pass_status], FlakyResult::Pass);
3036        let describe = success_statuses.describe();
3037        assert!(
3038            matches!(describe, ExecutionDescription::Success { .. }),
3039            "single pass is Success"
3040        );
3041        assert!(
3042            describe.is_success_for_output(),
3043            "Success: output is success output"
3044        );
3045
3046        // Flaky pass: fail then pass → true.
3047        let fail_status = make_execute_status(
3048            ExecutionResultDescription::Fail {
3049                failure: FailureDescription::ExitCode { code: 1 },
3050                leaked: false,
3051            },
3052            1,
3053            2,
3054        );
3055        let pass_status = make_execute_status(ExecutionResultDescription::Pass, 2, 2);
3056        let flaky_pass_statuses =
3057            ExecutionStatuses::new(vec![fail_status, pass_status], FlakyResult::Pass);
3058        let describe = flaky_pass_statuses.describe();
3059        assert!(
3060            matches!(
3061                describe,
3062                ExecutionDescription::Flaky {
3063                    result: FlakyResult::Pass,
3064                    ..
3065                }
3066            ),
3067            "fail then pass with FlakyResult::Pass is Flaky Pass"
3068        );
3069        assert!(
3070            describe.is_success_for_output(),
3071            "Flaky pass: output is success output"
3072        );
3073
3074        // Flaky fail: fail then pass with result=fail → true.
3075        let fail_status = make_execute_status(
3076            ExecutionResultDescription::Fail {
3077                failure: FailureDescription::ExitCode { code: 1 },
3078                leaked: false,
3079            },
3080            1,
3081            2,
3082        );
3083        let pass_status = make_execute_status(ExecutionResultDescription::Pass, 2, 2);
3084        let flaky_fail_statuses =
3085            ExecutionStatuses::new(vec![fail_status, pass_status], FlakyResult::Fail);
3086        let describe = flaky_fail_statuses.describe();
3087        assert!(
3088            matches!(
3089                describe,
3090                ExecutionDescription::Flaky {
3091                    result: FlakyResult::Fail,
3092                    ..
3093                }
3094            ),
3095            "fail then pass with FlakyResult::Fail is Flaky Fail"
3096        );
3097        assert!(
3098            describe.is_success_for_output(),
3099            "Flaky fail: output is still success output (last attempt passed)"
3100        );
3101
3102        // Failure: single fail → false.
3103        let fail_status = make_execute_status(
3104            ExecutionResultDescription::Fail {
3105                failure: FailureDescription::ExitCode { code: 1 },
3106                leaked: false,
3107            },
3108            1,
3109            1,
3110        );
3111        let failure_statuses = ExecutionStatuses::new(vec![fail_status], FlakyResult::Pass);
3112        let describe = failure_statuses.describe();
3113        assert!(
3114            matches!(describe, ExecutionDescription::Failure { .. }),
3115            "single fail is Failure"
3116        );
3117        assert!(
3118            !describe.is_success_for_output(),
3119            "Failure: output is not success output"
3120        );
3121
3122        // Failure with retries: all fail → false.
3123        let fail1 = make_execute_status(
3124            ExecutionResultDescription::Fail {
3125                failure: FailureDescription::ExitCode { code: 1 },
3126                leaked: false,
3127            },
3128            1,
3129            2,
3130        );
3131        let fail2 = make_execute_status(
3132            ExecutionResultDescription::Fail {
3133                failure: FailureDescription::ExitCode { code: 1 },
3134                leaked: false,
3135            },
3136            2,
3137            2,
3138        );
3139        let failure_retry_statuses = ExecutionStatuses::new(vec![fail1, fail2], FlakyResult::Pass);
3140        let describe = failure_retry_statuses.describe();
3141        assert!(
3142            matches!(describe, ExecutionDescription::Failure { .. }),
3143            "all-fail with retries is Failure"
3144        );
3145        assert!(
3146            !describe.is_success_for_output(),
3147            "Failure with retries: output is not success output"
3148        );
3149    }
3150
3151    #[test]
3152    fn abort_description_serialization() {
3153        // Unix signal with name.
3154        let unix_with_name = AbortDescription::UnixSignal {
3155            signal: 15,
3156            name: Some("TERM".into()),
3157        };
3158        let json = serde_json::to_string_pretty(&unix_with_name).unwrap();
3159        insta::assert_snapshot!("abort_unix_signal_with_name", json);
3160        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3161        assert_eq!(unix_with_name, roundtrip);
3162
3163        // Unix signal without name.
3164        let unix_no_name = AbortDescription::UnixSignal {
3165            signal: 42,
3166            name: None,
3167        };
3168        let json = serde_json::to_string_pretty(&unix_no_name).unwrap();
3169        insta::assert_snapshot!("abort_unix_signal_no_name", json);
3170        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3171        assert_eq!(unix_no_name, roundtrip);
3172
3173        // Windows NT status (0xC000013A is STATUS_CONTROL_C_EXIT).
3174        let windows_nt = AbortDescription::WindowsNtStatus {
3175            code: -1073741510_i32,
3176            message: Some("The application terminated as a result of a CTRL+C.".into()),
3177        };
3178        let json = serde_json::to_string_pretty(&windows_nt).unwrap();
3179        insta::assert_snapshot!("abort_windows_nt_status", json);
3180        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3181        assert_eq!(windows_nt, roundtrip);
3182
3183        // Windows NT status without message.
3184        let windows_nt_no_msg = AbortDescription::WindowsNtStatus {
3185            code: -1073741819_i32,
3186            message: None,
3187        };
3188        let json = serde_json::to_string_pretty(&windows_nt_no_msg).unwrap();
3189        insta::assert_snapshot!("abort_windows_nt_status_no_message", json);
3190        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3191        assert_eq!(windows_nt_no_msg, roundtrip);
3192
3193        // Windows job object.
3194        let job = AbortDescription::WindowsJobObject;
3195        let json = serde_json::to_string_pretty(&job).unwrap();
3196        insta::assert_snapshot!("abort_windows_job_object", json);
3197        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
3198        assert_eq!(job, roundtrip);
3199    }
3200
3201    #[test]
3202    fn abort_description_cross_platform_deserialization() {
3203        // Cross-platform deserialization: these JSON strings could come from any
3204        // platform. Verify they deserialize correctly regardless of current platform.
3205        let unix_json = r#"{"kind":"unix-signal","signal":11,"name":"SEGV"}"#;
3206        let unix_desc: AbortDescription = serde_json::from_str(unix_json).unwrap();
3207        assert_eq!(
3208            unix_desc,
3209            AbortDescription::UnixSignal {
3210                signal: 11,
3211                name: Some("SEGV".into()),
3212            }
3213        );
3214
3215        let windows_json = r#"{"kind":"windows-nt-status","code":-1073741510,"message":"CTRL+C"}"#;
3216        let windows_desc: AbortDescription = serde_json::from_str(windows_json).unwrap();
3217        assert_eq!(
3218            windows_desc,
3219            AbortDescription::WindowsNtStatus {
3220                code: -1073741510,
3221                message: Some("CTRL+C".into()),
3222            }
3223        );
3224
3225        let job_json = r#"{"kind":"windows-job-object"}"#;
3226        let job_desc: AbortDescription = serde_json::from_str(job_json).unwrap();
3227        assert_eq!(job_desc, AbortDescription::WindowsJobObject);
3228    }
3229
3230    #[test]
3231    fn abort_description_display() {
3232        // Unix signal with name.
3233        let unix = AbortDescription::UnixSignal {
3234            signal: 15,
3235            name: Some("TERM".into()),
3236        };
3237        assert_eq!(unix.to_string(), "aborted with signal 15 (SIGTERM)");
3238
3239        // Unix signal without a name.
3240        let unix_no_name = AbortDescription::UnixSignal {
3241            signal: 42,
3242            name: None,
3243        };
3244        assert_eq!(unix_no_name.to_string(), "aborted with signal 42");
3245
3246        // Windows NT status with message.
3247        let windows = AbortDescription::WindowsNtStatus {
3248            code: -1073741510,
3249            message: Some("CTRL+C exit".into()),
3250        };
3251        assert_eq!(
3252            windows.to_string(),
3253            "aborted with code 0xc000013a: CTRL+C exit"
3254        );
3255
3256        // Windows NT status without message.
3257        let windows_no_msg = AbortDescription::WindowsNtStatus {
3258            code: -1073741510,
3259            message: None,
3260        };
3261        assert_eq!(windows_no_msg.to_string(), "aborted with code 0xc000013a");
3262
3263        // Windows job object.
3264        let job = AbortDescription::WindowsJobObject;
3265        assert_eq!(job.to_string(), "terminated via job object");
3266    }
3267
3268    #[cfg(unix)]
3269    #[test]
3270    fn abort_description_from_abort_status() {
3271        // Test conversion from AbortStatus to AbortDescription on Unix.
3272        let status = AbortStatus::UnixSignal(15);
3273        let description = AbortDescription::from(status);
3274
3275        assert_eq!(
3276            description,
3277            AbortDescription::UnixSignal {
3278                signal: 15,
3279                name: Some("TERM".into()),
3280            }
3281        );
3282
3283        // Unknown signal.
3284        let unknown_status = AbortStatus::UnixSignal(42);
3285        let unknown_description = AbortDescription::from(unknown_status);
3286        assert_eq!(
3287            unknown_description,
3288            AbortDescription::UnixSignal {
3289                signal: 42,
3290                name: None,
3291            }
3292        );
3293    }
3294
3295    #[test]
3296    fn execution_result_description_serialization() {
3297        // Test all variants of ExecutionResultDescription for serialization roundtrips.
3298
3299        // Pass.
3300        let pass = ExecutionResultDescription::Pass;
3301        let json = serde_json::to_string_pretty(&pass).unwrap();
3302        insta::assert_snapshot!("pass", json);
3303        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3304        assert_eq!(pass, roundtrip);
3305
3306        // Leak with pass result.
3307        let leak_pass = ExecutionResultDescription::Leak {
3308            result: LeakTimeoutResult::Pass,
3309        };
3310        let json = serde_json::to_string_pretty(&leak_pass).unwrap();
3311        insta::assert_snapshot!("leak_pass", json);
3312        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3313        assert_eq!(leak_pass, roundtrip);
3314
3315        // Leak with fail result.
3316        let leak_fail = ExecutionResultDescription::Leak {
3317            result: LeakTimeoutResult::Fail,
3318        };
3319        let json = serde_json::to_string_pretty(&leak_fail).unwrap();
3320        insta::assert_snapshot!("leak_fail", json);
3321        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3322        assert_eq!(leak_fail, roundtrip);
3323
3324        // Fail with exit code, no leak.
3325        let fail_exit_code = ExecutionResultDescription::Fail {
3326            failure: FailureDescription::ExitCode { code: 101 },
3327            leaked: false,
3328        };
3329        let json = serde_json::to_string_pretty(&fail_exit_code).unwrap();
3330        insta::assert_snapshot!("fail_exit_code", json);
3331        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3332        assert_eq!(fail_exit_code, roundtrip);
3333
3334        // Fail with exit code and leak.
3335        let fail_exit_code_leaked = ExecutionResultDescription::Fail {
3336            failure: FailureDescription::ExitCode { code: 1 },
3337            leaked: true,
3338        };
3339        let json = serde_json::to_string_pretty(&fail_exit_code_leaked).unwrap();
3340        insta::assert_snapshot!("fail_exit_code_leaked", json);
3341        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3342        assert_eq!(fail_exit_code_leaked, roundtrip);
3343
3344        // Fail with Unix signal abort.
3345        let fail_unix_signal = ExecutionResultDescription::Fail {
3346            failure: FailureDescription::Abort {
3347                abort: AbortDescription::UnixSignal {
3348                    signal: 11,
3349                    name: Some("SEGV".into()),
3350                },
3351            },
3352            leaked: false,
3353        };
3354        let json = serde_json::to_string_pretty(&fail_unix_signal).unwrap();
3355        insta::assert_snapshot!("fail_unix_signal", json);
3356        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3357        assert_eq!(fail_unix_signal, roundtrip);
3358
3359        // Fail with Unix signal abort (no name) and leak.
3360        let fail_unix_signal_unknown = ExecutionResultDescription::Fail {
3361            failure: FailureDescription::Abort {
3362                abort: AbortDescription::UnixSignal {
3363                    signal: 42,
3364                    name: None,
3365                },
3366            },
3367            leaked: true,
3368        };
3369        let json = serde_json::to_string_pretty(&fail_unix_signal_unknown).unwrap();
3370        insta::assert_snapshot!("fail_unix_signal_unknown_leaked", json);
3371        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3372        assert_eq!(fail_unix_signal_unknown, roundtrip);
3373
3374        // Fail with Windows NT status abort.
3375        let fail_windows_nt = ExecutionResultDescription::Fail {
3376            failure: FailureDescription::Abort {
3377                abort: AbortDescription::WindowsNtStatus {
3378                    code: -1073741510,
3379                    message: Some("The application terminated as a result of a CTRL+C.".into()),
3380                },
3381            },
3382            leaked: false,
3383        };
3384        let json = serde_json::to_string_pretty(&fail_windows_nt).unwrap();
3385        insta::assert_snapshot!("fail_windows_nt_status", json);
3386        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3387        assert_eq!(fail_windows_nt, roundtrip);
3388
3389        // Fail with Windows NT status abort (no message).
3390        let fail_windows_nt_no_msg = ExecutionResultDescription::Fail {
3391            failure: FailureDescription::Abort {
3392                abort: AbortDescription::WindowsNtStatus {
3393                    code: -1073741819,
3394                    message: None,
3395                },
3396            },
3397            leaked: false,
3398        };
3399        let json = serde_json::to_string_pretty(&fail_windows_nt_no_msg).unwrap();
3400        insta::assert_snapshot!("fail_windows_nt_status_no_message", json);
3401        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3402        assert_eq!(fail_windows_nt_no_msg, roundtrip);
3403
3404        // Fail with Windows job object abort.
3405        let fail_job_object = ExecutionResultDescription::Fail {
3406            failure: FailureDescription::Abort {
3407                abort: AbortDescription::WindowsJobObject,
3408            },
3409            leaked: false,
3410        };
3411        let json = serde_json::to_string_pretty(&fail_job_object).unwrap();
3412        insta::assert_snapshot!("fail_windows_job_object", json);
3413        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3414        assert_eq!(fail_job_object, roundtrip);
3415
3416        // ExecFail.
3417        let exec_fail = ExecutionResultDescription::ExecFail;
3418        let json = serde_json::to_string_pretty(&exec_fail).unwrap();
3419        insta::assert_snapshot!("exec_fail", json);
3420        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3421        assert_eq!(exec_fail, roundtrip);
3422
3423        // Timeout with pass result.
3424        let timeout_pass = ExecutionResultDescription::Timeout {
3425            result: SlowTimeoutResult::Pass,
3426        };
3427        let json = serde_json::to_string_pretty(&timeout_pass).unwrap();
3428        insta::assert_snapshot!("timeout_pass", json);
3429        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3430        assert_eq!(timeout_pass, roundtrip);
3431
3432        // Timeout with fail result.
3433        let timeout_fail = ExecutionResultDescription::Timeout {
3434            result: SlowTimeoutResult::Fail,
3435        };
3436        let json = serde_json::to_string_pretty(&timeout_fail).unwrap();
3437        insta::assert_snapshot!("timeout_fail", json);
3438        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3439        assert_eq!(timeout_fail, roundtrip);
3440    }
3441
3442    // --- on_test_finished tests ---
3443
3444    /// Helper to create a fail-then-pass `ExecutionStatuses` for flaky
3445    /// test scenarios.
3446    fn make_flaky_statuses(
3447        pass_result: ExecutionResultDescription,
3448        flaky_result: FlakyResult,
3449        is_slow: bool,
3450    ) -> ExecutionStatuses<LiveSpec> {
3451        let fail = make_execute_status(
3452            ExecutionResultDescription::Fail {
3453                failure: FailureDescription::ExitCode { code: 1 },
3454                leaked: false,
3455            },
3456            1,
3457            2,
3458        );
3459        let pass = make_execute_status_slow(pass_result, 2, 2, is_slow);
3460        ExecutionStatuses::new(vec![fail, pass], flaky_result)
3461    }
3462
3463    /// Helper to run `on_test_finished` on a fresh `RunStats` and return it.
3464    fn run_on_test_finished(statuses: &ExecutionStatuses<LiveSpec>) -> RunStats {
3465        let mut stats = RunStats {
3466            initial_run_count: 1,
3467            ..RunStats::default()
3468        };
3469        stats.on_test_finished(statuses);
3470        stats
3471    }
3472
3473    #[test]
3474    fn on_test_finished_pass_flaky() {
3475        // FlakyResult::Fail (not slow): counts as failed.
3476        let stats = run_on_test_finished(&make_flaky_statuses(
3477            ExecutionResultDescription::Pass,
3478            FlakyResult::Fail,
3479            false,
3480        ));
3481        assert_eq!(stats.finished_count, 1);
3482        assert_eq!(stats.failed, 1);
3483        assert_eq!(stats.failed_slow, 0, "not slow");
3484        assert_eq!(stats.passed, 0);
3485        assert_eq!(stats.flaky, 0);
3486
3487        // FlakyResult::Fail (slow): counts as failed and failed_slow.
3488        let stats = run_on_test_finished(&make_flaky_statuses(
3489            ExecutionResultDescription::Pass,
3490            FlakyResult::Fail,
3491            true,
3492        ));
3493        assert_eq!(stats.failed, 1);
3494        assert_eq!(stats.failed_slow, 1);
3495        assert_eq!(stats.passed, 0);
3496        assert_eq!(stats.flaky, 0);
3497
3498        // FlakyResult::Pass: counts as passed and flaky.
3499        let stats = run_on_test_finished(&make_flaky_statuses(
3500            ExecutionResultDescription::Pass,
3501            FlakyResult::Pass,
3502            true,
3503        ));
3504        assert_eq!(stats.passed, 1);
3505        assert_eq!(stats.passed_slow, 1);
3506        assert_eq!(stats.flaky, 1);
3507        assert_eq!(stats.failed, 0);
3508    }
3509
3510    #[test]
3511    fn on_test_finished_leak_pass_flaky() {
3512        // FlakyResult::Fail (not slow): counts as failed and leaky.
3513        let stats = run_on_test_finished(&make_flaky_statuses(
3514            ExecutionResultDescription::Leak {
3515                result: LeakTimeoutResult::Pass,
3516            },
3517            FlakyResult::Fail,
3518            false,
3519        ));
3520        assert_eq!(stats.failed, 1);
3521        assert_eq!(stats.failed_slow, 0, "not slow");
3522        assert_eq!(stats.leaky, 1, "leak still tracked");
3523        assert_eq!(stats.passed, 0);
3524        assert_eq!(stats.flaky, 0);
3525
3526        // FlakyResult::Fail (slow): also tracks failed_slow.
3527        let stats = run_on_test_finished(&make_flaky_statuses(
3528            ExecutionResultDescription::Leak {
3529                result: LeakTimeoutResult::Pass,
3530            },
3531            FlakyResult::Fail,
3532            true,
3533        ));
3534        assert_eq!(stats.failed, 1);
3535        assert_eq!(stats.failed_slow, 1);
3536        assert_eq!(stats.leaky, 1);
3537        assert_eq!(stats.passed, 0);
3538        assert_eq!(stats.flaky, 0);
3539
3540        // FlakyResult::Pass: counts as passed, leaky, and flaky.
3541        let stats = run_on_test_finished(&make_flaky_statuses(
3542            ExecutionResultDescription::Leak {
3543                result: LeakTimeoutResult::Pass,
3544            },
3545            FlakyResult::Pass,
3546            true,
3547        ));
3548        assert_eq!(stats.passed, 1);
3549        assert_eq!(stats.passed_slow, 1);
3550        assert_eq!(stats.leaky, 1);
3551        assert_eq!(stats.flaky, 1);
3552        assert_eq!(stats.failed, 0);
3553    }
3554
3555    #[test]
3556    fn on_test_finished_timeout_pass_flaky() {
3557        // FlakyResult::Fail (slow): counts as failed and failed_slow,
3558        // not passed_timed_out.
3559        let stats = run_on_test_finished(&make_flaky_statuses(
3560            ExecutionResultDescription::Timeout {
3561                result: SlowTimeoutResult::Pass,
3562            },
3563            FlakyResult::Fail,
3564            true,
3565        ));
3566        assert_eq!(stats.failed, 1);
3567        assert_eq!(stats.failed_slow, 1);
3568        assert_eq!(stats.passed, 0);
3569        assert_eq!(stats.passed_timed_out, 0);
3570        assert_eq!(stats.flaky, 0);
3571
3572        // FlakyResult::Pass: counts as passed, passed_timed_out, and flaky.
3573        let stats = run_on_test_finished(&make_flaky_statuses(
3574            ExecutionResultDescription::Timeout {
3575                result: SlowTimeoutResult::Pass,
3576            },
3577            FlakyResult::Pass,
3578            false,
3579        ));
3580        assert_eq!(stats.passed, 1);
3581        assert_eq!(stats.passed_timed_out, 1);
3582        assert_eq!(stats.flaky, 1);
3583        assert_eq!(stats.failed, 0);
3584    }
3585
3586    #[test]
3587    fn on_test_finished_non_flaky() {
3588        // Single-attempt pass (slow): counts as passed, not flaky.
3589        let pass = make_execute_status_slow(ExecutionResultDescription::Pass, 1, 1, true);
3590        let stats = run_on_test_finished(&ExecutionStatuses::new(vec![pass], FlakyResult::Pass));
3591        assert_eq!(stats.passed, 1);
3592        assert_eq!(stats.passed_slow, 1);
3593        assert_eq!(stats.flaky, 0);
3594        assert_eq!(stats.failed, 0);
3595
3596        // Single-attempt failure (slow): counts as failed and failed_slow.
3597        let fail = make_execute_status_slow(
3598            ExecutionResultDescription::Fail {
3599                failure: FailureDescription::ExitCode { code: 1 },
3600                leaked: false,
3601            },
3602            1,
3603            1,
3604            true,
3605        );
3606        let stats = run_on_test_finished(&ExecutionStatuses::new(vec![fail], FlakyResult::Pass));
3607        assert_eq!(stats.failed, 1);
3608        assert_eq!(stats.failed_slow, 1);
3609        assert_eq!(stats.passed, 0);
3610        assert_eq!(stats.flaky, 0);
3611    }
3612
3613    /// All [`RunOutcome`] values.
3614    ///
3615    /// The domain is small enough to check `combine` exhaustively.
3616    const ALL_RUN_OUTCOMES: [RunOutcome; 6] = [
3617        RunOutcome::NoTestsRun,
3618        RunOutcome::Success,
3619        RunOutcome::Cancelled {
3620            step: RunFailureStep::Test,
3621        },
3622        RunOutcome::Cancelled {
3623            step: RunFailureStep::SetupScript,
3624        },
3625        RunOutcome::Failed {
3626            step: RunFailureStep::Test,
3627        },
3628        RunOutcome::Failed {
3629            step: RunFailureStep::SetupScript,
3630        },
3631    ];
3632
3633    /// Verifies that [`ALL_RUN_OUTCOMES`] actually does contain all
3634    /// [`RunOutcome`] values.
3635    #[proptest]
3636    fn all_run_outcomes_is_complete(outcome: RunOutcome) {
3637        prop_assert!(
3638            ALL_RUN_OUTCOMES.contains(&outcome),
3639            "ALL_RUN_OUTCOMES contains {outcome:?}"
3640        );
3641    }
3642
3643    #[test]
3644    fn run_outcome_combine_is_commutative() {
3645        for a in ALL_RUN_OUTCOMES {
3646            for b in ALL_RUN_OUTCOMES {
3647                assert_eq!(
3648                    a.combine(b),
3649                    b.combine(a),
3650                    "combine is commutative for {a:?} and {b:?}"
3651                );
3652            }
3653        }
3654    }
3655
3656    #[test]
3657    fn run_outcome_combine_is_associative() {
3658        for a in ALL_RUN_OUTCOMES {
3659            for b in ALL_RUN_OUTCOMES {
3660                for c in ALL_RUN_OUTCOMES {
3661                    assert_eq!(
3662                        a.combine(b).combine(c),
3663                        a.combine(b.combine(c)),
3664                        "combine is associative for {a:?}, {b:?}, and {c:?}"
3665                    );
3666                }
3667            }
3668        }
3669    }
3670
3671    #[test]
3672    fn run_outcome_combine_is_idempotent() {
3673        for a in ALL_RUN_OUTCOMES {
3674            assert_eq!(a.combine(a), a, "combine is idempotent for {a:?}");
3675        }
3676    }
3677
3678    #[test]
3679    fn run_outcome_combine_matches_oracle() {
3680        for a in ALL_RUN_OUTCOMES {
3681            for b in ALL_RUN_OUTCOMES {
3682                let expected = if outcome_rank(a) >= outcome_rank(b) {
3683                    a
3684                } else {
3685                    b
3686                };
3687                assert_eq!(
3688                    a.combine(b),
3689                    expected,
3690                    "combine returns the higher-ranked of {a:?} and {b:?}"
3691                );
3692            }
3693        }
3694    }
3695
3696    // Ranks are explicit integers so the oracle doesn't share RunFailureStep's
3697    // derived Ord with the implementation.
3698    fn outcome_rank(outcome: RunOutcome) -> (u8, u8) {
3699        match outcome {
3700            RunOutcome::NoTestsRun => (0, 0),
3701            RunOutcome::Success => (1, 0),
3702            RunOutcome::Cancelled { step } => (2, step_rank(step)),
3703            RunOutcome::Failed { step } => (3, step_rank(step)),
3704        }
3705    }
3706
3707    fn step_rank(step: RunFailureStep) -> u8 {
3708        match step {
3709            RunFailureStep::Test => 0,
3710            RunFailureStep::SetupScript => 1,
3711        }
3712    }
3713
3714    #[test]
3715    fn stress_progress_time_remaining() {
3716        let total = Duration::from_secs(30);
3717        let smallest_step = Duration::from_nanos(1);
3718        let progress_at = |elapsed: Duration| StressProgress::Time {
3719            total,
3720            elapsed,
3721            completed: 3,
3722        };
3723
3724        assert_eq!(
3725            progress_at(Duration::ZERO).remaining(),
3726            Some(StressRemaining::Time(
3727                NonZeroDuration::new(total).expect("total is non-zero")
3728            )),
3729            "no time elapsed => the whole duration remains"
3730        );
3731        assert_eq!(
3732            progress_at(total - smallest_step).remaining(),
3733            Some(StressRemaining::Time(
3734                NonZeroDuration::new(smallest_step).expect("smallest step is non-zero")
3735            )),
3736            "elapsed just below total => the difference remains"
3737        );
3738        assert_eq!(
3739            progress_at(total).remaining(),
3740            None,
3741            "elapsed equal to total => nothing remains"
3742        );
3743        assert_eq!(
3744            progress_at(total + smallest_step).remaining(),
3745            None,
3746            "elapsed above total => nothing remains"
3747        );
3748    }
3749}