1use crate::{
10 config::elements::FlakyResult,
11 errors::RecordReadError,
12 list::OwnedTestInstanceId,
13 record::{
14 CoreEventKind, OutputEventKind, PortableRecording, RecordReader, StoreReader,
15 TestEventKindSummary,
16 format::{RerunInfo, RerunRootInfo, RerunTestSuiteInfo},
17 },
18 reporter::events::ExecutionDescription,
19};
20use iddqd::IdOrdMap;
21use nextest_metadata::{
22 FilterMatch, MismatchReason, RustBinaryId, RustTestSuiteStatusSummary, TestCaseName,
23 TestListSummary,
24};
25use quick_junit::ReportUuid;
26use std::{
27 borrow::Borrow,
28 collections::{BTreeSet, HashMap},
29};
30
31pub(crate) trait TestListInfo {
36 type BinaryIter<'a>: Iterator<Item = (&'a RustBinaryId, BinaryInfo<'a>)>
38 where
39 Self: 'a;
40
41 fn binaries(&self) -> Self::BinaryIter<'_>;
43}
44
45pub(crate) enum BinaryInfo<'a> {
47 Listed {
49 test_cases: Box<dyn Iterator<Item = (&'a TestCaseName, FilterMatch)> + 'a>,
51 },
52 Skipped,
54}
55
56impl TestListInfo for TestListSummary {
57 type BinaryIter<'a> = TestListSummaryBinaryIter<'a>;
58
59 fn binaries(&self) -> Self::BinaryIter<'_> {
60 TestListSummaryBinaryIter {
61 inner: self.rust_suites.iter(),
62 }
63 }
64}
65
66pub(crate) struct TestListSummaryBinaryIter<'a> {
68 inner:
69 std::collections::btree_map::Iter<'a, RustBinaryId, nextest_metadata::RustTestSuiteSummary>,
70}
71
72impl<'a> Iterator for TestListSummaryBinaryIter<'a> {
73 type Item = (&'a RustBinaryId, BinaryInfo<'a>);
74
75 fn next(&mut self) -> Option<Self::Item> {
76 self.inner.next().map(|(binary_id, suite)| {
77 let info = if suite.status == RustTestSuiteStatusSummary::LISTED {
78 BinaryInfo::Listed {
79 test_cases: Box::new(
80 suite
81 .test_cases
82 .iter()
83 .map(|(name, tc)| (name, tc.filter_match)),
84 ),
85 }
86 } else {
87 BinaryInfo::Skipped
88 };
89 (binary_id, info)
90 })
91 }
92}
93
94pub(crate) fn compute_outstanding_pure(
96 prev_info: Option<&IdOrdMap<RerunTestSuiteInfo>>,
97 test_list: &impl TestListInfo,
98 outcomes: &HashMap<OwnedTestInstanceId, TestOutcome>,
99) -> IdOrdMap<RerunTestSuiteInfo> {
100 let mut new_outstanding = IdOrdMap::new();
101
102 let mut binaries_in_test_list = BTreeSet::new();
106
107 for (binary_id, binary_info) in test_list.binaries() {
108 binaries_in_test_list.insert(binary_id.clone());
109
110 match binary_info {
111 BinaryInfo::Listed { test_cases } => {
112 let prev = prev_info.and_then(|p| p.get(binary_id));
115
116 let mut curr = RerunTestSuiteInfo::new(binary_id.clone());
117 for (test_name, filter_match) in test_cases {
118 match filter_match {
119 FilterMatch::Matches => {
120 let key = OwnedTestInstanceId {
122 binary_id: binary_id.clone(),
123 test_name: test_name.clone(),
124 };
125 match outcomes.get(&key) {
126 Some(TestOutcome::Passed) => {
127 curr.passing.insert(test_name.clone());
129 }
130 Some(TestOutcome::Failed) => {
131 curr.outstanding.insert(test_name.clone());
133 }
134 Some(TestOutcome::Skipped(skipped)) => {
135 handle_skipped(test_name, *skipped, prev, &mut curr);
139 }
140 None => {
141 curr.outstanding.insert(test_name.clone());
144 }
145 }
146 }
147 FilterMatch::Mismatch { reason } => {
148 handle_skipped(
149 test_name,
150 TestOutcomeSkipped::from_mismatch_reason(reason),
151 prev,
152 &mut curr,
153 );
154 }
155 }
156 }
157
158 if let Some(prev) = prev {
162 for t in &prev.outstanding {
163 if !curr.passing.contains(t) && !curr.outstanding.contains(t) {
164 curr.outstanding.insert(t.clone());
165 }
166 }
167 }
168
169 if !curr.passing.is_empty() || !curr.outstanding.is_empty() {
176 new_outstanding
177 .insert_unique(curr)
178 .expect("binaries iterator should not yield duplicates");
179 }
180 }
181 BinaryInfo::Skipped => {
182 if let Some(prev_outstanding) = prev_info
192 && let Some(outstanding) = prev_outstanding.get(binary_id)
193 {
194 new_outstanding
196 .insert_unique(outstanding.clone())
197 .expect("binaries iterator should not yield duplicates");
198 }
199 }
204 }
205 }
206
207 if let Some(prev) = prev_info {
210 for prev_suite in prev.iter() {
211 if !binaries_in_test_list.contains(&prev_suite.binary_id) {
212 new_outstanding
213 .insert_unique(prev_suite.clone())
214 .expect("binary not in test list, so this should succeed");
215 }
216 }
217 }
218
219 new_outstanding
220}
221
222#[derive(Clone, Debug)]
224pub struct ComputedRerunInfo {
225 pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
229}
230
231impl ComputedRerunInfo {
232 pub fn expected_test_ids(&self) -> BTreeSet<OwnedTestInstanceId> {
236 self.test_suites
237 .iter()
238 .flat_map(|suite| {
239 suite.outstanding.iter().map(|name| OwnedTestInstanceId {
240 binary_id: suite.binary_id.clone(),
241 test_name: name.clone(),
242 })
243 })
244 .collect()
245 }
246
247 pub fn compute(
252 reader: &mut RecordReader,
253 ) -> Result<(Self, Option<RerunRootInfo>), RecordReadError> {
254 let rerun_info = reader.read_rerun_info()?;
255 let test_list = reader.read_test_list()?;
256 let outcomes = TestEventOutcomes::collect(reader)?;
257
258 let prev_test_suites = rerun_info.as_ref().map(|info| &info.test_suites);
259 let new_test_suites =
260 compute_outstanding_pure(prev_test_suites, &test_list, &outcomes.outcomes);
261
262 let root_info = rerun_info.map(|info| info.root_info);
263
264 Ok((
265 Self {
266 test_suites: new_test_suites,
267 },
268 root_info,
269 ))
270 }
271
272 pub fn compute_from_archive(
277 archive: &mut PortableRecording,
278 ) -> Result<(Self, Option<RerunRootInfo>), RecordReadError> {
279 let mut store = archive
280 .open_store()
281 .map_err(RecordReadError::PortableRecording)?;
282 let rerun_info = store.read_rerun_info()?;
283 let test_list = store.read_test_list()?;
284 let run_log = archive
288 .read_run_log()
289 .map_err(RecordReadError::PortableRecording)?;
290 let outcomes = collect_from_events(run_log.events()?.map(|r| r.map(|e| e.kind)))?;
291
292 let prev_test_suites = rerun_info.as_ref().map(|info| &info.test_suites);
293 let new_test_suites = compute_outstanding_pure(prev_test_suites, &test_list, &outcomes);
294
295 let root_info = rerun_info.map(|info| info.root_info);
296
297 Ok((
298 Self {
299 test_suites: new_test_suites,
300 },
301 root_info,
302 ))
303 }
304
305 pub fn into_rerun_info(self, parent_run_id: ReportUuid, root_info: RerunRootInfo) -> RerunInfo {
307 RerunInfo {
308 parent_run_id,
309 root_info,
310 test_suites: self.test_suites,
311 }
312 }
313}
314
315fn handle_skipped(
316 test_name: &TestCaseName,
317 skipped: TestOutcomeSkipped,
318 prev: Option<&RerunTestSuiteInfo>,
319 curr: &mut RerunTestSuiteInfo,
320) {
321 match skipped {
322 TestOutcomeSkipped::Rerun => {
323 curr.passing.insert(test_name.clone());
330 }
331 TestOutcomeSkipped::Explicit => {
332 if let Some(prev) = prev {
346 if prev.outstanding.contains(test_name) {
347 curr.outstanding.insert(test_name.clone());
348 } else if prev.passing.contains(test_name) {
349 curr.passing.insert(test_name.clone());
350 }
351 } else {
352 }
355 }
356 }
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361pub(crate) enum TestOutcomeSkipped {
362 Explicit,
364
365 Rerun,
367}
368
369impl TestOutcomeSkipped {
370 fn from_mismatch_reason(reason: MismatchReason) -> Self {
372 match reason {
373 MismatchReason::NotBenchmark
374 | MismatchReason::Ignored
375 | MismatchReason::String
376 | MismatchReason::Expression
377 | MismatchReason::Partition
378 | MismatchReason::DefaultFilter => TestOutcomeSkipped::Explicit,
379 MismatchReason::RerunAlreadyPassed => TestOutcomeSkipped::Rerun,
380 other => unreachable!("all known match arms are covered, found {other:?}"),
381 }
382 }
383}
384
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
387pub(crate) enum TestOutcome {
388 Passed,
390
391 Skipped(TestOutcomeSkipped),
393
394 Failed,
396}
397
398#[derive(Clone, Debug)]
402struct TestEventOutcomes {
403 outcomes: HashMap<OwnedTestInstanceId, TestOutcome>,
405}
406
407impl TestEventOutcomes {
408 fn collect(reader: &mut RecordReader) -> Result<Self, RecordReadError> {
413 reader.load_dictionaries()?;
414 let outcomes = collect_from_events(reader.events()?.map(|r| r.map(|e| e.kind)))?;
415
416 Ok(Self { outcomes })
417 }
418}
419
420fn collect_from_events<K, S: crate::output_spec::OutputSpec, E>(
426 events: impl Iterator<Item = Result<K, E>>,
427) -> Result<HashMap<OwnedTestInstanceId, TestOutcome>, E>
428where
429 K: Borrow<TestEventKindSummary<S>>,
430{
431 let mut outcomes = HashMap::new();
432
433 for kind_result in events {
434 let kind = kind_result?;
435 match kind.borrow() {
436 TestEventKindSummary::Output(OutputEventKind::TestFinished {
437 test_instance,
438 run_statuses,
439 ..
440 }) => {
441 let outcome = match run_statuses.describe() {
446 ExecutionDescription::Success { .. } => TestOutcome::Passed,
447 ExecutionDescription::Flaky {
448 result: FlakyResult::Pass,
449 ..
450 } => TestOutcome::Passed,
451 ExecutionDescription::Flaky {
452 result: FlakyResult::Fail,
453 ..
454 } => TestOutcome::Failed,
455 ExecutionDescription::Failure { .. } => TestOutcome::Failed,
456 };
457
458 outcomes
465 .entry(test_instance.clone())
466 .and_modify(|existing| {
467 if outcome == TestOutcome::Failed {
468 *existing = TestOutcome::Failed;
469 }
470 })
471 .or_insert(outcome);
472 }
473 TestEventKindSummary::Core(CoreEventKind::TestSkipped {
474 test_instance,
475 reason,
476 ..
477 }) => {
478 let skipped_reason = TestOutcomeSkipped::from_mismatch_reason(*reason);
479 outcomes.insert(test_instance.clone(), TestOutcome::Skipped(skipped_reason));
480 }
481 _ => {}
482 }
483 }
484
485 Ok(outcomes)
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use crate::{
492 config::elements::JunitFlakyFailStatus,
493 output_spec::RecordingSpec,
494 record::{
495 OutputEventKind, StressIndexSummary, TestEventKindSummary, ZipStoreOutputDescription,
496 },
497 reporter::{
498 TestOutputDisplay,
499 events::{
500 ChildExecutionOutputDescription, ExecuteStatus, ExecutionResultDescription,
501 ExecutionStatuses, FailureDescription, RetryData, RunStats,
502 },
503 },
504 };
505 use chrono::Utc;
506 use proptest::prelude::*;
507 use std::{
508 collections::{BTreeMap, btree_map},
509 convert::Infallible,
510 num::NonZero,
511 sync::OnceLock,
512 time::Duration,
513 };
514 use test_strategy::proptest;
515
516 #[proptest(cases = 200)]
522 fn sut_matches_oracle(#[strategy(arb_rerun_model())] model: RerunModel) {
523 let expected = model.compute_rerun_info_decision_table();
524 let actual = run_sut(&model);
525 prop_assert_eq!(actual, expected);
526 }
527
528 #[proptest(cases = 200)]
530 fn passing_and_outstanding_disjoint(#[strategy(arb_rerun_model())] model: RerunModel) {
531 let result = run_sut(&model);
532 for suite in result.iter() {
533 let intersection: BTreeSet<_> =
534 suite.passing.intersection(&suite.outstanding).collect();
535 prop_assert!(
536 intersection.is_empty(),
537 "passing and outstanding should be disjoint for {}: {:?}",
538 suite.binary_id,
539 intersection
540 );
541 }
542 }
543
544 #[proptest(cases = 200)]
550 fn matching_tests_with_outcomes_are_tracked(#[strategy(arb_rerun_model())] model: RerunModel) {
551 let result = run_sut(&model);
552
553 let final_step = model.reruns.last().unwrap_or(&model.initial);
555
556 for (binary_id, binary_model) in &final_step.test_list.binaries {
557 if let BinaryModel::Listed { tests } = binary_model {
558 let rust_binary_id = binary_id.rust_binary_id();
559
560 for (test_name, filter_match) in tests {
561 if matches!(filter_match, FilterMatch::Matches) {
562 let key = (*binary_id, *test_name);
563 let outcome = final_step.outcomes.get(&key);
564
565 let should_be_tracked = match outcome {
570 Some(TestOutcome::Passed)
571 | Some(TestOutcome::Failed)
572 | Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun))
573 | None => true,
574 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => false,
575 };
576
577 if should_be_tracked {
578 let tcn = test_name.test_case_name();
579 let suite = result.get(&rust_binary_id);
580 let in_passing = suite.is_some_and(|s| s.passing.contains(tcn));
581 let in_outstanding = suite.is_some_and(|s| s.outstanding.contains(tcn));
582 prop_assert!(
583 in_passing || in_outstanding,
584 "matching test {:?}::{:?} with outcome {:?} should be in passing or outstanding",
585 binary_id,
586 test_name,
587 outcome
588 );
589 }
590 }
591 }
592 }
593 }
594 }
595
596 #[test]
598 fn decide_test_outcome_truth_table() {
599 use Decision as D;
600 use FilterMatchResult as F;
601 use PrevStatus as P;
602
603 assert_eq!(
605 decide_test_outcome(P::Passing, F::BinaryNotPresent, None),
606 D::Passing
607 );
608 assert_eq!(
609 decide_test_outcome(P::Outstanding, F::BinaryNotPresent, None),
610 D::Outstanding
611 );
612 assert_eq!(
613 decide_test_outcome(P::Unknown, F::BinaryNotPresent, None),
614 D::NotTracked
615 );
616
617 assert_eq!(
619 decide_test_outcome(P::Passing, F::BinarySkipped, None),
620 D::Passing
621 );
622 assert_eq!(
623 decide_test_outcome(P::Outstanding, F::BinarySkipped, None),
624 D::Outstanding
625 );
626 assert_eq!(
627 decide_test_outcome(P::Unknown, F::BinarySkipped, None),
628 D::NotTracked
629 );
630
631 assert_eq!(
633 decide_test_outcome(P::Passing, F::TestNotInList, None),
634 D::NotTracked
635 );
636 assert_eq!(
637 decide_test_outcome(P::Outstanding, F::TestNotInList, None),
638 D::Outstanding
639 );
640 assert_eq!(
641 decide_test_outcome(P::Unknown, F::TestNotInList, None),
642 D::NotTracked
643 );
644
645 let matches = F::HasMatch(FilterMatch::Matches);
647
648 assert_eq!(
650 decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Passed)),
651 D::Passing
652 );
653 assert_eq!(
654 decide_test_outcome(P::Passing, matches, Some(TestOutcome::Passed)),
655 D::Passing
656 );
657 assert_eq!(
658 decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Passed)),
659 D::Passing
660 );
661
662 assert_eq!(
664 decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Failed)),
665 D::Outstanding
666 );
667 assert_eq!(
668 decide_test_outcome(P::Passing, matches, Some(TestOutcome::Failed)),
669 D::Outstanding
670 );
671 assert_eq!(
672 decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Failed)),
673 D::Outstanding
674 );
675
676 assert_eq!(
678 decide_test_outcome(P::Unknown, matches, None),
679 D::Outstanding
680 );
681 assert_eq!(
682 decide_test_outcome(P::Passing, matches, None),
683 D::Outstanding
684 );
685 assert_eq!(
686 decide_test_outcome(P::Outstanding, matches, None),
687 D::Outstanding
688 );
689
690 let rerun_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun));
692 assert_eq!(
693 decide_test_outcome(P::Unknown, matches, rerun_skipped),
694 D::Passing
695 );
696 assert_eq!(
697 decide_test_outcome(P::Passing, matches, rerun_skipped),
698 D::Passing
699 );
700 assert_eq!(
701 decide_test_outcome(P::Outstanding, matches, rerun_skipped),
702 D::Passing
703 );
704
705 let explicit_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit));
707 assert_eq!(
708 decide_test_outcome(P::Unknown, matches, explicit_skipped),
709 D::NotTracked
710 );
711 assert_eq!(
712 decide_test_outcome(P::Passing, matches, explicit_skipped),
713 D::Passing
714 );
715 assert_eq!(
716 decide_test_outcome(P::Outstanding, matches, explicit_skipped),
717 D::Outstanding
718 );
719
720 let rerun_mismatch = F::HasMatch(FilterMatch::Mismatch {
722 reason: MismatchReason::RerunAlreadyPassed,
723 });
724 assert_eq!(
725 decide_test_outcome(P::Unknown, rerun_mismatch, None),
726 D::Passing
727 );
728 assert_eq!(
729 decide_test_outcome(P::Passing, rerun_mismatch, None),
730 D::Passing
731 );
732 assert_eq!(
733 decide_test_outcome(P::Outstanding, rerun_mismatch, None),
734 D::Passing
735 );
736
737 let explicit_mismatch = F::HasMatch(FilterMatch::Mismatch {
739 reason: MismatchReason::Ignored,
740 });
741 assert_eq!(
742 decide_test_outcome(P::Unknown, explicit_mismatch, None),
743 D::NotTracked
744 );
745 assert_eq!(
746 decide_test_outcome(P::Passing, explicit_mismatch, None),
747 D::Passing
748 );
749 assert_eq!(
750 decide_test_outcome(P::Outstanding, explicit_mismatch, None),
751 D::Outstanding
752 );
753 }
754
755 const ALL_PREV_STATUSES: [PrevStatus; 3] = [
764 PrevStatus::Passing,
765 PrevStatus::Outstanding,
766 PrevStatus::Unknown,
767 ];
768
769 fn all_outcomes() -> [Option<TestOutcome>; 5] {
771 [
772 None,
773 Some(TestOutcome::Passed),
774 Some(TestOutcome::Failed),
775 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
776 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
777 ]
778 }
779
780 fn all_in_list_filter_results() -> Vec<FilterMatchResult> {
782 let mut results = vec![FilterMatchResult::HasMatch(FilterMatch::Matches)];
783 for &reason in MismatchReason::ALL_VARIANTS {
784 results.push(FilterMatchResult::HasMatch(FilterMatch::Mismatch {
785 reason,
786 }));
787 }
788 results
789 }
790
791 #[test]
799 fn spec_property_passing_monotonicity() {
800 let non_regressing_outcomes = [
801 Some(TestOutcome::Passed),
802 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
803 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
804 ];
805
806 for filter in all_in_list_filter_results() {
807 for outcome in non_regressing_outcomes {
808 let decision = decide_test_outcome(PrevStatus::Passing, filter, outcome);
809 assert_eq!(
810 decision,
811 Decision::Passing,
812 "monotonicity violated: Passing + {:?} + {:?} -> {:?}",
813 filter,
814 outcome,
815 decision
816 );
817 }
818 }
819 }
820
821 #[test]
826 fn spec_property_outstanding_to_passing_on_pass() {
827 let passing_outcomes = [
828 Some(TestOutcome::Passed),
829 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
830 ];
831
832 for outcome in passing_outcomes {
833 let decision = decide_test_outcome(
834 PrevStatus::Outstanding,
835 FilterMatchResult::HasMatch(FilterMatch::Matches),
836 outcome,
837 );
838 assert_eq!(
839 decision,
840 Decision::Passing,
841 "convergence violated: Outstanding + Matches + {:?} -> {:?}",
842 outcome,
843 decision
844 );
845 }
846 }
847
848 #[test]
852 fn spec_property_failed_becomes_outstanding() {
853 let failing_outcomes = [None, Some(TestOutcome::Failed)];
854
855 for prev in ALL_PREV_STATUSES {
856 for outcome in failing_outcomes {
857 let decision = decide_test_outcome(
858 prev,
859 FilterMatchResult::HasMatch(FilterMatch::Matches),
860 outcome,
861 );
862 assert_eq!(
863 decision,
864 Decision::Outstanding,
865 "FAILED->OUTSTANDING VIOLATED: {:?} + Matches + {:?} -> {:?}",
866 prev,
867 outcome,
868 decision
869 );
870 }
871 }
872 }
873
874 #[test]
881 fn spec_property_test_not_in_list_behavior() {
882 for outcome in all_outcomes() {
883 assert_eq!(
885 decide_test_outcome(
886 PrevStatus::Outstanding,
887 FilterMatchResult::TestNotInList,
888 outcome
889 ),
890 Decision::Outstanding,
891 );
892 assert_eq!(
894 decide_test_outcome(
895 PrevStatus::Passing,
896 FilterMatchResult::TestNotInList,
897 outcome
898 ),
899 Decision::NotTracked,
900 );
901 assert_eq!(
903 decide_test_outcome(
904 PrevStatus::Unknown,
905 FilterMatchResult::TestNotInList,
906 outcome
907 ),
908 Decision::NotTracked,
909 );
910 }
911 }
912
913 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
921 enum ModelBinaryId {
922 A,
923 B,
924 C,
925 D,
926 }
927
928 impl ModelBinaryId {
929 fn rust_binary_id(self) -> &'static RustBinaryId {
930 match self {
931 Self::A => {
932 static ID: OnceLock<RustBinaryId> = OnceLock::new();
933 ID.get_or_init(|| RustBinaryId::new("binary-a"))
934 }
935 Self::B => {
936 static ID: OnceLock<RustBinaryId> = OnceLock::new();
937 ID.get_or_init(|| RustBinaryId::new("binary-b"))
938 }
939 Self::C => {
940 static ID: OnceLock<RustBinaryId> = OnceLock::new();
941 ID.get_or_init(|| RustBinaryId::new("binary-c"))
942 }
943 Self::D => {
944 static ID: OnceLock<RustBinaryId> = OnceLock::new();
945 ID.get_or_init(|| RustBinaryId::new("binary-d"))
946 }
947 }
948 }
949 }
950
951 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
953 enum ModelTestName {
954 Test1,
955 Test2,
956 Test3,
957 Test4,
958 Test5,
959 }
960
961 impl ModelTestName {
962 fn test_case_name(self) -> &'static TestCaseName {
963 match self {
964 Self::Test1 => {
965 static NAME: OnceLock<TestCaseName> = OnceLock::new();
966 NAME.get_or_init(|| TestCaseName::new("test_1"))
967 }
968 Self::Test2 => {
969 static NAME: OnceLock<TestCaseName> = OnceLock::new();
970 NAME.get_or_init(|| TestCaseName::new("test_2"))
971 }
972 Self::Test3 => {
973 static NAME: OnceLock<TestCaseName> = OnceLock::new();
974 NAME.get_or_init(|| TestCaseName::new("test_3"))
975 }
976 Self::Test4 => {
977 static NAME: OnceLock<TestCaseName> = OnceLock::new();
978 NAME.get_or_init(|| TestCaseName::new("test_4"))
979 }
980 Self::Test5 => {
981 static NAME: OnceLock<TestCaseName> = OnceLock::new();
982 NAME.get_or_init(|| TestCaseName::new("test_5"))
983 }
984 }
985 }
986 }
987
988 #[derive(Clone, Debug)]
990 enum BinaryModel {
991 Listed {
993 tests: BTreeMap<ModelTestName, FilterMatch>,
994 },
995 Skipped,
997 }
998
999 #[derive(Clone, Debug)]
1001 struct TestListModel {
1002 binaries: BTreeMap<ModelBinaryId, BinaryModel>,
1003 }
1004
1005 #[derive(Clone, Debug)]
1007 struct RunStep {
1008 test_list: TestListModel,
1010 outcomes: BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
1012 }
1013
1014 #[derive(Clone, Debug)]
1016 struct RerunModel {
1017 initial: RunStep,
1019 reruns: Vec<RunStep>,
1021 }
1022
1023 impl TestListInfo for TestListModel {
1024 type BinaryIter<'a> = TestListModelBinaryIter<'a>;
1025
1026 fn binaries(&self) -> Self::BinaryIter<'_> {
1027 TestListModelBinaryIter {
1028 inner: self.binaries.iter(),
1029 }
1030 }
1031 }
1032
1033 struct TestListModelBinaryIter<'a> {
1035 inner: btree_map::Iter<'a, ModelBinaryId, BinaryModel>,
1036 }
1037
1038 impl<'a> Iterator for TestListModelBinaryIter<'a> {
1039 type Item = (&'a RustBinaryId, BinaryInfo<'a>);
1040
1041 fn next(&mut self) -> Option<Self::Item> {
1042 self.inner.next().map(|(model_id, binary_model)| {
1043 let rust_id = model_id.rust_binary_id();
1044 let info = match binary_model {
1045 BinaryModel::Listed { tests } => BinaryInfo::Listed {
1046 test_cases: Box::new(
1047 tests.iter().map(|(name, fm)| (name.test_case_name(), *fm)),
1048 ),
1049 },
1050 BinaryModel::Skipped => BinaryInfo::Skipped,
1051 };
1052 (rust_id, info)
1053 })
1054 }
1055 }
1056
1057 fn arb_model_binary_id() -> impl Strategy<Value = ModelBinaryId> {
1062 prop_oneof![
1063 Just(ModelBinaryId::A),
1064 Just(ModelBinaryId::B),
1065 Just(ModelBinaryId::C),
1066 Just(ModelBinaryId::D),
1067 ]
1068 }
1069
1070 fn arb_model_test_name() -> impl Strategy<Value = ModelTestName> {
1071 prop_oneof![
1072 Just(ModelTestName::Test1),
1073 Just(ModelTestName::Test2),
1074 Just(ModelTestName::Test3),
1075 Just(ModelTestName::Test4),
1076 Just(ModelTestName::Test5),
1077 ]
1078 }
1079
1080 fn arb_filter_match() -> impl Strategy<Value = FilterMatch> {
1081 prop_oneof![
1082 4 => Just(FilterMatch::Matches),
1083 1 => any::<MismatchReason>().prop_map(|reason| FilterMatch::Mismatch { reason }),
1084 ]
1085 }
1086
1087 fn arb_test_outcome() -> impl Strategy<Value = TestOutcome> {
1088 prop_oneof![
1089 4 => Just(TestOutcome::Passed),
1090 2 => Just(TestOutcome::Failed),
1091 1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
1092 1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
1093 ]
1094 }
1095
1096 fn arb_test_map() -> impl Strategy<Value = BTreeMap<ModelTestName, FilterMatch>> {
1097 proptest::collection::btree_map(arb_model_test_name(), arb_filter_match(), 0..5)
1098 }
1099
1100 fn arb_binary_model() -> impl Strategy<Value = BinaryModel> {
1101 prop_oneof![
1102 8 => arb_test_map().prop_map(|tests| BinaryModel::Listed { tests }),
1103 2 => Just(BinaryModel::Skipped),
1104 ]
1105 }
1106
1107 fn arb_test_list_model() -> impl Strategy<Value = TestListModel> {
1108 proptest::collection::btree_map(arb_model_binary_id(), arb_binary_model(), 0..4)
1109 .prop_map(|binaries| TestListModel { binaries })
1110 }
1111
1112 fn arb_outcomes_for_matching_tests(
1117 matching_tests: Vec<(ModelBinaryId, ModelTestName)>,
1118 ) -> BoxedStrategy<BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>> {
1119 if matching_tests.is_empty() {
1120 Just(BTreeMap::new()).boxed()
1121 } else {
1122 let len = matching_tests.len();
1123 proptest::collection::btree_map(
1124 proptest::sample::select(matching_tests),
1125 arb_test_outcome(),
1126 0..=len,
1127 )
1128 .boxed()
1129 }
1130 }
1131
1132 fn extract_matching_tests(test_list: &TestListModel) -> Vec<(ModelBinaryId, ModelTestName)> {
1134 test_list
1135 .binaries
1136 .iter()
1137 .filter_map(|(binary_id, model)| match model {
1138 BinaryModel::Listed { tests } => Some(
1139 tests
1140 .iter()
1141 .filter(|(_, fm)| matches!(fm, FilterMatch::Matches))
1142 .map(move |(tn, _)| (*binary_id, *tn)),
1143 ),
1144 BinaryModel::Skipped => None,
1145 })
1146 .flatten()
1147 .collect()
1148 }
1149
1150 fn arb_run_step() -> impl Strategy<Value = RunStep> {
1151 arb_test_list_model().prop_flat_map(|test_list| {
1152 let matching_tests = extract_matching_tests(&test_list);
1153 arb_outcomes_for_matching_tests(matching_tests).prop_map(move |outcomes| RunStep {
1154 test_list: test_list.clone(),
1155 outcomes,
1156 })
1157 })
1158 }
1159
1160 fn arb_rerun_model() -> impl Strategy<Value = RerunModel> {
1161 (
1162 arb_run_step(),
1163 proptest::collection::vec(arb_run_step(), 0..5),
1164 )
1165 .prop_map(|(initial, reruns)| RerunModel { initial, reruns })
1166 }
1167
1168 fn model_outcomes_to_hashmap(
1173 outcomes: &BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
1174 ) -> HashMap<OwnedTestInstanceId, TestOutcome> {
1175 outcomes
1176 .iter()
1177 .map(|((binary_id, test_name), outcome)| {
1178 let id = OwnedTestInstanceId {
1179 binary_id: binary_id.rust_binary_id().clone(),
1180 test_name: test_name.test_case_name().clone(),
1181 };
1182 (id, *outcome)
1183 })
1184 .collect()
1185 }
1186
1187 fn run_sut(model: &RerunModel) -> IdOrdMap<RerunTestSuiteInfo> {
1193 let outcomes = model_outcomes_to_hashmap(&model.initial.outcomes);
1194 let mut result = compute_outstanding_pure(None, &model.initial.test_list, &outcomes);
1195
1196 for rerun in &model.reruns {
1197 let outcomes = model_outcomes_to_hashmap(&rerun.outcomes);
1198 result = compute_outstanding_pure(Some(&result), &rerun.test_list, &outcomes);
1199 }
1200
1201 result
1202 }
1203
1204 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1214 enum PrevStatus {
1215 Passing,
1217 Outstanding,
1219 Unknown,
1221 }
1222
1223 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1225 enum Decision {
1226 Passing,
1228 Outstanding,
1230 NotTracked,
1232 }
1233
1234 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1239 enum FilterMatchResult {
1240 BinaryNotPresent,
1243 BinarySkipped,
1246 TestNotInList,
1249 HasMatch(FilterMatch),
1251 }
1252
1253 fn decide_test_outcome(
1258 prev: PrevStatus,
1259 filter_result: FilterMatchResult,
1260 outcome: Option<TestOutcome>,
1261 ) -> Decision {
1262 match filter_result {
1263 FilterMatchResult::BinaryNotPresent | FilterMatchResult::BinarySkipped => {
1264 match prev {
1266 PrevStatus::Passing => Decision::Passing,
1267 PrevStatus::Outstanding => Decision::Outstanding,
1268 PrevStatus::Unknown => Decision::NotTracked,
1269 }
1270 }
1271 FilterMatchResult::TestNotInList => {
1272 match prev {
1277 PrevStatus::Outstanding => Decision::Outstanding,
1278 PrevStatus::Passing | PrevStatus::Unknown => Decision::NotTracked,
1279 }
1280 }
1281 FilterMatchResult::HasMatch(FilterMatch::Matches) => {
1282 match outcome {
1283 Some(TestOutcome::Passed) => Decision::Passing,
1284 Some(TestOutcome::Failed) => Decision::Outstanding,
1285 None => {
1286 Decision::Outstanding
1288 }
1289 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)) => Decision::Passing,
1290 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => {
1291 match prev {
1293 PrevStatus::Passing => Decision::Passing,
1294 PrevStatus::Outstanding => Decision::Outstanding,
1295 PrevStatus::Unknown => Decision::NotTracked,
1296 }
1297 }
1298 }
1299 }
1300 FilterMatchResult::HasMatch(FilterMatch::Mismatch { reason }) => {
1301 match TestOutcomeSkipped::from_mismatch_reason(reason) {
1302 TestOutcomeSkipped::Rerun => Decision::Passing,
1303 TestOutcomeSkipped::Explicit => {
1304 match prev {
1306 PrevStatus::Passing => Decision::Passing,
1307 PrevStatus::Outstanding => Decision::Outstanding,
1308 PrevStatus::Unknown => Decision::NotTracked,
1309 }
1310 }
1311 }
1312 }
1313 }
1314 }
1315
1316 impl RerunModel {
1317 fn compute_rerun_info_decision_table(&self) -> IdOrdMap<RerunTestSuiteInfo> {
1323 let mut prev_state: HashMap<(ModelBinaryId, ModelTestName), PrevStatus> =
1325 HashMap::new();
1326
1327 self.update_state_from_step(&mut prev_state, &self.initial);
1329
1330 for rerun in &self.reruns {
1332 self.update_state_from_step(&mut prev_state, rerun);
1333 }
1334
1335 self.collect_final_state(&prev_state)
1337 }
1338
1339 fn update_state_from_step(
1340 &self,
1341 state: &mut HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1342 step: &RunStep,
1343 ) {
1344 let all_tests = self.enumerate_all_tests(state, step);
1348
1349 for (binary_id, test_name) in all_tests {
1350 let prev = state
1351 .get(&(binary_id, test_name))
1352 .copied()
1353 .unwrap_or(PrevStatus::Unknown);
1354
1355 let filter_result = self.get_filter_match_result(step, binary_id, test_name);
1356 let outcome = step.outcomes.get(&(binary_id, test_name)).copied();
1357
1358 let decision = decide_test_outcome(prev, filter_result, outcome);
1359
1360 match decision {
1362 Decision::Passing => {
1363 state.insert((binary_id, test_name), PrevStatus::Passing);
1364 }
1365 Decision::Outstanding => {
1366 state.insert((binary_id, test_name), PrevStatus::Outstanding);
1367 }
1368 Decision::NotTracked => {
1369 state.remove(&(binary_id, test_name));
1370 }
1371 }
1372 }
1373 }
1374
1375 fn get_filter_match_result(
1380 &self,
1381 step: &RunStep,
1382 binary_id: ModelBinaryId,
1383 test_name: ModelTestName,
1384 ) -> FilterMatchResult {
1385 match step.test_list.binaries.get(&binary_id) {
1386 None => FilterMatchResult::BinaryNotPresent,
1387 Some(BinaryModel::Skipped) => FilterMatchResult::BinarySkipped,
1388 Some(BinaryModel::Listed { tests }) => match tests.get(&test_name) {
1389 Some(filter_match) => FilterMatchResult::HasMatch(*filter_match),
1390 None => FilterMatchResult::TestNotInList,
1391 },
1392 }
1393 }
1394
1395 fn enumerate_all_tests(
1400 &self,
1401 prev_state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1402 step: &RunStep,
1403 ) -> BTreeSet<(ModelBinaryId, ModelTestName)> {
1404 let mut tests = BTreeSet::new();
1405
1406 for (binary_id, binary_model) in &step.test_list.binaries {
1408 if let BinaryModel::Listed { tests: test_map } = binary_model {
1409 for test_name in test_map.keys() {
1410 tests.insert((*binary_id, *test_name));
1411 }
1412 }
1413 }
1414
1415 for (binary_id, test_name) in prev_state.keys() {
1417 tests.insert((*binary_id, *test_name));
1418 }
1419
1420 tests
1421 }
1422
1423 fn collect_final_state(
1425 &self,
1426 state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1427 ) -> IdOrdMap<RerunTestSuiteInfo> {
1428 let mut result: BTreeMap<ModelBinaryId, RerunTestSuiteInfo> = BTreeMap::new();
1429
1430 for ((binary_id, test_name), status) in state {
1431 let suite = result
1432 .entry(*binary_id)
1433 .or_insert_with(|| RerunTestSuiteInfo::new(binary_id.rust_binary_id().clone()));
1434
1435 match status {
1436 PrevStatus::Passing => {
1437 suite.passing.insert(test_name.test_case_name().clone());
1438 }
1439 PrevStatus::Outstanding => {
1440 suite.outstanding.insert(test_name.test_case_name().clone());
1441 }
1442 PrevStatus::Unknown => {
1443 }
1445 }
1446 }
1447
1448 let mut id_map = IdOrdMap::new();
1449 for (_, suite) in result {
1450 id_map.insert_unique(suite).expect("unique binaries");
1451 }
1452 id_map
1453 }
1454 }
1455
1456 fn make_test_finished(
1465 test_instance: OwnedTestInstanceId,
1466 stress_index: Option<(u32, Option<u32>)>,
1467 passed: bool,
1468 ) -> TestEventKindSummary<RecordingSpec> {
1469 let result = if passed {
1470 ExecutionResultDescription::Pass
1471 } else {
1472 ExecutionResultDescription::Fail {
1473 failure: FailureDescription::ExitCode { code: 1 },
1474 leaked: false,
1475 }
1476 };
1477
1478 let execute_status = ExecuteStatus {
1479 retry_data: RetryData {
1480 attempt: 1,
1481 total_attempts: 1,
1482 },
1483 output: ChildExecutionOutputDescription::Output {
1484 result: Some(result.clone()),
1485 output: ZipStoreOutputDescription::Split {
1486 stdout: None,
1487 stderr: None,
1488 },
1489 errors: None,
1490 },
1491 result,
1492 start_time: Utc::now().into(),
1493 time_taken: Duration::from_millis(100),
1494 is_slow: false,
1495 delay_before_start: Duration::ZERO,
1496 error_summary: None,
1497 output_error_slice: None,
1498 };
1499
1500 TestEventKindSummary::Output(OutputEventKind::TestFinished {
1501 stress_index: stress_index.map(|(current, total)| StressIndexSummary {
1502 current,
1503 total: total.and_then(NonZero::new),
1504 }),
1505 test_instance,
1506 success_output: TestOutputDisplay::Never,
1507 failure_output: TestOutputDisplay::Never,
1508 junit_store_success_output: false,
1509 junit_store_failure_output: false,
1510 junit_flaky_fail_status: JunitFlakyFailStatus::default(),
1511 run_statuses: ExecutionStatuses::new(vec![execute_status], FlakyResult::default()),
1512 current_stats: RunStats::default(),
1513 running: 0,
1514 })
1515 }
1516
1517 fn make_flaky_test_finished(
1520 test_instance: OwnedTestInstanceId,
1521 flaky_result: FlakyResult,
1522 ) -> TestEventKindSummary<RecordingSpec> {
1523 let fail_result = ExecutionResultDescription::Fail {
1524 failure: FailureDescription::ExitCode { code: 1 },
1525 leaked: false,
1526 };
1527 let pass_result = ExecutionResultDescription::Pass;
1528
1529 let fail_status = ExecuteStatus {
1530 retry_data: RetryData {
1531 attempt: 1,
1532 total_attempts: 2,
1533 },
1534 output: ChildExecutionOutputDescription::Output {
1535 result: Some(fail_result.clone()),
1536 output: ZipStoreOutputDescription::Split {
1537 stdout: None,
1538 stderr: None,
1539 },
1540 errors: None,
1541 },
1542 result: fail_result,
1543 start_time: Utc::now().into(),
1544 time_taken: Duration::from_millis(100),
1545 is_slow: false,
1546 delay_before_start: Duration::ZERO,
1547 error_summary: None,
1548 output_error_slice: None,
1549 };
1550
1551 let pass_status = ExecuteStatus {
1552 retry_data: RetryData {
1553 attempt: 2,
1554 total_attempts: 2,
1555 },
1556 output: ChildExecutionOutputDescription::Output {
1557 result: Some(pass_result.clone()),
1558 output: ZipStoreOutputDescription::Split {
1559 stdout: None,
1560 stderr: None,
1561 },
1562 errors: None,
1563 },
1564 result: pass_result,
1565 start_time: Utc::now().into(),
1566 time_taken: Duration::from_millis(100),
1567 is_slow: false,
1568 delay_before_start: Duration::ZERO,
1569 error_summary: None,
1570 output_error_slice: None,
1571 };
1572
1573 TestEventKindSummary::Output(OutputEventKind::TestFinished {
1574 stress_index: None,
1575 test_instance,
1576 success_output: TestOutputDisplay::Never,
1577 failure_output: TestOutputDisplay::Never,
1578 junit_store_success_output: false,
1579 junit_store_failure_output: false,
1580 junit_flaky_fail_status: JunitFlakyFailStatus::default(),
1581 run_statuses: ExecutionStatuses::new(vec![fail_status, pass_status], flaky_result),
1582 current_stats: RunStats::default(),
1583 running: 0,
1584 })
1585 }
1586
1587 #[test]
1594 fn flaky_result_affects_rerun_outcome() {
1595 let test_flaky_fail = OwnedTestInstanceId {
1596 binary_id: RustBinaryId::new("test-binary"),
1597 test_name: TestCaseName::new("flaky_fail"),
1598 };
1599 let test_flaky_pass = OwnedTestInstanceId {
1600 binary_id: RustBinaryId::new("test-binary"),
1601 test_name: TestCaseName::new("flaky_pass"),
1602 };
1603
1604 let events = [
1605 make_flaky_test_finished(test_flaky_fail.clone(), FlakyResult::Fail),
1606 make_flaky_test_finished(test_flaky_pass.clone(), FlakyResult::Pass),
1607 ];
1608
1609 let outcomes = collect_from_events(events.iter().map(Ok::<_, Infallible>)).unwrap();
1610
1611 assert_eq!(
1612 outcomes.get(&test_flaky_fail),
1613 Some(&TestOutcome::Failed),
1614 "flaky test with FlakyResult::Fail should be Failed for rerun"
1615 );
1616 assert_eq!(
1617 outcomes.get(&test_flaky_pass),
1618 Some(&TestOutcome::Passed),
1619 "flaky test with FlakyResult::Pass should be Passed for rerun"
1620 );
1621 }
1622
1623 #[test]
1629 fn stress_run_accumulation() {
1630 let test_pass_fail_pass = OwnedTestInstanceId {
1632 binary_id: RustBinaryId::new("test-binary"),
1633 test_name: TestCaseName::new("pass_fail_pass"),
1634 };
1635
1636 let test_all_pass = OwnedTestInstanceId {
1638 binary_id: RustBinaryId::new("test-binary"),
1639 test_name: TestCaseName::new("all_pass"),
1640 };
1641
1642 let test_all_fail = OwnedTestInstanceId {
1644 binary_id: RustBinaryId::new("test-binary"),
1645 test_name: TestCaseName::new("all_fail"),
1646 };
1647
1648 let test_fail_first = OwnedTestInstanceId {
1650 binary_id: RustBinaryId::new("test-binary"),
1651 test_name: TestCaseName::new("fail_first"),
1652 };
1653
1654 let test_regular_pass = OwnedTestInstanceId {
1656 binary_id: RustBinaryId::new("test-binary"),
1657 test_name: TestCaseName::new("regular_pass"),
1658 };
1659
1660 let test_regular_fail = OwnedTestInstanceId {
1662 binary_id: RustBinaryId::new("test-binary"),
1663 test_name: TestCaseName::new("regular_fail"),
1664 };
1665
1666 let events = [
1668 make_test_finished(test_pass_fail_pass.clone(), Some((0, Some(3))), true),
1670 make_test_finished(test_pass_fail_pass.clone(), Some((1, Some(3))), false),
1671 make_test_finished(test_pass_fail_pass.clone(), Some((2, Some(3))), true),
1672 make_test_finished(test_all_pass.clone(), Some((0, Some(3))), true),
1674 make_test_finished(test_all_pass.clone(), Some((1, Some(3))), true),
1675 make_test_finished(test_all_pass.clone(), Some((2, Some(3))), true),
1676 make_test_finished(test_all_fail.clone(), Some((0, Some(3))), false),
1678 make_test_finished(test_all_fail.clone(), Some((1, Some(3))), false),
1679 make_test_finished(test_all_fail.clone(), Some((2, Some(3))), false),
1680 make_test_finished(test_fail_first.clone(), Some((0, Some(3))), false),
1682 make_test_finished(test_fail_first.clone(), Some((1, Some(3))), true),
1683 make_test_finished(test_fail_first.clone(), Some((2, Some(3))), true),
1684 make_test_finished(test_regular_pass.clone(), None, true),
1686 make_test_finished(test_regular_fail.clone(), None, false),
1688 ];
1689
1690 let outcomes = collect_from_events(events.iter().map(Ok::<_, Infallible>)).unwrap();
1691
1692 assert_eq!(
1693 outcomes.get(&test_pass_fail_pass),
1694 Some(&TestOutcome::Failed),
1695 "[Pass, Fail, Pass] should be Failed"
1696 );
1697 assert_eq!(
1698 outcomes.get(&test_all_pass),
1699 Some(&TestOutcome::Passed),
1700 "[Pass, Pass, Pass] should be Passed"
1701 );
1702 assert_eq!(
1703 outcomes.get(&test_all_fail),
1704 Some(&TestOutcome::Failed),
1705 "[Fail, Fail, Fail] should be Failed"
1706 );
1707 assert_eq!(
1708 outcomes.get(&test_fail_first),
1709 Some(&TestOutcome::Failed),
1710 "[Fail, Pass, Pass] should be Failed"
1711 );
1712 assert_eq!(
1713 outcomes.get(&test_regular_pass),
1714 Some(&TestOutcome::Passed),
1715 "regular pass should be Passed"
1716 );
1717 assert_eq!(
1718 outcomes.get(&test_regular_fail),
1719 Some(&TestOutcome::Failed),
1720 "regular fail should be Failed"
1721 );
1722 }
1723
1724 #[test]
1729 fn stress_run_multiple_tests_independent() {
1730 let test_a = OwnedTestInstanceId {
1731 binary_id: RustBinaryId::new("test-binary"),
1732 test_name: TestCaseName::new("test_a"),
1733 };
1734 let test_b = OwnedTestInstanceId {
1735 binary_id: RustBinaryId::new("test-binary"),
1736 test_name: TestCaseName::new("test_b"),
1737 };
1738
1739 let events = [
1743 make_test_finished(test_a.clone(), Some((0, Some(2))), true),
1744 make_test_finished(test_b.clone(), Some((0, Some(2))), true),
1745 make_test_finished(test_a.clone(), Some((1, Some(2))), true),
1746 make_test_finished(test_b.clone(), Some((1, Some(2))), false),
1747 ];
1748
1749 let outcomes = collect_from_events(events.iter().map(Ok::<_, Infallible>)).unwrap();
1750
1751 assert_eq!(
1752 outcomes.get(&test_a),
1753 Some(&TestOutcome::Passed),
1754 "test_a [Pass, Pass] should be Passed"
1755 );
1756 assert_eq!(
1757 outcomes.get(&test_b),
1758 Some(&TestOutcome::Failed),
1759 "test_b [Pass, Fail] should be Failed"
1760 );
1761 }
1762}