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.checked_sub(*elapsed).map(StressRemaining::Time),
517        }
518    }
519
520    /// Returns a unique ID for this stress sub-run, consisting of the run ID and stress index.
521    pub fn unique_id(&self, run_id: ReportUuid) -> String {
522        let stress_current = match self {
523            Self::Count { completed, .. } | Self::Time { completed, .. } => *completed,
524        };
525        format!("{}:@stress-{}", run_id, stress_current)
526    }
527}
528
529/// For a stress test, the amount of time or number of stress runs remaining.
530#[derive(Clone, Debug)]
531pub enum StressRemaining {
532    /// The number of stress runs remaining, guaranteed to be non-zero.
533    Count(NonZero<u32>),
534
535    /// Infinite number of stress runs remaining.
536    Infinite,
537
538    /// The amount of time remaining.
539    Time(Duration),
540}
541
542/// The index of the current stress run.
543#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
544#[serde(rename_all = "kebab-case")]
545#[cfg_attr(test, derive(test_strategy::Arbitrary))]
546pub struct StressIndex {
547    /// The 0-indexed index.
548    pub current: u32,
549
550    /// The total number of stress runs, if that is available.
551    pub total: Option<NonZero<u32>>,
552}
553
554impl StressIndex {
555    /// Returns the total as a plain `u32`, if available.
556    pub fn total_get(&self) -> Option<u32> {
557        self.total.map(|t| t.get())
558    }
559}
560
561/// Statistics for a completed test run or stress run.
562#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
563#[serde(tag = "type", rename_all = "kebab-case")]
564#[cfg_attr(test, derive(test_strategy::Arbitrary))]
565pub enum RunFinishedStats {
566    /// A single test run was completed.
567    Single(RunStats),
568
569    /// A stress run was completed.
570    Stress(StressRunStats),
571}
572
573impl RunFinishedStats {
574    /// For a single run, returns a summary of statistics as an enum. For a
575    /// stress run, returns a summary for the last sub-run.
576    pub fn final_stats(&self) -> FinalRunStats {
577        match self {
578            Self::Single(stats) => stats.summarize_final(),
579            Self::Stress(stats) => stats.last_final_stats,
580        }
581    }
582}
583
584/// Statistics for a test run.
585#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
586#[serde(rename_all = "kebab-case")]
587#[cfg_attr(test, derive(test_strategy::Arbitrary))]
588pub struct RunStats {
589    /// The total number of tests that were expected to be run at the beginning.
590    ///
591    /// If the test run is cancelled, this will be more than `finished_count` at the end.
592    pub initial_run_count: usize,
593
594    /// The total number of tests that finished running.
595    pub finished_count: usize,
596
597    /// The total number of setup scripts that were expected to be run at the beginning.
598    ///
599    /// If the test run is cancelled, this will be more than `finished_count` at the end.
600    pub setup_scripts_initial_count: usize,
601
602    /// The total number of setup scripts that finished running.
603    pub setup_scripts_finished_count: usize,
604
605    /// The number of setup scripts that passed.
606    pub setup_scripts_passed: usize,
607
608    /// The number of setup scripts that failed.
609    pub setup_scripts_failed: usize,
610
611    /// The number of setup scripts that encountered an execution failure.
612    pub setup_scripts_exec_failed: usize,
613
614    /// The number of setup scripts that timed out.
615    pub setup_scripts_timed_out: usize,
616
617    /// The number of tests that passed. Includes `passed_slow`, `passed_timed_out`, `flaky`, and
618    /// `leaky`.
619    pub passed: usize,
620
621    /// The number of slow tests that passed.
622    pub passed_slow: usize,
623
624    /// The number of timed out tests that passed.
625    pub passed_timed_out: usize,
626
627    /// The number of tests that passed on retry.
628    pub flaky: usize,
629
630    /// The number of tests that failed. Includes `leaky_failed` and tests that
631    /// were flaky but treated as failed due to `flaky-result = "fail"` configuration.
632    pub failed: usize,
633
634    /// The number of failed tests that were slow.
635    pub failed_slow: usize,
636
637    /// The number of timed out tests that failed.
638    pub failed_timed_out: usize,
639
640    /// The number of tests that passed but leaked handles.
641    pub leaky: usize,
642
643    /// The number of tests that otherwise passed, but leaked handles and were
644    /// treated as failed as a result.
645    ///
646    /// Included in `failed`.
647    pub leaky_failed: usize,
648
649    /// The number of tests that encountered an execution failure.
650    pub exec_failed: usize,
651
652    /// The number of tests that were skipped.
653    pub skipped: usize,
654
655    /// If the run is cancelled, the reason the cancellation is happening.
656    pub cancel_reason: Option<CancelReason>,
657}
658
659impl RunStats {
660    /// Returns true if there are any failures recorded in the stats.
661    pub fn has_failures(&self) -> bool {
662        self.failed_setup_script_count() > 0 || self.failed_count() > 0
663    }
664
665    /// Returns count of setup scripts that did not pass.
666    pub fn failed_setup_script_count(&self) -> usize {
667        self.setup_scripts_failed + self.setup_scripts_exec_failed + self.setup_scripts_timed_out
668    }
669
670    /// Returns count of tests that did not pass.
671    pub fn failed_count(&self) -> usize {
672        self.failed + self.exec_failed + self.failed_timed_out
673    }
674
675    /// Summarizes the stats as an enum at the end of a test run.
676    pub fn summarize_final(&self) -> FinalRunStats {
677        // Check for failures first. The order of setup scripts vs tests should
678        // not be important, though we don't assert that here.
679        if self.failed_setup_script_count() > 0 {
680            // Is this related to a cancellation other than one directly caused
681            // by the failure?
682            if self.cancel_reason > Some(CancelReason::TestFailure) {
683                FinalRunStats::Cancelled {
684                    reason: self.cancel_reason,
685                    kind: RunStatsFailureKind::SetupScript,
686                }
687            } else {
688                FinalRunStats::Failed {
689                    kind: RunStatsFailureKind::SetupScript,
690                }
691            }
692        } else if self.setup_scripts_initial_count > self.setup_scripts_finished_count {
693            FinalRunStats::Cancelled {
694                reason: self.cancel_reason,
695                kind: RunStatsFailureKind::SetupScript,
696            }
697        } else if self.failed_count() > 0 {
698            let kind = RunStatsFailureKind::Test {
699                initial_run_count: self.initial_run_count,
700                not_run: self.initial_run_count.saturating_sub(self.finished_count),
701            };
702
703            // Is this related to a cancellation other than one directly caused
704            // by the failure?
705            if self.cancel_reason > Some(CancelReason::TestFailure) {
706                FinalRunStats::Cancelled {
707                    reason: self.cancel_reason,
708                    kind,
709                }
710            } else {
711                FinalRunStats::Failed { kind }
712            }
713        } else if self.initial_run_count > self.finished_count {
714            FinalRunStats::Cancelled {
715                reason: self.cancel_reason,
716                kind: RunStatsFailureKind::Test {
717                    initial_run_count: self.initial_run_count,
718                    not_run: self.initial_run_count.saturating_sub(self.finished_count),
719                },
720            }
721        } else if self.finished_count == 0 {
722            FinalRunStats::NoTestsRun
723        } else {
724            FinalRunStats::Success
725        }
726    }
727
728    pub(crate) fn on_setup_script_finished(&mut self, status: &SetupScriptExecuteStatus<LiveSpec>) {
729        self.setup_scripts_finished_count += 1;
730
731        match status.result {
732            ExecutionResultDescription::Pass
733            | ExecutionResultDescription::Leak {
734                result: LeakTimeoutResult::Pass,
735            } => {
736                self.setup_scripts_passed += 1;
737            }
738            ExecutionResultDescription::Fail { .. }
739            | ExecutionResultDescription::Leak {
740                result: LeakTimeoutResult::Fail,
741            } => {
742                self.setup_scripts_failed += 1;
743            }
744            ExecutionResultDescription::ExecFail => {
745                self.setup_scripts_exec_failed += 1;
746            }
747            // Timed out setup scripts are always treated as failures.
748            ExecutionResultDescription::Timeout { .. } => {
749                self.setup_scripts_timed_out += 1;
750            }
751        }
752    }
753
754    pub(crate) fn on_test_finished(&mut self, run_statuses: &ExecutionStatuses<LiveSpec>) {
755        self.finished_count += 1;
756        // run_statuses is guaranteed to have at least one element.
757        // * If the last element is success, treat it as success (and possibly flaky).
758        // * If the last element is a failure, use it to determine fail/exec fail.
759        // Note that this is different from what Maven Surefire does (use the first failure):
760        // https://maven.apache.org/surefire/maven-surefire-plugin/examples/rerun-failing-tests.html
761        //
762        // This is not likely to matter much in practice since failures are likely to be of the
763        // same type.
764        let last_status = run_statuses.last_status();
765        match last_status.result {
766            ExecutionResultDescription::Pass => {
767                // The test is flaky if there were multiple attempts. How
768                // it's counted depends on flaky_result — match
769                // exhaustively so the compiler catches new variants.
770                let is_flaky = run_statuses.len() > 1;
771                if is_flaky {
772                    match run_statuses.flaky_result() {
773                        FlakyResult::Fail => {
774                            self.failed += 1;
775                            if last_status.is_slow {
776                                self.failed_slow += 1;
777                            }
778                        }
779                        FlakyResult::Pass => {
780                            self.passed += 1;
781                            if last_status.is_slow {
782                                self.passed_slow += 1;
783                            }
784                            self.flaky += 1;
785                        }
786                    }
787                } else {
788                    self.passed += 1;
789                    if last_status.is_slow {
790                        self.passed_slow += 1;
791                    }
792                }
793            }
794            ExecutionResultDescription::Leak {
795                result: LeakTimeoutResult::Pass,
796            } => {
797                let is_flaky = run_statuses.len() > 1;
798                if is_flaky {
799                    match run_statuses.flaky_result() {
800                        FlakyResult::Fail => {
801                            self.failed += 1;
802                            if last_status.is_slow {
803                                self.failed_slow += 1;
804                            }
805                            // Still count as leaky since the leak was detected.
806                            self.leaky += 1;
807                        }
808                        FlakyResult::Pass => {
809                            self.passed += 1;
810                            self.leaky += 1;
811                            if last_status.is_slow {
812                                self.passed_slow += 1;
813                            }
814                            self.flaky += 1;
815                        }
816                    }
817                } else {
818                    self.passed += 1;
819                    self.leaky += 1;
820                    if last_status.is_slow {
821                        self.passed_slow += 1;
822                    }
823                }
824            }
825            ExecutionResultDescription::Leak {
826                result: LeakTimeoutResult::Fail,
827            } => {
828                self.failed += 1;
829                self.leaky_failed += 1;
830                if last_status.is_slow {
831                    self.failed_slow += 1;
832                }
833            }
834            ExecutionResultDescription::Fail { .. } => {
835                self.failed += 1;
836                if last_status.is_slow {
837                    self.failed_slow += 1;
838                }
839            }
840            ExecutionResultDescription::Timeout {
841                result: SlowTimeoutResult::Pass,
842            } => {
843                let is_flaky = run_statuses.len() > 1;
844                if is_flaky {
845                    match run_statuses.flaky_result() {
846                        FlakyResult::Fail => {
847                            self.failed += 1;
848                            // Track as failed_slow since the overall result
849                            // is failure.
850                            if last_status.is_slow {
851                                self.failed_slow += 1;
852                            }
853                        }
854                        FlakyResult::Pass => {
855                            self.passed += 1;
856                            self.passed_timed_out += 1;
857                            self.flaky += 1;
858                        }
859                    }
860                } else {
861                    self.passed += 1;
862                    self.passed_timed_out += 1;
863                }
864            }
865            ExecutionResultDescription::Timeout {
866                result: SlowTimeoutResult::Fail,
867            } => {
868                self.failed_timed_out += 1;
869            }
870            ExecutionResultDescription::ExecFail => self.exec_failed += 1,
871        }
872    }
873}
874
875/// A type summarizing the possible outcomes of a test run.
876#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
877#[serde(tag = "outcome", rename_all = "kebab-case")]
878#[cfg_attr(test, derive(test_strategy::Arbitrary))]
879pub enum FinalRunStats {
880    /// The test run was successful, or is successful so far.
881    Success,
882
883    /// The test run was successful, or is successful so far, but no tests were selected to run.
884    NoTestsRun,
885
886    /// The test run was cancelled.
887    Cancelled {
888        /// The reason for cancellation, if available.
889        ///
890        /// This should generally be available, but may be None if some tests
891        /// that were selected to run were not executed.
892        reason: Option<CancelReason>,
893
894        /// The kind of failure that occurred.
895        kind: RunStatsFailureKind,
896    },
897
898    /// At least one test failed.
899    Failed {
900        /// The kind of failure that occurred.
901        kind: RunStatsFailureKind,
902    },
903}
904
905/// Statistics for a stress run.
906#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
907#[serde(rename_all = "kebab-case")]
908#[cfg_attr(test, derive(test_strategy::Arbitrary))]
909pub struct StressRunStats {
910    /// The number of stress runs completed.
911    pub completed: StressIndex,
912
913    /// The number of stress runs that succeeded.
914    pub success_count: u32,
915
916    /// The number of stress runs that failed.
917    pub failed_count: u32,
918
919    /// The last stress run's `FinalRunStats`.
920    pub last_final_stats: FinalRunStats,
921}
922
923impl StressRunStats {
924    /// Summarizes the stats as an enum at the end of a test run.
925    pub fn summarize_final(&self) -> StressFinalRunStats {
926        if self.failed_count > 0 {
927            StressFinalRunStats::Failed
928        } else if matches!(self.last_final_stats, FinalRunStats::Cancelled { .. }) {
929            StressFinalRunStats::Cancelled
930        } else if matches!(self.last_final_stats, FinalRunStats::NoTestsRun) {
931            StressFinalRunStats::NoTestsRun
932        } else {
933            StressFinalRunStats::Success
934        }
935    }
936}
937
938/// A summary of final statistics for a stress run.
939pub enum StressFinalRunStats {
940    /// The stress run was successful.
941    Success,
942
943    /// No tests were run.
944    NoTestsRun,
945
946    /// The stress run was cancelled.
947    Cancelled,
948
949    /// At least one stress run failed.
950    Failed,
951}
952
953/// A type summarizing the step at which a test run failed.
954#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
955#[serde(tag = "step", rename_all = "kebab-case")]
956#[cfg_attr(test, derive(test_strategy::Arbitrary))]
957pub enum RunStatsFailureKind {
958    /// The run was interrupted during setup script execution.
959    SetupScript,
960
961    /// The run was interrupted during test execution.
962    Test {
963        /// The total number of tests scheduled.
964        initial_run_count: usize,
965
966        /// The number of tests not run, or for a currently-executing test the number queued up to
967        /// run.
968        not_run: usize,
969    },
970}
971
972/// Information about executions of a test, including retries.
973///
974/// The type parameter `S` specifies how test output is stored (see
975/// [`OutputSpec`]).
976#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
977#[derive(Serialize)]
978#[serde(
979    rename_all = "kebab-case",
980    bound(serialize = "S: SerializableOutputSpec")
981)]
982#[cfg_attr(
983    test,
984    derive(test_strategy::Arbitrary),
985    arbitrary(bound(S: ArbitraryOutputSpec))
986)]
987pub struct ExecutionStatuses<S: OutputSpec> {
988    /// This is guaranteed to be non-empty.
989    #[cfg_attr(test, strategy(proptest::collection::vec(proptest::arbitrary::any::<ExecuteStatus<S>>(), 1..=3)))]
990    statuses: Vec<ExecuteStatus<S>>,
991
992    /// Controls whether a flaky test is treated as a pass or a failure.
993    #[serde(default)]
994    flaky_result: FlakyResult,
995}
996
997impl<'de, S: SerializableOutputSpec> Deserialize<'de> for ExecutionStatuses<S> {
998    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
999        // Deserialize as the wrapper struct that matches the Serialize output.
1000        // S is already bound as SerializableOutputSpec on this impl.
1001        #[derive(Deserialize)]
1002        #[serde(
1003            rename_all = "kebab-case",
1004            bound(deserialize = "S: SerializableOutputSpec")
1005        )]
1006        struct Helper<S: OutputSpec> {
1007            statuses: Vec<ExecuteStatus<S>>,
1008            #[serde(default)]
1009            flaky_result: FlakyResult,
1010        }
1011
1012        let helper = Helper::<S>::deserialize(deserializer)?;
1013        if helper.statuses.is_empty() {
1014            return Err(serde::de::Error::custom("expected non-empty statuses"));
1015        }
1016        Ok(Self {
1017            statuses: helper.statuses,
1018            flaky_result: helper.flaky_result,
1019        })
1020    }
1021}
1022
1023#[expect(clippy::len_without_is_empty)] // RunStatuses is never empty
1024impl<S: OutputSpec> ExecutionStatuses<S> {
1025    pub(crate) fn new(statuses: Vec<ExecuteStatus<S>>, flaky_result: FlakyResult) -> Self {
1026        debug_assert!(!statuses.is_empty(), "ExecutionStatuses must be non-empty");
1027        Self {
1028            statuses,
1029            flaky_result,
1030        }
1031    }
1032
1033    /// Returns the configured flaky result for this test.
1034    pub fn flaky_result(&self) -> FlakyResult {
1035        self.flaky_result
1036    }
1037
1038    /// Returns the last execution status.
1039    ///
1040    /// This status is typically used as the final result.
1041    pub fn last_status(&self) -> &ExecuteStatus<S> {
1042        self.statuses
1043            .last()
1044            .expect("execution statuses is non-empty")
1045    }
1046
1047    /// Iterates over all the statuses.
1048    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus<S>> + '_ {
1049        self.statuses.iter()
1050    }
1051
1052    /// Returns the number of times the test was executed.
1053    pub fn len(&self) -> usize {
1054        self.statuses.len()
1055    }
1056
1057    /// Returns a description of self.
1058    pub fn describe(&self) -> ExecutionDescription<'_, S> {
1059        let last_status = self.last_status();
1060        if last_status.result.is_success() {
1061            if self.statuses.len() > 1 {
1062                ExecutionDescription::Flaky {
1063                    last_status,
1064                    prior_statuses: &self.statuses[..self.statuses.len() - 1],
1065                    result: self.flaky_result,
1066                }
1067            } else {
1068                ExecutionDescription::Success {
1069                    single_status: last_status,
1070                }
1071            }
1072        } else {
1073            let first_status = self
1074                .statuses
1075                .first()
1076                .expect("execution statuses is non-empty");
1077            let retries = &self.statuses[1..];
1078            ExecutionDescription::Failure {
1079                first_status,
1080                last_status,
1081                retries,
1082            }
1083        }
1084    }
1085}
1086
1087impl<S: OutputSpec> IntoIterator for ExecutionStatuses<S> {
1088    type Item = ExecuteStatus<S>;
1089    type IntoIter = std::vec::IntoIter<ExecuteStatus<S>>;
1090
1091    fn into_iter(self) -> Self::IntoIter {
1092        self.statuses.into_iter()
1093    }
1094}
1095
1096/// A description of test executions obtained from `ExecuteStatuses`.
1097///
1098/// This can be used to quickly determine whether a test passed, failed or was flaky.
1099///
1100/// The type parameter `S` specifies how test output is stored (see
1101/// [`OutputSpec`]).
1102#[derive_where::derive_where(Debug; S::ChildOutputDesc)]
1103pub enum ExecutionDescription<'a, S: OutputSpec> {
1104    /// The test was run once and was successful.
1105    Success {
1106        /// The status of the test.
1107        single_status: &'a ExecuteStatus<S>,
1108    },
1109
1110    /// The test was run more than once. The final result was successful.
1111    Flaky {
1112        /// The last, successful status.
1113        last_status: &'a ExecuteStatus<S>,
1114
1115        /// Previous statuses, none of which are successes.
1116        prior_statuses: &'a [ExecuteStatus<S>],
1117
1118        /// Controls whether this flaky test is treated as a pass or a failure.
1119        result: FlakyResult,
1120    },
1121
1122    /// The test was run once, or possibly multiple times. All runs failed.
1123    Failure {
1124        /// The first, failing status.
1125        first_status: &'a ExecuteStatus<S>,
1126
1127        /// The last, failing status. Same as the first status if no retries were performed.
1128        last_status: &'a ExecuteStatus<S>,
1129
1130        /// Any retries that were performed. All of these runs failed.
1131        ///
1132        /// May be empty.
1133        retries: &'a [ExecuteStatus<S>],
1134    },
1135}
1136
1137// Manual Copy and Clone implementations to avoid requiring S::ChildOutputDesc:
1138// Copy/Clone, since ExecutionDescription only stores references.
1139impl<S: OutputSpec> Clone for ExecutionDescription<'_, S> {
1140    fn clone(&self) -> Self {
1141        *self
1142    }
1143}
1144
1145impl<S: OutputSpec> Copy for ExecutionDescription<'_, S> {}
1146
1147impl<'a, S: OutputSpec> ExecutionDescription<'a, S> {
1148    /// Returns the status level for this `ExecutionDescription`.
1149    pub fn status_level(&self) -> StatusLevel {
1150        match self {
1151            ExecutionDescription::Success { single_status } => match single_status.result {
1152                ExecutionResultDescription::Leak {
1153                    result: LeakTimeoutResult::Pass,
1154                } => StatusLevel::Leak,
1155                ExecutionResultDescription::Pass => StatusLevel::Pass,
1156                ExecutionResultDescription::Timeout {
1157                    result: SlowTimeoutResult::Pass,
1158                } => StatusLevel::Slow,
1159                ref other => unreachable!(
1160                    "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1161                ),
1162            },
1163            // A flaky test implies that we print out retry information for it.
1164            ExecutionDescription::Flaky {
1165                result: FlakyResult::Pass,
1166                ..
1167            } => StatusLevel::Retry,
1168            ExecutionDescription::Flaky {
1169                result: FlakyResult::Fail,
1170                ..
1171            } => StatusLevel::Fail,
1172            ExecutionDescription::Failure { .. } => StatusLevel::Fail,
1173        }
1174    }
1175
1176    /// Returns the final status level for this `ExecutionDescription`.
1177    pub fn final_status_level(&self) -> FinalStatusLevel {
1178        match self {
1179            ExecutionDescription::Success { single_status, .. } => {
1180                // Slow is higher priority than leaky, so return slow first here.
1181                if single_status.is_slow {
1182                    FinalStatusLevel::Slow
1183                } else {
1184                    match single_status.result {
1185                        ExecutionResultDescription::Pass => FinalStatusLevel::Pass,
1186                        ExecutionResultDescription::Leak {
1187                            result: LeakTimeoutResult::Pass,
1188                        } => FinalStatusLevel::Leak,
1189                        // Timeout with Pass should return Slow, but this case
1190                        // shouldn't be reached because is_slow is true for
1191                        // timeout scenarios. Handle it for completeness.
1192                        ExecutionResultDescription::Timeout {
1193                            result: SlowTimeoutResult::Pass,
1194                        } => FinalStatusLevel::Slow,
1195                        ref other => unreachable!(
1196                            "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
1197                        ),
1198                    }
1199                }
1200            }
1201            // A flaky-pass test implies that we print out retry information.
1202            ExecutionDescription::Flaky {
1203                result: FlakyResult::Pass,
1204                ..
1205            } => FinalStatusLevel::Flaky,
1206            // A flaky-fail test is treated as a failure.
1207            ExecutionDescription::Flaky {
1208                result: FlakyResult::Fail,
1209                ..
1210            } => FinalStatusLevel::Fail,
1211            ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
1212        }
1213    }
1214
1215    /// Returns whether this test's output should be treated as success output
1216    /// for display and storage purposes.
1217    ///
1218    /// For flaky tests (both pass and fail variants), the last attempt
1219    /// succeeded, so its output is success output: it contains no panics or
1220    /// errors, and is generally not interesting. The failure information comes
1221    /// from the status line and from prior retry attempts' output (shown via
1222    /// `TestAttemptFailedWillRetry` events, controlled by `failure-output`).
1223    ///
1224    /// This means:
1225    /// - The _visibility_ is controlled by `success-output`.
1226    /// - The _styling_ is pass/green headers, no error extraction.
1227    /// - _JUnit storage_ is controlled by `store-success-output` (default:
1228    ///   `false`).
1229    ///
1230    /// The status line uses failure semantics independently (e.g. `FLKY-FL` in
1231    /// red for flaky-fail tests).
1232    pub fn is_success_for_output(&self) -> bool {
1233        match self {
1234            ExecutionDescription::Success { .. } => true,
1235            // All flaky tests have a successful last attempt — the output
1236            // from that attempt is success output regardless of the overall
1237            // test outcome.
1238            ExecutionDescription::Flaky { .. } => true,
1239            ExecutionDescription::Failure { .. } => false,
1240        }
1241    }
1242
1243    /// Returns the last run status.
1244    pub fn last_status(&self) -> &'a ExecuteStatus<S> {
1245        match self {
1246            ExecutionDescription::Success {
1247                single_status: last_status,
1248            }
1249            | ExecutionDescription::Flaky { last_status, .. }
1250            | ExecutionDescription::Failure { last_status, .. } => last_status,
1251        }
1252    }
1253}
1254
1255/// Pre-computed error summary for display.
1256///
1257/// This contains the formatted error messages, pre-computed from the execution
1258/// output and result. Useful for record-replay scenarios where the rendering
1259/// is done on the server.
1260#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1261#[serde(rename_all = "kebab-case")]
1262#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1263pub struct ErrorSummary {
1264    /// A short summary of the error, suitable for display in a single line.
1265    pub short_message: String,
1266
1267    /// A full description of the error chain, suitable for detailed display.
1268    pub description: String,
1269}
1270
1271/// Pre-computed output error slice for display.
1272///
1273/// This contains an error message heuristically extracted from test output,
1274/// such as a panic message or error string.
1275#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1276#[serde(rename_all = "kebab-case")]
1277#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1278pub struct OutputErrorSlice {
1279    /// The extracted error slice as a string.
1280    pub slice: String,
1281
1282    /// The byte offset in the original output where this slice starts.
1283    pub start: usize,
1284}
1285
1286/// Information about a single execution of a test.
1287///
1288/// This is the external-facing type used by reporters. The `result` field uses
1289/// [`ExecutionResultDescription`], a platform-independent type that can be
1290/// serialized and deserialized across platforms.
1291///
1292/// The type parameter `S` specifies how test output is stored (see
1293/// [`OutputSpec`]).
1294#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1295#[derive(Serialize, Deserialize)]
1296#[serde(
1297    rename_all = "kebab-case",
1298    bound(
1299        serialize = "S: SerializableOutputSpec",
1300        deserialize = "S: SerializableOutputSpec"
1301    )
1302)]
1303#[cfg_attr(
1304    test,
1305    derive(test_strategy::Arbitrary),
1306    arbitrary(bound(S: ArbitraryOutputSpec))
1307)]
1308pub struct ExecuteStatus<S: OutputSpec> {
1309    /// Retry-related data.
1310    pub retry_data: RetryData,
1311    /// The stdout and stderr output for this test.
1312    pub output: ChildExecutionOutputDescription<S>,
1313    /// The execution result for this test: pass, fail or execution error.
1314    pub result: ExecutionResultDescription,
1315    /// The time at which the test started.
1316    #[cfg_attr(
1317        test,
1318        strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1319    )]
1320    pub start_time: DateTime<FixedOffset>,
1321    /// The time it took for the test to run.
1322    #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1323    pub time_taken: Duration,
1324    /// Whether this test counts as slow.
1325    pub is_slow: bool,
1326    /// The delay will be non-zero if this is a retry and delay was specified.
1327    #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1328    pub delay_before_start: Duration,
1329    /// Pre-computed error summary, if available.
1330    ///
1331    /// This is computed from the execution output and result, and can be used
1332    /// for display without needing to re-compute the error chain.
1333    pub error_summary: Option<ErrorSummary>,
1334    /// Pre-computed output error slice, if available.
1335    ///
1336    /// This is a heuristically extracted error message from the test output,
1337    /// such as a panic message or error string.
1338    pub output_error_slice: Option<OutputErrorSlice>,
1339}
1340
1341/// Information about the execution of a setup script.
1342///
1343/// This is the external-facing type used by reporters. The `result` field uses
1344/// [`ExecutionResultDescription`], a platform-independent type that can be
1345/// serialized and deserialized across platforms.
1346///
1347/// The type parameter `S` specifies how test output is stored (see
1348/// [`OutputSpec`]).
1349#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1350#[derive(Serialize, Deserialize)]
1351#[serde(
1352    rename_all = "kebab-case",
1353    bound(
1354        serialize = "S: SerializableOutputSpec",
1355        deserialize = "S: SerializableOutputSpec"
1356    )
1357)]
1358#[cfg_attr(
1359    test,
1360    derive(test_strategy::Arbitrary),
1361    arbitrary(bound(S: ArbitraryOutputSpec))
1362)]
1363pub struct SetupScriptExecuteStatus<S: OutputSpec> {
1364    /// Output for this setup script.
1365    pub output: ChildExecutionOutputDescription<S>,
1366
1367    /// The execution result for this setup script: pass, fail or execution error.
1368    pub result: ExecutionResultDescription,
1369
1370    /// The time at which the script started.
1371    #[cfg_attr(
1372        test,
1373        strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
1374    )]
1375    pub start_time: DateTime<FixedOffset>,
1376
1377    /// The time it took for the script to run.
1378    #[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
1379    pub time_taken: Duration,
1380
1381    /// Whether this script counts as slow.
1382    pub is_slow: bool,
1383
1384    /// The map of environment variables that were set by this script.
1385    ///
1386    /// `None` if an error occurred while running the script or reading the
1387    /// environment map.
1388    pub env_map: Option<SetupScriptEnvMap>,
1389
1390    /// Pre-computed error summary, if available.
1391    ///
1392    /// This is computed from the execution output and result, and can be used
1393    /// for display without needing to re-compute the error chain.
1394    pub error_summary: Option<ErrorSummary>,
1395}
1396
1397/// A map of environment variables set by a setup script.
1398///
1399/// Part of [`SetupScriptExecuteStatus`].
1400#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1401#[serde(rename_all = "kebab-case")]
1402#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1403pub struct SetupScriptEnvMap {
1404    /// The map of environment variables set by the script.
1405    pub env_map: BTreeMap<String, String>,
1406}
1407
1408// ---
1409// Child execution output description types
1410// ---
1411
1412/// The result of executing a child process, generic over output storage.
1413///
1414/// This is the external-facing counterpart to [`ChildExecutionOutput`]. The
1415/// type parameter `S` specifies how output is stored (see [`OutputSpec`]).
1416#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
1417#[derive(Serialize, Deserialize)]
1418#[serde(
1419    tag = "type",
1420    rename_all = "kebab-case",
1421    bound(
1422        serialize = "S: SerializableOutputSpec",
1423        deserialize = "S: SerializableOutputSpec"
1424    )
1425)]
1426#[cfg_attr(
1427    test,
1428    derive(test_strategy::Arbitrary),
1429    arbitrary(bound(S: ArbitraryOutputSpec))
1430)]
1431pub enum ChildExecutionOutputDescription<S: OutputSpec> {
1432    /// The process was run and the output was captured.
1433    Output {
1434        /// If the process has finished executing, the final state it is in.
1435        ///
1436        /// `None` means execution is currently in progress.
1437        result: Option<ExecutionResultDescription>,
1438
1439        /// The captured output.
1440        output: S::ChildOutputDesc,
1441
1442        /// Errors that occurred while waiting on the child process or parsing
1443        /// its output.
1444        errors: Option<ErrorList<ChildErrorDescription>>,
1445    },
1446
1447    /// There was a failure to start the process.
1448    StartError(ChildStartErrorDescription),
1449}
1450
1451impl<S: OutputSpec> ChildExecutionOutputDescription<S> {
1452    /// Returns true if there are any errors in this output.
1453    pub fn has_errors(&self) -> bool {
1454        match self {
1455            Self::Output { errors, result, .. } => {
1456                if errors.is_some() {
1457                    return true;
1458                }
1459                if let Some(result) = result {
1460                    return !result.is_success();
1461                }
1462                false
1463            }
1464            Self::StartError(_) => true,
1465        }
1466    }
1467}
1468
1469/// The output of a child process during live execution.
1470///
1471/// This represents either split stdout/stderr or combined output. The `Option`
1472/// wrappers distinguish between "not captured" (`None`) and "captured but
1473/// empty" (`Some` with empty content).
1474///
1475/// The `NotLoaded` variant is used during replay when the display
1476/// configuration indicates that output won't be shown.
1477///
1478/// For the recording counterpart, see
1479/// [`ZipStoreOutputDescription`](crate::record::ZipStoreOutputDescription).
1480#[derive(Clone, Debug)]
1481pub enum ChildOutputDescription {
1482    /// The output was split into stdout and stderr.
1483    Split {
1484        /// Standard output, or `None` if not captured.
1485        stdout: Option<ChildSingleOutput>,
1486        /// Standard error, or `None` if not captured.
1487        stderr: Option<ChildSingleOutput>,
1488    },
1489
1490    /// The output was combined into a single stream.
1491    Combined {
1492        /// The combined output.
1493        output: ChildSingleOutput,
1494    },
1495
1496    /// Output exists but was not loaded.
1497    ///
1498    /// This variant is used during replay when the display configuration
1499    /// indicates that output won't be shown. Code that accesses output
1500    /// bytes must never be reached with this variant.
1501    NotLoaded,
1502}
1503
1504impl ChildOutputDescription {
1505    /// Returns the lengths of stdout and stderr in bytes.
1506    ///
1507    /// Returns `None` for each stream that wasn't captured.
1508    pub fn stdout_stderr_len(&self) -> (Option<u64>, Option<u64>) {
1509        match self {
1510            Self::Split { stdout, stderr } => (
1511                stdout.as_ref().map(|s| s.buf().len() as u64),
1512                stderr.as_ref().map(|s| s.buf().len() as u64),
1513            ),
1514            Self::Combined { output } => (Some(output.buf().len() as u64), None),
1515            Self::NotLoaded => {
1516                unreachable!(
1517                    "attempted to get output lengths from output that was not loaded \
1518                     (this method is only called from the live runner, where NotLoaded \
1519                     is never produced)"
1520                );
1521            }
1522        }
1523    }
1524}
1525
1526/// A serializable description of an error that occurred while starting a child process.
1527///
1528/// This is the external-facing counterpart to [`ChildStartError`].
1529#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1530#[serde(tag = "kind", rename_all = "kebab-case")]
1531#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1532pub enum ChildStartErrorDescription {
1533    /// An error occurred while creating a temporary path for a setup script.
1534    TempPath {
1535        /// The source error.
1536        source: SerializableError,
1537    },
1538
1539    /// An error occurred while spawning the child process.
1540    Spawn {
1541        /// The source error.
1542        source: SerializableError,
1543    },
1544}
1545
1546impl fmt::Display for ChildStartErrorDescription {
1547    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1548        match self {
1549            Self::TempPath { .. } => {
1550                write!(f, "error creating temporary path for setup script")
1551            }
1552            Self::Spawn { .. } => write!(f, "error spawning child process"),
1553        }
1554    }
1555}
1556
1557impl std::error::Error for ChildStartErrorDescription {
1558    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1559        match self {
1560            Self::TempPath { source } | Self::Spawn { source } => Some(source),
1561        }
1562    }
1563}
1564
1565/// A serializable description of an error that occurred while managing a child process.
1566///
1567/// This is the external-facing counterpart to [`ChildError`].
1568#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1569#[serde(tag = "kind", rename_all = "kebab-case")]
1570#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1571pub enum ChildErrorDescription {
1572    /// An error occurred while reading standard output.
1573    ReadStdout {
1574        /// The source error.
1575        source: SerializableError,
1576    },
1577
1578    /// An error occurred while reading standard error.
1579    ReadStderr {
1580        /// The source error.
1581        source: SerializableError,
1582    },
1583
1584    /// An error occurred while reading combined output.
1585    ReadCombined {
1586        /// The source error.
1587        source: SerializableError,
1588    },
1589
1590    /// An error occurred while waiting for the child process to exit.
1591    Wait {
1592        /// The source error.
1593        source: SerializableError,
1594    },
1595
1596    /// An error occurred while reading the output of a setup script.
1597    SetupScriptOutput {
1598        /// The source error.
1599        source: SerializableError,
1600    },
1601}
1602
1603impl fmt::Display for ChildErrorDescription {
1604    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1605        match self {
1606            Self::ReadStdout { .. } => write!(f, "error reading standard output"),
1607            Self::ReadStderr { .. } => write!(f, "error reading standard error"),
1608            Self::ReadCombined { .. } => {
1609                write!(f, "error reading combined stream")
1610            }
1611            Self::Wait { .. } => {
1612                write!(f, "error waiting for child process to exit")
1613            }
1614            Self::SetupScriptOutput { .. } => {
1615                write!(f, "error reading setup script output")
1616            }
1617        }
1618    }
1619}
1620
1621impl std::error::Error for ChildErrorDescription {
1622    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1623        match self {
1624            Self::ReadStdout { source }
1625            | Self::ReadStderr { source }
1626            | Self::ReadCombined { source }
1627            | Self::Wait { source }
1628            | Self::SetupScriptOutput { source } => Some(source),
1629        }
1630    }
1631}
1632
1633/// A serializable representation of an error chain.
1634///
1635/// This captures the error message and the chain of source errors from
1636/// any [`std::error::Error`] implementation.
1637#[derive(Clone, Debug, PartialEq, Eq)]
1638pub struct SerializableError {
1639    message: String,
1640    source: Option<Box<SerializableError>>,
1641}
1642
1643impl SerializableError {
1644    /// Creates a new `SerializableError` from an error, walking the
1645    /// full source chain.
1646    pub fn new(error: &dyn std::error::Error) -> Self {
1647        let message = error.to_string();
1648        let mut causes = Vec::new();
1649        let mut source = error.source();
1650        while let Some(err) = source {
1651            causes.push(err.to_string());
1652            source = err.source();
1653        }
1654        Self::from_message_and_causes(message, causes)
1655    }
1656
1657    /// Creates a new `SerializableError` from a message and a list of
1658    /// causes.
1659    pub fn from_message_and_causes(message: String, causes: Vec<String>) -> Self {
1660        // This builds a singly-linked list from the causes. You rarely
1661        // see them in Rust, but they're required to implement
1662        // Error::source.
1663        let mut next = None;
1664        for cause in causes.into_iter().rev() {
1665            let error = Self {
1666                message: cause,
1667                source: next.map(Box::new),
1668            };
1669            next = Some(error);
1670        }
1671        Self {
1672            message,
1673            source: next.map(Box::new),
1674        }
1675    }
1676
1677    /// Returns the message associated with this error.
1678    pub fn message(&self) -> &str {
1679        &self.message
1680    }
1681
1682    /// Returns the causes of this error as an iterator.
1683    pub fn sources(&self) -> SerializableErrorSources<'_> {
1684        SerializableErrorSources {
1685            current: self.source.as_deref(),
1686        }
1687    }
1688}
1689
1690impl fmt::Display for SerializableError {
1691    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1692        f.write_str(&self.message)
1693    }
1694}
1695
1696impl std::error::Error for SerializableError {
1697    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1698        self.source
1699            .as_deref()
1700            .map(|s| s as &(dyn std::error::Error + 'static))
1701    }
1702}
1703
1704/// The sources of a [`SerializableError`] as an iterator.
1705#[derive(Debug)]
1706pub struct SerializableErrorSources<'a> {
1707    current: Option<&'a SerializableError>,
1708}
1709
1710impl<'a> Iterator for SerializableErrorSources<'a> {
1711    type Item = &'a SerializableError;
1712
1713    fn next(&mut self) -> Option<Self::Item> {
1714        let current = self.current?;
1715        self.current = current.source.as_deref();
1716        Some(current)
1717    }
1718}
1719
1720mod serializable_error_serde {
1721    use super::*;
1722
1723    #[derive(Serialize, Deserialize)]
1724    struct Ser {
1725        message: String,
1726        // For backwards compatibility with IoErrorDescription, which
1727        // didn't have a causes field.
1728        #[serde(default)]
1729        causes: Vec<String>,
1730    }
1731
1732    impl Serialize for SerializableError {
1733        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1734            let mut causes = Vec::new();
1735            let mut cause = self.source.as_ref();
1736            while let Some(c) = cause {
1737                causes.push(c.message.clone());
1738                cause = c.source.as_ref();
1739            }
1740
1741            let ser = Ser {
1742                message: self.message.clone(),
1743                causes,
1744            };
1745            ser.serialize(serializer)
1746        }
1747    }
1748
1749    impl<'de> Deserialize<'de> for SerializableError {
1750        fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1751            let ser = Ser::deserialize(deserializer)?;
1752            Ok(SerializableError::from_message_and_causes(
1753                ser.message,
1754                ser.causes,
1755            ))
1756        }
1757    }
1758}
1759
1760#[cfg(test)]
1761mod serializable_error_arbitrary {
1762    use super::*;
1763    use proptest::prelude::*;
1764
1765    impl Arbitrary for SerializableError {
1766        type Parameters = ();
1767        type Strategy = BoxedStrategy<Self>;
1768
1769        fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
1770            (
1771                any::<String>(),
1772                proptest::collection::vec(any::<String>(), 0..3),
1773            )
1774                .prop_map(|(message, causes)| {
1775                    SerializableError::from_message_and_causes(message, causes)
1776                })
1777                .boxed()
1778        }
1779    }
1780}
1781
1782impl From<ChildExecutionOutput> for ChildExecutionOutputDescription<LiveSpec> {
1783    fn from(output: ChildExecutionOutput) -> Self {
1784        match output {
1785            ChildExecutionOutput::Output {
1786                result,
1787                output,
1788                errors,
1789            } => Self::Output {
1790                result: result.map(ExecutionResultDescription::from),
1791                output: ChildOutputDescription::from(output),
1792                errors: errors.map(|e| e.map(ChildErrorDescription::from)),
1793            },
1794            ChildExecutionOutput::StartError(error) => {
1795                Self::StartError(ChildStartErrorDescription::from(error))
1796            }
1797        }
1798    }
1799}
1800
1801impl From<ChildOutput> for ChildOutputDescription {
1802    fn from(output: ChildOutput) -> Self {
1803        match output {
1804            ChildOutput::Split(split) => Self::Split {
1805                stdout: split.stdout,
1806                stderr: split.stderr,
1807            },
1808            ChildOutput::Combined { output } => Self::Combined { output },
1809        }
1810    }
1811}
1812
1813impl From<ChildStartError> for ChildStartErrorDescription {
1814    fn from(error: ChildStartError) -> Self {
1815        match error {
1816            ChildStartError::TempPath(e) => Self::TempPath {
1817                source: SerializableError::new(&*e),
1818            },
1819            ChildStartError::Spawn(e) => Self::Spawn {
1820                source: SerializableError::new(&*e),
1821            },
1822        }
1823    }
1824}
1825
1826impl From<ChildError> for ChildErrorDescription {
1827    fn from(error: ChildError) -> Self {
1828        match error {
1829            ChildError::Fd(ChildFdError::ReadStdout(e)) => Self::ReadStdout {
1830                source: SerializableError::new(&*e),
1831            },
1832            ChildError::Fd(ChildFdError::ReadStderr(e)) => Self::ReadStderr {
1833                source: SerializableError::new(&*e),
1834            },
1835            ChildError::Fd(ChildFdError::ReadCombined(e)) => Self::ReadCombined {
1836                source: SerializableError::new(&*e),
1837            },
1838            ChildError::Fd(ChildFdError::Wait(e)) => Self::Wait {
1839                source: SerializableError::new(&*e),
1840            },
1841            ChildError::SetupScriptOutput(e) => Self::SetupScriptOutput {
1842                source: SerializableError::new(&e),
1843            },
1844        }
1845    }
1846}
1847
1848/// Data related to retries for a test.
1849#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
1850#[serde(rename_all = "kebab-case")]
1851#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1852pub struct RetryData {
1853    /// The current attempt. In the range `[1, total_attempts]`.
1854    pub attempt: u32,
1855
1856    /// The total number of times this test can be run. Equal to `1 + retries`.
1857    pub total_attempts: u32,
1858}
1859
1860impl RetryData {
1861    /// Returns true if there are no more attempts after this.
1862    pub fn is_last_attempt(&self) -> bool {
1863        self.attempt >= self.total_attempts
1864    }
1865}
1866
1867/// Whether a test passed, failed or an error occurred while executing the test.
1868#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1869pub enum ExecutionResult {
1870    /// The test passed.
1871    Pass,
1872    /// The test passed but leaked handles. This usually indicates that
1873    /// a subprocess that inherit standard IO was created, but it didn't shut down when
1874    /// the test failed.
1875    Leak {
1876        /// Whether this leak was treated as a failure.
1877        ///
1878        /// Note the difference between `Fail { leaked: true }` and `Leak {
1879        /// failed: true }`. In the former case, the test failed and also leaked
1880        /// handles. In the latter case, the test passed but leaked handles, and
1881        /// configuration indicated that this is a failure.
1882        result: LeakTimeoutResult,
1883    },
1884    /// The test failed.
1885    Fail {
1886        /// The abort status of the test, if any (for example, the signal on Unix).
1887        failure_status: FailureStatus,
1888
1889        /// Whether a test leaked handles. If set to true, this usually indicates that
1890        /// a subprocess that inherit standard IO was created, but it didn't shut down when
1891        /// the test failed.
1892        leaked: bool,
1893    },
1894    /// An error occurred while executing the test.
1895    ExecFail,
1896    /// The test was terminated due to a timeout.
1897    Timeout {
1898        /// Whether this timeout was treated as a failure.
1899        result: SlowTimeoutResult,
1900    },
1901}
1902
1903impl ExecutionResult {
1904    /// Returns true if the test was successful.
1905    pub fn is_success(self) -> bool {
1906        match self {
1907            ExecutionResult::Pass
1908            | ExecutionResult::Timeout {
1909                result: SlowTimeoutResult::Pass,
1910            }
1911            | ExecutionResult::Leak {
1912                result: LeakTimeoutResult::Pass,
1913            } => true,
1914            ExecutionResult::Leak {
1915                result: LeakTimeoutResult::Fail,
1916            }
1917            | ExecutionResult::Fail { .. }
1918            | ExecutionResult::ExecFail
1919            | ExecutionResult::Timeout {
1920                result: SlowTimeoutResult::Fail,
1921            } => false,
1922        }
1923    }
1924
1925    /// Returns a static string representation of the result.
1926    pub fn as_static_str(&self) -> &'static str {
1927        match self {
1928            ExecutionResult::Pass => "pass",
1929            ExecutionResult::Leak { .. } => "leak",
1930            ExecutionResult::Fail { .. } => "fail",
1931            ExecutionResult::ExecFail => "exec-fail",
1932            ExecutionResult::Timeout { .. } => "timeout",
1933        }
1934    }
1935}
1936
1937/// Failure status: either an exit code or an abort status.
1938#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1939pub enum FailureStatus {
1940    /// The test exited with a non-zero exit code.
1941    ExitCode(i32),
1942
1943    /// The test aborted.
1944    Abort(AbortStatus),
1945}
1946
1947impl FailureStatus {
1948    /// Extract the failure status from an `ExitStatus`.
1949    pub fn extract(exit_status: ExitStatus) -> Self {
1950        if let Some(abort_status) = AbortStatus::extract(exit_status) {
1951            FailureStatus::Abort(abort_status)
1952        } else {
1953            FailureStatus::ExitCode(
1954                exit_status
1955                    .code()
1956                    .expect("if abort_status is None, then code must be present"),
1957            )
1958        }
1959    }
1960}
1961
1962/// A regular exit code or Windows NT abort status for a test.
1963///
1964/// Returned as part of the [`ExecutionResult::Fail`] variant.
1965#[derive(Copy, Clone, Eq, PartialEq)]
1966pub enum AbortStatus {
1967    /// The test was aborted due to a signal on Unix.
1968    #[cfg(unix)]
1969    UnixSignal(i32),
1970
1971    /// The test was determined to have aborted because the high bit was set on Windows.
1972    #[cfg(windows)]
1973    WindowsNtStatus(windows_sys::Win32::Foundation::NTSTATUS),
1974
1975    /// The test was terminated via job object on Windows.
1976    #[cfg(windows)]
1977    JobObject,
1978}
1979
1980impl AbortStatus {
1981    /// Extract the abort status from an [`ExitStatus`].
1982    pub fn extract(exit_status: ExitStatus) -> Option<Self> {
1983        cfg_if::cfg_if! {
1984            if #[cfg(unix)] {
1985                // On Unix, extract the signal if it's found.
1986                use std::os::unix::process::ExitStatusExt;
1987                exit_status.signal().map(AbortStatus::UnixSignal)
1988            } else if #[cfg(windows)] {
1989                exit_status.code().and_then(|code| {
1990                    (code < 0).then_some(AbortStatus::WindowsNtStatus(code))
1991                })
1992            } else {
1993                None
1994            }
1995        }
1996    }
1997}
1998
1999impl fmt::Debug for AbortStatus {
2000    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2001        match self {
2002            #[cfg(unix)]
2003            AbortStatus::UnixSignal(signal) => write!(f, "UnixSignal({signal})"),
2004            #[cfg(windows)]
2005            AbortStatus::WindowsNtStatus(status) => write!(f, "WindowsNtStatus({status:x})"),
2006            #[cfg(windows)]
2007            AbortStatus::JobObject => write!(f, "JobObject"),
2008        }
2009    }
2010}
2011
2012/// A platform-independent description of an abort status.
2013///
2014/// This type can be serialized on one platform and deserialized on another,
2015/// containing all information needed for display without requiring
2016/// platform-specific lookups.
2017#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2018#[serde(tag = "kind", rename_all = "kebab-case")]
2019#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2020#[non_exhaustive]
2021pub enum AbortDescription {
2022    /// The process was aborted by a Unix signal.
2023    UnixSignal {
2024        /// The signal number.
2025        signal: i32,
2026        /// The signal name without the "SIG" prefix (e.g., "TERM", "SEGV"),
2027        /// if known.
2028        #[cfg_attr(
2029            test,
2030            strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
2031        )]
2032        name: Option<SmolStr>,
2033    },
2034
2035    /// The process was aborted with a Windows NT status code.
2036    WindowsNtStatus {
2037        /// The NTSTATUS code.
2038        code: i32,
2039        /// The human-readable message from the Win32 error code, if available.
2040        #[cfg_attr(
2041            test,
2042            strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
2043        )]
2044        message: Option<SmolStr>,
2045    },
2046
2047    /// The process was terminated via a Windows job object.
2048    WindowsJobObject,
2049}
2050
2051impl fmt::Display for AbortDescription {
2052    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2053        match self {
2054            Self::UnixSignal { signal, name } => {
2055                write!(f, "aborted with signal {signal}")?;
2056                if let Some(name) = name {
2057                    write!(f, " (SIG{name})")?;
2058                }
2059                Ok(())
2060            }
2061            Self::WindowsNtStatus { code, message } => {
2062                write!(f, "aborted with code {code:#010x}")?;
2063                if let Some(message) = message {
2064                    write!(f, ": {message}")?;
2065                }
2066                Ok(())
2067            }
2068            Self::WindowsJobObject => {
2069                write!(f, "terminated via job object")
2070            }
2071        }
2072    }
2073}
2074
2075impl From<AbortStatus> for AbortDescription {
2076    fn from(status: AbortStatus) -> Self {
2077        cfg_if::cfg_if! {
2078            if #[cfg(unix)] {
2079                match status {
2080                    AbortStatus::UnixSignal(signal) => Self::UnixSignal {
2081                        signal,
2082                        name: crate::helpers::signal_str(signal).map(SmolStr::new_static),
2083                    },
2084                }
2085            } else if #[cfg(windows)] {
2086                match status {
2087                    AbortStatus::WindowsNtStatus(code) => Self::WindowsNtStatus {
2088                        code,
2089                        message: crate::helpers::windows_nt_status_message(code),
2090                    },
2091                    AbortStatus::JobObject => Self::WindowsJobObject,
2092                }
2093            } else {
2094                match status {}
2095            }
2096        }
2097    }
2098}
2099
2100/// A platform-independent description of a test failure status.
2101///
2102/// This is the platform-independent counterpart to [`FailureStatus`].
2103#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2104#[serde(tag = "kind", rename_all = "kebab-case")]
2105#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2106#[non_exhaustive]
2107pub enum FailureDescription {
2108    /// The test exited with a non-zero exit code.
2109    ExitCode {
2110        /// The exit code.
2111        code: i32,
2112    },
2113
2114    /// The test was aborted (e.g., by a signal on Unix or NT status on Windows).
2115    ///
2116    /// Note: this is a struct variant rather than a newtype variant to ensure
2117    /// proper JSON nesting. Both `FailureDescription` and `AbortDescription`
2118    /// use `#[serde(tag = "kind")]`, and if this were a newtype variant, serde
2119    /// would flatten the inner type causing duplicate `"kind"` fields.
2120    Abort {
2121        /// The abort description.
2122        abort: AbortDescription,
2123    },
2124}
2125
2126impl From<FailureStatus> for FailureDescription {
2127    fn from(status: FailureStatus) -> Self {
2128        match status {
2129            FailureStatus::ExitCode(code) => Self::ExitCode { code },
2130            FailureStatus::Abort(abort) => Self::Abort {
2131                abort: AbortDescription::from(abort),
2132            },
2133        }
2134    }
2135}
2136
2137impl fmt::Display for FailureDescription {
2138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2139        match self {
2140            Self::ExitCode { code } => write!(f, "exited with code {code}"),
2141            Self::Abort { abort } => write!(f, "{abort}"),
2142        }
2143    }
2144}
2145
2146/// A platform-independent description of a test execution result.
2147///
2148/// This is the platform-independent counterpart to [`ExecutionResult`], used
2149/// in external-facing types like [`ExecuteStatus`].
2150#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2151#[serde(tag = "status", rename_all = "kebab-case")]
2152#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2153#[non_exhaustive]
2154pub enum ExecutionResultDescription {
2155    /// The test passed.
2156    Pass,
2157
2158    /// The test passed but leaked handles.
2159    Leak {
2160        /// Whether this leak was treated as a failure.
2161        result: LeakTimeoutResult,
2162    },
2163
2164    /// The test failed.
2165    Fail {
2166        /// The failure status.
2167        failure: FailureDescription,
2168
2169        /// Whether the test leaked handles.
2170        leaked: bool,
2171    },
2172
2173    /// An error occurred while executing the test.
2174    ExecFail,
2175
2176    /// The test was terminated due to a timeout.
2177    Timeout {
2178        /// Whether this timeout was treated as a failure.
2179        result: SlowTimeoutResult,
2180    },
2181}
2182
2183impl ExecutionResultDescription {
2184    /// Returns true if the test was successful.
2185    pub fn is_success(&self) -> bool {
2186        match self {
2187            Self::Pass
2188            | Self::Timeout {
2189                result: SlowTimeoutResult::Pass,
2190            }
2191            | Self::Leak {
2192                result: LeakTimeoutResult::Pass,
2193            } => true,
2194            Self::Leak {
2195                result: LeakTimeoutResult::Fail,
2196            }
2197            | Self::Fail { .. }
2198            | Self::ExecFail
2199            | Self::Timeout {
2200                result: SlowTimeoutResult::Fail,
2201            } => false,
2202        }
2203    }
2204
2205    /// Returns a static string representation of the result.
2206    pub fn as_static_str(&self) -> &'static str {
2207        match self {
2208            Self::Pass => "pass",
2209            Self::Leak { .. } => "leak",
2210            Self::Fail { .. } => "fail",
2211            Self::ExecFail => "exec-fail",
2212            Self::Timeout { .. } => "timeout",
2213        }
2214    }
2215
2216    /// Returns true if this result represents a test that was terminated by nextest
2217    /// (as opposed to failing naturally).
2218    ///
2219    /// This is used to suppress output spam when running under
2220    /// TestFailureImmediate.
2221    ///
2222    /// TODO: This is a heuristic that checks if the test was terminated by
2223    /// SIGTERM (Unix) or job object (Windows). In an edge case, a test could
2224    /// send SIGTERM to itself, which would incorrectly be detected as a
2225    /// nextest-initiated termination. A more robust solution would track which
2226    /// tests were explicitly sent termination signals by nextest.
2227    pub fn is_termination_failure(&self) -> bool {
2228        matches!(
2229            self,
2230            Self::Fail {
2231                failure: FailureDescription::Abort {
2232                    abort: AbortDescription::UnixSignal {
2233                        signal: SIGTERM,
2234                        ..
2235                    },
2236                },
2237                ..
2238            } | Self::Fail {
2239                failure: FailureDescription::Abort {
2240                    abort: AbortDescription::WindowsJobObject,
2241                },
2242                ..
2243            }
2244        )
2245    }
2246}
2247
2248impl From<ExecutionResult> for ExecutionResultDescription {
2249    fn from(result: ExecutionResult) -> Self {
2250        match result {
2251            ExecutionResult::Pass => Self::Pass,
2252            ExecutionResult::Leak { result } => Self::Leak { result },
2253            ExecutionResult::Fail {
2254                failure_status,
2255                leaked,
2256            } => Self::Fail {
2257                failure: FailureDescription::from(failure_status),
2258                leaked,
2259            },
2260            ExecutionResult::ExecFail => Self::ExecFail,
2261            ExecutionResult::Timeout { result } => Self::Timeout { result },
2262        }
2263    }
2264}
2265
2266// Note: the order here matters -- it indicates severity of cancellation
2267/// The reason why a test run is being cancelled.
2268#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
2269#[serde(rename_all = "kebab-case")]
2270#[cfg_attr(test, derive(test_strategy::Arbitrary))]
2271pub enum CancelReason {
2272    /// A setup script failed.
2273    SetupScriptFailure,
2274
2275    /// A test failed and --no-fail-fast wasn't specified.
2276    TestFailure,
2277
2278    /// An error occurred while reporting results.
2279    ReportError,
2280
2281    /// The global timeout was exceeded.
2282    GlobalTimeout,
2283
2284    /// A test failed and fail-fast with immediate termination was specified.
2285    TestFailureImmediate,
2286
2287    /// A termination signal (on Unix, SIGTERM or SIGHUP) was received.
2288    Signal,
2289
2290    /// An interrupt (on Unix, Ctrl-C) was received.
2291    Interrupt,
2292
2293    /// A second signal was received, and the run is being forcibly killed.
2294    SecondSignal,
2295}
2296
2297impl CancelReason {
2298    pub(crate) fn to_static_str(self) -> &'static str {
2299        match self {
2300            CancelReason::SetupScriptFailure => "setup script failure",
2301            CancelReason::TestFailure => "test failure",
2302            CancelReason::ReportError => "reporting error",
2303            CancelReason::GlobalTimeout => "global timeout",
2304            CancelReason::TestFailureImmediate => "test failure",
2305            CancelReason::Signal => "signal",
2306            CancelReason::Interrupt => "interrupt",
2307            CancelReason::SecondSignal => "second signal",
2308        }
2309    }
2310}
2311/// The kind of unit of work that nextest is executing.
2312#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2313pub enum UnitKind {
2314    /// A test.
2315    Test,
2316
2317    /// A script (e.g. a setup script).
2318    Script,
2319}
2320
2321impl UnitKind {
2322    pub(crate) const WAITING_ON_TEST_MESSAGE: &str = "waiting on test process";
2323    pub(crate) const WAITING_ON_SCRIPT_MESSAGE: &str = "waiting on script process";
2324
2325    pub(crate) const EXECUTING_TEST_MESSAGE: &str = "executing test";
2326    pub(crate) const EXECUTING_SCRIPT_MESSAGE: &str = "executing script";
2327
2328    pub(crate) fn waiting_on_message(&self) -> &'static str {
2329        match self {
2330            UnitKind::Test => Self::WAITING_ON_TEST_MESSAGE,
2331            UnitKind::Script => Self::WAITING_ON_SCRIPT_MESSAGE,
2332        }
2333    }
2334
2335    pub(crate) fn executing_message(&self) -> &'static str {
2336        match self {
2337            UnitKind::Test => Self::EXECUTING_TEST_MESSAGE,
2338            UnitKind::Script => Self::EXECUTING_SCRIPT_MESSAGE,
2339        }
2340    }
2341}
2342
2343/// A response to an information request.
2344#[derive(Clone, Debug)]
2345pub enum InfoResponse<'a> {
2346    /// A setup script's response.
2347    SetupScript(SetupScriptInfoResponse),
2348
2349    /// A test's response.
2350    Test(TestInfoResponse<'a>),
2351}
2352
2353/// A setup script's response to an information request.
2354#[derive(Clone, Debug)]
2355pub struct SetupScriptInfoResponse {
2356    /// The stress index of the setup script.
2357    pub stress_index: Option<StressIndex>,
2358
2359    /// The identifier of the setup script instance.
2360    pub script_id: ScriptId,
2361
2362    /// The program to run.
2363    pub program: String,
2364
2365    /// The list of arguments to the program.
2366    pub args: Vec<String>,
2367
2368    /// The state of the setup script.
2369    pub state: UnitState,
2370
2371    /// Output obtained from the setup script.
2372    pub output: ChildExecutionOutputDescription<LiveSpec>,
2373}
2374
2375/// A test's response to an information request.
2376#[derive(Clone, Debug)]
2377pub struct TestInfoResponse<'a> {
2378    /// The stress index of the test.
2379    pub stress_index: Option<StressIndex>,
2380
2381    /// The test instance that the information is about.
2382    pub test_instance: TestInstanceId<'a>,
2383
2384    /// Information about retries.
2385    pub retry_data: RetryData,
2386
2387    /// The state of the test.
2388    pub state: UnitState,
2389
2390    /// Output obtained from the test.
2391    pub output: ChildExecutionOutputDescription<LiveSpec>,
2392}
2393
2394/// The current state of a test or script process: running, exiting, or
2395/// terminating.
2396///
2397/// Part of information response requests.
2398#[derive(Clone, Debug)]
2399pub enum UnitState {
2400    /// The unit is currently running.
2401    Running {
2402        /// The process ID.
2403        pid: u32,
2404
2405        /// The amount of time the unit has been running.
2406        time_taken: Duration,
2407
2408        /// `Some` if the test is marked as slow, along with the duration after
2409        /// which it was marked as slow.
2410        slow_after: Option<Duration>,
2411    },
2412
2413    /// The test has finished running, and is currently in the process of
2414    /// exiting.
2415    Exiting {
2416        /// The process ID.
2417        pid: u32,
2418
2419        /// The amount of time the unit ran for.
2420        time_taken: Duration,
2421
2422        /// `Some` if the unit is marked as slow, along with the duration after
2423        /// which it was marked as slow.
2424        slow_after: Option<Duration>,
2425
2426        /// The tentative execution result before leaked status is determined.
2427        ///
2428        /// None means that the exit status could not be read, and should be
2429        /// treated as a failure.
2430        tentative_result: Option<ExecutionResultDescription>,
2431
2432        /// How long has been spent waiting for the process to exit.
2433        waiting_duration: Duration,
2434
2435        /// How much longer nextest will wait until the test is marked leaky.
2436        remaining: Duration,
2437    },
2438
2439    /// The child process is being terminated by nextest.
2440    Terminating(UnitTerminatingState),
2441
2442    /// The unit has finished running and the process has exited.
2443    Exited {
2444        /// The result of executing the unit.
2445        result: ExecutionResultDescription,
2446
2447        /// The amount of time the unit ran for.
2448        time_taken: Duration,
2449
2450        /// `Some` if the unit is marked as slow, along with the duration after
2451        /// which it was marked as slow.
2452        slow_after: Option<Duration>,
2453    },
2454
2455    /// A delay is being waited out before the next attempt of the test is
2456    /// started. (Only relevant for tests.)
2457    DelayBeforeNextAttempt {
2458        /// The previous execution result.
2459        previous_result: ExecutionResultDescription,
2460
2461        /// Whether the previous attempt was marked as slow.
2462        previous_slow: bool,
2463
2464        /// How long has been spent waiting so far.
2465        waiting_duration: Duration,
2466
2467        /// How much longer nextest will wait until retrying the test.
2468        remaining: Duration,
2469    },
2470}
2471
2472impl UnitState {
2473    /// Returns true if the state has a valid output attached to it.
2474    pub fn has_valid_output(&self) -> bool {
2475        match self {
2476            UnitState::Running { .. }
2477            | UnitState::Exiting { .. }
2478            | UnitState::Terminating(_)
2479            | UnitState::Exited { .. } => true,
2480            UnitState::DelayBeforeNextAttempt { .. } => false,
2481        }
2482    }
2483}
2484
2485/// The current terminating state of a test or script process.
2486///
2487/// Part of [`UnitState::Terminating`].
2488#[derive(Clone, Debug)]
2489pub struct UnitTerminatingState {
2490    /// The process ID.
2491    pub pid: u32,
2492
2493    /// The amount of time the unit ran for.
2494    pub time_taken: Duration,
2495
2496    /// The reason for the termination.
2497    pub reason: UnitTerminateReason,
2498
2499    /// The method by which the process is being terminated.
2500    pub method: UnitTerminateMethod,
2501
2502    /// How long has been spent waiting for the process to exit.
2503    pub waiting_duration: Duration,
2504
2505    /// How much longer nextest will wait until a kill command is sent to the process.
2506    pub remaining: Duration,
2507}
2508
2509/// The reason for a script or test being forcibly terminated by nextest.
2510///
2511/// Part of information response requests.
2512#[derive(Clone, Copy, Debug)]
2513pub enum UnitTerminateReason {
2514    /// The unit is being terminated due to a test timeout being hit.
2515    Timeout,
2516
2517    /// The unit is being terminated due to nextest receiving a signal.
2518    Signal,
2519
2520    /// The unit is being terminated due to an interrupt (i.e. Ctrl-C).
2521    Interrupt,
2522}
2523
2524impl fmt::Display for UnitTerminateReason {
2525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2526        match self {
2527            UnitTerminateReason::Timeout => write!(f, "timeout"),
2528            UnitTerminateReason::Signal => write!(f, "signal"),
2529            UnitTerminateReason::Interrupt => write!(f, "interrupt"),
2530        }
2531    }
2532}
2533
2534/// The way in which a script or test is being forcibly terminated by nextest.
2535#[derive(Clone, Copy, Debug)]
2536pub enum UnitTerminateMethod {
2537    /// The unit is being terminated by sending a signal.
2538    #[cfg(unix)]
2539    Signal(UnitTerminateSignal),
2540
2541    /// The unit is being terminated by terminating the Windows job object.
2542    #[cfg(windows)]
2543    JobObject,
2544
2545    /// The unit is being waited on to exit. A termination signal will be sent
2546    /// if it doesn't exit within the grace period.
2547    ///
2548    /// On Windows, this occurs when nextest receives Ctrl-C. In that case, it
2549    /// is assumed that tests will also receive Ctrl-C and exit on their own. If
2550    /// tests do not exit within the grace period configured for them, their
2551    /// corresponding job objects will be terminated.
2552    #[cfg(windows)]
2553    Wait,
2554
2555    /// A fake method used for testing.
2556    #[cfg(test)]
2557    Fake,
2558}
2559
2560#[cfg(unix)]
2561/// The signal that is or was sent to terminate a script or test.
2562#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2563pub enum UnitTerminateSignal {
2564    /// The unit is being terminated by sending a SIGINT.
2565    Interrupt,
2566
2567    /// The unit is being terminated by sending a SIGTERM signal.
2568    Term,
2569
2570    /// The unit is being terminated by sending a SIGHUP signal.
2571    Hangup,
2572
2573    /// The unit is being terminated by sending a SIGQUIT signal.
2574    Quit,
2575
2576    /// The unit is being terminated by sending a SIGKILL signal.
2577    Kill,
2578}
2579
2580#[cfg(unix)]
2581impl fmt::Display for UnitTerminateSignal {
2582    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2583        match self {
2584            UnitTerminateSignal::Interrupt => write!(f, "SIGINT"),
2585            UnitTerminateSignal::Term => write!(f, "SIGTERM"),
2586            UnitTerminateSignal::Hangup => write!(f, "SIGHUP"),
2587            UnitTerminateSignal::Quit => write!(f, "SIGQUIT"),
2588            UnitTerminateSignal::Kill => write!(f, "SIGKILL"),
2589        }
2590    }
2591}
2592
2593#[cfg(test)]
2594mod tests {
2595    use super::*;
2596
2597    #[test]
2598    fn test_is_success() {
2599        assert_eq!(
2600            RunStats::default().summarize_final(),
2601            FinalRunStats::NoTestsRun,
2602            "empty run => no tests run"
2603        );
2604        assert_eq!(
2605            RunStats {
2606                initial_run_count: 42,
2607                finished_count: 42,
2608                ..RunStats::default()
2609            }
2610            .summarize_final(),
2611            FinalRunStats::Success,
2612            "initial run count = final run count => success"
2613        );
2614        assert_eq!(
2615            RunStats {
2616                initial_run_count: 42,
2617                finished_count: 41,
2618                ..RunStats::default()
2619            }
2620            .summarize_final(),
2621            FinalRunStats::Cancelled {
2622                reason: None,
2623                kind: RunStatsFailureKind::Test {
2624                    initial_run_count: 42,
2625                    not_run: 1
2626                }
2627            },
2628            "initial run count > final run count => cancelled"
2629        );
2630        assert_eq!(
2631            RunStats {
2632                initial_run_count: 42,
2633                finished_count: 42,
2634                failed: 1,
2635                ..RunStats::default()
2636            }
2637            .summarize_final(),
2638            FinalRunStats::Failed {
2639                kind: RunStatsFailureKind::Test {
2640                    initial_run_count: 42,
2641                    not_run: 0,
2642                },
2643            },
2644            "failed => failure"
2645        );
2646        assert_eq!(
2647            RunStats {
2648                initial_run_count: 42,
2649                finished_count: 42,
2650                exec_failed: 1,
2651                ..RunStats::default()
2652            }
2653            .summarize_final(),
2654            FinalRunStats::Failed {
2655                kind: RunStatsFailureKind::Test {
2656                    initial_run_count: 42,
2657                    not_run: 0,
2658                },
2659            },
2660            "exec failed => failure"
2661        );
2662        assert_eq!(
2663            RunStats {
2664                initial_run_count: 42,
2665                finished_count: 42,
2666                failed_timed_out: 1,
2667                ..RunStats::default()
2668            }
2669            .summarize_final(),
2670            FinalRunStats::Failed {
2671                kind: RunStatsFailureKind::Test {
2672                    initial_run_count: 42,
2673                    not_run: 0,
2674                },
2675            },
2676            "timed out => failure {:?} {:?}",
2677            RunStats {
2678                initial_run_count: 42,
2679                finished_count: 42,
2680                failed_timed_out: 1,
2681                ..RunStats::default()
2682            }
2683            .summarize_final(),
2684            FinalRunStats::Failed {
2685                kind: RunStatsFailureKind::Test {
2686                    initial_run_count: 42,
2687                    not_run: 0,
2688                },
2689            },
2690        );
2691        assert_eq!(
2692            RunStats {
2693                initial_run_count: 42,
2694                finished_count: 42,
2695                skipped: 1,
2696                ..RunStats::default()
2697            }
2698            .summarize_final(),
2699            FinalRunStats::Success,
2700            "skipped => not considered a failure"
2701        );
2702
2703        assert_eq!(
2704            RunStats {
2705                setup_scripts_initial_count: 2,
2706                setup_scripts_finished_count: 1,
2707                ..RunStats::default()
2708            }
2709            .summarize_final(),
2710            FinalRunStats::Cancelled {
2711                reason: None,
2712                kind: RunStatsFailureKind::SetupScript,
2713            },
2714            "setup script failed => failure"
2715        );
2716
2717        assert_eq!(
2718            RunStats {
2719                setup_scripts_initial_count: 2,
2720                setup_scripts_finished_count: 2,
2721                setup_scripts_failed: 1,
2722                ..RunStats::default()
2723            }
2724            .summarize_final(),
2725            FinalRunStats::Failed {
2726                kind: RunStatsFailureKind::SetupScript,
2727            },
2728            "setup script failed => failure"
2729        );
2730        assert_eq!(
2731            RunStats {
2732                setup_scripts_initial_count: 2,
2733                setup_scripts_finished_count: 2,
2734                setup_scripts_exec_failed: 1,
2735                ..RunStats::default()
2736            }
2737            .summarize_final(),
2738            FinalRunStats::Failed {
2739                kind: RunStatsFailureKind::SetupScript,
2740            },
2741            "setup script exec failed => failure"
2742        );
2743        assert_eq!(
2744            RunStats {
2745                setup_scripts_initial_count: 2,
2746                setup_scripts_finished_count: 2,
2747                setup_scripts_timed_out: 1,
2748                ..RunStats::default()
2749            }
2750            .summarize_final(),
2751            FinalRunStats::Failed {
2752                kind: RunStatsFailureKind::SetupScript,
2753            },
2754            "setup script timed out => failure"
2755        );
2756        assert_eq!(
2757            RunStats {
2758                setup_scripts_initial_count: 2,
2759                setup_scripts_finished_count: 2,
2760                setup_scripts_passed: 2,
2761                ..RunStats::default()
2762            }
2763            .summarize_final(),
2764            FinalRunStats::NoTestsRun,
2765            "setup scripts passed => success, but no tests run"
2766        );
2767
2768        // Flaky tests with flaky-result = "fail" are included in `failed`, so this
2769        // is covered by the general failure tests above.
2770    }
2771
2772    /// Helper to build a minimal `ExecuteStatus<LiveSpec>` for tests.
2773    fn make_execute_status(
2774        result: ExecutionResultDescription,
2775        attempt: u32,
2776        total_attempts: u32,
2777    ) -> ExecuteStatus<LiveSpec> {
2778        make_execute_status_slow(result, attempt, total_attempts, false)
2779    }
2780
2781    /// Helper to build a minimal `ExecuteStatus<LiveSpec>` for tests, with
2782    /// the `is_slow` flag set.
2783    fn make_execute_status_slow(
2784        result: ExecutionResultDescription,
2785        attempt: u32,
2786        total_attempts: u32,
2787        is_slow: bool,
2788    ) -> ExecuteStatus<LiveSpec> {
2789        ExecuteStatus {
2790            retry_data: RetryData {
2791                attempt,
2792                total_attempts,
2793            },
2794            output: ChildExecutionOutputDescription::Output {
2795                result: Some(result.clone()),
2796                output: ChildOutputDescription::Split {
2797                    stdout: None,
2798                    stderr: None,
2799                },
2800                errors: None,
2801            },
2802            result,
2803            start_time: chrono::Utc::now().into(),
2804            time_taken: Duration::from_millis(100),
2805            is_slow,
2806            delay_before_start: Duration::ZERO,
2807            error_summary: None,
2808            output_error_slice: None,
2809        }
2810    }
2811
2812    #[test]
2813    fn is_success_for_output_by_variant() {
2814        // Success: single passing run → true.
2815        let pass_status = make_execute_status(ExecutionResultDescription::Pass, 1, 1);
2816        let success_statuses = ExecutionStatuses::new(vec![pass_status], FlakyResult::Pass);
2817        let describe = success_statuses.describe();
2818        assert!(
2819            matches!(describe, ExecutionDescription::Success { .. }),
2820            "single pass is Success"
2821        );
2822        assert!(
2823            describe.is_success_for_output(),
2824            "Success: output is success output"
2825        );
2826
2827        // Flaky pass: fail then pass → true.
2828        let fail_status = make_execute_status(
2829            ExecutionResultDescription::Fail {
2830                failure: FailureDescription::ExitCode { code: 1 },
2831                leaked: false,
2832            },
2833            1,
2834            2,
2835        );
2836        let pass_status = make_execute_status(ExecutionResultDescription::Pass, 2, 2);
2837        let flaky_pass_statuses =
2838            ExecutionStatuses::new(vec![fail_status, pass_status], FlakyResult::Pass);
2839        let describe = flaky_pass_statuses.describe();
2840        assert!(
2841            matches!(
2842                describe,
2843                ExecutionDescription::Flaky {
2844                    result: FlakyResult::Pass,
2845                    ..
2846                }
2847            ),
2848            "fail then pass with FlakyResult::Pass is Flaky Pass"
2849        );
2850        assert!(
2851            describe.is_success_for_output(),
2852            "Flaky pass: output is success output"
2853        );
2854
2855        // Flaky fail: fail then pass with result=fail → true.
2856        let fail_status = make_execute_status(
2857            ExecutionResultDescription::Fail {
2858                failure: FailureDescription::ExitCode { code: 1 },
2859                leaked: false,
2860            },
2861            1,
2862            2,
2863        );
2864        let pass_status = make_execute_status(ExecutionResultDescription::Pass, 2, 2);
2865        let flaky_fail_statuses =
2866            ExecutionStatuses::new(vec![fail_status, pass_status], FlakyResult::Fail);
2867        let describe = flaky_fail_statuses.describe();
2868        assert!(
2869            matches!(
2870                describe,
2871                ExecutionDescription::Flaky {
2872                    result: FlakyResult::Fail,
2873                    ..
2874                }
2875            ),
2876            "fail then pass with FlakyResult::Fail is Flaky Fail"
2877        );
2878        assert!(
2879            describe.is_success_for_output(),
2880            "Flaky fail: output is still success output (last attempt passed)"
2881        );
2882
2883        // Failure: single fail → false.
2884        let fail_status = make_execute_status(
2885            ExecutionResultDescription::Fail {
2886                failure: FailureDescription::ExitCode { code: 1 },
2887                leaked: false,
2888            },
2889            1,
2890            1,
2891        );
2892        let failure_statuses = ExecutionStatuses::new(vec![fail_status], FlakyResult::Pass);
2893        let describe = failure_statuses.describe();
2894        assert!(
2895            matches!(describe, ExecutionDescription::Failure { .. }),
2896            "single fail is Failure"
2897        );
2898        assert!(
2899            !describe.is_success_for_output(),
2900            "Failure: output is not success output"
2901        );
2902
2903        // Failure with retries: all fail → false.
2904        let fail1 = make_execute_status(
2905            ExecutionResultDescription::Fail {
2906                failure: FailureDescription::ExitCode { code: 1 },
2907                leaked: false,
2908            },
2909            1,
2910            2,
2911        );
2912        let fail2 = make_execute_status(
2913            ExecutionResultDescription::Fail {
2914                failure: FailureDescription::ExitCode { code: 1 },
2915                leaked: false,
2916            },
2917            2,
2918            2,
2919        );
2920        let failure_retry_statuses = ExecutionStatuses::new(vec![fail1, fail2], FlakyResult::Pass);
2921        let describe = failure_retry_statuses.describe();
2922        assert!(
2923            matches!(describe, ExecutionDescription::Failure { .. }),
2924            "all-fail with retries is Failure"
2925        );
2926        assert!(
2927            !describe.is_success_for_output(),
2928            "Failure with retries: output is not success output"
2929        );
2930    }
2931
2932    #[test]
2933    fn abort_description_serialization() {
2934        // Unix signal with name.
2935        let unix_with_name = AbortDescription::UnixSignal {
2936            signal: 15,
2937            name: Some("TERM".into()),
2938        };
2939        let json = serde_json::to_string_pretty(&unix_with_name).unwrap();
2940        insta::assert_snapshot!("abort_unix_signal_with_name", json);
2941        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2942        assert_eq!(unix_with_name, roundtrip);
2943
2944        // Unix signal without name.
2945        let unix_no_name = AbortDescription::UnixSignal {
2946            signal: 42,
2947            name: None,
2948        };
2949        let json = serde_json::to_string_pretty(&unix_no_name).unwrap();
2950        insta::assert_snapshot!("abort_unix_signal_no_name", json);
2951        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2952        assert_eq!(unix_no_name, roundtrip);
2953
2954        // Windows NT status (0xC000013A is STATUS_CONTROL_C_EXIT).
2955        let windows_nt = AbortDescription::WindowsNtStatus {
2956            code: -1073741510_i32,
2957            message: Some("The application terminated as a result of a CTRL+C.".into()),
2958        };
2959        let json = serde_json::to_string_pretty(&windows_nt).unwrap();
2960        insta::assert_snapshot!("abort_windows_nt_status", json);
2961        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2962        assert_eq!(windows_nt, roundtrip);
2963
2964        // Windows NT status without message.
2965        let windows_nt_no_msg = AbortDescription::WindowsNtStatus {
2966            code: -1073741819_i32,
2967            message: None,
2968        };
2969        let json = serde_json::to_string_pretty(&windows_nt_no_msg).unwrap();
2970        insta::assert_snapshot!("abort_windows_nt_status_no_message", json);
2971        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2972        assert_eq!(windows_nt_no_msg, roundtrip);
2973
2974        // Windows job object.
2975        let job = AbortDescription::WindowsJobObject;
2976        let json = serde_json::to_string_pretty(&job).unwrap();
2977        insta::assert_snapshot!("abort_windows_job_object", json);
2978        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2979        assert_eq!(job, roundtrip);
2980    }
2981
2982    #[test]
2983    fn abort_description_cross_platform_deserialization() {
2984        // Cross-platform deserialization: these JSON strings could come from any
2985        // platform. Verify they deserialize correctly regardless of current platform.
2986        let unix_json = r#"{"kind":"unix-signal","signal":11,"name":"SEGV"}"#;
2987        let unix_desc: AbortDescription = serde_json::from_str(unix_json).unwrap();
2988        assert_eq!(
2989            unix_desc,
2990            AbortDescription::UnixSignal {
2991                signal: 11,
2992                name: Some("SEGV".into()),
2993            }
2994        );
2995
2996        let windows_json = r#"{"kind":"windows-nt-status","code":-1073741510,"message":"CTRL+C"}"#;
2997        let windows_desc: AbortDescription = serde_json::from_str(windows_json).unwrap();
2998        assert_eq!(
2999            windows_desc,
3000            AbortDescription::WindowsNtStatus {
3001                code: -1073741510,
3002                message: Some("CTRL+C".into()),
3003            }
3004        );
3005
3006        let job_json = r#"{"kind":"windows-job-object"}"#;
3007        let job_desc: AbortDescription = serde_json::from_str(job_json).unwrap();
3008        assert_eq!(job_desc, AbortDescription::WindowsJobObject);
3009    }
3010
3011    #[test]
3012    fn abort_description_display() {
3013        // Unix signal with name.
3014        let unix = AbortDescription::UnixSignal {
3015            signal: 15,
3016            name: Some("TERM".into()),
3017        };
3018        assert_eq!(unix.to_string(), "aborted with signal 15 (SIGTERM)");
3019
3020        // Unix signal without a name.
3021        let unix_no_name = AbortDescription::UnixSignal {
3022            signal: 42,
3023            name: None,
3024        };
3025        assert_eq!(unix_no_name.to_string(), "aborted with signal 42");
3026
3027        // Windows NT status with message.
3028        let windows = AbortDescription::WindowsNtStatus {
3029            code: -1073741510,
3030            message: Some("CTRL+C exit".into()),
3031        };
3032        assert_eq!(
3033            windows.to_string(),
3034            "aborted with code 0xc000013a: CTRL+C exit"
3035        );
3036
3037        // Windows NT status without message.
3038        let windows_no_msg = AbortDescription::WindowsNtStatus {
3039            code: -1073741510,
3040            message: None,
3041        };
3042        assert_eq!(windows_no_msg.to_string(), "aborted with code 0xc000013a");
3043
3044        // Windows job object.
3045        let job = AbortDescription::WindowsJobObject;
3046        assert_eq!(job.to_string(), "terminated via job object");
3047    }
3048
3049    #[cfg(unix)]
3050    #[test]
3051    fn abort_description_from_abort_status() {
3052        // Test conversion from AbortStatus to AbortDescription on Unix.
3053        let status = AbortStatus::UnixSignal(15);
3054        let description = AbortDescription::from(status);
3055
3056        assert_eq!(
3057            description,
3058            AbortDescription::UnixSignal {
3059                signal: 15,
3060                name: Some("TERM".into()),
3061            }
3062        );
3063
3064        // Unknown signal.
3065        let unknown_status = AbortStatus::UnixSignal(42);
3066        let unknown_description = AbortDescription::from(unknown_status);
3067        assert_eq!(
3068            unknown_description,
3069            AbortDescription::UnixSignal {
3070                signal: 42,
3071                name: None,
3072            }
3073        );
3074    }
3075
3076    #[test]
3077    fn execution_result_description_serialization() {
3078        // Test all variants of ExecutionResultDescription for serialization roundtrips.
3079
3080        // Pass.
3081        let pass = ExecutionResultDescription::Pass;
3082        let json = serde_json::to_string_pretty(&pass).unwrap();
3083        insta::assert_snapshot!("pass", json);
3084        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3085        assert_eq!(pass, roundtrip);
3086
3087        // Leak with pass result.
3088        let leak_pass = ExecutionResultDescription::Leak {
3089            result: LeakTimeoutResult::Pass,
3090        };
3091        let json = serde_json::to_string_pretty(&leak_pass).unwrap();
3092        insta::assert_snapshot!("leak_pass", json);
3093        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3094        assert_eq!(leak_pass, roundtrip);
3095
3096        // Leak with fail result.
3097        let leak_fail = ExecutionResultDescription::Leak {
3098            result: LeakTimeoutResult::Fail,
3099        };
3100        let json = serde_json::to_string_pretty(&leak_fail).unwrap();
3101        insta::assert_snapshot!("leak_fail", json);
3102        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3103        assert_eq!(leak_fail, roundtrip);
3104
3105        // Fail with exit code, no leak.
3106        let fail_exit_code = ExecutionResultDescription::Fail {
3107            failure: FailureDescription::ExitCode { code: 101 },
3108            leaked: false,
3109        };
3110        let json = serde_json::to_string_pretty(&fail_exit_code).unwrap();
3111        insta::assert_snapshot!("fail_exit_code", json);
3112        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3113        assert_eq!(fail_exit_code, roundtrip);
3114
3115        // Fail with exit code and leak.
3116        let fail_exit_code_leaked = ExecutionResultDescription::Fail {
3117            failure: FailureDescription::ExitCode { code: 1 },
3118            leaked: true,
3119        };
3120        let json = serde_json::to_string_pretty(&fail_exit_code_leaked).unwrap();
3121        insta::assert_snapshot!("fail_exit_code_leaked", json);
3122        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3123        assert_eq!(fail_exit_code_leaked, roundtrip);
3124
3125        // Fail with Unix signal abort.
3126        let fail_unix_signal = ExecutionResultDescription::Fail {
3127            failure: FailureDescription::Abort {
3128                abort: AbortDescription::UnixSignal {
3129                    signal: 11,
3130                    name: Some("SEGV".into()),
3131                },
3132            },
3133            leaked: false,
3134        };
3135        let json = serde_json::to_string_pretty(&fail_unix_signal).unwrap();
3136        insta::assert_snapshot!("fail_unix_signal", json);
3137        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3138        assert_eq!(fail_unix_signal, roundtrip);
3139
3140        // Fail with Unix signal abort (no name) and leak.
3141        let fail_unix_signal_unknown = ExecutionResultDescription::Fail {
3142            failure: FailureDescription::Abort {
3143                abort: AbortDescription::UnixSignal {
3144                    signal: 42,
3145                    name: None,
3146                },
3147            },
3148            leaked: true,
3149        };
3150        let json = serde_json::to_string_pretty(&fail_unix_signal_unknown).unwrap();
3151        insta::assert_snapshot!("fail_unix_signal_unknown_leaked", json);
3152        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3153        assert_eq!(fail_unix_signal_unknown, roundtrip);
3154
3155        // Fail with Windows NT status abort.
3156        let fail_windows_nt = ExecutionResultDescription::Fail {
3157            failure: FailureDescription::Abort {
3158                abort: AbortDescription::WindowsNtStatus {
3159                    code: -1073741510,
3160                    message: Some("The application terminated as a result of a CTRL+C.".into()),
3161                },
3162            },
3163            leaked: false,
3164        };
3165        let json = serde_json::to_string_pretty(&fail_windows_nt).unwrap();
3166        insta::assert_snapshot!("fail_windows_nt_status", json);
3167        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3168        assert_eq!(fail_windows_nt, roundtrip);
3169
3170        // Fail with Windows NT status abort (no message).
3171        let fail_windows_nt_no_msg = ExecutionResultDescription::Fail {
3172            failure: FailureDescription::Abort {
3173                abort: AbortDescription::WindowsNtStatus {
3174                    code: -1073741819,
3175                    message: None,
3176                },
3177            },
3178            leaked: false,
3179        };
3180        let json = serde_json::to_string_pretty(&fail_windows_nt_no_msg).unwrap();
3181        insta::assert_snapshot!("fail_windows_nt_status_no_message", json);
3182        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3183        assert_eq!(fail_windows_nt_no_msg, roundtrip);
3184
3185        // Fail with Windows job object abort.
3186        let fail_job_object = ExecutionResultDescription::Fail {
3187            failure: FailureDescription::Abort {
3188                abort: AbortDescription::WindowsJobObject,
3189            },
3190            leaked: false,
3191        };
3192        let json = serde_json::to_string_pretty(&fail_job_object).unwrap();
3193        insta::assert_snapshot!("fail_windows_job_object", json);
3194        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3195        assert_eq!(fail_job_object, roundtrip);
3196
3197        // ExecFail.
3198        let exec_fail = ExecutionResultDescription::ExecFail;
3199        let json = serde_json::to_string_pretty(&exec_fail).unwrap();
3200        insta::assert_snapshot!("exec_fail", json);
3201        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3202        assert_eq!(exec_fail, roundtrip);
3203
3204        // Timeout with pass result.
3205        let timeout_pass = ExecutionResultDescription::Timeout {
3206            result: SlowTimeoutResult::Pass,
3207        };
3208        let json = serde_json::to_string_pretty(&timeout_pass).unwrap();
3209        insta::assert_snapshot!("timeout_pass", json);
3210        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3211        assert_eq!(timeout_pass, roundtrip);
3212
3213        // Timeout with fail result.
3214        let timeout_fail = ExecutionResultDescription::Timeout {
3215            result: SlowTimeoutResult::Fail,
3216        };
3217        let json = serde_json::to_string_pretty(&timeout_fail).unwrap();
3218        insta::assert_snapshot!("timeout_fail", json);
3219        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
3220        assert_eq!(timeout_fail, roundtrip);
3221    }
3222
3223    // --- on_test_finished tests ---
3224
3225    /// Helper to create a fail-then-pass `ExecutionStatuses` for flaky
3226    /// test scenarios.
3227    fn make_flaky_statuses(
3228        pass_result: ExecutionResultDescription,
3229        flaky_result: FlakyResult,
3230        is_slow: bool,
3231    ) -> ExecutionStatuses<LiveSpec> {
3232        let fail = make_execute_status(
3233            ExecutionResultDescription::Fail {
3234                failure: FailureDescription::ExitCode { code: 1 },
3235                leaked: false,
3236            },
3237            1,
3238            2,
3239        );
3240        let pass = make_execute_status_slow(pass_result, 2, 2, is_slow);
3241        ExecutionStatuses::new(vec![fail, pass], flaky_result)
3242    }
3243
3244    /// Helper to run `on_test_finished` on a fresh `RunStats` and return it.
3245    fn run_on_test_finished(statuses: &ExecutionStatuses<LiveSpec>) -> RunStats {
3246        let mut stats = RunStats {
3247            initial_run_count: 1,
3248            ..RunStats::default()
3249        };
3250        stats.on_test_finished(statuses);
3251        stats
3252    }
3253
3254    #[test]
3255    fn on_test_finished_pass_flaky() {
3256        // FlakyResult::Fail (not slow): counts as failed.
3257        let stats = run_on_test_finished(&make_flaky_statuses(
3258            ExecutionResultDescription::Pass,
3259            FlakyResult::Fail,
3260            false,
3261        ));
3262        assert_eq!(stats.finished_count, 1);
3263        assert_eq!(stats.failed, 1);
3264        assert_eq!(stats.failed_slow, 0, "not slow");
3265        assert_eq!(stats.passed, 0);
3266        assert_eq!(stats.flaky, 0);
3267
3268        // FlakyResult::Fail (slow): counts as failed and failed_slow.
3269        let stats = run_on_test_finished(&make_flaky_statuses(
3270            ExecutionResultDescription::Pass,
3271            FlakyResult::Fail,
3272            true,
3273        ));
3274        assert_eq!(stats.failed, 1);
3275        assert_eq!(stats.failed_slow, 1);
3276        assert_eq!(stats.passed, 0);
3277        assert_eq!(stats.flaky, 0);
3278
3279        // FlakyResult::Pass: counts as passed and flaky.
3280        let stats = run_on_test_finished(&make_flaky_statuses(
3281            ExecutionResultDescription::Pass,
3282            FlakyResult::Pass,
3283            true,
3284        ));
3285        assert_eq!(stats.passed, 1);
3286        assert_eq!(stats.passed_slow, 1);
3287        assert_eq!(stats.flaky, 1);
3288        assert_eq!(stats.failed, 0);
3289    }
3290
3291    #[test]
3292    fn on_test_finished_leak_pass_flaky() {
3293        // FlakyResult::Fail (not slow): counts as failed and leaky.
3294        let stats = run_on_test_finished(&make_flaky_statuses(
3295            ExecutionResultDescription::Leak {
3296                result: LeakTimeoutResult::Pass,
3297            },
3298            FlakyResult::Fail,
3299            false,
3300        ));
3301        assert_eq!(stats.failed, 1);
3302        assert_eq!(stats.failed_slow, 0, "not slow");
3303        assert_eq!(stats.leaky, 1, "leak still tracked");
3304        assert_eq!(stats.passed, 0);
3305        assert_eq!(stats.flaky, 0);
3306
3307        // FlakyResult::Fail (slow): also tracks failed_slow.
3308        let stats = run_on_test_finished(&make_flaky_statuses(
3309            ExecutionResultDescription::Leak {
3310                result: LeakTimeoutResult::Pass,
3311            },
3312            FlakyResult::Fail,
3313            true,
3314        ));
3315        assert_eq!(stats.failed, 1);
3316        assert_eq!(stats.failed_slow, 1);
3317        assert_eq!(stats.leaky, 1);
3318        assert_eq!(stats.passed, 0);
3319        assert_eq!(stats.flaky, 0);
3320
3321        // FlakyResult::Pass: counts as passed, leaky, and flaky.
3322        let stats = run_on_test_finished(&make_flaky_statuses(
3323            ExecutionResultDescription::Leak {
3324                result: LeakTimeoutResult::Pass,
3325            },
3326            FlakyResult::Pass,
3327            true,
3328        ));
3329        assert_eq!(stats.passed, 1);
3330        assert_eq!(stats.passed_slow, 1);
3331        assert_eq!(stats.leaky, 1);
3332        assert_eq!(stats.flaky, 1);
3333        assert_eq!(stats.failed, 0);
3334    }
3335
3336    #[test]
3337    fn on_test_finished_timeout_pass_flaky() {
3338        // FlakyResult::Fail (slow): counts as failed and failed_slow,
3339        // not passed_timed_out.
3340        let stats = run_on_test_finished(&make_flaky_statuses(
3341            ExecutionResultDescription::Timeout {
3342                result: SlowTimeoutResult::Pass,
3343            },
3344            FlakyResult::Fail,
3345            true,
3346        ));
3347        assert_eq!(stats.failed, 1);
3348        assert_eq!(stats.failed_slow, 1);
3349        assert_eq!(stats.passed, 0);
3350        assert_eq!(stats.passed_timed_out, 0);
3351        assert_eq!(stats.flaky, 0);
3352
3353        // FlakyResult::Pass: counts as passed, passed_timed_out, and flaky.
3354        let stats = run_on_test_finished(&make_flaky_statuses(
3355            ExecutionResultDescription::Timeout {
3356                result: SlowTimeoutResult::Pass,
3357            },
3358            FlakyResult::Pass,
3359            false,
3360        ));
3361        assert_eq!(stats.passed, 1);
3362        assert_eq!(stats.passed_timed_out, 1);
3363        assert_eq!(stats.flaky, 1);
3364        assert_eq!(stats.failed, 0);
3365    }
3366
3367    #[test]
3368    fn on_test_finished_non_flaky() {
3369        // Single-attempt pass (slow): counts as passed, not flaky.
3370        let pass = make_execute_status_slow(ExecutionResultDescription::Pass, 1, 1, true);
3371        let stats = run_on_test_finished(&ExecutionStatuses::new(vec![pass], FlakyResult::Pass));
3372        assert_eq!(stats.passed, 1);
3373        assert_eq!(stats.passed_slow, 1);
3374        assert_eq!(stats.flaky, 0);
3375        assert_eq!(stats.failed, 0);
3376
3377        // Single-attempt failure (slow): counts as failed and failed_slow.
3378        let fail = make_execute_status_slow(
3379            ExecutionResultDescription::Fail {
3380                failure: FailureDescription::ExitCode { code: 1 },
3381                leaked: false,
3382            },
3383            1,
3384            1,
3385            true,
3386        );
3387        let stats = run_on_test_finished(&ExecutionStatuses::new(vec![fail], FlakyResult::Pass));
3388        assert_eq!(stats.failed, 1);
3389        assert_eq!(stats.failed_slow, 1);
3390        assert_eq!(stats.passed, 0);
3391        assert_eq!(stats.flaky, 0);
3392    }
3393}