nextest_runner/record/
rerun.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Rerun support for nextest.
5//!
6//! This module provides types and functions for rerunning tests that failed or
7//! didn't complete in a previous recorded run.
8
9use crate::{
10    errors::RecordReadError,
11    list::OwnedTestInstanceId,
12    record::{
13        CoreEventKind, OutputEventKind, RecordReader, TestEventKindSummary,
14        format::{RerunInfo, RerunRootInfo, RerunTestSuiteInfo},
15    },
16};
17use iddqd::IdOrdMap;
18use nextest_metadata::{
19    FilterMatch, MismatchReason, RustBinaryId, RustTestSuiteStatusSummary, TestCaseName,
20    TestListSummary,
21};
22use quick_junit::ReportUuid;
23use std::collections::{BTreeSet, HashMap};
24
25/// Trait abstracting over test list access for rerun computation.
26///
27/// This allows the same logic to work with both the real [`TestListSummary`]
28/// and a simplified model for property-based testing.
29pub(crate) trait TestListInfo {
30    /// Iterator type for binaries.
31    type BinaryIter<'a>: Iterator<Item = (&'a RustBinaryId, BinaryInfo<'a>)>
32    where
33        Self: 'a;
34
35    /// Returns an iterator over all binaries in the test list.
36    fn binaries(&self) -> Self::BinaryIter<'_>;
37}
38
39/// Information about a single binary in the test list.
40pub(crate) enum BinaryInfo<'a> {
41    /// Binary was listed; contains test cases.
42    Listed {
43        /// Iterator over test cases: (name, filter match).
44        test_cases: Box<dyn Iterator<Item = (&'a TestCaseName, FilterMatch)> + 'a>,
45    },
46    /// Binary was skipped (not listed).
47    Skipped,
48}
49
50impl TestListInfo for TestListSummary {
51    type BinaryIter<'a> = TestListSummaryBinaryIter<'a>;
52
53    fn binaries(&self) -> Self::BinaryIter<'_> {
54        TestListSummaryBinaryIter {
55            inner: self.rust_suites.iter(),
56        }
57    }
58}
59
60/// Iterator over binaries in a [`TestListSummary`].
61pub(crate) struct TestListSummaryBinaryIter<'a> {
62    inner:
63        std::collections::btree_map::Iter<'a, RustBinaryId, nextest_metadata::RustTestSuiteSummary>,
64}
65
66impl<'a> Iterator for TestListSummaryBinaryIter<'a> {
67    type Item = (&'a RustBinaryId, BinaryInfo<'a>);
68
69    fn next(&mut self) -> Option<Self::Item> {
70        self.inner.next().map(|(binary_id, suite)| {
71            let info = if suite.status == RustTestSuiteStatusSummary::LISTED {
72                BinaryInfo::Listed {
73                    test_cases: Box::new(
74                        suite
75                            .test_cases
76                            .iter()
77                            .map(|(name, tc)| (name, tc.filter_match)),
78                    ),
79                }
80            } else {
81                BinaryInfo::Skipped
82            };
83            (binary_id, info)
84        })
85    }
86}
87
88/// Pure computation of outstanding tests.
89pub(crate) fn compute_outstanding_pure(
90    prev_info: Option<&IdOrdMap<RerunTestSuiteInfo>>,
91    test_list: &impl TestListInfo,
92    outcomes: &HashMap<OwnedTestInstanceId, TestOutcome>,
93) -> IdOrdMap<RerunTestSuiteInfo> {
94    let mut new_outstanding = IdOrdMap::new();
95
96    // Track which binaries were in the test list (listed or skipped) so we can
97    // distinguish between "binary is in test list but has no tests to track"
98    // vs "binary is not in test list at all".
99    let mut binaries_in_test_list = BTreeSet::new();
100
101    for (binary_id, binary_info) in test_list.binaries() {
102        binaries_in_test_list.insert(binary_id.clone());
103
104        match binary_info {
105            BinaryInfo::Listed { test_cases } => {
106                // The binary was listed, so we can rely on the set of test cases
107                // produced by it.
108                let prev = prev_info.and_then(|p| p.get(binary_id));
109
110                let mut curr = RerunTestSuiteInfo::new(binary_id.clone());
111                for (test_name, filter_match) in test_cases {
112                    match filter_match {
113                        FilterMatch::Matches => {
114                            // This test should have been run.
115                            let key = OwnedTestInstanceId {
116                                binary_id: binary_id.clone(),
117                                test_name: test_name.clone(),
118                            };
119                            match outcomes.get(&key) {
120                                Some(TestOutcome::Passed) => {
121                                    // This test passed.
122                                    curr.passing.insert(test_name.clone());
123                                }
124                                Some(TestOutcome::Failed) => {
125                                    // This test failed, and so is outstanding.
126                                    curr.outstanding.insert(test_name.clone());
127                                }
128                                Some(TestOutcome::Skipped(skipped)) => {
129                                    // This is strange! FilterMatch::Matches means
130                                    // the test should not be skipped. But compute
131                                    // this anyway.
132                                    handle_skipped(test_name, *skipped, prev, &mut curr);
133                                }
134                                None => {
135                                    // The test was scheduled, but was not seen in
136                                    // the event log. It must be re-run.
137                                    curr.outstanding.insert(test_name.clone());
138                                }
139                            }
140                        }
141                        FilterMatch::Mismatch { reason } => {
142                            handle_skipped(
143                                test_name,
144                                TestOutcomeSkipped::from_mismatch_reason(reason),
145                                prev,
146                                &mut curr,
147                            );
148                        }
149                    }
150                }
151
152                // Any outstanding tests that were not accounted for in the
153                // loop above should be carried forward, since we're still
154                // tracking them.
155                if let Some(prev) = prev {
156                    for t in &prev.outstanding {
157                        if !curr.passing.contains(t) && !curr.outstanding.contains(t) {
158                            curr.outstanding.insert(t.clone());
159                        }
160                    }
161                }
162
163                // What about tests that were originally passing, and now not
164                // present? We want to treat them as implicitly outstanding (not
165                // actively tracking, but if they show up again we'll want to
166                // re-run them).
167
168                // Only insert if there are tests to track.
169                if !curr.passing.is_empty() || !curr.outstanding.is_empty() {
170                    new_outstanding
171                        .insert_unique(curr)
172                        .expect("binaries iterator should not yield duplicates");
173                }
174            }
175            BinaryInfo::Skipped => {
176                // The suite was not listed.
177                //
178                // If this is an original run, then there's not much we can do. (If
179                // the subsequent rerun causes a test to be included, it will be run
180                // by dint of not being in the passing set.)
181                //
182                // If this is a rerun, then we should carry forward the cached list
183                // of passing tests for this binary. The next time the binary is
184                // seen, we'll reuse the serialized cached list.
185                if let Some(prev_outstanding) = prev_info
186                    && let Some(outstanding) = prev_outstanding.get(binary_id)
187                {
188                    // We know the set of outstanding tests.
189                    new_outstanding
190                        .insert_unique(outstanding.clone())
191                        .expect("binaries iterator should not yield duplicates");
192                }
193                // Else: An interesting case -- the test suite was discovered but
194                // not listed, and also was not known. Not much we can do
195                // here for now, but maybe we want to track this explicitly
196                // in the future?
197            }
198        }
199    }
200
201    // Carry forward binaries from previous run that are not in the current test
202    // list at all (neither listed nor skipped).
203    if let Some(prev) = prev_info {
204        for prev_suite in prev.iter() {
205            if !binaries_in_test_list.contains(&prev_suite.binary_id) {
206                new_outstanding
207                    .insert_unique(prev_suite.clone())
208                    .expect("binary not in test list, so this should succeed");
209            }
210        }
211    }
212
213    new_outstanding
214}
215
216/// Result of computing outstanding and passing tests from a recorded run.
217#[derive(Clone, Debug)]
218pub struct ComputedRerunInfo {
219    /// The set of tests that are outstanding.
220    ///
221    /// This set is serialized into `rerun-info.json`.
222    pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
223}
224
225impl ComputedRerunInfo {
226    /// Returns the set of all outstanding test instance IDs.
227    ///
228    /// This is used to track which tests were expected to run in a rerun.
229    pub fn expected_test_ids(&self) -> BTreeSet<OwnedTestInstanceId> {
230        self.test_suites
231            .iter()
232            .flat_map(|suite| {
233                suite.outstanding.iter().map(|name| OwnedTestInstanceId {
234                    binary_id: suite.binary_id.clone(),
235                    test_name: name.clone(),
236                })
237            })
238            .collect()
239    }
240
241    /// Computes outstanding tests from a recorded run.
242    ///
243    /// If this is a rerun chain, also returns information about the root of the
244    /// chain.
245    pub fn compute(
246        reader: &mut RecordReader,
247    ) -> Result<(Self, Option<RerunRootInfo>), RecordReadError> {
248        let rerun_info = reader.read_rerun_info()?;
249        let test_list = reader.read_test_list()?;
250        let outcomes = TestEventOutcomes::collect(reader)?;
251
252        let prev_test_suites = rerun_info.as_ref().map(|info| &info.test_suites);
253        let new_test_suites =
254            compute_outstanding_pure(prev_test_suites, &test_list, &outcomes.outcomes);
255
256        let root_info = rerun_info.map(|info| info.root_info);
257
258        Ok((
259            Self {
260                test_suites: new_test_suites,
261            },
262            root_info,
263        ))
264    }
265
266    /// Consumes self, converting to a [`RerunInfo`] for storage.
267    pub fn into_rerun_info(self, parent_run_id: ReportUuid, root_info: RerunRootInfo) -> RerunInfo {
268        RerunInfo {
269            parent_run_id,
270            root_info,
271            test_suites: self.test_suites,
272        }
273    }
274}
275
276fn handle_skipped(
277    test_name: &TestCaseName,
278    skipped: TestOutcomeSkipped,
279    prev: Option<&RerunTestSuiteInfo>,
280    curr: &mut RerunTestSuiteInfo,
281) {
282    match skipped {
283        TestOutcomeSkipped::Rerun => {
284            // This test was skipped due to having passed in a prior run in this
285            // rerun chain. Add it to passing.
286            //
287            // Note that if a test goes from passing to not being present in the
288            // list at all, and then back to being present, it becomes
289            // outstanding. This is deliberate.
290            curr.passing.insert(test_name.clone());
291        }
292        TestOutcomeSkipped::Explicit => {
293            // If a test is explicitly skipped, the behavior depends on whether
294            // this is the rerun of an initial run or part of a rerun chain.
295            //
296            // If this is a rerun of an initial run, then it doesn't make sense
297            // to add the test to the outstanding list, because the user
298            // explicitly skipped it.
299            //
300            // If this is a rerun chain, then whether it is still outstanding
301            // depends on whether it was originally outstanding. If it was
302            // originally outstanding, then that should be carried forward. If
303            // it was originally passing, we should assume that that hasn't
304            // changed and it is still passing. If neither, then it's not part
305            // of the set of tests we care about.
306            if let Some(prev) = prev {
307                if prev.outstanding.contains(test_name) {
308                    curr.outstanding.insert(test_name.clone());
309                } else if prev.passing.contains(test_name) {
310                    curr.passing.insert(test_name.clone());
311                }
312            } else {
313                // This is either not a rerun chain, or it is a rerun chain and
314                // this binary has never been seen before.
315            }
316        }
317    }
318}
319
320/// Reason why a test was skipped.
321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub(crate) enum TestOutcomeSkipped {
323    /// Test was explicitly skipped by the user.
324    Explicit,
325
326    /// Test was skipped due to this being a rerun.
327    Rerun,
328}
329
330impl TestOutcomeSkipped {
331    /// Computes the skipped reason from a `MismatchReason`.
332    fn from_mismatch_reason(reason: MismatchReason) -> Self {
333        match reason {
334            MismatchReason::NotBenchmark
335            | MismatchReason::Ignored
336            | MismatchReason::String
337            | MismatchReason::Expression
338            | MismatchReason::Partition
339            | MismatchReason::DefaultFilter => TestOutcomeSkipped::Explicit,
340            MismatchReason::RerunAlreadyPassed => TestOutcomeSkipped::Rerun,
341            other => unreachable!("all known match arms are covered, found {other:?}"),
342        }
343    }
344}
345
346/// Outcome of a single test from a run's event log.
347#[derive(Clone, Copy, Debug, PartialEq, Eq)]
348pub(crate) enum TestOutcome {
349    /// Test passed (had a successful `TestFinished` event).
350    Passed,
351
352    /// Test was skipped.
353    Skipped(TestOutcomeSkipped),
354
355    /// Test failed (had a `TestFinished` event but did not pass).
356    Failed,
357}
358
359/// Outcomes extracted from a run's event log.
360///
361/// This is used for computing outstanding and passing tests.
362#[derive(Clone, Debug)]
363struct TestEventOutcomes {
364    /// Map from test instance to its outcome.
365    outcomes: HashMap<OwnedTestInstanceId, TestOutcome>,
366}
367
368impl TestEventOutcomes {
369    /// Collects test outcomes from the event log.
370    ///
371    /// Returns information about which tests passed and which tests were seen
372    /// (had any event: started, finished, or skipped).
373    fn collect(reader: &mut RecordReader) -> Result<Self, RecordReadError> {
374        reader.load_dictionaries()?;
375
376        let events: Vec<_> = reader.events()?.collect::<Result<Vec<_>, _>>()?;
377        let outcomes = collect_from_events(events.iter().map(|e| &e.kind));
378
379        Ok(Self { outcomes })
380    }
381}
382
383/// Collects test outcomes from an iterator of events.
384///
385/// This helper exists to make the event processing logic testable without
386/// requiring a full `RecordReader`.
387fn collect_from_events<'a, O>(
388    events: impl Iterator<Item = &'a TestEventKindSummary<O>>,
389) -> HashMap<OwnedTestInstanceId, TestOutcome>
390where
391    O: 'a,
392{
393    let mut outcomes = HashMap::new();
394
395    for kind in events {
396        match kind {
397            TestEventKindSummary::Output(OutputEventKind::TestFinished {
398                test_instance,
399                run_statuses,
400                ..
401            }) => {
402                // Determine outcome for this iteration/finish event.
403                let outcome = if run_statuses.last_status().result.is_success() {
404                    TestOutcome::Passed
405                } else {
406                    TestOutcome::Failed
407                };
408
409                // For stress runs: multiple TestFinished events occur for the
410                // same test_instance (one per stress iteration). The overall
411                // outcome is Failed if any iteration failed.
412                //
413                // We use entry() to only "upgrade" from Passed to Failed, never
414                // downgrade. This ensures [Pass, Fail, Pass] → Failed.
415                outcomes
416                    .entry(test_instance.clone())
417                    .and_modify(|existing| {
418                        if outcome == TestOutcome::Failed {
419                            *existing = TestOutcome::Failed;
420                        }
421                    })
422                    .or_insert(outcome);
423            }
424            TestEventKindSummary::Core(CoreEventKind::TestSkipped {
425                test_instance,
426                reason,
427                ..
428            }) => {
429                let skipped_reason = TestOutcomeSkipped::from_mismatch_reason(*reason);
430                outcomes.insert(test_instance.clone(), TestOutcome::Skipped(skipped_reason));
431            }
432            _ => {}
433        }
434    }
435
436    outcomes
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use crate::{
443        record::{OutputEventKind, StressIndexSummary, TestEventKindSummary},
444        reporter::{
445            TestOutputDisplay,
446            events::{
447                ChildExecutionOutputDescription, ChildOutputDescription, ExecuteStatus,
448                ExecutionResultDescription, ExecutionStatuses, FailureDescription, RetryData,
449                RunStats,
450            },
451        },
452    };
453    use chrono::Utc;
454    use proptest::prelude::*;
455    use std::{
456        collections::{BTreeMap, btree_map},
457        num::NonZero,
458        sync::OnceLock,
459        time::Duration,
460    };
461    use test_strategy::proptest;
462
463    // ---
464    // Tests
465    // ---
466
467    /// Main property: the SUT matches the oracle.
468    #[proptest(cases = 200)]
469    fn sut_matches_oracle(#[strategy(arb_rerun_model())] model: RerunModel) {
470        let expected = model.compute_rerun_info_decision_table();
471        let actual = run_sut(&model);
472        prop_assert_eq!(actual, expected);
473    }
474
475    /// Property: passing and outstanding are always disjoint.
476    #[proptest(cases = 200)]
477    fn passing_and_outstanding_disjoint(#[strategy(arb_rerun_model())] model: RerunModel) {
478        let result = run_sut(&model);
479        for suite in result.iter() {
480            let intersection: BTreeSet<_> =
481                suite.passing.intersection(&suite.outstanding).collect();
482            prop_assert!(
483                intersection.is_empty(),
484                "passing and outstanding should be disjoint for {}: {:?}",
485                suite.binary_id,
486                intersection
487            );
488        }
489    }
490
491    /// Property: every matching test with a definitive outcome ends up in either
492    /// passing or outstanding.
493    ///
494    /// Tests that are explicitly skipped (with no prior tracking history) are
495    /// not tracked, so they may not be in either set.
496    #[proptest(cases = 200)]
497    fn matching_tests_with_outcomes_are_tracked(#[strategy(arb_rerun_model())] model: RerunModel) {
498        let result = run_sut(&model);
499
500        // Check final state against final test list.
501        let final_step = model.reruns.last().unwrap_or(&model.initial);
502
503        for (binary_id, binary_model) in &final_step.test_list.binaries {
504            if let BinaryModel::Listed { tests } = binary_model {
505                let rust_binary_id = binary_id.rust_binary_id();
506
507                for (test_name, filter_match) in tests {
508                    if matches!(filter_match, FilterMatch::Matches) {
509                        let key = (*binary_id, *test_name);
510                        let outcome = final_step.outcomes.get(&key);
511
512                        // Tests with Passed/Failed/Skipped(Rerun) or no outcome
513                        // (not seen) should be tracked. Tests with
514                        // Skipped(Explicit) might not be tracked if there's no
515                        // prior history.
516                        let should_be_tracked = match outcome {
517                            Some(TestOutcome::Passed)
518                            | Some(TestOutcome::Failed)
519                            | Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun))
520                            | None => true,
521                            Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => false,
522                        };
523
524                        if should_be_tracked {
525                            let tcn = test_name.test_case_name();
526                            let suite = result.get(&rust_binary_id);
527                            let in_passing = suite.is_some_and(|s| s.passing.contains(tcn));
528                            let in_outstanding = suite.is_some_and(|s| s.outstanding.contains(tcn));
529                            prop_assert!(
530                                in_passing || in_outstanding,
531                                "matching test {:?}::{:?} with outcome {:?} should be in passing or outstanding",
532                                binary_id,
533                                test_name,
534                                outcome
535                            );
536                        }
537                    }
538                }
539            }
540        }
541    }
542
543    /// Test the decision table function directly with all combinations.
544    #[test]
545    fn decide_test_outcome_truth_table() {
546        use Decision as D;
547        use FilterMatchResult as F;
548        use PrevStatus as P;
549
550        // Binary not present: carry forward previous status.
551        assert_eq!(
552            decide_test_outcome(P::Passing, F::BinaryNotPresent, None),
553            D::Passing
554        );
555        assert_eq!(
556            decide_test_outcome(P::Outstanding, F::BinaryNotPresent, None),
557            D::Outstanding
558        );
559        assert_eq!(
560            decide_test_outcome(P::Unknown, F::BinaryNotPresent, None),
561            D::NotTracked
562        );
563
564        // Binary skipped: carry forward previous status.
565        assert_eq!(
566            decide_test_outcome(P::Passing, F::BinarySkipped, None),
567            D::Passing
568        );
569        assert_eq!(
570            decide_test_outcome(P::Outstanding, F::BinarySkipped, None),
571            D::Outstanding
572        );
573        assert_eq!(
574            decide_test_outcome(P::Unknown, F::BinarySkipped, None),
575            D::NotTracked
576        );
577
578        // Test not in list: only carry forward outstanding.
579        assert_eq!(
580            decide_test_outcome(P::Passing, F::TestNotInList, None),
581            D::NotTracked
582        );
583        assert_eq!(
584            decide_test_outcome(P::Outstanding, F::TestNotInList, None),
585            D::Outstanding
586        );
587        assert_eq!(
588            decide_test_outcome(P::Unknown, F::TestNotInList, None),
589            D::NotTracked
590        );
591
592        // FilterMatch::Matches with various outcomes.
593        let matches = F::HasMatch(FilterMatch::Matches);
594
595        // Passed -> Passing.
596        assert_eq!(
597            decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Passed)),
598            D::Passing
599        );
600        assert_eq!(
601            decide_test_outcome(P::Passing, matches, Some(TestOutcome::Passed)),
602            D::Passing
603        );
604        assert_eq!(
605            decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Passed)),
606            D::Passing
607        );
608
609        // Failed -> Outstanding.
610        assert_eq!(
611            decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Failed)),
612            D::Outstanding
613        );
614        assert_eq!(
615            decide_test_outcome(P::Passing, matches, Some(TestOutcome::Failed)),
616            D::Outstanding
617        );
618        assert_eq!(
619            decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Failed)),
620            D::Outstanding
621        );
622
623        // Not seen (None outcome) -> Outstanding.
624        assert_eq!(
625            decide_test_outcome(P::Unknown, matches, None),
626            D::Outstanding
627        );
628        assert_eq!(
629            decide_test_outcome(P::Passing, matches, None),
630            D::Outstanding
631        );
632        assert_eq!(
633            decide_test_outcome(P::Outstanding, matches, None),
634            D::Outstanding
635        );
636
637        // Skipped(Rerun) -> Passing.
638        let rerun_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun));
639        assert_eq!(
640            decide_test_outcome(P::Unknown, matches, rerun_skipped),
641            D::Passing
642        );
643        assert_eq!(
644            decide_test_outcome(P::Passing, matches, rerun_skipped),
645            D::Passing
646        );
647        assert_eq!(
648            decide_test_outcome(P::Outstanding, matches, rerun_skipped),
649            D::Passing
650        );
651
652        // Skipped(Explicit) -> carry forward.
653        let explicit_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit));
654        assert_eq!(
655            decide_test_outcome(P::Unknown, matches, explicit_skipped),
656            D::NotTracked
657        );
658        assert_eq!(
659            decide_test_outcome(P::Passing, matches, explicit_skipped),
660            D::Passing
661        );
662        assert_eq!(
663            decide_test_outcome(P::Outstanding, matches, explicit_skipped),
664            D::Outstanding
665        );
666
667        // FilterMatch::Mismatch with RerunAlreadyPassed -> Passing.
668        let rerun_mismatch = F::HasMatch(FilterMatch::Mismatch {
669            reason: MismatchReason::RerunAlreadyPassed,
670        });
671        assert_eq!(
672            decide_test_outcome(P::Unknown, rerun_mismatch, None),
673            D::Passing
674        );
675        assert_eq!(
676            decide_test_outcome(P::Passing, rerun_mismatch, None),
677            D::Passing
678        );
679        assert_eq!(
680            decide_test_outcome(P::Outstanding, rerun_mismatch, None),
681            D::Passing
682        );
683
684        // FilterMatch::Mismatch with other reasons -> carry forward.
685        let explicit_mismatch = F::HasMatch(FilterMatch::Mismatch {
686            reason: MismatchReason::Ignored,
687        });
688        assert_eq!(
689            decide_test_outcome(P::Unknown, explicit_mismatch, None),
690            D::NotTracked
691        );
692        assert_eq!(
693            decide_test_outcome(P::Passing, explicit_mismatch, None),
694            D::Passing
695        );
696        assert_eq!(
697            decide_test_outcome(P::Outstanding, explicit_mismatch, None),
698            D::Outstanding
699        );
700    }
701
702    // ---
703    // Model types
704    // ---
705
706    /// A fixed universe of binary IDs for testing.
707    ///
708    /// Using a small, fixed set ensures meaningful interactions between reruns.
709    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
710    enum ModelBinaryId {
711        A,
712        B,
713        C,
714        D,
715    }
716
717    impl ModelBinaryId {
718        fn rust_binary_id(self) -> &'static RustBinaryId {
719            match self {
720                Self::A => {
721                    static ID: OnceLock<RustBinaryId> = OnceLock::new();
722                    ID.get_or_init(|| RustBinaryId::new("binary-a"))
723                }
724                Self::B => {
725                    static ID: OnceLock<RustBinaryId> = OnceLock::new();
726                    ID.get_or_init(|| RustBinaryId::new("binary-b"))
727                }
728                Self::C => {
729                    static ID: OnceLock<RustBinaryId> = OnceLock::new();
730                    ID.get_or_init(|| RustBinaryId::new("binary-c"))
731                }
732                Self::D => {
733                    static ID: OnceLock<RustBinaryId> = OnceLock::new();
734                    ID.get_or_init(|| RustBinaryId::new("binary-d"))
735                }
736            }
737        }
738    }
739
740    /// A fixed universe of test names for testing.
741    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
742    enum ModelTestName {
743        Test1,
744        Test2,
745        Test3,
746        Test4,
747        Test5,
748    }
749
750    impl ModelTestName {
751        fn test_case_name(self) -> &'static TestCaseName {
752            match self {
753                Self::Test1 => {
754                    static NAME: OnceLock<TestCaseName> = OnceLock::new();
755                    NAME.get_or_init(|| TestCaseName::new("test_1"))
756                }
757                Self::Test2 => {
758                    static NAME: OnceLock<TestCaseName> = OnceLock::new();
759                    NAME.get_or_init(|| TestCaseName::new("test_2"))
760                }
761                Self::Test3 => {
762                    static NAME: OnceLock<TestCaseName> = OnceLock::new();
763                    NAME.get_or_init(|| TestCaseName::new("test_3"))
764                }
765                Self::Test4 => {
766                    static NAME: OnceLock<TestCaseName> = OnceLock::new();
767                    NAME.get_or_init(|| TestCaseName::new("test_4"))
768                }
769                Self::Test5 => {
770                    static NAME: OnceLock<TestCaseName> = OnceLock::new();
771                    NAME.get_or_init(|| TestCaseName::new("test_5"))
772                }
773            }
774        }
775    }
776
777    /// Model of a binary's state.
778    #[derive(Clone, Debug)]
779    enum BinaryModel {
780        /// Binary was listed; contains test cases with their filter match.
781        Listed {
782            tests: BTreeMap<ModelTestName, FilterMatch>,
783        },
784        /// Binary was skipped, so it cannot have tests.
785        Skipped,
786    }
787
788    /// Test list state for one run.
789    #[derive(Clone, Debug)]
790    struct TestListModel {
791        binaries: BTreeMap<ModelBinaryId, BinaryModel>,
792    }
793
794    /// A single run (initial or rerun).
795    #[derive(Clone, Debug)]
796    struct RunStep {
797        /// The test list state for this run.
798        test_list: TestListModel,
799        /// Outcomes for tests that ran.
800        outcomes: BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
801    }
802
803    /// The complete model: initial run + subsequent reruns.
804    #[derive(Clone, Debug)]
805    struct RerunModel {
806        /// The initial run.
807        initial: RunStep,
808        /// The sequence of reruns.
809        reruns: Vec<RunStep>,
810    }
811
812    impl TestListInfo for TestListModel {
813        type BinaryIter<'a> = TestListModelBinaryIter<'a>;
814
815        fn binaries(&self) -> Self::BinaryIter<'_> {
816            TestListModelBinaryIter {
817                inner: self.binaries.iter(),
818            }
819        }
820    }
821
822    /// Iterator over binaries in a [`TestListModel`].
823    struct TestListModelBinaryIter<'a> {
824        inner: btree_map::Iter<'a, ModelBinaryId, BinaryModel>,
825    }
826
827    impl<'a> Iterator for TestListModelBinaryIter<'a> {
828        type Item = (&'a RustBinaryId, BinaryInfo<'a>);
829
830        fn next(&mut self) -> Option<Self::Item> {
831            self.inner.next().map(|(model_id, binary_model)| {
832                let rust_id = model_id.rust_binary_id();
833                let info = match binary_model {
834                    BinaryModel::Listed { tests } => BinaryInfo::Listed {
835                        test_cases: Box::new(
836                            tests.iter().map(|(name, fm)| (name.test_case_name(), *fm)),
837                        ),
838                    },
839                    BinaryModel::Skipped => BinaryInfo::Skipped,
840                };
841                (rust_id, info)
842            })
843        }
844    }
845
846    // ---
847    // Generators
848    // ---
849
850    fn arb_model_binary_id() -> impl Strategy<Value = ModelBinaryId> {
851        prop_oneof![
852            Just(ModelBinaryId::A),
853            Just(ModelBinaryId::B),
854            Just(ModelBinaryId::C),
855            Just(ModelBinaryId::D),
856        ]
857    }
858
859    fn arb_model_test_name() -> impl Strategy<Value = ModelTestName> {
860        prop_oneof![
861            Just(ModelTestName::Test1),
862            Just(ModelTestName::Test2),
863            Just(ModelTestName::Test3),
864            Just(ModelTestName::Test4),
865            Just(ModelTestName::Test5),
866        ]
867    }
868
869    fn arb_filter_match() -> impl Strategy<Value = FilterMatch> {
870        prop_oneof![
871            4 => Just(FilterMatch::Matches),
872            1 => any::<MismatchReason>().prop_map(|reason| FilterMatch::Mismatch { reason }),
873        ]
874    }
875
876    fn arb_test_outcome() -> impl Strategy<Value = TestOutcome> {
877        prop_oneof![
878            4 => Just(TestOutcome::Passed),
879            2 => Just(TestOutcome::Failed),
880            1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
881            1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
882        ]
883    }
884
885    fn arb_test_map() -> impl Strategy<Value = BTreeMap<ModelTestName, FilterMatch>> {
886        proptest::collection::btree_map(arb_model_test_name(), arb_filter_match(), 0..5)
887    }
888
889    fn arb_binary_model() -> impl Strategy<Value = BinaryModel> {
890        prop_oneof![
891            8 => arb_test_map().prop_map(|tests| BinaryModel::Listed { tests }),
892            2 => Just(BinaryModel::Skipped),
893        ]
894    }
895
896    fn arb_test_list_model() -> impl Strategy<Value = TestListModel> {
897        proptest::collection::btree_map(arb_model_binary_id(), arb_binary_model(), 0..4)
898            .prop_map(|binaries| TestListModel { binaries })
899    }
900
901    /// Generate outcomes consistent with a test list.
902    ///
903    /// Only generates outcomes for tests that match the filter in listed binaries.
904    /// Takes a list of matching tests to generate outcomes for.
905    fn arb_outcomes_for_matching_tests(
906        matching_tests: Vec<(ModelBinaryId, ModelTestName)>,
907    ) -> BoxedStrategy<BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>> {
908        if matching_tests.is_empty() {
909            Just(BTreeMap::new()).boxed()
910        } else {
911            let len = matching_tests.len();
912            proptest::collection::btree_map(
913                proptest::sample::select(matching_tests),
914                arb_test_outcome(),
915                0..=len,
916            )
917            .boxed()
918        }
919    }
920
921    /// Extract matching tests from a test list model.
922    fn extract_matching_tests(test_list: &TestListModel) -> Vec<(ModelBinaryId, ModelTestName)> {
923        test_list
924            .binaries
925            .iter()
926            .filter_map(|(binary_id, model)| match model {
927                BinaryModel::Listed { tests } => Some(
928                    tests
929                        .iter()
930                        .filter(|(_, fm)| matches!(fm, FilterMatch::Matches))
931                        .map(move |(tn, _)| (*binary_id, *tn)),
932                ),
933                BinaryModel::Skipped => None,
934            })
935            .flatten()
936            .collect()
937    }
938
939    fn arb_run_step() -> impl Strategy<Value = RunStep> {
940        arb_test_list_model().prop_flat_map(|test_list| {
941            let matching_tests = extract_matching_tests(&test_list);
942            arb_outcomes_for_matching_tests(matching_tests).prop_map(move |outcomes| RunStep {
943                test_list: test_list.clone(),
944                outcomes,
945            })
946        })
947    }
948
949    fn arb_rerun_model() -> impl Strategy<Value = RerunModel> {
950        (
951            arb_run_step(),
952            proptest::collection::vec(arb_run_step(), 0..5),
953        )
954            .prop_map(|(initial, reruns)| RerunModel { initial, reruns })
955    }
956
957    // ---
958    // Helper to convert model outcomes to HashMap<OwnedTestInstanceId, TestOutcome>
959    // ---
960
961    fn model_outcomes_to_hashmap(
962        outcomes: &BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
963    ) -> HashMap<OwnedTestInstanceId, TestOutcome> {
964        outcomes
965            .iter()
966            .map(|((binary_id, test_name), outcome)| {
967                let id = OwnedTestInstanceId {
968                    binary_id: binary_id.rust_binary_id().clone(),
969                    test_name: test_name.test_case_name().clone(),
970                };
971                (id, *outcome)
972            })
973            .collect()
974    }
975
976    // ---
977    // Helpers
978    // ---
979
980    /// Runs the SUT through an entire `RerunModel`.
981    fn run_sut(model: &RerunModel) -> IdOrdMap<RerunTestSuiteInfo> {
982        let outcomes = model_outcomes_to_hashmap(&model.initial.outcomes);
983        let mut result = compute_outstanding_pure(None, &model.initial.test_list, &outcomes);
984
985        for rerun in &model.reruns {
986            let outcomes = model_outcomes_to_hashmap(&rerun.outcomes);
987            result = compute_outstanding_pure(Some(&result), &rerun.test_list, &outcomes);
988        }
989
990        result
991    }
992
993    // ---
994    // Oracle: per-test decision table
995    // ---
996    //
997    // The oracle determines each test's fate independently using a decision
998    // table (`decide_test_outcome`). This is verifiable by inspection and
999    // structurally different from the SUT.
1000
1001    /// Status of a test in the previous run.
1002    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1003    enum PrevStatus {
1004        /// Test was in the passing set.
1005        Passing,
1006        /// Test was in the outstanding set.
1007        Outstanding,
1008        /// Test was not tracked (not in either set).
1009        Unknown,
1010    }
1011
1012    /// What to do with this test after applying the decision table.
1013    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1014    enum Decision {
1015        /// Add to the passing set.
1016        Passing,
1017        /// Add to the outstanding set.
1018        Outstanding,
1019        /// Don't track this test.
1020        NotTracked,
1021    }
1022
1023    /// Result of looking up a test's filter match in the current step.
1024    ///
1025    /// This distinguishes between different reasons a filter match might not
1026    /// exist, which affects how the test's state is handled.
1027    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1028    enum FilterMatchResult {
1029        /// Binary is not in the test list at all. Carry forward the entire
1030        /// suite.
1031        BinaryNotPresent,
1032        /// Binary is in the test list but skipped. Carry forward the entire
1033        /// suite.
1034        BinarySkipped,
1035        /// Binary is listed but this test is not in its test map. Only carry
1036        /// forward outstanding tests; passing tests become untracked.
1037        TestNotInList,
1038        /// Test has a filter match.
1039        HasMatch(FilterMatch),
1040    }
1041
1042    /// Pure decision table for a single test.
1043    ///
1044    /// This is the core logic expressed as a truth table, making it easy to verify
1045    /// by inspection that each case is handled correctly.
1046    fn decide_test_outcome(
1047        prev: PrevStatus,
1048        filter_result: FilterMatchResult,
1049        outcome: Option<TestOutcome>,
1050    ) -> Decision {
1051        match filter_result {
1052            FilterMatchResult::BinaryNotPresent | FilterMatchResult::BinarySkipped => {
1053                // Binary not present or skipped: carry forward previous status.
1054                match prev {
1055                    PrevStatus::Passing => Decision::Passing,
1056                    PrevStatus::Outstanding => Decision::Outstanding,
1057                    PrevStatus::Unknown => Decision::NotTracked,
1058                }
1059            }
1060            FilterMatchResult::TestNotInList => {
1061                // Test is not in the current test list of a listed binary.
1062                // Only carry forward outstanding tests. Passing tests that
1063                // disappear from the list become untracked (and will be re-run
1064                // if they reappear).
1065                match prev {
1066                    PrevStatus::Outstanding => Decision::Outstanding,
1067                    PrevStatus::Passing | PrevStatus::Unknown => Decision::NotTracked,
1068                }
1069            }
1070            FilterMatchResult::HasMatch(FilterMatch::Matches) => {
1071                match outcome {
1072                    Some(TestOutcome::Passed) => Decision::Passing,
1073                    Some(TestOutcome::Failed) => Decision::Outstanding,
1074                    None => {
1075                        // Test was scheduled but not seen in event log: outstanding.
1076                        Decision::Outstanding
1077                    }
1078                    Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)) => Decision::Passing,
1079                    Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => {
1080                        // Carry forward, or not tracked if unknown.
1081                        match prev {
1082                            PrevStatus::Passing => Decision::Passing,
1083                            PrevStatus::Outstanding => Decision::Outstanding,
1084                            PrevStatus::Unknown => Decision::NotTracked,
1085                        }
1086                    }
1087                }
1088            }
1089            FilterMatchResult::HasMatch(FilterMatch::Mismatch { reason }) => {
1090                match TestOutcomeSkipped::from_mismatch_reason(reason) {
1091                    TestOutcomeSkipped::Rerun => Decision::Passing,
1092                    TestOutcomeSkipped::Explicit => {
1093                        // Carry forward, or not tracked if unknown.
1094                        match prev {
1095                            PrevStatus::Passing => Decision::Passing,
1096                            PrevStatus::Outstanding => Decision::Outstanding,
1097                            PrevStatus::Unknown => Decision::NotTracked,
1098                        }
1099                    }
1100                }
1101            }
1102        }
1103    }
1104
1105    impl RerunModel {
1106        /// Per-test decision table oracle.
1107        ///
1108        /// This is structurally different from the main oracle: instead of iterating
1109        /// through binaries and updating state imperatively, it determines each
1110        /// test's fate independently using a truth table.
1111        fn compute_rerun_info_decision_table(&self) -> IdOrdMap<RerunTestSuiteInfo> {
1112            // Compute all previous states by running through the chain.
1113            let mut prev_state: HashMap<(ModelBinaryId, ModelTestName), PrevStatus> =
1114                HashMap::new();
1115
1116            // Process initial run.
1117            self.update_state_from_step(&mut prev_state, &self.initial);
1118
1119            // Process reruns.
1120            for rerun in &self.reruns {
1121                self.update_state_from_step(&mut prev_state, rerun);
1122            }
1123
1124            // Convert final state to result.
1125            self.collect_final_state(&prev_state)
1126        }
1127
1128        fn update_state_from_step(
1129            &self,
1130            state: &mut HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1131            step: &RunStep,
1132        ) {
1133            // Enumerate all tests we need to consider:
1134            // - Tests in the current test list
1135            // - Tests from previous state (for carry-forward)
1136            let all_tests = self.enumerate_all_tests(state, step);
1137
1138            for (binary_id, test_name) in all_tests {
1139                let prev = state
1140                    .get(&(binary_id, test_name))
1141                    .copied()
1142                    .unwrap_or(PrevStatus::Unknown);
1143
1144                let filter_result = self.get_filter_match_result(step, binary_id, test_name);
1145                let outcome = step.outcomes.get(&(binary_id, test_name)).copied();
1146
1147                let decision = decide_test_outcome(prev, filter_result, outcome);
1148
1149                // Update state based on decision.
1150                match decision {
1151                    Decision::Passing => {
1152                        state.insert((binary_id, test_name), PrevStatus::Passing);
1153                    }
1154                    Decision::Outstanding => {
1155                        state.insert((binary_id, test_name), PrevStatus::Outstanding);
1156                    }
1157                    Decision::NotTracked => {
1158                        state.remove(&(binary_id, test_name));
1159                    }
1160                }
1161            }
1162        }
1163
1164        /// Gets the filter match result for a test in a step.
1165        ///
1166        /// Returns a `FilterMatchResult` indicating why the filter match is
1167        /// present or absent.
1168        fn get_filter_match_result(
1169            &self,
1170            step: &RunStep,
1171            binary_id: ModelBinaryId,
1172            test_name: ModelTestName,
1173        ) -> FilterMatchResult {
1174            match step.test_list.binaries.get(&binary_id) {
1175                None => FilterMatchResult::BinaryNotPresent,
1176                Some(BinaryModel::Skipped) => FilterMatchResult::BinarySkipped,
1177                Some(BinaryModel::Listed { tests }) => match tests.get(&test_name) {
1178                    Some(filter_match) => FilterMatchResult::HasMatch(*filter_match),
1179                    None => FilterMatchResult::TestNotInList,
1180                },
1181            }
1182        }
1183
1184        /// Enumerates all tests that need to be considered for a step.
1185        ///
1186        /// This includes tests from the current test list and tests from the
1187        /// previous state (for carry-forward).
1188        fn enumerate_all_tests(
1189            &self,
1190            prev_state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1191            step: &RunStep,
1192        ) -> BTreeSet<(ModelBinaryId, ModelTestName)> {
1193            let mut tests = BTreeSet::new();
1194
1195            // Tests from current test list.
1196            for (binary_id, binary_model) in &step.test_list.binaries {
1197                if let BinaryModel::Listed { tests: test_map } = binary_model {
1198                    for test_name in test_map.keys() {
1199                        tests.insert((*binary_id, *test_name));
1200                    }
1201                }
1202            }
1203
1204            // Tests from previous state (for carry-forward).
1205            for (binary_id, test_name) in prev_state.keys() {
1206                tests.insert((*binary_id, *test_name));
1207            }
1208
1209            tests
1210        }
1211
1212        /// Converts the final state to an `IdOrdMap<TestSuiteOutstanding>`.
1213        fn collect_final_state(
1214            &self,
1215            state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1216        ) -> IdOrdMap<RerunTestSuiteInfo> {
1217            let mut result: BTreeMap<ModelBinaryId, RerunTestSuiteInfo> = BTreeMap::new();
1218
1219            for ((binary_id, test_name), status) in state {
1220                let suite = result
1221                    .entry(*binary_id)
1222                    .or_insert_with(|| RerunTestSuiteInfo::new(binary_id.rust_binary_id().clone()));
1223
1224                match status {
1225                    PrevStatus::Passing => {
1226                        suite.passing.insert(test_name.test_case_name().clone());
1227                    }
1228                    PrevStatus::Outstanding => {
1229                        suite.outstanding.insert(test_name.test_case_name().clone());
1230                    }
1231                    PrevStatus::Unknown => {
1232                        // Not tracked: don't add.
1233                    }
1234                }
1235            }
1236
1237            let mut id_map = IdOrdMap::new();
1238            for (_, suite) in result {
1239                id_map.insert_unique(suite).expect("unique binaries");
1240            }
1241            id_map
1242        }
1243    }
1244
1245    // ---
1246    // Stress run accumulation tests.
1247    // ---
1248
1249    /// Creates a `TestFinished` event for testing.
1250    ///
1251    /// Uses `()` as the output type since we don't need actual output data for
1252    /// these tests.
1253    fn make_test_finished(
1254        test_instance: OwnedTestInstanceId,
1255        stress_index: Option<(u32, Option<u32>)>,
1256        passed: bool,
1257    ) -> TestEventKindSummary<()> {
1258        let result = if passed {
1259            ExecutionResultDescription::Pass
1260        } else {
1261            ExecutionResultDescription::Fail {
1262                failure: FailureDescription::ExitCode { code: 1 },
1263                leaked: false,
1264            }
1265        };
1266
1267        let execute_status = ExecuteStatus {
1268            retry_data: RetryData {
1269                attempt: 1,
1270                total_attempts: 1,
1271            },
1272            output: ChildExecutionOutputDescription::Output {
1273                result: Some(result.clone()),
1274                output: ChildOutputDescription::Split {
1275                    stdout: None,
1276                    stderr: None,
1277                },
1278                errors: None,
1279            },
1280            result,
1281            start_time: Utc::now().into(),
1282            time_taken: Duration::from_millis(100),
1283            is_slow: false,
1284            delay_before_start: Duration::ZERO,
1285            error_summary: None,
1286            output_error_slice: None,
1287        };
1288
1289        TestEventKindSummary::Output(OutputEventKind::TestFinished {
1290            stress_index: stress_index.map(|(current, total)| StressIndexSummary {
1291                current,
1292                total: total.and_then(NonZero::new),
1293            }),
1294            test_instance,
1295            success_output: TestOutputDisplay::Never,
1296            failure_output: TestOutputDisplay::Never,
1297            junit_store_success_output: false,
1298            junit_store_failure_output: false,
1299            run_statuses: ExecutionStatuses::new(vec![execute_status]),
1300            current_stats: RunStats::default(),
1301            running: 0,
1302        })
1303    }
1304
1305    /// Test stress run accumulation: if any iteration fails, the test is Failed.
1306    ///
1307    /// This tests the fix for the stress run accumulation logic. Multiple
1308    /// `TestFinished` events for the same test (one per stress iteration) should
1309    /// result in Failed if any iteration failed, regardless of order.
1310    #[test]
1311    fn stress_run_accumulation() {
1312        // [Pass, Fail, Pass] -> Failed.
1313        let test_pass_fail_pass = OwnedTestInstanceId {
1314            binary_id: RustBinaryId::new("test-binary"),
1315            test_name: TestCaseName::new("pass_fail_pass"),
1316        };
1317
1318        // [Pass, Pass, Pass] -> Passed.
1319        let test_all_pass = OwnedTestInstanceId {
1320            binary_id: RustBinaryId::new("test-binary"),
1321            test_name: TestCaseName::new("all_pass"),
1322        };
1323
1324        // [Fail, Fail, Fail] -> Failed.
1325        let test_all_fail = OwnedTestInstanceId {
1326            binary_id: RustBinaryId::new("test-binary"),
1327            test_name: TestCaseName::new("all_fail"),
1328        };
1329
1330        // [Fail, Pass, Pass] -> Failed.
1331        let test_fail_first = OwnedTestInstanceId {
1332            binary_id: RustBinaryId::new("test-binary"),
1333            test_name: TestCaseName::new("fail_first"),
1334        };
1335
1336        // Regular (non-stress) pass.
1337        let test_regular_pass = OwnedTestInstanceId {
1338            binary_id: RustBinaryId::new("test-binary"),
1339            test_name: TestCaseName::new("regular_pass"),
1340        };
1341
1342        // Regular (non-stress) fail.
1343        let test_regular_fail = OwnedTestInstanceId {
1344            binary_id: RustBinaryId::new("test-binary"),
1345            test_name: TestCaseName::new("regular_fail"),
1346        };
1347
1348        // Construct all events in one stream.
1349        let events = [
1350            // pass_fail_pass: [Pass, Fail, Pass]
1351            make_test_finished(test_pass_fail_pass.clone(), Some((0, Some(3))), true),
1352            make_test_finished(test_pass_fail_pass.clone(), Some((1, Some(3))), false),
1353            make_test_finished(test_pass_fail_pass.clone(), Some((2, Some(3))), true),
1354            // all_pass: [Pass, Pass, Pass]
1355            make_test_finished(test_all_pass.clone(), Some((0, Some(3))), true),
1356            make_test_finished(test_all_pass.clone(), Some((1, Some(3))), true),
1357            make_test_finished(test_all_pass.clone(), Some((2, Some(3))), true),
1358            // all_fail: [Fail, Fail, Fail]
1359            make_test_finished(test_all_fail.clone(), Some((0, Some(3))), false),
1360            make_test_finished(test_all_fail.clone(), Some((1, Some(3))), false),
1361            make_test_finished(test_all_fail.clone(), Some((2, Some(3))), false),
1362            // fail_first: [Fail, Pass, Pass]
1363            make_test_finished(test_fail_first.clone(), Some((0, Some(3))), false),
1364            make_test_finished(test_fail_first.clone(), Some((1, Some(3))), true),
1365            make_test_finished(test_fail_first.clone(), Some((2, Some(3))), true),
1366            // regular_pass: single pass (no stress index)
1367            make_test_finished(test_regular_pass.clone(), None, true),
1368            // regular_fail: single fail (no stress index)
1369            make_test_finished(test_regular_fail.clone(), None, false),
1370        ];
1371
1372        let outcomes = collect_from_events(events.iter());
1373
1374        assert_eq!(
1375            outcomes.get(&test_pass_fail_pass),
1376            Some(&TestOutcome::Failed),
1377            "[Pass, Fail, Pass] should be Failed"
1378        );
1379        assert_eq!(
1380            outcomes.get(&test_all_pass),
1381            Some(&TestOutcome::Passed),
1382            "[Pass, Pass, Pass] should be Passed"
1383        );
1384        assert_eq!(
1385            outcomes.get(&test_all_fail),
1386            Some(&TestOutcome::Failed),
1387            "[Fail, Fail, Fail] should be Failed"
1388        );
1389        assert_eq!(
1390            outcomes.get(&test_fail_first),
1391            Some(&TestOutcome::Failed),
1392            "[Fail, Pass, Pass] should be Failed"
1393        );
1394        assert_eq!(
1395            outcomes.get(&test_regular_pass),
1396            Some(&TestOutcome::Passed),
1397            "regular pass should be Passed"
1398        );
1399        assert_eq!(
1400            outcomes.get(&test_regular_fail),
1401            Some(&TestOutcome::Failed),
1402            "regular fail should be Failed"
1403        );
1404    }
1405
1406    /// Test that multiple tests in a stress run are tracked independently.
1407    ///
1408    /// Interleaved events for different tests should not interfere with each
1409    /// other's outcome accumulation.
1410    #[test]
1411    fn stress_run_multiple_tests_independent() {
1412        let test_a = OwnedTestInstanceId {
1413            binary_id: RustBinaryId::new("test-binary"),
1414            test_name: TestCaseName::new("test_a"),
1415        };
1416        let test_b = OwnedTestInstanceId {
1417            binary_id: RustBinaryId::new("test-binary"),
1418            test_name: TestCaseName::new("test_b"),
1419        };
1420
1421        // Interleaved stress run events for two tests:
1422        // test_a: [Pass, Pass] -> Passed
1423        // test_b: [Pass, Fail] -> Failed
1424        let events = [
1425            make_test_finished(test_a.clone(), Some((0, Some(2))), true),
1426            make_test_finished(test_b.clone(), Some((0, Some(2))), true),
1427            make_test_finished(test_a.clone(), Some((1, Some(2))), true),
1428            make_test_finished(test_b.clone(), Some((1, Some(2))), false),
1429        ];
1430
1431        let outcomes = collect_from_events(events.iter());
1432
1433        assert_eq!(
1434            outcomes.get(&test_a),
1435            Some(&TestOutcome::Passed),
1436            "test_a [Pass, Pass] should be Passed"
1437        );
1438        assert_eq!(
1439            outcomes.get(&test_b),
1440            Some(&TestOutcome::Failed),
1441            "test_b [Pass, Fail] should be Failed"
1442        );
1443    }
1444}