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