nextest_runner/reporter/
events.rs

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