Skip to main content

nextest_runner/reporter/
events.rs

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