1use crate::{
10 errors::RecordReadError,
11 list::OwnedTestInstanceId,
12 record::{
13 CoreEventKind, OutputEventKind, PortableRecording, RecordReader, StoreReader,
14 TestEventKindSummary,
15 format::{RerunInfo, RerunRootInfo, RerunTestSuiteInfo},
16 },
17};
18use iddqd::IdOrdMap;
19use nextest_metadata::{
20 FilterMatch, MismatchReason, RustBinaryId, RustTestSuiteStatusSummary, TestCaseName,
21 TestListSummary,
22};
23use quick_junit::ReportUuid;
24use std::{
25 borrow::Borrow,
26 collections::{BTreeSet, HashMap},
27};
28
29pub(crate) trait TestListInfo {
34 type BinaryIter<'a>: Iterator<Item = (&'a RustBinaryId, BinaryInfo<'a>)>
36 where
37 Self: 'a;
38
39 fn binaries(&self) -> Self::BinaryIter<'_>;
41}
42
43pub(crate) enum BinaryInfo<'a> {
45 Listed {
47 test_cases: Box<dyn Iterator<Item = (&'a TestCaseName, FilterMatch)> + 'a>,
49 },
50 Skipped,
52}
53
54impl TestListInfo for TestListSummary {
55 type BinaryIter<'a> = TestListSummaryBinaryIter<'a>;
56
57 fn binaries(&self) -> Self::BinaryIter<'_> {
58 TestListSummaryBinaryIter {
59 inner: self.rust_suites.iter(),
60 }
61 }
62}
63
64pub(crate) struct TestListSummaryBinaryIter<'a> {
66 inner:
67 std::collections::btree_map::Iter<'a, RustBinaryId, nextest_metadata::RustTestSuiteSummary>,
68}
69
70impl<'a> Iterator for TestListSummaryBinaryIter<'a> {
71 type Item = (&'a RustBinaryId, BinaryInfo<'a>);
72
73 fn next(&mut self) -> Option<Self::Item> {
74 self.inner.next().map(|(binary_id, suite)| {
75 let info = if suite.status == RustTestSuiteStatusSummary::LISTED {
76 BinaryInfo::Listed {
77 test_cases: Box::new(
78 suite
79 .test_cases
80 .iter()
81 .map(|(name, tc)| (name, tc.filter_match)),
82 ),
83 }
84 } else {
85 BinaryInfo::Skipped
86 };
87 (binary_id, info)
88 })
89 }
90}
91
92pub(crate) fn compute_outstanding_pure(
94 prev_info: Option<&IdOrdMap<RerunTestSuiteInfo>>,
95 test_list: &impl TestListInfo,
96 outcomes: &HashMap<OwnedTestInstanceId, TestOutcome>,
97) -> IdOrdMap<RerunTestSuiteInfo> {
98 let mut new_outstanding = IdOrdMap::new();
99
100 let mut binaries_in_test_list = BTreeSet::new();
104
105 for (binary_id, binary_info) in test_list.binaries() {
106 binaries_in_test_list.insert(binary_id.clone());
107
108 match binary_info {
109 BinaryInfo::Listed { test_cases } => {
110 let prev = prev_info.and_then(|p| p.get(binary_id));
113
114 let mut curr = RerunTestSuiteInfo::new(binary_id.clone());
115 for (test_name, filter_match) in test_cases {
116 match filter_match {
117 FilterMatch::Matches => {
118 let key = OwnedTestInstanceId {
120 binary_id: binary_id.clone(),
121 test_name: test_name.clone(),
122 };
123 match outcomes.get(&key) {
124 Some(TestOutcome::Passed) => {
125 curr.passing.insert(test_name.clone());
127 }
128 Some(TestOutcome::Failed) => {
129 curr.outstanding.insert(test_name.clone());
131 }
132 Some(TestOutcome::Skipped(skipped)) => {
133 handle_skipped(test_name, *skipped, prev, &mut curr);
137 }
138 None => {
139 curr.outstanding.insert(test_name.clone());
142 }
143 }
144 }
145 FilterMatch::Mismatch { reason } => {
146 handle_skipped(
147 test_name,
148 TestOutcomeSkipped::from_mismatch_reason(reason),
149 prev,
150 &mut curr,
151 );
152 }
153 }
154 }
155
156 if let Some(prev) = prev {
160 for t in &prev.outstanding {
161 if !curr.passing.contains(t) && !curr.outstanding.contains(t) {
162 curr.outstanding.insert(t.clone());
163 }
164 }
165 }
166
167 if !curr.passing.is_empty() || !curr.outstanding.is_empty() {
174 new_outstanding
175 .insert_unique(curr)
176 .expect("binaries iterator should not yield duplicates");
177 }
178 }
179 BinaryInfo::Skipped => {
180 if let Some(prev_outstanding) = prev_info
190 && let Some(outstanding) = prev_outstanding.get(binary_id)
191 {
192 new_outstanding
194 .insert_unique(outstanding.clone())
195 .expect("binaries iterator should not yield duplicates");
196 }
197 }
202 }
203 }
204
205 if let Some(prev) = prev_info {
208 for prev_suite in prev.iter() {
209 if !binaries_in_test_list.contains(&prev_suite.binary_id) {
210 new_outstanding
211 .insert_unique(prev_suite.clone())
212 .expect("binary not in test list, so this should succeed");
213 }
214 }
215 }
216
217 new_outstanding
218}
219
220#[derive(Clone, Debug)]
222pub struct ComputedRerunInfo {
223 pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
227}
228
229impl ComputedRerunInfo {
230 pub fn expected_test_ids(&self) -> BTreeSet<OwnedTestInstanceId> {
234 self.test_suites
235 .iter()
236 .flat_map(|suite| {
237 suite.outstanding.iter().map(|name| OwnedTestInstanceId {
238 binary_id: suite.binary_id.clone(),
239 test_name: name.clone(),
240 })
241 })
242 .collect()
243 }
244
245 pub fn compute(
250 reader: &mut RecordReader,
251 ) -> Result<(Self, Option<RerunRootInfo>), RecordReadError> {
252 let rerun_info = reader.read_rerun_info()?;
253 let test_list = reader.read_test_list()?;
254 let outcomes = TestEventOutcomes::collect(reader)?;
255
256 let prev_test_suites = rerun_info.as_ref().map(|info| &info.test_suites);
257 let new_test_suites =
258 compute_outstanding_pure(prev_test_suites, &test_list, &outcomes.outcomes);
259
260 let root_info = rerun_info.map(|info| info.root_info);
261
262 Ok((
263 Self {
264 test_suites: new_test_suites,
265 },
266 root_info,
267 ))
268 }
269
270 pub fn compute_from_archive(
275 archive: &mut PortableRecording,
276 ) -> Result<(Self, Option<RerunRootInfo>), RecordReadError> {
277 let mut store = archive
278 .open_store()
279 .map_err(RecordReadError::PortableRecording)?;
280 let rerun_info = store.read_rerun_info()?;
281 let test_list = store.read_test_list()?;
282 let run_log = archive
286 .read_run_log()
287 .map_err(RecordReadError::PortableRecording)?;
288 let outcomes = collect_from_events(run_log.events()?.map(|r| r.map(|e| e.kind)))?;
289
290 let prev_test_suites = rerun_info.as_ref().map(|info| &info.test_suites);
291 let new_test_suites = compute_outstanding_pure(prev_test_suites, &test_list, &outcomes);
292
293 let root_info = rerun_info.map(|info| info.root_info);
294
295 Ok((
296 Self {
297 test_suites: new_test_suites,
298 },
299 root_info,
300 ))
301 }
302
303 pub fn into_rerun_info(self, parent_run_id: ReportUuid, root_info: RerunRootInfo) -> RerunInfo {
305 RerunInfo {
306 parent_run_id,
307 root_info,
308 test_suites: self.test_suites,
309 }
310 }
311}
312
313fn handle_skipped(
314 test_name: &TestCaseName,
315 skipped: TestOutcomeSkipped,
316 prev: Option<&RerunTestSuiteInfo>,
317 curr: &mut RerunTestSuiteInfo,
318) {
319 match skipped {
320 TestOutcomeSkipped::Rerun => {
321 curr.passing.insert(test_name.clone());
328 }
329 TestOutcomeSkipped::Explicit => {
330 if let Some(prev) = prev {
344 if prev.outstanding.contains(test_name) {
345 curr.outstanding.insert(test_name.clone());
346 } else if prev.passing.contains(test_name) {
347 curr.passing.insert(test_name.clone());
348 }
349 } else {
350 }
353 }
354 }
355}
356
357#[derive(Clone, Copy, Debug, PartialEq, Eq)]
359pub(crate) enum TestOutcomeSkipped {
360 Explicit,
362
363 Rerun,
365}
366
367impl TestOutcomeSkipped {
368 fn from_mismatch_reason(reason: MismatchReason) -> Self {
370 match reason {
371 MismatchReason::NotBenchmark
372 | MismatchReason::Ignored
373 | MismatchReason::String
374 | MismatchReason::Expression
375 | MismatchReason::Partition
376 | MismatchReason::DefaultFilter => TestOutcomeSkipped::Explicit,
377 MismatchReason::RerunAlreadyPassed => TestOutcomeSkipped::Rerun,
378 other => unreachable!("all known match arms are covered, found {other:?}"),
379 }
380 }
381}
382
383#[derive(Clone, Copy, Debug, PartialEq, Eq)]
385pub(crate) enum TestOutcome {
386 Passed,
388
389 Skipped(TestOutcomeSkipped),
391
392 Failed,
394}
395
396#[derive(Clone, Debug)]
400struct TestEventOutcomes {
401 outcomes: HashMap<OwnedTestInstanceId, TestOutcome>,
403}
404
405impl TestEventOutcomes {
406 fn collect(reader: &mut RecordReader) -> Result<Self, RecordReadError> {
411 reader.load_dictionaries()?;
412 let outcomes = collect_from_events(reader.events()?.map(|r| r.map(|e| e.kind)))?;
413
414 Ok(Self { outcomes })
415 }
416}
417
418fn collect_from_events<K, S: crate::output_spec::OutputSpec, E>(
424 events: impl Iterator<Item = Result<K, E>>,
425) -> Result<HashMap<OwnedTestInstanceId, TestOutcome>, E>
426where
427 K: Borrow<TestEventKindSummary<S>>,
428{
429 let mut outcomes = HashMap::new();
430
431 for kind_result in events {
432 let kind = kind_result?;
433 match kind.borrow() {
434 TestEventKindSummary::Output(OutputEventKind::TestFinished {
435 test_instance,
436 run_statuses,
437 ..
438 }) => {
439 let outcome = if run_statuses.last_status().result.is_success() {
441 TestOutcome::Passed
442 } else {
443 TestOutcome::Failed
444 };
445
446 outcomes
453 .entry(test_instance.clone())
454 .and_modify(|existing| {
455 if outcome == TestOutcome::Failed {
456 *existing = TestOutcome::Failed;
457 }
458 })
459 .or_insert(outcome);
460 }
461 TestEventKindSummary::Core(CoreEventKind::TestSkipped {
462 test_instance,
463 reason,
464 ..
465 }) => {
466 let skipped_reason = TestOutcomeSkipped::from_mismatch_reason(*reason);
467 outcomes.insert(test_instance.clone(), TestOutcome::Skipped(skipped_reason));
468 }
469 _ => {}
470 }
471 }
472
473 Ok(outcomes)
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::{
480 output_spec::RecordingSpec,
481 record::{
482 OutputEventKind, StressIndexSummary, TestEventKindSummary, ZipStoreOutputDescription,
483 },
484 reporter::{
485 TestOutputDisplay,
486 events::{
487 ChildExecutionOutputDescription, ExecuteStatus, ExecutionResultDescription,
488 ExecutionStatuses, FailureDescription, RetryData, RunStats,
489 },
490 },
491 };
492 use chrono::Utc;
493 use proptest::prelude::*;
494 use std::{
495 collections::{BTreeMap, btree_map},
496 convert::Infallible,
497 num::NonZero,
498 sync::OnceLock,
499 time::Duration,
500 };
501 use test_strategy::proptest;
502
503 #[proptest(cases = 200)]
509 fn sut_matches_oracle(#[strategy(arb_rerun_model())] model: RerunModel) {
510 let expected = model.compute_rerun_info_decision_table();
511 let actual = run_sut(&model);
512 prop_assert_eq!(actual, expected);
513 }
514
515 #[proptest(cases = 200)]
517 fn passing_and_outstanding_disjoint(#[strategy(arb_rerun_model())] model: RerunModel) {
518 let result = run_sut(&model);
519 for suite in result.iter() {
520 let intersection: BTreeSet<_> =
521 suite.passing.intersection(&suite.outstanding).collect();
522 prop_assert!(
523 intersection.is_empty(),
524 "passing and outstanding should be disjoint for {}: {:?}",
525 suite.binary_id,
526 intersection
527 );
528 }
529 }
530
531 #[proptest(cases = 200)]
537 fn matching_tests_with_outcomes_are_tracked(#[strategy(arb_rerun_model())] model: RerunModel) {
538 let result = run_sut(&model);
539
540 let final_step = model.reruns.last().unwrap_or(&model.initial);
542
543 for (binary_id, binary_model) in &final_step.test_list.binaries {
544 if let BinaryModel::Listed { tests } = binary_model {
545 let rust_binary_id = binary_id.rust_binary_id();
546
547 for (test_name, filter_match) in tests {
548 if matches!(filter_match, FilterMatch::Matches) {
549 let key = (*binary_id, *test_name);
550 let outcome = final_step.outcomes.get(&key);
551
552 let should_be_tracked = match outcome {
557 Some(TestOutcome::Passed)
558 | Some(TestOutcome::Failed)
559 | Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun))
560 | None => true,
561 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => false,
562 };
563
564 if should_be_tracked {
565 let tcn = test_name.test_case_name();
566 let suite = result.get(&rust_binary_id);
567 let in_passing = suite.is_some_and(|s| s.passing.contains(tcn));
568 let in_outstanding = suite.is_some_and(|s| s.outstanding.contains(tcn));
569 prop_assert!(
570 in_passing || in_outstanding,
571 "matching test {:?}::{:?} with outcome {:?} should be in passing or outstanding",
572 binary_id,
573 test_name,
574 outcome
575 );
576 }
577 }
578 }
579 }
580 }
581 }
582
583 #[test]
585 fn decide_test_outcome_truth_table() {
586 use Decision as D;
587 use FilterMatchResult as F;
588 use PrevStatus as P;
589
590 assert_eq!(
592 decide_test_outcome(P::Passing, F::BinaryNotPresent, None),
593 D::Passing
594 );
595 assert_eq!(
596 decide_test_outcome(P::Outstanding, F::BinaryNotPresent, None),
597 D::Outstanding
598 );
599 assert_eq!(
600 decide_test_outcome(P::Unknown, F::BinaryNotPresent, None),
601 D::NotTracked
602 );
603
604 assert_eq!(
606 decide_test_outcome(P::Passing, F::BinarySkipped, None),
607 D::Passing
608 );
609 assert_eq!(
610 decide_test_outcome(P::Outstanding, F::BinarySkipped, None),
611 D::Outstanding
612 );
613 assert_eq!(
614 decide_test_outcome(P::Unknown, F::BinarySkipped, None),
615 D::NotTracked
616 );
617
618 assert_eq!(
620 decide_test_outcome(P::Passing, F::TestNotInList, None),
621 D::NotTracked
622 );
623 assert_eq!(
624 decide_test_outcome(P::Outstanding, F::TestNotInList, None),
625 D::Outstanding
626 );
627 assert_eq!(
628 decide_test_outcome(P::Unknown, F::TestNotInList, None),
629 D::NotTracked
630 );
631
632 let matches = F::HasMatch(FilterMatch::Matches);
634
635 assert_eq!(
637 decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Passed)),
638 D::Passing
639 );
640 assert_eq!(
641 decide_test_outcome(P::Passing, matches, Some(TestOutcome::Passed)),
642 D::Passing
643 );
644 assert_eq!(
645 decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Passed)),
646 D::Passing
647 );
648
649 assert_eq!(
651 decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Failed)),
652 D::Outstanding
653 );
654 assert_eq!(
655 decide_test_outcome(P::Passing, matches, Some(TestOutcome::Failed)),
656 D::Outstanding
657 );
658 assert_eq!(
659 decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Failed)),
660 D::Outstanding
661 );
662
663 assert_eq!(
665 decide_test_outcome(P::Unknown, matches, None),
666 D::Outstanding
667 );
668 assert_eq!(
669 decide_test_outcome(P::Passing, matches, None),
670 D::Outstanding
671 );
672 assert_eq!(
673 decide_test_outcome(P::Outstanding, matches, None),
674 D::Outstanding
675 );
676
677 let rerun_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun));
679 assert_eq!(
680 decide_test_outcome(P::Unknown, matches, rerun_skipped),
681 D::Passing
682 );
683 assert_eq!(
684 decide_test_outcome(P::Passing, matches, rerun_skipped),
685 D::Passing
686 );
687 assert_eq!(
688 decide_test_outcome(P::Outstanding, matches, rerun_skipped),
689 D::Passing
690 );
691
692 let explicit_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit));
694 assert_eq!(
695 decide_test_outcome(P::Unknown, matches, explicit_skipped),
696 D::NotTracked
697 );
698 assert_eq!(
699 decide_test_outcome(P::Passing, matches, explicit_skipped),
700 D::Passing
701 );
702 assert_eq!(
703 decide_test_outcome(P::Outstanding, matches, explicit_skipped),
704 D::Outstanding
705 );
706
707 let rerun_mismatch = F::HasMatch(FilterMatch::Mismatch {
709 reason: MismatchReason::RerunAlreadyPassed,
710 });
711 assert_eq!(
712 decide_test_outcome(P::Unknown, rerun_mismatch, None),
713 D::Passing
714 );
715 assert_eq!(
716 decide_test_outcome(P::Passing, rerun_mismatch, None),
717 D::Passing
718 );
719 assert_eq!(
720 decide_test_outcome(P::Outstanding, rerun_mismatch, None),
721 D::Passing
722 );
723
724 let explicit_mismatch = F::HasMatch(FilterMatch::Mismatch {
726 reason: MismatchReason::Ignored,
727 });
728 assert_eq!(
729 decide_test_outcome(P::Unknown, explicit_mismatch, None),
730 D::NotTracked
731 );
732 assert_eq!(
733 decide_test_outcome(P::Passing, explicit_mismatch, None),
734 D::Passing
735 );
736 assert_eq!(
737 decide_test_outcome(P::Outstanding, explicit_mismatch, None),
738 D::Outstanding
739 );
740 }
741
742 const ALL_PREV_STATUSES: [PrevStatus; 3] = [
751 PrevStatus::Passing,
752 PrevStatus::Outstanding,
753 PrevStatus::Unknown,
754 ];
755
756 fn all_outcomes() -> [Option<TestOutcome>; 5] {
758 [
759 None,
760 Some(TestOutcome::Passed),
761 Some(TestOutcome::Failed),
762 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
763 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
764 ]
765 }
766
767 fn all_in_list_filter_results() -> Vec<FilterMatchResult> {
769 let mut results = vec![FilterMatchResult::HasMatch(FilterMatch::Matches)];
770 for &reason in MismatchReason::ALL_VARIANTS {
771 results.push(FilterMatchResult::HasMatch(FilterMatch::Mismatch {
772 reason,
773 }));
774 }
775 results
776 }
777
778 #[test]
786 fn spec_property_passing_monotonicity() {
787 let non_regressing_outcomes = [
788 Some(TestOutcome::Passed),
789 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
790 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
791 ];
792
793 for filter in all_in_list_filter_results() {
794 for outcome in non_regressing_outcomes {
795 let decision = decide_test_outcome(PrevStatus::Passing, filter, outcome);
796 assert_eq!(
797 decision,
798 Decision::Passing,
799 "monotonicity violated: Passing + {:?} + {:?} -> {:?}",
800 filter,
801 outcome,
802 decision
803 );
804 }
805 }
806 }
807
808 #[test]
813 fn spec_property_outstanding_to_passing_on_pass() {
814 let passing_outcomes = [
815 Some(TestOutcome::Passed),
816 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
817 ];
818
819 for outcome in passing_outcomes {
820 let decision = decide_test_outcome(
821 PrevStatus::Outstanding,
822 FilterMatchResult::HasMatch(FilterMatch::Matches),
823 outcome,
824 );
825 assert_eq!(
826 decision,
827 Decision::Passing,
828 "convergence violated: Outstanding + Matches + {:?} -> {:?}",
829 outcome,
830 decision
831 );
832 }
833 }
834
835 #[test]
839 fn spec_property_failed_becomes_outstanding() {
840 let failing_outcomes = [None, Some(TestOutcome::Failed)];
841
842 for prev in ALL_PREV_STATUSES {
843 for outcome in failing_outcomes {
844 let decision = decide_test_outcome(
845 prev,
846 FilterMatchResult::HasMatch(FilterMatch::Matches),
847 outcome,
848 );
849 assert_eq!(
850 decision,
851 Decision::Outstanding,
852 "FAILED->OUTSTANDING VIOLATED: {:?} + Matches + {:?} -> {:?}",
853 prev,
854 outcome,
855 decision
856 );
857 }
858 }
859 }
860
861 #[test]
868 fn spec_property_test_not_in_list_behavior() {
869 for outcome in all_outcomes() {
870 assert_eq!(
872 decide_test_outcome(
873 PrevStatus::Outstanding,
874 FilterMatchResult::TestNotInList,
875 outcome
876 ),
877 Decision::Outstanding,
878 );
879 assert_eq!(
881 decide_test_outcome(
882 PrevStatus::Passing,
883 FilterMatchResult::TestNotInList,
884 outcome
885 ),
886 Decision::NotTracked,
887 );
888 assert_eq!(
890 decide_test_outcome(
891 PrevStatus::Unknown,
892 FilterMatchResult::TestNotInList,
893 outcome
894 ),
895 Decision::NotTracked,
896 );
897 }
898 }
899
900 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
908 enum ModelBinaryId {
909 A,
910 B,
911 C,
912 D,
913 }
914
915 impl ModelBinaryId {
916 fn rust_binary_id(self) -> &'static RustBinaryId {
917 match self {
918 Self::A => {
919 static ID: OnceLock<RustBinaryId> = OnceLock::new();
920 ID.get_or_init(|| RustBinaryId::new("binary-a"))
921 }
922 Self::B => {
923 static ID: OnceLock<RustBinaryId> = OnceLock::new();
924 ID.get_or_init(|| RustBinaryId::new("binary-b"))
925 }
926 Self::C => {
927 static ID: OnceLock<RustBinaryId> = OnceLock::new();
928 ID.get_or_init(|| RustBinaryId::new("binary-c"))
929 }
930 Self::D => {
931 static ID: OnceLock<RustBinaryId> = OnceLock::new();
932 ID.get_or_init(|| RustBinaryId::new("binary-d"))
933 }
934 }
935 }
936 }
937
938 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
940 enum ModelTestName {
941 Test1,
942 Test2,
943 Test3,
944 Test4,
945 Test5,
946 }
947
948 impl ModelTestName {
949 fn test_case_name(self) -> &'static TestCaseName {
950 match self {
951 Self::Test1 => {
952 static NAME: OnceLock<TestCaseName> = OnceLock::new();
953 NAME.get_or_init(|| TestCaseName::new("test_1"))
954 }
955 Self::Test2 => {
956 static NAME: OnceLock<TestCaseName> = OnceLock::new();
957 NAME.get_or_init(|| TestCaseName::new("test_2"))
958 }
959 Self::Test3 => {
960 static NAME: OnceLock<TestCaseName> = OnceLock::new();
961 NAME.get_or_init(|| TestCaseName::new("test_3"))
962 }
963 Self::Test4 => {
964 static NAME: OnceLock<TestCaseName> = OnceLock::new();
965 NAME.get_or_init(|| TestCaseName::new("test_4"))
966 }
967 Self::Test5 => {
968 static NAME: OnceLock<TestCaseName> = OnceLock::new();
969 NAME.get_or_init(|| TestCaseName::new("test_5"))
970 }
971 }
972 }
973 }
974
975 #[derive(Clone, Debug)]
977 enum BinaryModel {
978 Listed {
980 tests: BTreeMap<ModelTestName, FilterMatch>,
981 },
982 Skipped,
984 }
985
986 #[derive(Clone, Debug)]
988 struct TestListModel {
989 binaries: BTreeMap<ModelBinaryId, BinaryModel>,
990 }
991
992 #[derive(Clone, Debug)]
994 struct RunStep {
995 test_list: TestListModel,
997 outcomes: BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
999 }
1000
1001 #[derive(Clone, Debug)]
1003 struct RerunModel {
1004 initial: RunStep,
1006 reruns: Vec<RunStep>,
1008 }
1009
1010 impl TestListInfo for TestListModel {
1011 type BinaryIter<'a> = TestListModelBinaryIter<'a>;
1012
1013 fn binaries(&self) -> Self::BinaryIter<'_> {
1014 TestListModelBinaryIter {
1015 inner: self.binaries.iter(),
1016 }
1017 }
1018 }
1019
1020 struct TestListModelBinaryIter<'a> {
1022 inner: btree_map::Iter<'a, ModelBinaryId, BinaryModel>,
1023 }
1024
1025 impl<'a> Iterator for TestListModelBinaryIter<'a> {
1026 type Item = (&'a RustBinaryId, BinaryInfo<'a>);
1027
1028 fn next(&mut self) -> Option<Self::Item> {
1029 self.inner.next().map(|(model_id, binary_model)| {
1030 let rust_id = model_id.rust_binary_id();
1031 let info = match binary_model {
1032 BinaryModel::Listed { tests } => BinaryInfo::Listed {
1033 test_cases: Box::new(
1034 tests.iter().map(|(name, fm)| (name.test_case_name(), *fm)),
1035 ),
1036 },
1037 BinaryModel::Skipped => BinaryInfo::Skipped,
1038 };
1039 (rust_id, info)
1040 })
1041 }
1042 }
1043
1044 fn arb_model_binary_id() -> impl Strategy<Value = ModelBinaryId> {
1049 prop_oneof![
1050 Just(ModelBinaryId::A),
1051 Just(ModelBinaryId::B),
1052 Just(ModelBinaryId::C),
1053 Just(ModelBinaryId::D),
1054 ]
1055 }
1056
1057 fn arb_model_test_name() -> impl Strategy<Value = ModelTestName> {
1058 prop_oneof![
1059 Just(ModelTestName::Test1),
1060 Just(ModelTestName::Test2),
1061 Just(ModelTestName::Test3),
1062 Just(ModelTestName::Test4),
1063 Just(ModelTestName::Test5),
1064 ]
1065 }
1066
1067 fn arb_filter_match() -> impl Strategy<Value = FilterMatch> {
1068 prop_oneof![
1069 4 => Just(FilterMatch::Matches),
1070 1 => any::<MismatchReason>().prop_map(|reason| FilterMatch::Mismatch { reason }),
1071 ]
1072 }
1073
1074 fn arb_test_outcome() -> impl Strategy<Value = TestOutcome> {
1075 prop_oneof![
1076 4 => Just(TestOutcome::Passed),
1077 2 => Just(TestOutcome::Failed),
1078 1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
1079 1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
1080 ]
1081 }
1082
1083 fn arb_test_map() -> impl Strategy<Value = BTreeMap<ModelTestName, FilterMatch>> {
1084 proptest::collection::btree_map(arb_model_test_name(), arb_filter_match(), 0..5)
1085 }
1086
1087 fn arb_binary_model() -> impl Strategy<Value = BinaryModel> {
1088 prop_oneof![
1089 8 => arb_test_map().prop_map(|tests| BinaryModel::Listed { tests }),
1090 2 => Just(BinaryModel::Skipped),
1091 ]
1092 }
1093
1094 fn arb_test_list_model() -> impl Strategy<Value = TestListModel> {
1095 proptest::collection::btree_map(arb_model_binary_id(), arb_binary_model(), 0..4)
1096 .prop_map(|binaries| TestListModel { binaries })
1097 }
1098
1099 fn arb_outcomes_for_matching_tests(
1104 matching_tests: Vec<(ModelBinaryId, ModelTestName)>,
1105 ) -> BoxedStrategy<BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>> {
1106 if matching_tests.is_empty() {
1107 Just(BTreeMap::new()).boxed()
1108 } else {
1109 let len = matching_tests.len();
1110 proptest::collection::btree_map(
1111 proptest::sample::select(matching_tests),
1112 arb_test_outcome(),
1113 0..=len,
1114 )
1115 .boxed()
1116 }
1117 }
1118
1119 fn extract_matching_tests(test_list: &TestListModel) -> Vec<(ModelBinaryId, ModelTestName)> {
1121 test_list
1122 .binaries
1123 .iter()
1124 .filter_map(|(binary_id, model)| match model {
1125 BinaryModel::Listed { tests } => Some(
1126 tests
1127 .iter()
1128 .filter(|(_, fm)| matches!(fm, FilterMatch::Matches))
1129 .map(move |(tn, _)| (*binary_id, *tn)),
1130 ),
1131 BinaryModel::Skipped => None,
1132 })
1133 .flatten()
1134 .collect()
1135 }
1136
1137 fn arb_run_step() -> impl Strategy<Value = RunStep> {
1138 arb_test_list_model().prop_flat_map(|test_list| {
1139 let matching_tests = extract_matching_tests(&test_list);
1140 arb_outcomes_for_matching_tests(matching_tests).prop_map(move |outcomes| RunStep {
1141 test_list: test_list.clone(),
1142 outcomes,
1143 })
1144 })
1145 }
1146
1147 fn arb_rerun_model() -> impl Strategy<Value = RerunModel> {
1148 (
1149 arb_run_step(),
1150 proptest::collection::vec(arb_run_step(), 0..5),
1151 )
1152 .prop_map(|(initial, reruns)| RerunModel { initial, reruns })
1153 }
1154
1155 fn model_outcomes_to_hashmap(
1160 outcomes: &BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
1161 ) -> HashMap<OwnedTestInstanceId, TestOutcome> {
1162 outcomes
1163 .iter()
1164 .map(|((binary_id, test_name), outcome)| {
1165 let id = OwnedTestInstanceId {
1166 binary_id: binary_id.rust_binary_id().clone(),
1167 test_name: test_name.test_case_name().clone(),
1168 };
1169 (id, *outcome)
1170 })
1171 .collect()
1172 }
1173
1174 fn run_sut(model: &RerunModel) -> IdOrdMap<RerunTestSuiteInfo> {
1180 let outcomes = model_outcomes_to_hashmap(&model.initial.outcomes);
1181 let mut result = compute_outstanding_pure(None, &model.initial.test_list, &outcomes);
1182
1183 for rerun in &model.reruns {
1184 let outcomes = model_outcomes_to_hashmap(&rerun.outcomes);
1185 result = compute_outstanding_pure(Some(&result), &rerun.test_list, &outcomes);
1186 }
1187
1188 result
1189 }
1190
1191 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1201 enum PrevStatus {
1202 Passing,
1204 Outstanding,
1206 Unknown,
1208 }
1209
1210 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1212 enum Decision {
1213 Passing,
1215 Outstanding,
1217 NotTracked,
1219 }
1220
1221 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1226 enum FilterMatchResult {
1227 BinaryNotPresent,
1230 BinarySkipped,
1233 TestNotInList,
1236 HasMatch(FilterMatch),
1238 }
1239
1240 fn decide_test_outcome(
1245 prev: PrevStatus,
1246 filter_result: FilterMatchResult,
1247 outcome: Option<TestOutcome>,
1248 ) -> Decision {
1249 match filter_result {
1250 FilterMatchResult::BinaryNotPresent | FilterMatchResult::BinarySkipped => {
1251 match prev {
1253 PrevStatus::Passing => Decision::Passing,
1254 PrevStatus::Outstanding => Decision::Outstanding,
1255 PrevStatus::Unknown => Decision::NotTracked,
1256 }
1257 }
1258 FilterMatchResult::TestNotInList => {
1259 match prev {
1264 PrevStatus::Outstanding => Decision::Outstanding,
1265 PrevStatus::Passing | PrevStatus::Unknown => Decision::NotTracked,
1266 }
1267 }
1268 FilterMatchResult::HasMatch(FilterMatch::Matches) => {
1269 match outcome {
1270 Some(TestOutcome::Passed) => Decision::Passing,
1271 Some(TestOutcome::Failed) => Decision::Outstanding,
1272 None => {
1273 Decision::Outstanding
1275 }
1276 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)) => Decision::Passing,
1277 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => {
1278 match prev {
1280 PrevStatus::Passing => Decision::Passing,
1281 PrevStatus::Outstanding => Decision::Outstanding,
1282 PrevStatus::Unknown => Decision::NotTracked,
1283 }
1284 }
1285 }
1286 }
1287 FilterMatchResult::HasMatch(FilterMatch::Mismatch { reason }) => {
1288 match TestOutcomeSkipped::from_mismatch_reason(reason) {
1289 TestOutcomeSkipped::Rerun => Decision::Passing,
1290 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 }
1301 }
1302
1303 impl RerunModel {
1304 fn compute_rerun_info_decision_table(&self) -> IdOrdMap<RerunTestSuiteInfo> {
1310 let mut prev_state: HashMap<(ModelBinaryId, ModelTestName), PrevStatus> =
1312 HashMap::new();
1313
1314 self.update_state_from_step(&mut prev_state, &self.initial);
1316
1317 for rerun in &self.reruns {
1319 self.update_state_from_step(&mut prev_state, rerun);
1320 }
1321
1322 self.collect_final_state(&prev_state)
1324 }
1325
1326 fn update_state_from_step(
1327 &self,
1328 state: &mut HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1329 step: &RunStep,
1330 ) {
1331 let all_tests = self.enumerate_all_tests(state, step);
1335
1336 for (binary_id, test_name) in all_tests {
1337 let prev = state
1338 .get(&(binary_id, test_name))
1339 .copied()
1340 .unwrap_or(PrevStatus::Unknown);
1341
1342 let filter_result = self.get_filter_match_result(step, binary_id, test_name);
1343 let outcome = step.outcomes.get(&(binary_id, test_name)).copied();
1344
1345 let decision = decide_test_outcome(prev, filter_result, outcome);
1346
1347 match decision {
1349 Decision::Passing => {
1350 state.insert((binary_id, test_name), PrevStatus::Passing);
1351 }
1352 Decision::Outstanding => {
1353 state.insert((binary_id, test_name), PrevStatus::Outstanding);
1354 }
1355 Decision::NotTracked => {
1356 state.remove(&(binary_id, test_name));
1357 }
1358 }
1359 }
1360 }
1361
1362 fn get_filter_match_result(
1367 &self,
1368 step: &RunStep,
1369 binary_id: ModelBinaryId,
1370 test_name: ModelTestName,
1371 ) -> FilterMatchResult {
1372 match step.test_list.binaries.get(&binary_id) {
1373 None => FilterMatchResult::BinaryNotPresent,
1374 Some(BinaryModel::Skipped) => FilterMatchResult::BinarySkipped,
1375 Some(BinaryModel::Listed { tests }) => match tests.get(&test_name) {
1376 Some(filter_match) => FilterMatchResult::HasMatch(*filter_match),
1377 None => FilterMatchResult::TestNotInList,
1378 },
1379 }
1380 }
1381
1382 fn enumerate_all_tests(
1387 &self,
1388 prev_state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1389 step: &RunStep,
1390 ) -> BTreeSet<(ModelBinaryId, ModelTestName)> {
1391 let mut tests = BTreeSet::new();
1392
1393 for (binary_id, binary_model) in &step.test_list.binaries {
1395 if let BinaryModel::Listed { tests: test_map } = binary_model {
1396 for test_name in test_map.keys() {
1397 tests.insert((*binary_id, *test_name));
1398 }
1399 }
1400 }
1401
1402 for (binary_id, test_name) in prev_state.keys() {
1404 tests.insert((*binary_id, *test_name));
1405 }
1406
1407 tests
1408 }
1409
1410 fn collect_final_state(
1412 &self,
1413 state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1414 ) -> IdOrdMap<RerunTestSuiteInfo> {
1415 let mut result: BTreeMap<ModelBinaryId, RerunTestSuiteInfo> = BTreeMap::new();
1416
1417 for ((binary_id, test_name), status) in state {
1418 let suite = result
1419 .entry(*binary_id)
1420 .or_insert_with(|| RerunTestSuiteInfo::new(binary_id.rust_binary_id().clone()));
1421
1422 match status {
1423 PrevStatus::Passing => {
1424 suite.passing.insert(test_name.test_case_name().clone());
1425 }
1426 PrevStatus::Outstanding => {
1427 suite.outstanding.insert(test_name.test_case_name().clone());
1428 }
1429 PrevStatus::Unknown => {
1430 }
1432 }
1433 }
1434
1435 let mut id_map = IdOrdMap::new();
1436 for (_, suite) in result {
1437 id_map.insert_unique(suite).expect("unique binaries");
1438 }
1439 id_map
1440 }
1441 }
1442
1443 fn make_test_finished(
1452 test_instance: OwnedTestInstanceId,
1453 stress_index: Option<(u32, Option<u32>)>,
1454 passed: bool,
1455 ) -> TestEventKindSummary<RecordingSpec> {
1456 let result = if passed {
1457 ExecutionResultDescription::Pass
1458 } else {
1459 ExecutionResultDescription::Fail {
1460 failure: FailureDescription::ExitCode { code: 1 },
1461 leaked: false,
1462 }
1463 };
1464
1465 let execute_status = ExecuteStatus {
1466 retry_data: RetryData {
1467 attempt: 1,
1468 total_attempts: 1,
1469 },
1470 output: ChildExecutionOutputDescription::Output {
1471 result: Some(result.clone()),
1472 output: ZipStoreOutputDescription::Split {
1473 stdout: None,
1474 stderr: None,
1475 },
1476 errors: None,
1477 },
1478 result,
1479 start_time: Utc::now().into(),
1480 time_taken: Duration::from_millis(100),
1481 is_slow: false,
1482 delay_before_start: Duration::ZERO,
1483 error_summary: None,
1484 output_error_slice: None,
1485 };
1486
1487 TestEventKindSummary::Output(OutputEventKind::TestFinished {
1488 stress_index: stress_index.map(|(current, total)| StressIndexSummary {
1489 current,
1490 total: total.and_then(NonZero::new),
1491 }),
1492 test_instance,
1493 success_output: TestOutputDisplay::Never,
1494 failure_output: TestOutputDisplay::Never,
1495 junit_store_success_output: false,
1496 junit_store_failure_output: false,
1497 run_statuses: ExecutionStatuses::new(vec![execute_status]),
1498 current_stats: RunStats::default(),
1499 running: 0,
1500 })
1501 }
1502
1503 #[test]
1509 fn stress_run_accumulation() {
1510 let test_pass_fail_pass = OwnedTestInstanceId {
1512 binary_id: RustBinaryId::new("test-binary"),
1513 test_name: TestCaseName::new("pass_fail_pass"),
1514 };
1515
1516 let test_all_pass = OwnedTestInstanceId {
1518 binary_id: RustBinaryId::new("test-binary"),
1519 test_name: TestCaseName::new("all_pass"),
1520 };
1521
1522 let test_all_fail = OwnedTestInstanceId {
1524 binary_id: RustBinaryId::new("test-binary"),
1525 test_name: TestCaseName::new("all_fail"),
1526 };
1527
1528 let test_fail_first = OwnedTestInstanceId {
1530 binary_id: RustBinaryId::new("test-binary"),
1531 test_name: TestCaseName::new("fail_first"),
1532 };
1533
1534 let test_regular_pass = OwnedTestInstanceId {
1536 binary_id: RustBinaryId::new("test-binary"),
1537 test_name: TestCaseName::new("regular_pass"),
1538 };
1539
1540 let test_regular_fail = OwnedTestInstanceId {
1542 binary_id: RustBinaryId::new("test-binary"),
1543 test_name: TestCaseName::new("regular_fail"),
1544 };
1545
1546 let events = [
1548 make_test_finished(test_pass_fail_pass.clone(), Some((0, Some(3))), true),
1550 make_test_finished(test_pass_fail_pass.clone(), Some((1, Some(3))), false),
1551 make_test_finished(test_pass_fail_pass.clone(), Some((2, Some(3))), true),
1552 make_test_finished(test_all_pass.clone(), Some((0, Some(3))), true),
1554 make_test_finished(test_all_pass.clone(), Some((1, Some(3))), true),
1555 make_test_finished(test_all_pass.clone(), Some((2, Some(3))), true),
1556 make_test_finished(test_all_fail.clone(), Some((0, Some(3))), false),
1558 make_test_finished(test_all_fail.clone(), Some((1, Some(3))), false),
1559 make_test_finished(test_all_fail.clone(), Some((2, Some(3))), false),
1560 make_test_finished(test_fail_first.clone(), Some((0, Some(3))), false),
1562 make_test_finished(test_fail_first.clone(), Some((1, Some(3))), true),
1563 make_test_finished(test_fail_first.clone(), Some((2, Some(3))), true),
1564 make_test_finished(test_regular_pass.clone(), None, true),
1566 make_test_finished(test_regular_fail.clone(), None, false),
1568 ];
1569
1570 let outcomes = collect_from_events(events.iter().map(Ok::<_, Infallible>)).unwrap();
1571
1572 assert_eq!(
1573 outcomes.get(&test_pass_fail_pass),
1574 Some(&TestOutcome::Failed),
1575 "[Pass, Fail, Pass] should be Failed"
1576 );
1577 assert_eq!(
1578 outcomes.get(&test_all_pass),
1579 Some(&TestOutcome::Passed),
1580 "[Pass, Pass, Pass] should be Passed"
1581 );
1582 assert_eq!(
1583 outcomes.get(&test_all_fail),
1584 Some(&TestOutcome::Failed),
1585 "[Fail, Fail, Fail] should be Failed"
1586 );
1587 assert_eq!(
1588 outcomes.get(&test_fail_first),
1589 Some(&TestOutcome::Failed),
1590 "[Fail, Pass, Pass] should be Failed"
1591 );
1592 assert_eq!(
1593 outcomes.get(&test_regular_pass),
1594 Some(&TestOutcome::Passed),
1595 "regular pass should be Passed"
1596 );
1597 assert_eq!(
1598 outcomes.get(&test_regular_fail),
1599 Some(&TestOutcome::Failed),
1600 "regular fail should be Failed"
1601 );
1602 }
1603
1604 #[test]
1609 fn stress_run_multiple_tests_independent() {
1610 let test_a = OwnedTestInstanceId {
1611 binary_id: RustBinaryId::new("test-binary"),
1612 test_name: TestCaseName::new("test_a"),
1613 };
1614 let test_b = OwnedTestInstanceId {
1615 binary_id: RustBinaryId::new("test-binary"),
1616 test_name: TestCaseName::new("test_b"),
1617 };
1618
1619 let events = [
1623 make_test_finished(test_a.clone(), Some((0, Some(2))), true),
1624 make_test_finished(test_b.clone(), Some((0, Some(2))), true),
1625 make_test_finished(test_a.clone(), Some((1, Some(2))), true),
1626 make_test_finished(test_b.clone(), Some((1, Some(2))), false),
1627 ];
1628
1629 let outcomes = collect_from_events(events.iter().map(Ok::<_, Infallible>)).unwrap();
1630
1631 assert_eq!(
1632 outcomes.get(&test_a),
1633 Some(&TestOutcome::Passed),
1634 "test_a [Pass, Pass] should be Passed"
1635 );
1636 assert_eq!(
1637 outcomes.get(&test_b),
1638 Some(&TestOutcome::Failed),
1639 "test_b [Pass, Fail] should be Failed"
1640 );
1641 }
1642}