nextest_runner/reporter/
events.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Events for the reporter.
5//!
6//! These types form the interface between the test runner and the test
7//! reporter. The root structure for all events is [`TestEvent`].
8
9use super::{FinalStatusLevel, StatusLevel, TestOutputDisplay};
10use crate::{
11    config::{
12        elements::{LeakTimeoutResult, SlowTimeoutResult},
13        scripts::ScriptId,
14    },
15    errors::{ChildError, ChildFdError, ChildStartError, ErrorList},
16    list::{TestInstanceId, TestList},
17    runner::{StressCondition, StressCount},
18    test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
19};
20use chrono::{DateTime, FixedOffset};
21use nextest_metadata::MismatchReason;
22use quick_junit::ReportUuid;
23use serde::{Deserialize, Serialize};
24use smol_str::SmolStr;
25use std::{
26    collections::BTreeMap, ffi::c_int, fmt, num::NonZero, process::ExitStatus, time::Duration,
27};
28
29/// The signal number for SIGTERM.
30///
31/// This is 15 on all platforms. We define it here rather than using `SIGTERM` because
32/// `SIGTERM` is not available on Windows, but the value is platform-independent.
33pub const SIGTERM: c_int = 15;
34
35/// A reporter event.
36#[derive(Clone, Debug)]
37pub enum ReporterEvent<'a> {
38    /// A periodic tick.
39    Tick,
40
41    /// A test event.
42    Test(Box<TestEvent<'a>>),
43}
44/// A test event.
45///
46/// Events are produced by a [`TestRunner`](crate::runner::TestRunner) and
47/// consumed by a [`Reporter`](crate::reporter::Reporter).
48#[derive(Clone, Debug)]
49pub struct TestEvent<'a> {
50    /// The time at which the event was generated, including the offset from UTC.
51    pub timestamp: DateTime<FixedOffset>,
52
53    /// The amount of time elapsed since the start of the test run.
54    pub elapsed: Duration,
55
56    /// The kind of test event this is.
57    pub kind: TestEventKind<'a>,
58}
59
60/// The kind of test event this is.
61///
62/// Forms part of [`TestEvent`].
63#[derive(Clone, Debug)]
64pub enum TestEventKind<'a> {
65    /// The test run started.
66    RunStarted {
67        /// The list of tests that will be run.
68        ///
69        /// The methods on the test list indicate the number of tests that will be run.
70        test_list: &'a TestList<'a>,
71
72        /// The UUID for this run.
73        run_id: ReportUuid,
74
75        /// The nextest profile chosen for this run.
76        profile_name: String,
77
78        /// The command-line arguments for the process.
79        cli_args: Vec<String>,
80
81        /// The stress condition for this run, if any.
82        stress_condition: Option<StressCondition>,
83    },
84
85    /// When running stress tests serially, a sub-run started.
86    StressSubRunStarted {
87        /// The amount of progress completed so far.
88        progress: StressProgress,
89    },
90
91    /// A setup script started.
92    SetupScriptStarted {
93        /// If a stress test is being run, the stress index, starting from 0.
94        stress_index: Option<StressIndex>,
95
96        /// The setup script index.
97        index: usize,
98
99        /// The total number of setup scripts.
100        total: usize,
101
102        /// The script ID.
103        script_id: ScriptId,
104
105        /// The program to run.
106        program: String,
107
108        /// The arguments to the program.
109        args: &'a [String],
110
111        /// True if some output from the setup script is being passed through.
112        no_capture: bool,
113    },
114
115    /// A setup script was slow.
116    SetupScriptSlow {
117        /// If a stress test is being run, the stress index, starting from 0.
118        stress_index: Option<StressIndex>,
119
120        /// The script ID.
121        script_id: ScriptId,
122
123        /// The program to run.
124        program: String,
125
126        /// The arguments to the program.
127        args: &'a [String],
128
129        /// The amount of time elapsed since the start of execution.
130        elapsed: Duration,
131
132        /// True if the script has hit its timeout and is about to be terminated.
133        will_terminate: bool,
134    },
135
136    /// A setup script completed execution.
137    SetupScriptFinished {
138        /// If a stress test is being run, the stress index, starting from 0.
139        stress_index: Option<StressIndex>,
140
141        /// The setup script index.
142        index: usize,
143
144        /// The total number of setup scripts.
145        total: usize,
146
147        /// The script ID.
148        script_id: ScriptId,
149
150        /// The program to run.
151        program: String,
152
153        /// The arguments to the program.
154        args: &'a [String],
155
156        /// Whether the JUnit report should store success output for this script.
157        junit_store_success_output: bool,
158
159        /// Whether the JUnit report should store failure output for this script.
160        junit_store_failure_output: bool,
161
162        /// True if some output from the setup script was passed through.
163        no_capture: bool,
164
165        /// The execution status of the setup script.
166        run_status: SetupScriptExecuteStatus<ChildSingleOutput>,
167    },
168
169    // TODO: add events for BinaryStarted and BinaryFinished? May want a slightly different way to
170    // do things, maybe a couple of reporter traits (one for the run as a whole and one for each
171    // binary).
172    /// A test started running.
173    TestStarted {
174        /// If a stress test is being run, the stress index, starting from 0.
175        stress_index: Option<StressIndex>,
176
177        /// The test instance that was started.
178        test_instance: TestInstanceId<'a>,
179
180        /// Current run statistics so far.
181        current_stats: RunStats,
182
183        /// The number of tests currently running, including this one.
184        running: usize,
185
186        /// The command line that will be used to run this test.
187        command_line: Vec<String>,
188    },
189
190    /// A test was slower than a configured soft timeout.
191    TestSlow {
192        /// If a stress test is being run, the stress index, starting from 0.
193        stress_index: Option<StressIndex>,
194
195        /// The test instance that was slow.
196        test_instance: TestInstanceId<'a>,
197
198        /// Retry data.
199        retry_data: RetryData,
200
201        /// The amount of time that has elapsed since the beginning of the test.
202        elapsed: Duration,
203
204        /// True if the test has hit its timeout and is about to be terminated.
205        will_terminate: bool,
206    },
207
208    /// A test attempt failed and will be retried in the future.
209    ///
210    /// This event does not occur on the final run of a failing test.
211    TestAttemptFailedWillRetry {
212        /// If a stress test is being run, the stress index, starting from 0.
213        stress_index: Option<StressIndex>,
214
215        /// The test instance that is being retried.
216        test_instance: TestInstanceId<'a>,
217
218        /// The status of this attempt to run the test. Will never be success.
219        run_status: ExecuteStatus<ChildSingleOutput>,
220
221        /// The delay before the next attempt to run the test.
222        delay_before_next_attempt: Duration,
223
224        /// Whether failure outputs are printed out.
225        failure_output: TestOutputDisplay,
226
227        /// The current number of running tests.
228        running: usize,
229    },
230
231    /// A retry has started.
232    TestRetryStarted {
233        /// If a stress test is being run, the stress index, starting from 0.
234        stress_index: Option<StressIndex>,
235
236        /// The test instance that is being retried.
237        test_instance: TestInstanceId<'a>,
238
239        /// Data related to retries.
240        retry_data: RetryData,
241
242        /// The current number of running tests.
243        running: usize,
244
245        /// The command line that will be used to run this test.
246        command_line: Vec<String>,
247    },
248
249    /// A test finished running.
250    TestFinished {
251        /// If a stress test is being run, the stress index, starting from 0.
252        stress_index: Option<StressIndex>,
253
254        /// The test instance that finished running.
255        test_instance: TestInstanceId<'a>,
256
257        /// Test setting for success output.
258        success_output: TestOutputDisplay,
259
260        /// Test setting for failure output.
261        failure_output: TestOutputDisplay,
262
263        /// Whether the JUnit report should store success output for this test.
264        junit_store_success_output: bool,
265
266        /// Whether the JUnit report should store failure output for this test.
267        junit_store_failure_output: bool,
268
269        /// Information about all the runs for this test.
270        run_statuses: ExecutionStatuses<ChildSingleOutput>,
271
272        /// Current statistics for number of tests so far.
273        current_stats: RunStats,
274
275        /// The number of tests that are currently running, excluding this one.
276        running: usize,
277    },
278
279    /// A test was skipped.
280    TestSkipped {
281        /// If a stress test is being run, the stress index, starting from 0.
282        stress_index: Option<StressIndex>,
283
284        /// The test instance that was skipped.
285        test_instance: TestInstanceId<'a>,
286
287        /// The reason this test was skipped.
288        reason: MismatchReason,
289    },
290
291    /// An information request was received.
292    InfoStarted {
293        /// The number of tasks currently running. This is the same as the
294        /// number of expected responses.
295        total: usize,
296
297        /// Statistics for the run.
298        run_stats: RunStats,
299    },
300
301    /// Information about a script or test was received.
302    InfoResponse {
303        /// The index of the response, starting from 0.
304        index: usize,
305
306        /// The total number of responses expected.
307        total: usize,
308
309        /// The response itself.
310        response: InfoResponse<'a>,
311    },
312
313    /// An information request was completed.
314    InfoFinished {
315        /// The number of responses that were not received. In most cases, this
316        /// is 0.
317        missing: usize,
318    },
319
320    /// `Enter` was pressed. Either a newline or a progress bar snapshot needs
321    /// to be printed.
322    InputEnter {
323        /// Current statistics for number of tests so far.
324        current_stats: RunStats,
325
326        /// The number of tests running.
327        running: usize,
328    },
329
330    /// A cancellation notice was received.
331    RunBeginCancel {
332        /// The number of setup scripts still running.
333        setup_scripts_running: usize,
334
335        /// Current statistics for number of tests so far.
336        ///
337        /// `current_stats.cancel_reason` is set to `Some`.
338        current_stats: RunStats,
339
340        /// The number of tests still running.
341        running: usize,
342    },
343
344    /// A forcible kill was requested due to receiving a signal.
345    RunBeginKill {
346        /// The number of setup scripts still running.
347        setup_scripts_running: usize,
348
349        /// Current statistics for number of tests so far.
350        ///
351        /// `current_stats.cancel_reason` is set to `Some`.
352        current_stats: RunStats,
353
354        /// The number of tests still running.
355        running: usize,
356    },
357
358    /// A SIGTSTP event was received and the run was paused.
359    RunPaused {
360        /// The number of setup scripts running.
361        setup_scripts_running: usize,
362
363        /// The number of tests currently running.
364        running: usize,
365    },
366
367    /// A SIGCONT event was received and the run is being continued.
368    RunContinued {
369        /// The number of setup scripts that will be started up again.
370        setup_scripts_running: usize,
371
372        /// The number of tests that will be started up again.
373        running: usize,
374    },
375
376    /// When running stress tests serially, a sub-run finished.
377    StressSubRunFinished {
378        /// The amount of progress completed so far.
379        progress: StressProgress,
380
381        /// The amount of time it took for this sub-run to complete.
382        sub_elapsed: Duration,
383
384        /// Statistics for the sub-run.
385        sub_stats: RunStats,
386    },
387
388    /// The test run finished.
389    RunFinished {
390        /// The unique ID for this run.
391        run_id: ReportUuid,
392
393        /// The time at which the run was started.
394        start_time: DateTime<FixedOffset>,
395
396        /// The amount of time it took for the tests to run.
397        elapsed: Duration,
398
399        /// Statistics for the run, or overall statistics for stress tests.
400        run_stats: RunFinishedStats,
401    },
402}
403
404/// Progress for a stress test.
405#[derive(Clone, Debug)]
406pub enum StressProgress {
407    /// This is a count-based stress run.
408    Count {
409        /// The total number of stress runs.
410        total: StressCount,
411
412        /// The total time that has elapsed across all stress runs so far.
413        elapsed: Duration,
414
415        /// The number of stress runs that have been completed.
416        completed: u32,
417    },
418
419    /// This is a time-based stress run.
420    Time {
421        /// The total time for the stress run.
422        total: Duration,
423
424        /// The total time that has elapsed across all stress runs so far.
425        elapsed: Duration,
426
427        /// The number of stress runs that have been completed.
428        completed: u32,
429    },
430}
431
432impl StressProgress {
433    /// Returns the remaining amount of work if the progress indicates there's
434    /// still more to do, otherwise `None`.
435    pub fn remaining(&self) -> Option<StressRemaining> {
436        match self {
437            Self::Count {
438                total: StressCount::Count { count },
439                elapsed: _,
440                completed,
441            } => count
442                .get()
443                .checked_sub(*completed)
444                .and_then(|remaining| NonZero::try_from(remaining).ok())
445                .map(StressRemaining::Count),
446            Self::Count {
447                total: StressCount::Infinite,
448                ..
449            } => Some(StressRemaining::Infinite),
450            Self::Time {
451                total,
452                elapsed,
453                completed: _,
454            } => total.checked_sub(*elapsed).map(StressRemaining::Time),
455        }
456    }
457
458    /// Returns a unique ID for this stress sub-run, consisting of the run ID and stress index.
459    pub fn unique_id(&self, run_id: ReportUuid) -> String {
460        let stress_current = match self {
461            Self::Count { completed, .. } | Self::Time { completed, .. } => *completed,
462        };
463        format!("{}:@stress-{}", run_id, stress_current)
464    }
465}
466
467/// For a stress test, the amount of time or number of stress runs remaining.
468#[derive(Clone, Debug)]
469pub enum StressRemaining {
470    /// The number of stress runs remaining, guaranteed to be non-zero.
471    Count(NonZero<u32>),
472
473    /// Infinite number of stress runs remaining.
474    Infinite,
475
476    /// The amount of time remaining.
477    Time(Duration),
478}
479
480/// The index of the current stress run.
481#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
482pub struct StressIndex {
483    /// The 0-indexed index.
484    pub current: u32,
485
486    /// The total number of stress runs, if that is available.
487    pub total: Option<NonZero<u32>>,
488}
489
490/// Statistics for a completed test run or stress run.
491#[derive(Clone, Debug)]
492pub enum RunFinishedStats {
493    /// A single test run was completed.
494    Single(RunStats),
495
496    /// A stress run was completed.
497    Stress(StressRunStats),
498}
499
500impl RunFinishedStats {
501    /// For a single run, returns a summary of statistics as an enum. For a
502    /// stress run, returns a summary for the last sub-run.
503    pub fn final_stats(&self) -> FinalRunStats {
504        match self {
505            Self::Single(stats) => stats.summarize_final(),
506            Self::Stress(stats) => stats.last_final_stats,
507        }
508    }
509}
510
511/// Statistics for a test run.
512#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
513pub struct RunStats {
514    /// The total number of tests that were expected to be run at the beginning.
515    ///
516    /// If the test run is cancelled, this will be more than `finished_count` at the end.
517    pub initial_run_count: usize,
518
519    /// The total number of tests that finished running.
520    pub finished_count: usize,
521
522    /// The total number of setup scripts that were expected to be run at the beginning.
523    ///
524    /// If the test run is cancelled, this will be more than `finished_count` at the end.
525    pub setup_scripts_initial_count: usize,
526
527    /// The total number of setup scripts that finished running.
528    pub setup_scripts_finished_count: usize,
529
530    /// The number of setup scripts that passed.
531    pub setup_scripts_passed: usize,
532
533    /// The number of setup scripts that failed.
534    pub setup_scripts_failed: usize,
535
536    /// The number of setup scripts that encountered an execution failure.
537    pub setup_scripts_exec_failed: usize,
538
539    /// The number of setup scripts that timed out.
540    pub setup_scripts_timed_out: usize,
541
542    /// The number of tests that passed. Includes `passed_slow`, `passed_timed_out`, `flaky` and `leaky`.
543    pub passed: usize,
544
545    /// The number of slow tests that passed.
546    pub passed_slow: usize,
547
548    /// The number of timed out tests that passed.
549    pub passed_timed_out: usize,
550
551    /// The number of tests that passed on retry.
552    pub flaky: usize,
553
554    /// The number of tests that failed. Includes `leaky_failed`.
555    pub failed: usize,
556
557    /// The number of failed tests that were slow.
558    pub failed_slow: usize,
559
560    /// The number of timed out tests that failed.
561    pub failed_timed_out: usize,
562
563    /// The number of tests that passed but leaked handles.
564    pub leaky: usize,
565
566    /// The number of tests that otherwise passed, but leaked handles and were
567    /// treated as failed as a result.
568    pub leaky_failed: usize,
569
570    /// The number of tests that encountered an execution failure.
571    pub exec_failed: usize,
572
573    /// The number of tests that were skipped.
574    pub skipped: usize,
575
576    /// If the run is cancelled, the reason the cancellation is happening.
577    pub cancel_reason: Option<CancelReason>,
578}
579
580impl RunStats {
581    /// Returns true if there are any failures recorded in the stats.
582    pub fn has_failures(&self) -> bool {
583        self.failed_setup_script_count() > 0 || self.failed_count() > 0
584    }
585
586    /// Returns count of setup scripts that did not pass.
587    pub fn failed_setup_script_count(&self) -> usize {
588        self.setup_scripts_failed + self.setup_scripts_exec_failed + self.setup_scripts_timed_out
589    }
590
591    /// Returns count of tests that did not pass.
592    pub fn failed_count(&self) -> usize {
593        self.failed + self.exec_failed + self.failed_timed_out
594    }
595
596    /// Summarizes the stats as an enum at the end of a test run.
597    pub fn summarize_final(&self) -> FinalRunStats {
598        // Check for failures first. The order of setup scripts vs tests should
599        // not be important, though we don't assert that here.
600        if self.failed_setup_script_count() > 0 {
601            // Is this related to a cancellation other than one directly caused
602            // by the failure?
603            if self.cancel_reason > Some(CancelReason::TestFailure) {
604                FinalRunStats::Cancelled {
605                    reason: self.cancel_reason,
606                    kind: RunStatsFailureKind::SetupScript,
607                }
608            } else {
609                FinalRunStats::Failed {
610                    kind: RunStatsFailureKind::SetupScript,
611                }
612            }
613        } else if self.setup_scripts_initial_count > self.setup_scripts_finished_count {
614            FinalRunStats::Cancelled {
615                reason: self.cancel_reason,
616                kind: RunStatsFailureKind::SetupScript,
617            }
618        } else if self.failed_count() > 0 {
619            let kind = RunStatsFailureKind::Test {
620                initial_run_count: self.initial_run_count,
621                not_run: self.initial_run_count.saturating_sub(self.finished_count),
622            };
623
624            // Is this related to a cancellation other than one directly caused
625            // by the failure?
626            if self.cancel_reason > Some(CancelReason::TestFailure) {
627                FinalRunStats::Cancelled {
628                    reason: self.cancel_reason,
629                    kind,
630                }
631            } else {
632                FinalRunStats::Failed { kind }
633            }
634        } else if self.initial_run_count > self.finished_count {
635            FinalRunStats::Cancelled {
636                reason: self.cancel_reason,
637                kind: RunStatsFailureKind::Test {
638                    initial_run_count: self.initial_run_count,
639                    not_run: self.initial_run_count.saturating_sub(self.finished_count),
640                },
641            }
642        } else if self.finished_count == 0 {
643            FinalRunStats::NoTestsRun
644        } else {
645            FinalRunStats::Success
646        }
647    }
648
649    pub(crate) fn on_setup_script_finished(
650        &mut self,
651        status: &SetupScriptExecuteStatus<ChildSingleOutput>,
652    ) {
653        self.setup_scripts_finished_count += 1;
654
655        match status.result {
656            ExecutionResultDescription::Pass
657            | ExecutionResultDescription::Leak {
658                result: LeakTimeoutResult::Pass,
659            } => {
660                self.setup_scripts_passed += 1;
661            }
662            ExecutionResultDescription::Fail { .. }
663            | ExecutionResultDescription::Leak {
664                result: LeakTimeoutResult::Fail,
665            } => {
666                self.setup_scripts_failed += 1;
667            }
668            ExecutionResultDescription::ExecFail => {
669                self.setup_scripts_exec_failed += 1;
670            }
671            // Timed out setup scripts are always treated as failures.
672            ExecutionResultDescription::Timeout { .. } => {
673                self.setup_scripts_timed_out += 1;
674            }
675        }
676    }
677
678    pub(crate) fn on_test_finished(&mut self, run_statuses: &ExecutionStatuses<ChildSingleOutput>) {
679        self.finished_count += 1;
680        // run_statuses is guaranteed to have at least one element.
681        // * If the last element is success, treat it as success (and possibly flaky).
682        // * If the last element is a failure, use it to determine fail/exec fail.
683        // Note that this is different from what Maven Surefire does (use the first failure):
684        // https://maven.apache.org/surefire/maven-surefire-plugin/examples/rerun-failing-tests.html
685        //
686        // This is not likely to matter much in practice since failures are likely to be of the
687        // same type.
688        let last_status = run_statuses.last_status();
689        match last_status.result {
690            ExecutionResultDescription::Pass => {
691                self.passed += 1;
692                if last_status.is_slow {
693                    self.passed_slow += 1;
694                }
695                if run_statuses.len() > 1 {
696                    self.flaky += 1;
697                }
698            }
699            ExecutionResultDescription::Leak {
700                result: LeakTimeoutResult::Pass,
701            } => {
702                self.passed += 1;
703                self.leaky += 1;
704                if last_status.is_slow {
705                    self.passed_slow += 1;
706                }
707                if run_statuses.len() > 1 {
708                    self.flaky += 1;
709                }
710            }
711            ExecutionResultDescription::Leak {
712                result: LeakTimeoutResult::Fail,
713            } => {
714                self.failed += 1;
715                self.leaky_failed += 1;
716                if last_status.is_slow {
717                    self.failed_slow += 1;
718                }
719            }
720            ExecutionResultDescription::Fail { .. } => {
721                self.failed += 1;
722                if last_status.is_slow {
723                    self.failed_slow += 1;
724                }
725            }
726            ExecutionResultDescription::Timeout {
727                result: SlowTimeoutResult::Pass,
728            } => {
729                self.passed += 1;
730                self.passed_timed_out += 1;
731                if run_statuses.len() > 1 {
732                    self.flaky += 1;
733                }
734            }
735            ExecutionResultDescription::Timeout {
736                result: SlowTimeoutResult::Fail,
737            } => {
738                self.failed_timed_out += 1;
739            }
740            ExecutionResultDescription::ExecFail => self.exec_failed += 1,
741        }
742    }
743}
744
745/// A type summarizing the possible outcomes of a test run.
746#[derive(Copy, Clone, Debug, Eq, PartialEq)]
747pub enum FinalRunStats {
748    /// The test run was successful, or is successful so far.
749    Success,
750
751    /// The test run was successful, or is successful so far, but no tests were selected to run.
752    NoTestsRun,
753
754    /// The test run was cancelled.
755    Cancelled {
756        /// The reason for cancellation, if available.
757        ///
758        /// This should generally be available, but may be None if some tests
759        /// that were selected to run were not executed.
760        reason: Option<CancelReason>,
761
762        /// The kind of failure that occurred.
763        kind: RunStatsFailureKind,
764    },
765
766    /// At least one test failed.
767    Failed {
768        /// The kind of failure that occurred.
769        kind: RunStatsFailureKind,
770    },
771}
772
773/// Statistics for a stress run.
774#[derive(Clone, Debug)]
775pub struct StressRunStats {
776    /// The number of stress runs completed.
777    pub completed: StressIndex,
778
779    /// The number of stress runs that succeeded.
780    pub success_count: u32,
781
782    /// The number of stress runs that failed.
783    pub failed_count: u32,
784
785    /// The last stress run's `FinalRunStats`.
786    pub last_final_stats: FinalRunStats,
787}
788
789impl StressRunStats {
790    /// Summarizes the stats as an enum at the end of a test run.
791    pub fn summarize_final(&self) -> StressFinalRunStats {
792        if self.failed_count > 0 {
793            StressFinalRunStats::Failed
794        } else if matches!(self.last_final_stats, FinalRunStats::Cancelled { .. }) {
795            StressFinalRunStats::Cancelled
796        } else if matches!(self.last_final_stats, FinalRunStats::NoTestsRun) {
797            StressFinalRunStats::NoTestsRun
798        } else {
799            StressFinalRunStats::Success
800        }
801    }
802}
803
804/// A summary of final statistics for a stress run.
805pub enum StressFinalRunStats {
806    /// The stress run was successful.
807    Success,
808
809    /// No tests were run.
810    NoTestsRun,
811
812    /// The stress run was cancelled.
813    Cancelled,
814
815    /// At least one stress run failed.
816    Failed,
817}
818
819/// A type summarizing the step at which a test run failed.
820#[derive(Copy, Clone, Debug, Eq, PartialEq)]
821pub enum RunStatsFailureKind {
822    /// The run was interrupted during setup script execution.
823    SetupScript,
824
825    /// The run was interrupted during test execution.
826    Test {
827        /// The total number of tests scheduled.
828        initial_run_count: usize,
829
830        /// The number of tests not run, or for a currently-executing test the number queued up to
831        /// run.
832        not_run: usize,
833    },
834}
835
836/// Information about executions of a test, including retries.
837///
838/// The type parameter `O` represents how test output is stored.
839#[derive(Clone, Debug)]
840pub struct ExecutionStatuses<O> {
841    /// This is guaranteed to be non-empty.
842    statuses: Vec<ExecuteStatus<O>>,
843}
844
845#[expect(clippy::len_without_is_empty)] // RunStatuses is never empty
846impl<O> ExecutionStatuses<O> {
847    pub(crate) fn new(statuses: Vec<ExecuteStatus<O>>) -> Self {
848        Self { statuses }
849    }
850
851    /// Returns the last execution status.
852    ///
853    /// This status is typically used as the final result.
854    pub fn last_status(&self) -> &ExecuteStatus<O> {
855        self.statuses
856            .last()
857            .expect("execution statuses is non-empty")
858    }
859
860    /// Iterates over all the statuses.
861    pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus<O>> + '_ {
862        self.statuses.iter()
863    }
864
865    /// Returns the number of times the test was executed.
866    pub fn len(&self) -> usize {
867        self.statuses.len()
868    }
869
870    /// Returns a description of self.
871    pub fn describe(&self) -> ExecutionDescription<'_, O> {
872        let last_status = self.last_status();
873        if last_status.result.is_success() {
874            if self.statuses.len() > 1 {
875                ExecutionDescription::Flaky {
876                    last_status,
877                    prior_statuses: &self.statuses[..self.statuses.len() - 1],
878                }
879            } else {
880                ExecutionDescription::Success {
881                    single_status: last_status,
882                }
883            }
884        } else {
885            let first_status = self
886                .statuses
887                .first()
888                .expect("execution statuses is non-empty");
889            let retries = &self.statuses[1..];
890            ExecutionDescription::Failure {
891                first_status,
892                last_status,
893                retries,
894            }
895        }
896    }
897}
898
899/// A description of test executions obtained from `ExecuteStatuses`.
900///
901/// This can be used to quickly determine whether a test passed, failed or was flaky.
902///
903/// The type parameter `O` represents how test output is stored.
904#[derive(Debug)]
905pub enum ExecutionDescription<'a, O> {
906    /// The test was run once and was successful.
907    Success {
908        /// The status of the test.
909        single_status: &'a ExecuteStatus<O>,
910    },
911
912    /// The test was run more than once. The final result was successful.
913    Flaky {
914        /// The last, successful status.
915        last_status: &'a ExecuteStatus<O>,
916
917        /// Previous statuses, none of which are successes.
918        prior_statuses: &'a [ExecuteStatus<O>],
919    },
920
921    /// The test was run once, or possibly multiple times. All runs failed.
922    Failure {
923        /// The first, failing status.
924        first_status: &'a ExecuteStatus<O>,
925
926        /// The last, failing status. Same as the first status if no retries were performed.
927        last_status: &'a ExecuteStatus<O>,
928
929        /// Any retries that were performed. All of these runs failed.
930        ///
931        /// May be empty.
932        retries: &'a [ExecuteStatus<O>],
933    },
934}
935
936// Manual Copy and Clone implementations to avoid requiring O: Copy/Clone, since
937// ExecutionDescription just stores references.
938impl<O> Clone for ExecutionDescription<'_, O> {
939    fn clone(&self) -> Self {
940        *self
941    }
942}
943
944impl<O> Copy for ExecutionDescription<'_, O> {}
945
946impl<'a, O> ExecutionDescription<'a, O> {
947    /// Returns the status level for this `ExecutionDescription`.
948    pub fn status_level(&self) -> StatusLevel {
949        match self {
950            ExecutionDescription::Success { single_status } => match single_status.result {
951                ExecutionResultDescription::Leak {
952                    result: LeakTimeoutResult::Pass,
953                } => StatusLevel::Leak,
954                ExecutionResultDescription::Pass => StatusLevel::Pass,
955                ExecutionResultDescription::Timeout {
956                    result: SlowTimeoutResult::Pass,
957                } => StatusLevel::Slow,
958                ref other => unreachable!(
959                    "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
960                ),
961            },
962            // A flaky test implies that we print out retry information for it.
963            ExecutionDescription::Flaky { .. } => StatusLevel::Retry,
964            ExecutionDescription::Failure { .. } => StatusLevel::Fail,
965        }
966    }
967
968    /// Returns the final status level for this `ExecutionDescription`.
969    pub fn final_status_level(&self) -> FinalStatusLevel {
970        match self {
971            ExecutionDescription::Success { single_status, .. } => {
972                // Slow is higher priority than leaky, so return slow first here.
973                if single_status.is_slow {
974                    FinalStatusLevel::Slow
975                } else {
976                    match single_status.result {
977                        ExecutionResultDescription::Pass => FinalStatusLevel::Pass,
978                        ExecutionResultDescription::Leak {
979                            result: LeakTimeoutResult::Pass,
980                        } => FinalStatusLevel::Leak,
981                        // Timeout with Pass should return Slow, but this case
982                        // shouldn't be reached because is_slow is true for
983                        // timeout scenarios. Handle it for completeness.
984                        ExecutionResultDescription::Timeout {
985                            result: SlowTimeoutResult::Pass,
986                        } => FinalStatusLevel::Slow,
987                        ref other => unreachable!(
988                            "Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
989                        ),
990                    }
991                }
992            }
993            // A flaky test implies that we print out retry information for it.
994            ExecutionDescription::Flaky { .. } => FinalStatusLevel::Flaky,
995            ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
996        }
997    }
998
999    /// Returns the last run status.
1000    pub fn last_status(&self) -> &'a ExecuteStatus<O> {
1001        match self {
1002            ExecutionDescription::Success {
1003                single_status: last_status,
1004            }
1005            | ExecutionDescription::Flaky { last_status, .. }
1006            | ExecutionDescription::Failure { last_status, .. } => last_status,
1007        }
1008    }
1009}
1010
1011/// Pre-computed error summary for display.
1012///
1013/// This contains the formatted error messages, pre-computed from the execution
1014/// output and result. Useful for record-replay scenarios where the rendering
1015/// is done on the server.
1016#[derive(Clone, Debug)]
1017pub struct ErrorSummary {
1018    /// A short summary of the error, suitable for display in a single line.
1019    pub short_message: String,
1020
1021    /// A full description of the error chain, suitable for detailed display.
1022    pub description: String,
1023}
1024
1025/// Pre-computed output error slice for display.
1026///
1027/// This contains an error message heuristically extracted from test output,
1028/// such as a panic message or error string.
1029#[derive(Clone, Debug)]
1030pub struct OutputErrorSlice {
1031    /// The extracted error slice as a string.
1032    pub slice: String,
1033
1034    /// The byte offset in the original output where this slice starts.
1035    pub start: usize,
1036}
1037
1038/// Information about a single execution of a test.
1039///
1040/// This is the external-facing type used by reporters. The `result` field uses
1041/// [`ExecutionResultDescription`], a platform-independent type that can be
1042/// serialized and deserialized across platforms.
1043///
1044/// The type parameter `O` represents how test output is stored:
1045/// - [`ChildSingleOutput`]: Output stored in memory with lazy string conversion.
1046/// - Other types may be used for serialization to archives.
1047#[derive(Clone, Debug)]
1048pub struct ExecuteStatus<O> {
1049    /// Retry-related data.
1050    pub retry_data: RetryData,
1051    /// The stdout and stderr output for this test.
1052    pub output: ChildExecutionOutputDescription<O>,
1053    /// The execution result for this test: pass, fail or execution error.
1054    pub result: ExecutionResultDescription,
1055    /// The time at which the test started.
1056    pub start_time: DateTime<FixedOffset>,
1057    /// The time it took for the test to run.
1058    pub time_taken: Duration,
1059    /// Whether this test counts as slow.
1060    pub is_slow: bool,
1061    /// The delay will be non-zero if this is a retry and delay was specified.
1062    pub delay_before_start: Duration,
1063    /// Pre-computed error summary, if available.
1064    ///
1065    /// This is computed from the execution output and result, and can be used
1066    /// for display without needing to re-compute the error chain.
1067    pub error_summary: Option<ErrorSummary>,
1068    /// Pre-computed output error slice, if available.
1069    ///
1070    /// This is a heuristically extracted error message from the test output,
1071    /// such as a panic message or error string.
1072    pub output_error_slice: Option<OutputErrorSlice>,
1073}
1074
1075/// Information about the execution of a setup script.
1076///
1077/// This is the external-facing type used by reporters. The `result` field uses
1078/// [`ExecutionResultDescription`], a platform-independent type that can be
1079/// serialized and deserialized across platforms.
1080///
1081/// The type parameter `O` represents how test output is stored.
1082#[derive(Clone, Debug)]
1083pub struct SetupScriptExecuteStatus<O> {
1084    /// Output for this setup script.
1085    pub output: ChildExecutionOutputDescription<O>,
1086
1087    /// The execution result for this setup script: pass, fail or execution error.
1088    pub result: ExecutionResultDescription,
1089
1090    /// The time at which the script started.
1091    pub start_time: DateTime<FixedOffset>,
1092
1093    /// The time it took for the script to run.
1094    pub time_taken: Duration,
1095
1096    /// Whether this script counts as slow.
1097    pub is_slow: bool,
1098
1099    /// The map of environment variables that were set by this script.
1100    ///
1101    /// `None` if an error occurred while running the script or reading the
1102    /// environment map.
1103    pub env_map: Option<SetupScriptEnvMap>,
1104
1105    /// Pre-computed error summary, if available.
1106    ///
1107    /// This is computed from the execution output and result, and can be used
1108    /// for display without needing to re-compute the error chain.
1109    pub error_summary: Option<ErrorSummary>,
1110}
1111
1112/// A map of environment variables set by a setup script.
1113///
1114/// Part of [`SetupScriptExecuteStatus`].
1115#[derive(Clone, Debug)]
1116pub struct SetupScriptEnvMap {
1117    /// The map of environment variables set by the script.
1118    pub env_map: BTreeMap<String, String>,
1119}
1120
1121// ---
1122// Child execution output description types
1123// ---
1124
1125/// The result of executing a child process, generic over output storage.
1126///
1127/// This is the external-facing counterpart to [`ChildExecutionOutput`]. The
1128/// type parameter `O` represents how output is stored:
1129///
1130/// - [`ChildSingleOutput`]: Output stored in memory with lazy string caching.
1131///   Used by reporter event types during live runs.
1132#[derive(Clone, Debug)]
1133pub enum ChildExecutionOutputDescription<O> {
1134    /// The process was run and the output was captured.
1135    Output {
1136        /// If the process has finished executing, the final state it is in.
1137        ///
1138        /// `None` means execution is currently in progress.
1139        result: Option<ExecutionResultDescription>,
1140
1141        /// The captured output.
1142        output: ChildOutputDescription<O>,
1143
1144        /// Errors that occurred while waiting on the child process or parsing
1145        /// its output.
1146        errors: Option<ErrorList<ChildErrorDescription>>,
1147    },
1148
1149    /// There was a failure to start the process.
1150    StartError(ChildStartErrorDescription),
1151}
1152
1153impl<O> ChildExecutionOutputDescription<O> {
1154    /// Returns true if there are any errors in this output.
1155    pub fn has_errors(&self) -> bool {
1156        match self {
1157            Self::Output { errors, result, .. } => {
1158                if errors.is_some() {
1159                    return true;
1160                }
1161                if let Some(result) = result {
1162                    return !result.is_success();
1163                }
1164                false
1165            }
1166            Self::StartError(_) => true,
1167        }
1168    }
1169}
1170
1171/// The output of a child process, generic over output storage.
1172///
1173/// This represents either split stdout/stderr or combined output. The `Option`
1174/// wrappers distinguish between "not captured" (`None`) and "captured but
1175/// empty" (`Some` with empty content).
1176#[derive(Clone, Debug, Serialize, Deserialize)]
1177#[serde(tag = "kind", rename_all = "kebab-case")]
1178pub enum ChildOutputDescription<O> {
1179    /// The output was split into stdout and stderr.
1180    Split {
1181        /// Standard output, or `None` if not captured.
1182        stdout: Option<O>,
1183        /// Standard error, or `None` if not captured.
1184        stderr: Option<O>,
1185    },
1186
1187    /// The output was combined into a single stream.
1188    Combined {
1189        /// The combined output.
1190        output: O,
1191    },
1192}
1193
1194impl ChildOutputDescription<ChildSingleOutput> {
1195    /// Returns the lengths of stdout and stderr in bytes.
1196    ///
1197    /// Returns `None` for each stream that wasn't captured.
1198    pub fn stdout_stderr_len(&self) -> (Option<u64>, Option<u64>) {
1199        match self {
1200            Self::Split { stdout, stderr } => (
1201                stdout.as_ref().map(|s| s.buf.len() as u64),
1202                stderr.as_ref().map(|s| s.buf.len() as u64),
1203            ),
1204            Self::Combined { output } => (Some(output.buf.len() as u64), None),
1205        }
1206    }
1207}
1208
1209/// A serializable description of an error that occurred while starting a child process.
1210///
1211/// This is the external-facing counterpart to [`ChildStartError`].
1212#[derive(Clone, Debug)]
1213pub enum ChildStartErrorDescription {
1214    /// An error occurred while creating a temporary path for a setup script.
1215    TempPath {
1216        /// The source error.
1217        source: IoErrorDescription,
1218    },
1219
1220    /// An error occurred while spawning the child process.
1221    Spawn {
1222        /// The source error.
1223        source: IoErrorDescription,
1224    },
1225}
1226
1227impl fmt::Display for ChildStartErrorDescription {
1228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1229        match self {
1230            Self::TempPath { .. } => {
1231                write!(f, "error creating temporary path for setup script")
1232            }
1233            Self::Spawn { .. } => write!(f, "error spawning child process"),
1234        }
1235    }
1236}
1237
1238impl std::error::Error for ChildStartErrorDescription {
1239    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1240        match self {
1241            Self::TempPath { source } | Self::Spawn { source } => Some(source),
1242        }
1243    }
1244}
1245
1246/// A serializable description of an error that occurred while managing a child process.
1247///
1248/// This is the external-facing counterpart to [`ChildError`].
1249#[derive(Clone, Debug)]
1250pub enum ChildErrorDescription {
1251    /// An error occurred while reading standard output.
1252    ReadStdout {
1253        /// The source error.
1254        source: IoErrorDescription,
1255    },
1256
1257    /// An error occurred while reading standard error.
1258    ReadStderr {
1259        /// The source error.
1260        source: IoErrorDescription,
1261    },
1262
1263    /// An error occurred while reading combined output.
1264    ReadCombined {
1265        /// The source error.
1266        source: IoErrorDescription,
1267    },
1268
1269    /// An error occurred while waiting for the child process to exit.
1270    Wait {
1271        /// The source error.
1272        source: IoErrorDescription,
1273    },
1274
1275    /// An error occurred while reading the output of a setup script.
1276    SetupScriptOutput {
1277        /// The source error.
1278        source: IoErrorDescription,
1279    },
1280}
1281
1282impl fmt::Display for ChildErrorDescription {
1283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1284        match self {
1285            Self::ReadStdout { .. } => write!(f, "error reading standard output"),
1286            Self::ReadStderr { .. } => write!(f, "error reading standard error"),
1287            Self::ReadCombined { .. } => {
1288                write!(f, "error reading combined stream")
1289            }
1290            Self::Wait { .. } => {
1291                write!(f, "error waiting for child process to exit")
1292            }
1293            Self::SetupScriptOutput { .. } => {
1294                write!(f, "error reading setup script output")
1295            }
1296        }
1297    }
1298}
1299
1300impl std::error::Error for ChildErrorDescription {
1301    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1302        match self {
1303            Self::ReadStdout { source }
1304            | Self::ReadStderr { source }
1305            | Self::ReadCombined { source }
1306            | Self::Wait { source }
1307            | Self::SetupScriptOutput { source } => Some(source),
1308        }
1309    }
1310}
1311
1312/// A serializable description of an I/O error.
1313///
1314/// This captures the error message from an [`std::io::Error`].
1315#[derive(Clone, Debug)]
1316pub struct IoErrorDescription {
1317    message: String,
1318}
1319
1320impl fmt::Display for IoErrorDescription {
1321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1322        write!(f, "{}", self.message)
1323    }
1324}
1325
1326impl std::error::Error for IoErrorDescription {}
1327
1328impl From<ChildExecutionOutput> for ChildExecutionOutputDescription<ChildSingleOutput> {
1329    fn from(output: ChildExecutionOutput) -> Self {
1330        match output {
1331            ChildExecutionOutput::Output {
1332                result,
1333                output,
1334                errors,
1335            } => Self::Output {
1336                result: result.map(ExecutionResultDescription::from),
1337                output: ChildOutputDescription::from(output),
1338                errors: errors.map(|e| e.map(ChildErrorDescription::from)),
1339            },
1340            ChildExecutionOutput::StartError(error) => {
1341                Self::StartError(ChildStartErrorDescription::from(error))
1342            }
1343        }
1344    }
1345}
1346
1347impl From<ChildOutput> for ChildOutputDescription<ChildSingleOutput> {
1348    fn from(output: ChildOutput) -> Self {
1349        match output {
1350            ChildOutput::Split(split) => Self::Split {
1351                stdout: split.stdout,
1352                stderr: split.stderr,
1353            },
1354            ChildOutput::Combined { output } => Self::Combined { output },
1355        }
1356    }
1357}
1358
1359impl From<ChildStartError> for ChildStartErrorDescription {
1360    fn from(error: ChildStartError) -> Self {
1361        match error {
1362            ChildStartError::TempPath(e) => Self::TempPath {
1363                source: IoErrorDescription {
1364                    message: e.to_string(),
1365                },
1366            },
1367            ChildStartError::Spawn(e) => Self::Spawn {
1368                source: IoErrorDescription {
1369                    message: e.to_string(),
1370                },
1371            },
1372        }
1373    }
1374}
1375
1376impl From<ChildError> for ChildErrorDescription {
1377    fn from(error: ChildError) -> Self {
1378        match error {
1379            ChildError::Fd(ChildFdError::ReadStdout(e)) => Self::ReadStdout {
1380                source: IoErrorDescription {
1381                    message: e.to_string(),
1382                },
1383            },
1384            ChildError::Fd(ChildFdError::ReadStderr(e)) => Self::ReadStderr {
1385                source: IoErrorDescription {
1386                    message: e.to_string(),
1387                },
1388            },
1389            ChildError::Fd(ChildFdError::ReadCombined(e)) => Self::ReadCombined {
1390                source: IoErrorDescription {
1391                    message: e.to_string(),
1392                },
1393            },
1394            ChildError::Fd(ChildFdError::Wait(e)) => Self::Wait {
1395                source: IoErrorDescription {
1396                    message: e.to_string(),
1397                },
1398            },
1399            ChildError::SetupScriptOutput(e) => Self::SetupScriptOutput {
1400                source: IoErrorDescription {
1401                    message: e.to_string(),
1402                },
1403            },
1404        }
1405    }
1406}
1407
1408/// Data related to retries for a test.
1409#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
1410pub struct RetryData {
1411    /// The current attempt. In the range `[1, total_attempts]`.
1412    pub attempt: u32,
1413
1414    /// The total number of times this test can be run. Equal to `1 + retries`.
1415    pub total_attempts: u32,
1416}
1417
1418impl RetryData {
1419    /// Returns true if there are no more attempts after this.
1420    pub fn is_last_attempt(&self) -> bool {
1421        self.attempt >= self.total_attempts
1422    }
1423}
1424
1425/// Whether a test passed, failed or an error occurred while executing the test.
1426#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1427pub enum ExecutionResult {
1428    /// The test passed.
1429    Pass,
1430    /// The test passed but leaked handles. This usually indicates that
1431    /// a subprocess that inherit standard IO was created, but it didn't shut down when
1432    /// the test failed.
1433    Leak {
1434        /// Whether this leak was treated as a failure.
1435        ///
1436        /// Note the difference between `Fail { leaked: true }` and `Leak {
1437        /// failed: true }`. In the former case, the test failed and also leaked
1438        /// handles. In the latter case, the test passed but leaked handles, and
1439        /// configuration indicated that this is a failure.
1440        result: LeakTimeoutResult,
1441    },
1442    /// The test failed.
1443    Fail {
1444        /// The abort status of the test, if any (for example, the signal on Unix).
1445        failure_status: FailureStatus,
1446
1447        /// Whether a test leaked handles. If set to true, this usually indicates that
1448        /// a subprocess that inherit standard IO was created, but it didn't shut down when
1449        /// the test failed.
1450        leaked: bool,
1451    },
1452    /// An error occurred while executing the test.
1453    ExecFail,
1454    /// The test was terminated due to a timeout.
1455    Timeout {
1456        /// Whether this timeout was treated as a failure.
1457        result: SlowTimeoutResult,
1458    },
1459}
1460
1461impl ExecutionResult {
1462    /// Returns true if the test was successful.
1463    pub fn is_success(self) -> bool {
1464        match self {
1465            ExecutionResult::Pass
1466            | ExecutionResult::Timeout {
1467                result: SlowTimeoutResult::Pass,
1468            }
1469            | ExecutionResult::Leak {
1470                result: LeakTimeoutResult::Pass,
1471            } => true,
1472            ExecutionResult::Leak {
1473                result: LeakTimeoutResult::Fail,
1474            }
1475            | ExecutionResult::Fail { .. }
1476            | ExecutionResult::ExecFail
1477            | ExecutionResult::Timeout {
1478                result: SlowTimeoutResult::Fail,
1479            } => false,
1480        }
1481    }
1482
1483    /// Returns a static string representation of the result.
1484    pub fn as_static_str(&self) -> &'static str {
1485        match self {
1486            ExecutionResult::Pass => "pass",
1487            ExecutionResult::Leak { .. } => "leak",
1488            ExecutionResult::Fail { .. } => "fail",
1489            ExecutionResult::ExecFail => "exec-fail",
1490            ExecutionResult::Timeout { .. } => "timeout",
1491        }
1492    }
1493}
1494
1495/// Failure status: either an exit code or an abort status.
1496#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1497pub enum FailureStatus {
1498    /// The test exited with a non-zero exit code.
1499    ExitCode(i32),
1500
1501    /// The test aborted.
1502    Abort(AbortStatus),
1503}
1504
1505impl FailureStatus {
1506    /// Extract the failure status from an `ExitStatus`.
1507    pub fn extract(exit_status: ExitStatus) -> Self {
1508        if let Some(abort_status) = AbortStatus::extract(exit_status) {
1509            FailureStatus::Abort(abort_status)
1510        } else {
1511            FailureStatus::ExitCode(
1512                exit_status
1513                    .code()
1514                    .expect("if abort_status is None, then code must be present"),
1515            )
1516        }
1517    }
1518}
1519
1520/// A regular exit code or Windows NT abort status for a test.
1521///
1522/// Returned as part of the [`ExecutionResult::Fail`] variant.
1523#[derive(Copy, Clone, Eq, PartialEq)]
1524pub enum AbortStatus {
1525    /// The test was aborted due to a signal on Unix.
1526    #[cfg(unix)]
1527    UnixSignal(i32),
1528
1529    /// The test was determined to have aborted because the high bit was set on Windows.
1530    #[cfg(windows)]
1531    WindowsNtStatus(windows_sys::Win32::Foundation::NTSTATUS),
1532
1533    /// The test was terminated via job object on Windows.
1534    #[cfg(windows)]
1535    JobObject,
1536}
1537
1538impl AbortStatus {
1539    /// Extract the abort status from an [`ExitStatus`].
1540    pub fn extract(exit_status: ExitStatus) -> Option<Self> {
1541        cfg_if::cfg_if! {
1542            if #[cfg(unix)] {
1543                // On Unix, extract the signal if it's found.
1544                use std::os::unix::process::ExitStatusExt;
1545                exit_status.signal().map(AbortStatus::UnixSignal)
1546            } else if #[cfg(windows)] {
1547                exit_status.code().and_then(|code| {
1548                    (code < 0).then_some(AbortStatus::WindowsNtStatus(code))
1549                })
1550            } else {
1551                None
1552            }
1553        }
1554    }
1555}
1556
1557impl fmt::Debug for AbortStatus {
1558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1559        match self {
1560            #[cfg(unix)]
1561            AbortStatus::UnixSignal(signal) => write!(f, "UnixSignal({signal})"),
1562            #[cfg(windows)]
1563            AbortStatus::WindowsNtStatus(status) => write!(f, "WindowsNtStatus({status:x})"),
1564            #[cfg(windows)]
1565            AbortStatus::JobObject => write!(f, "JobObject"),
1566        }
1567    }
1568}
1569
1570/// A platform-independent description of an abort status.
1571///
1572/// This type can be serialized on one platform and deserialized on another,
1573/// containing all information needed for display without requiring
1574/// platform-specific lookups.
1575#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1576#[serde(tag = "kind", rename_all = "kebab-case")]
1577#[non_exhaustive]
1578pub enum AbortDescription {
1579    /// The process was aborted by a Unix signal.
1580    UnixSignal {
1581        /// The signal number.
1582        signal: i32,
1583        /// The signal name without the "SIG" prefix (e.g., "TERM", "SEGV"),
1584        /// if known.
1585        name: Option<SmolStr>,
1586    },
1587
1588    /// The process was aborted with a Windows NT status code.
1589    WindowsNtStatus {
1590        /// The NTSTATUS code.
1591        code: i32,
1592        /// The human-readable message from the Win32 error code, if available.
1593        message: Option<SmolStr>,
1594    },
1595
1596    /// The process was terminated via a Windows job object.
1597    WindowsJobObject,
1598}
1599
1600impl fmt::Display for AbortDescription {
1601    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1602        match self {
1603            Self::UnixSignal { signal, name } => {
1604                write!(f, "aborted with signal {signal}")?;
1605                if let Some(name) = name {
1606                    write!(f, " (SIG{name})")?;
1607                }
1608                Ok(())
1609            }
1610            Self::WindowsNtStatus { code, message } => {
1611                write!(f, "aborted with code {code:#010x}")?;
1612                if let Some(message) = message {
1613                    write!(f, ": {message}")?;
1614                }
1615                Ok(())
1616            }
1617            Self::WindowsJobObject => {
1618                write!(f, "terminated via job object")
1619            }
1620        }
1621    }
1622}
1623
1624impl From<AbortStatus> for AbortDescription {
1625    fn from(status: AbortStatus) -> Self {
1626        cfg_if::cfg_if! {
1627            if #[cfg(unix)] {
1628                match status {
1629                    AbortStatus::UnixSignal(signal) => Self::UnixSignal {
1630                        signal,
1631                        name: crate::helpers::signal_str(signal).map(SmolStr::new_static),
1632                    },
1633                }
1634            } else if #[cfg(windows)] {
1635                match status {
1636                    AbortStatus::WindowsNtStatus(code) => Self::WindowsNtStatus {
1637                        code,
1638                        message: crate::helpers::windows_nt_status_message(code),
1639                    },
1640                    AbortStatus::JobObject => Self::WindowsJobObject,
1641                }
1642            } else {
1643                match status {}
1644            }
1645        }
1646    }
1647}
1648
1649/// A platform-independent description of a test failure status.
1650///
1651/// This is the platform-independent counterpart to [`FailureStatus`].
1652#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1653#[serde(tag = "kind", rename_all = "kebab-case")]
1654#[non_exhaustive]
1655pub enum FailureDescription {
1656    /// The test exited with a non-zero exit code.
1657    ExitCode {
1658        /// The exit code.
1659        code: i32,
1660    },
1661
1662    /// The test was aborted (e.g., by a signal on Unix or NT status on Windows).
1663    ///
1664    /// Note: this is a struct variant rather than a newtype variant to ensure
1665    /// proper JSON nesting. Both `FailureDescription` and `AbortDescription`
1666    /// use `#[serde(tag = "kind")]`, and if this were a newtype variant, serde
1667    /// would flatten the inner type causing duplicate `"kind"` fields.
1668    Abort {
1669        /// The abort description.
1670        abort: AbortDescription,
1671    },
1672}
1673
1674impl From<FailureStatus> for FailureDescription {
1675    fn from(status: FailureStatus) -> Self {
1676        match status {
1677            FailureStatus::ExitCode(code) => Self::ExitCode { code },
1678            FailureStatus::Abort(abort) => Self::Abort {
1679                abort: AbortDescription::from(abort),
1680            },
1681        }
1682    }
1683}
1684
1685impl fmt::Display for FailureDescription {
1686    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1687        match self {
1688            Self::ExitCode { code } => write!(f, "exited with code {code}"),
1689            Self::Abort { abort } => write!(f, "{abort}"),
1690        }
1691    }
1692}
1693
1694/// A platform-independent description of a test execution result.
1695///
1696/// This is the platform-independent counterpart to [`ExecutionResult`], used
1697/// in external-facing types like [`ExecuteStatus`].
1698#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1699#[serde(tag = "status", rename_all = "kebab-case")]
1700#[non_exhaustive]
1701pub enum ExecutionResultDescription {
1702    /// The test passed.
1703    Pass,
1704
1705    /// The test passed but leaked handles.
1706    Leak {
1707        /// Whether this leak was treated as a failure.
1708        result: LeakTimeoutResult,
1709    },
1710
1711    /// The test failed.
1712    Fail {
1713        /// The failure status.
1714        failure: FailureDescription,
1715
1716        /// Whether the test leaked handles.
1717        leaked: bool,
1718    },
1719
1720    /// An error occurred while executing the test.
1721    ExecFail,
1722
1723    /// The test was terminated due to a timeout.
1724    Timeout {
1725        /// Whether this timeout was treated as a failure.
1726        result: SlowTimeoutResult,
1727    },
1728}
1729
1730impl ExecutionResultDescription {
1731    /// Returns true if the test was successful.
1732    pub fn is_success(&self) -> bool {
1733        match self {
1734            Self::Pass
1735            | Self::Timeout {
1736                result: SlowTimeoutResult::Pass,
1737            }
1738            | Self::Leak {
1739                result: LeakTimeoutResult::Pass,
1740            } => true,
1741            Self::Leak {
1742                result: LeakTimeoutResult::Fail,
1743            }
1744            | Self::Fail { .. }
1745            | Self::ExecFail
1746            | Self::Timeout {
1747                result: SlowTimeoutResult::Fail,
1748            } => false,
1749        }
1750    }
1751
1752    /// Returns a static string representation of the result.
1753    pub fn as_static_str(&self) -> &'static str {
1754        match self {
1755            Self::Pass => "pass",
1756            Self::Leak { .. } => "leak",
1757            Self::Fail { .. } => "fail",
1758            Self::ExecFail => "exec-fail",
1759            Self::Timeout { .. } => "timeout",
1760        }
1761    }
1762
1763    /// Returns true if this result represents a test that was terminated by nextest
1764    /// (as opposed to failing naturally).
1765    ///
1766    /// This is used to suppress output spam when running under
1767    /// TestFailureImmediate.
1768    ///
1769    /// TODO: This is a heuristic that checks if the test was terminated by
1770    /// SIGTERM (Unix) or job object (Windows). In an edge case, a test could
1771    /// send SIGTERM to itself, which would incorrectly be detected as a
1772    /// nextest-initiated termination. A more robust solution would track which
1773    /// tests were explicitly sent termination signals by nextest.
1774    pub fn is_termination_failure(&self) -> bool {
1775        matches!(
1776            self,
1777            Self::Fail {
1778                failure: FailureDescription::Abort {
1779                    abort: AbortDescription::UnixSignal {
1780                        signal: SIGTERM,
1781                        ..
1782                    },
1783                },
1784                ..
1785            } | Self::Fail {
1786                failure: FailureDescription::Abort {
1787                    abort: AbortDescription::WindowsJobObject,
1788                },
1789                ..
1790            }
1791        )
1792    }
1793}
1794
1795impl From<ExecutionResult> for ExecutionResultDescription {
1796    fn from(result: ExecutionResult) -> Self {
1797        match result {
1798            ExecutionResult::Pass => Self::Pass,
1799            ExecutionResult::Leak { result } => Self::Leak { result },
1800            ExecutionResult::Fail {
1801                failure_status,
1802                leaked,
1803            } => Self::Fail {
1804                failure: FailureDescription::from(failure_status),
1805                leaked,
1806            },
1807            ExecutionResult::ExecFail => Self::ExecFail,
1808            ExecutionResult::Timeout { result } => Self::Timeout { result },
1809        }
1810    }
1811}
1812
1813// Note: the order here matters -- it indicates severity of cancellation
1814/// The reason why a test run is being cancelled.
1815#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
1816#[cfg_attr(test, derive(test_strategy::Arbitrary))]
1817pub enum CancelReason {
1818    /// A setup script failed.
1819    SetupScriptFailure,
1820
1821    /// A test failed and --no-fail-fast wasn't specified.
1822    TestFailure,
1823
1824    /// An error occurred while reporting results.
1825    ReportError,
1826
1827    /// The global timeout was exceeded.
1828    GlobalTimeout,
1829
1830    /// A test failed and fail-fast with immediate termination was specified.
1831    TestFailureImmediate,
1832
1833    /// A termination signal (on Unix, SIGTERM or SIGHUP) was received.
1834    Signal,
1835
1836    /// An interrupt (on Unix, Ctrl-C) was received.
1837    Interrupt,
1838
1839    /// A second signal was received, and the run is being forcibly killed.
1840    SecondSignal,
1841}
1842
1843impl CancelReason {
1844    pub(crate) fn to_static_str(self) -> &'static str {
1845        match self {
1846            CancelReason::SetupScriptFailure => "setup script failure",
1847            CancelReason::TestFailure => "test failure",
1848            CancelReason::ReportError => "reporting error",
1849            CancelReason::GlobalTimeout => "global timeout",
1850            CancelReason::TestFailureImmediate => "test failure",
1851            CancelReason::Signal => "signal",
1852            CancelReason::Interrupt => "interrupt",
1853            CancelReason::SecondSignal => "second signal",
1854        }
1855    }
1856}
1857/// The kind of unit of work that nextest is executing.
1858#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1859pub enum UnitKind {
1860    /// A test.
1861    Test,
1862
1863    /// A script (e.g. a setup script).
1864    Script,
1865}
1866
1867impl UnitKind {
1868    pub(crate) const WAITING_ON_TEST_MESSAGE: &str = "waiting on test process";
1869    pub(crate) const WAITING_ON_SCRIPT_MESSAGE: &str = "waiting on script process";
1870
1871    pub(crate) const EXECUTING_TEST_MESSAGE: &str = "executing test";
1872    pub(crate) const EXECUTING_SCRIPT_MESSAGE: &str = "executing script";
1873
1874    pub(crate) fn waiting_on_message(&self) -> &'static str {
1875        match self {
1876            UnitKind::Test => Self::WAITING_ON_TEST_MESSAGE,
1877            UnitKind::Script => Self::WAITING_ON_SCRIPT_MESSAGE,
1878        }
1879    }
1880
1881    pub(crate) fn executing_message(&self) -> &'static str {
1882        match self {
1883            UnitKind::Test => Self::EXECUTING_TEST_MESSAGE,
1884            UnitKind::Script => Self::EXECUTING_SCRIPT_MESSAGE,
1885        }
1886    }
1887}
1888
1889/// A response to an information request.
1890#[derive(Clone, Debug)]
1891pub enum InfoResponse<'a> {
1892    /// A setup script's response.
1893    SetupScript(SetupScriptInfoResponse<'a>),
1894
1895    /// A test's response.
1896    Test(TestInfoResponse<'a>),
1897}
1898
1899/// A setup script's response to an information request.
1900#[derive(Clone, Debug)]
1901pub struct SetupScriptInfoResponse<'a> {
1902    /// The stress index of the setup script.
1903    pub stress_index: Option<StressIndex>,
1904
1905    /// The identifier of the setup script instance.
1906    pub script_id: ScriptId,
1907
1908    /// The program to run.
1909    pub program: String,
1910
1911    /// The list of arguments to the program.
1912    pub args: &'a [String],
1913
1914    /// The state of the setup script.
1915    pub state: UnitState,
1916
1917    /// Output obtained from the setup script.
1918    pub output: ChildExecutionOutputDescription<ChildSingleOutput>,
1919}
1920
1921/// A test's response to an information request.
1922#[derive(Clone, Debug)]
1923pub struct TestInfoResponse<'a> {
1924    /// The stress index of the test.
1925    pub stress_index: Option<StressIndex>,
1926
1927    /// The test instance that the information is about.
1928    pub test_instance: TestInstanceId<'a>,
1929
1930    /// Information about retries.
1931    pub retry_data: RetryData,
1932
1933    /// The state of the test.
1934    pub state: UnitState,
1935
1936    /// Output obtained from the test.
1937    pub output: ChildExecutionOutputDescription<ChildSingleOutput>,
1938}
1939
1940/// The current state of a test or script process: running, exiting, or
1941/// terminating.
1942///
1943/// Part of information response requests.
1944#[derive(Clone, Debug)]
1945pub enum UnitState {
1946    /// The unit is currently running.
1947    Running {
1948        /// The process ID.
1949        pid: u32,
1950
1951        /// The amount of time the unit has been running.
1952        time_taken: Duration,
1953
1954        /// `Some` if the test is marked as slow, along with the duration after
1955        /// which it was marked as slow.
1956        slow_after: Option<Duration>,
1957    },
1958
1959    /// The test has finished running, and is currently in the process of
1960    /// exiting.
1961    Exiting {
1962        /// The process ID.
1963        pid: u32,
1964
1965        /// The amount of time the unit ran for.
1966        time_taken: Duration,
1967
1968        /// `Some` if the unit is marked as slow, along with the duration after
1969        /// which it was marked as slow.
1970        slow_after: Option<Duration>,
1971
1972        /// The tentative execution result before leaked status is determined.
1973        ///
1974        /// None means that the exit status could not be read, and should be
1975        /// treated as a failure.
1976        tentative_result: Option<ExecutionResult>,
1977
1978        /// How long has been spent waiting for the process to exit.
1979        waiting_duration: Duration,
1980
1981        /// How much longer nextest will wait until the test is marked leaky.
1982        remaining: Duration,
1983    },
1984
1985    /// The child process is being terminated by nextest.
1986    Terminating(UnitTerminatingState),
1987
1988    /// The unit has finished running and the process has exited.
1989    Exited {
1990        /// The result of executing the unit.
1991        result: ExecutionResult,
1992
1993        /// The amount of time the unit ran for.
1994        time_taken: Duration,
1995
1996        /// `Some` if the unit is marked as slow, along with the duration after
1997        /// which it was marked as slow.
1998        slow_after: Option<Duration>,
1999    },
2000
2001    /// A delay is being waited out before the next attempt of the test is
2002    /// started. (Only relevant for tests.)
2003    DelayBeforeNextAttempt {
2004        /// The previous execution result.
2005        previous_result: ExecutionResult,
2006
2007        /// Whether the previous attempt was marked as slow.
2008        previous_slow: bool,
2009
2010        /// How long has been spent waiting so far.
2011        waiting_duration: Duration,
2012
2013        /// How much longer nextest will wait until retrying the test.
2014        remaining: Duration,
2015    },
2016}
2017
2018impl UnitState {
2019    /// Returns true if the state has a valid output attached to it.
2020    pub fn has_valid_output(&self) -> bool {
2021        match self {
2022            UnitState::Running { .. }
2023            | UnitState::Exiting { .. }
2024            | UnitState::Terminating(_)
2025            | UnitState::Exited { .. } => true,
2026            UnitState::DelayBeforeNextAttempt { .. } => false,
2027        }
2028    }
2029}
2030
2031/// The current terminating state of a test or script process.
2032///
2033/// Part of [`UnitState::Terminating`].
2034#[derive(Clone, Debug)]
2035pub struct UnitTerminatingState {
2036    /// The process ID.
2037    pub pid: u32,
2038
2039    /// The amount of time the unit ran for.
2040    pub time_taken: Duration,
2041
2042    /// The reason for the termination.
2043    pub reason: UnitTerminateReason,
2044
2045    /// The method by which the process is being terminated.
2046    pub method: UnitTerminateMethod,
2047
2048    /// How long has been spent waiting for the process to exit.
2049    pub waiting_duration: Duration,
2050
2051    /// How much longer nextest will wait until a kill command is sent to the process.
2052    pub remaining: Duration,
2053}
2054
2055/// The reason for a script or test being forcibly terminated by nextest.
2056///
2057/// Part of information response requests.
2058#[derive(Clone, Copy, Debug)]
2059pub enum UnitTerminateReason {
2060    /// The unit is being terminated due to a test timeout being hit.
2061    Timeout,
2062
2063    /// The unit is being terminated due to nextest receiving a signal.
2064    Signal,
2065
2066    /// The unit is being terminated due to an interrupt (i.e. Ctrl-C).
2067    Interrupt,
2068}
2069
2070impl fmt::Display for UnitTerminateReason {
2071    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2072        match self {
2073            UnitTerminateReason::Timeout => write!(f, "timeout"),
2074            UnitTerminateReason::Signal => write!(f, "signal"),
2075            UnitTerminateReason::Interrupt => write!(f, "interrupt"),
2076        }
2077    }
2078}
2079
2080/// The way in which a script or test is being forcibly terminated by nextest.
2081#[derive(Clone, Copy, Debug)]
2082pub enum UnitTerminateMethod {
2083    /// The unit is being terminated by sending a signal.
2084    #[cfg(unix)]
2085    Signal(UnitTerminateSignal),
2086
2087    /// The unit is being terminated by terminating the Windows job object.
2088    #[cfg(windows)]
2089    JobObject,
2090
2091    /// The unit is being waited on to exit. A termination signal will be sent
2092    /// if it doesn't exit within the grace period.
2093    ///
2094    /// On Windows, this occurs when nextest receives Ctrl-C. In that case, it
2095    /// is assumed that tests will also receive Ctrl-C and exit on their own. If
2096    /// tests do not exit within the grace period configured for them, their
2097    /// corresponding job objects will be terminated.
2098    #[cfg(windows)]
2099    Wait,
2100
2101    /// A fake method used for testing.
2102    #[cfg(test)]
2103    Fake,
2104}
2105
2106#[cfg(unix)]
2107/// The signal that is or was sent to terminate a script or test.
2108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2109pub enum UnitTerminateSignal {
2110    /// The unit is being terminated by sending a SIGINT.
2111    Interrupt,
2112
2113    /// The unit is being terminated by sending a SIGTERM signal.
2114    Term,
2115
2116    /// The unit is being terminated by sending a SIGHUP signal.
2117    Hangup,
2118
2119    /// The unit is being terminated by sending a SIGQUIT signal.
2120    Quit,
2121
2122    /// The unit is being terminated by sending a SIGKILL signal.
2123    Kill,
2124}
2125
2126#[cfg(unix)]
2127impl fmt::Display for UnitTerminateSignal {
2128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2129        match self {
2130            UnitTerminateSignal::Interrupt => write!(f, "SIGINT"),
2131            UnitTerminateSignal::Term => write!(f, "SIGTERM"),
2132            UnitTerminateSignal::Hangup => write!(f, "SIGHUP"),
2133            UnitTerminateSignal::Quit => write!(f, "SIGQUIT"),
2134            UnitTerminateSignal::Kill => write!(f, "SIGKILL"),
2135        }
2136    }
2137}
2138
2139#[cfg(test)]
2140mod tests {
2141    use super::*;
2142
2143    #[test]
2144    fn test_is_success() {
2145        assert_eq!(
2146            RunStats::default().summarize_final(),
2147            FinalRunStats::NoTestsRun,
2148            "empty run => no tests run"
2149        );
2150        assert_eq!(
2151            RunStats {
2152                initial_run_count: 42,
2153                finished_count: 42,
2154                ..RunStats::default()
2155            }
2156            .summarize_final(),
2157            FinalRunStats::Success,
2158            "initial run count = final run count => success"
2159        );
2160        assert_eq!(
2161            RunStats {
2162                initial_run_count: 42,
2163                finished_count: 41,
2164                ..RunStats::default()
2165            }
2166            .summarize_final(),
2167            FinalRunStats::Cancelled {
2168                reason: None,
2169                kind: RunStatsFailureKind::Test {
2170                    initial_run_count: 42,
2171                    not_run: 1
2172                }
2173            },
2174            "initial run count > final run count => cancelled"
2175        );
2176        assert_eq!(
2177            RunStats {
2178                initial_run_count: 42,
2179                finished_count: 42,
2180                failed: 1,
2181                ..RunStats::default()
2182            }
2183            .summarize_final(),
2184            FinalRunStats::Failed {
2185                kind: RunStatsFailureKind::Test {
2186                    initial_run_count: 42,
2187                    not_run: 0,
2188                },
2189            },
2190            "failed => failure"
2191        );
2192        assert_eq!(
2193            RunStats {
2194                initial_run_count: 42,
2195                finished_count: 42,
2196                exec_failed: 1,
2197                ..RunStats::default()
2198            }
2199            .summarize_final(),
2200            FinalRunStats::Failed {
2201                kind: RunStatsFailureKind::Test {
2202                    initial_run_count: 42,
2203                    not_run: 0,
2204                },
2205            },
2206            "exec failed => failure"
2207        );
2208        assert_eq!(
2209            RunStats {
2210                initial_run_count: 42,
2211                finished_count: 42,
2212                failed_timed_out: 1,
2213                ..RunStats::default()
2214            }
2215            .summarize_final(),
2216            FinalRunStats::Failed {
2217                kind: RunStatsFailureKind::Test {
2218                    initial_run_count: 42,
2219                    not_run: 0,
2220                },
2221            },
2222            "timed out => failure {:?} {:?}",
2223            RunStats {
2224                initial_run_count: 42,
2225                finished_count: 42,
2226                failed_timed_out: 1,
2227                ..RunStats::default()
2228            }
2229            .summarize_final(),
2230            FinalRunStats::Failed {
2231                kind: RunStatsFailureKind::Test {
2232                    initial_run_count: 42,
2233                    not_run: 0,
2234                },
2235            },
2236        );
2237        assert_eq!(
2238            RunStats {
2239                initial_run_count: 42,
2240                finished_count: 42,
2241                skipped: 1,
2242                ..RunStats::default()
2243            }
2244            .summarize_final(),
2245            FinalRunStats::Success,
2246            "skipped => not considered a failure"
2247        );
2248
2249        assert_eq!(
2250            RunStats {
2251                setup_scripts_initial_count: 2,
2252                setup_scripts_finished_count: 1,
2253                ..RunStats::default()
2254            }
2255            .summarize_final(),
2256            FinalRunStats::Cancelled {
2257                reason: None,
2258                kind: RunStatsFailureKind::SetupScript,
2259            },
2260            "setup script failed => failure"
2261        );
2262
2263        assert_eq!(
2264            RunStats {
2265                setup_scripts_initial_count: 2,
2266                setup_scripts_finished_count: 2,
2267                setup_scripts_failed: 1,
2268                ..RunStats::default()
2269            }
2270            .summarize_final(),
2271            FinalRunStats::Failed {
2272                kind: RunStatsFailureKind::SetupScript,
2273            },
2274            "setup script failed => failure"
2275        );
2276        assert_eq!(
2277            RunStats {
2278                setup_scripts_initial_count: 2,
2279                setup_scripts_finished_count: 2,
2280                setup_scripts_exec_failed: 1,
2281                ..RunStats::default()
2282            }
2283            .summarize_final(),
2284            FinalRunStats::Failed {
2285                kind: RunStatsFailureKind::SetupScript,
2286            },
2287            "setup script exec failed => failure"
2288        );
2289        assert_eq!(
2290            RunStats {
2291                setup_scripts_initial_count: 2,
2292                setup_scripts_finished_count: 2,
2293                setup_scripts_timed_out: 1,
2294                ..RunStats::default()
2295            }
2296            .summarize_final(),
2297            FinalRunStats::Failed {
2298                kind: RunStatsFailureKind::SetupScript,
2299            },
2300            "setup script timed out => failure"
2301        );
2302        assert_eq!(
2303            RunStats {
2304                setup_scripts_initial_count: 2,
2305                setup_scripts_finished_count: 2,
2306                setup_scripts_passed: 2,
2307                ..RunStats::default()
2308            }
2309            .summarize_final(),
2310            FinalRunStats::NoTestsRun,
2311            "setup scripts passed => success, but no tests run"
2312        );
2313    }
2314
2315    #[test]
2316    fn abort_description_serialization() {
2317        // Unix signal with name.
2318        let unix_with_name = AbortDescription::UnixSignal {
2319            signal: 15,
2320            name: Some("TERM".into()),
2321        };
2322        let json = serde_json::to_string_pretty(&unix_with_name).unwrap();
2323        insta::assert_snapshot!("abort_unix_signal_with_name", json);
2324        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2325        assert_eq!(unix_with_name, roundtrip);
2326
2327        // Unix signal without name.
2328        let unix_no_name = AbortDescription::UnixSignal {
2329            signal: 42,
2330            name: None,
2331        };
2332        let json = serde_json::to_string_pretty(&unix_no_name).unwrap();
2333        insta::assert_snapshot!("abort_unix_signal_no_name", json);
2334        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2335        assert_eq!(unix_no_name, roundtrip);
2336
2337        // Windows NT status (0xC000013A is STATUS_CONTROL_C_EXIT).
2338        let windows_nt = AbortDescription::WindowsNtStatus {
2339            code: -1073741510_i32,
2340            message: Some("The application terminated as a result of a CTRL+C.".into()),
2341        };
2342        let json = serde_json::to_string_pretty(&windows_nt).unwrap();
2343        insta::assert_snapshot!("abort_windows_nt_status", json);
2344        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2345        assert_eq!(windows_nt, roundtrip);
2346
2347        // Windows NT status without message.
2348        let windows_nt_no_msg = AbortDescription::WindowsNtStatus {
2349            code: -1073741819_i32,
2350            message: None,
2351        };
2352        let json = serde_json::to_string_pretty(&windows_nt_no_msg).unwrap();
2353        insta::assert_snapshot!("abort_windows_nt_status_no_message", json);
2354        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2355        assert_eq!(windows_nt_no_msg, roundtrip);
2356
2357        // Windows job object.
2358        let job = AbortDescription::WindowsJobObject;
2359        let json = serde_json::to_string_pretty(&job).unwrap();
2360        insta::assert_snapshot!("abort_windows_job_object", json);
2361        let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
2362        assert_eq!(job, roundtrip);
2363    }
2364
2365    #[test]
2366    fn abort_description_cross_platform_deserialization() {
2367        // Cross-platform deserialization: these JSON strings could come from any
2368        // platform. Verify they deserialize correctly regardless of current platform.
2369        let unix_json = r#"{"kind":"unix-signal","signal":11,"name":"SEGV"}"#;
2370        let unix_desc: AbortDescription = serde_json::from_str(unix_json).unwrap();
2371        assert_eq!(
2372            unix_desc,
2373            AbortDescription::UnixSignal {
2374                signal: 11,
2375                name: Some("SEGV".into()),
2376            }
2377        );
2378
2379        let windows_json = r#"{"kind":"windows-nt-status","code":-1073741510,"message":"CTRL+C"}"#;
2380        let windows_desc: AbortDescription = serde_json::from_str(windows_json).unwrap();
2381        assert_eq!(
2382            windows_desc,
2383            AbortDescription::WindowsNtStatus {
2384                code: -1073741510,
2385                message: Some("CTRL+C".into()),
2386            }
2387        );
2388
2389        let job_json = r#"{"kind":"windows-job-object"}"#;
2390        let job_desc: AbortDescription = serde_json::from_str(job_json).unwrap();
2391        assert_eq!(job_desc, AbortDescription::WindowsJobObject);
2392    }
2393
2394    #[test]
2395    fn abort_description_display() {
2396        // Unix signal with name.
2397        let unix = AbortDescription::UnixSignal {
2398            signal: 15,
2399            name: Some("TERM".into()),
2400        };
2401        assert_eq!(unix.to_string(), "aborted with signal 15 (SIGTERM)");
2402
2403        // Unix signal without a name.
2404        let unix_no_name = AbortDescription::UnixSignal {
2405            signal: 42,
2406            name: None,
2407        };
2408        assert_eq!(unix_no_name.to_string(), "aborted with signal 42");
2409
2410        // Windows NT status with message.
2411        let windows = AbortDescription::WindowsNtStatus {
2412            code: -1073741510,
2413            message: Some("CTRL+C exit".into()),
2414        };
2415        assert_eq!(
2416            windows.to_string(),
2417            "aborted with code 0xc000013a: CTRL+C exit"
2418        );
2419
2420        // Windows NT status without message.
2421        let windows_no_msg = AbortDescription::WindowsNtStatus {
2422            code: -1073741510,
2423            message: None,
2424        };
2425        assert_eq!(windows_no_msg.to_string(), "aborted with code 0xc000013a");
2426
2427        // Windows job object.
2428        let job = AbortDescription::WindowsJobObject;
2429        assert_eq!(job.to_string(), "terminated via job object");
2430    }
2431
2432    #[cfg(unix)]
2433    #[test]
2434    fn abort_description_from_abort_status() {
2435        // Test conversion from AbortStatus to AbortDescription on Unix.
2436        let status = AbortStatus::UnixSignal(15);
2437        let description = AbortDescription::from(status);
2438
2439        assert_eq!(
2440            description,
2441            AbortDescription::UnixSignal {
2442                signal: 15,
2443                name: Some("TERM".into()),
2444            }
2445        );
2446
2447        // Unknown signal.
2448        let unknown_status = AbortStatus::UnixSignal(42);
2449        let unknown_description = AbortDescription::from(unknown_status);
2450        assert_eq!(
2451            unknown_description,
2452            AbortDescription::UnixSignal {
2453                signal: 42,
2454                name: None,
2455            }
2456        );
2457    }
2458
2459    #[test]
2460    fn execution_result_description_serialization() {
2461        // Test all variants of ExecutionResultDescription for serialization roundtrips.
2462
2463        // Pass.
2464        let pass = ExecutionResultDescription::Pass;
2465        let json = serde_json::to_string_pretty(&pass).unwrap();
2466        insta::assert_snapshot!("pass", json);
2467        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2468        assert_eq!(pass, roundtrip);
2469
2470        // Leak with pass result.
2471        let leak_pass = ExecutionResultDescription::Leak {
2472            result: LeakTimeoutResult::Pass,
2473        };
2474        let json = serde_json::to_string_pretty(&leak_pass).unwrap();
2475        insta::assert_snapshot!("leak_pass", json);
2476        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2477        assert_eq!(leak_pass, roundtrip);
2478
2479        // Leak with fail result.
2480        let leak_fail = ExecutionResultDescription::Leak {
2481            result: LeakTimeoutResult::Fail,
2482        };
2483        let json = serde_json::to_string_pretty(&leak_fail).unwrap();
2484        insta::assert_snapshot!("leak_fail", json);
2485        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2486        assert_eq!(leak_fail, roundtrip);
2487
2488        // Fail with exit code, no leak.
2489        let fail_exit_code = ExecutionResultDescription::Fail {
2490            failure: FailureDescription::ExitCode { code: 101 },
2491            leaked: false,
2492        };
2493        let json = serde_json::to_string_pretty(&fail_exit_code).unwrap();
2494        insta::assert_snapshot!("fail_exit_code", json);
2495        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2496        assert_eq!(fail_exit_code, roundtrip);
2497
2498        // Fail with exit code and leak.
2499        let fail_exit_code_leaked = ExecutionResultDescription::Fail {
2500            failure: FailureDescription::ExitCode { code: 1 },
2501            leaked: true,
2502        };
2503        let json = serde_json::to_string_pretty(&fail_exit_code_leaked).unwrap();
2504        insta::assert_snapshot!("fail_exit_code_leaked", json);
2505        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2506        assert_eq!(fail_exit_code_leaked, roundtrip);
2507
2508        // Fail with Unix signal abort.
2509        let fail_unix_signal = ExecutionResultDescription::Fail {
2510            failure: FailureDescription::Abort {
2511                abort: AbortDescription::UnixSignal {
2512                    signal: 11,
2513                    name: Some("SEGV".into()),
2514                },
2515            },
2516            leaked: false,
2517        };
2518        let json = serde_json::to_string_pretty(&fail_unix_signal).unwrap();
2519        insta::assert_snapshot!("fail_unix_signal", json);
2520        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2521        assert_eq!(fail_unix_signal, roundtrip);
2522
2523        // Fail with Unix signal abort (no name) and leak.
2524        let fail_unix_signal_unknown = ExecutionResultDescription::Fail {
2525            failure: FailureDescription::Abort {
2526                abort: AbortDescription::UnixSignal {
2527                    signal: 42,
2528                    name: None,
2529                },
2530            },
2531            leaked: true,
2532        };
2533        let json = serde_json::to_string_pretty(&fail_unix_signal_unknown).unwrap();
2534        insta::assert_snapshot!("fail_unix_signal_unknown_leaked", json);
2535        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2536        assert_eq!(fail_unix_signal_unknown, roundtrip);
2537
2538        // Fail with Windows NT status abort.
2539        let fail_windows_nt = ExecutionResultDescription::Fail {
2540            failure: FailureDescription::Abort {
2541                abort: AbortDescription::WindowsNtStatus {
2542                    code: -1073741510,
2543                    message: Some("The application terminated as a result of a CTRL+C.".into()),
2544                },
2545            },
2546            leaked: false,
2547        };
2548        let json = serde_json::to_string_pretty(&fail_windows_nt).unwrap();
2549        insta::assert_snapshot!("fail_windows_nt_status", json);
2550        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2551        assert_eq!(fail_windows_nt, roundtrip);
2552
2553        // Fail with Windows NT status abort (no message).
2554        let fail_windows_nt_no_msg = ExecutionResultDescription::Fail {
2555            failure: FailureDescription::Abort {
2556                abort: AbortDescription::WindowsNtStatus {
2557                    code: -1073741819,
2558                    message: None,
2559                },
2560            },
2561            leaked: false,
2562        };
2563        let json = serde_json::to_string_pretty(&fail_windows_nt_no_msg).unwrap();
2564        insta::assert_snapshot!("fail_windows_nt_status_no_message", json);
2565        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2566        assert_eq!(fail_windows_nt_no_msg, roundtrip);
2567
2568        // Fail with Windows job object abort.
2569        let fail_job_object = ExecutionResultDescription::Fail {
2570            failure: FailureDescription::Abort {
2571                abort: AbortDescription::WindowsJobObject,
2572            },
2573            leaked: false,
2574        };
2575        let json = serde_json::to_string_pretty(&fail_job_object).unwrap();
2576        insta::assert_snapshot!("fail_windows_job_object", json);
2577        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2578        assert_eq!(fail_job_object, roundtrip);
2579
2580        // ExecFail.
2581        let exec_fail = ExecutionResultDescription::ExecFail;
2582        let json = serde_json::to_string_pretty(&exec_fail).unwrap();
2583        insta::assert_snapshot!("exec_fail", json);
2584        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2585        assert_eq!(exec_fail, roundtrip);
2586
2587        // Timeout with pass result.
2588        let timeout_pass = ExecutionResultDescription::Timeout {
2589            result: SlowTimeoutResult::Pass,
2590        };
2591        let json = serde_json::to_string_pretty(&timeout_pass).unwrap();
2592        insta::assert_snapshot!("timeout_pass", json);
2593        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2594        assert_eq!(timeout_pass, roundtrip);
2595
2596        // Timeout with fail result.
2597        let timeout_fail = ExecutionResultDescription::Timeout {
2598            result: SlowTimeoutResult::Fail,
2599        };
2600        let json = serde_json::to_string_pretty(&timeout_fail).unwrap();
2601        insta::assert_snapshot!("timeout_fail", json);
2602        let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
2603        assert_eq!(timeout_fail, roundtrip);
2604    }
2605}