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, O, E>(
424 events: impl Iterator<Item = Result<K, E>>,
425) -> Result<HashMap<OwnedTestInstanceId, TestOutcome>, E>
426where
427 K: Borrow<TestEventKindSummary<O>>,
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 record::{OutputEventKind, StressIndexSummary, TestEventKindSummary},
481 reporter::{
482 TestOutputDisplay,
483 events::{
484 ChildExecutionOutputDescription, ChildOutputDescription, ExecuteStatus,
485 ExecutionResultDescription, ExecutionStatuses, FailureDescription, RetryData,
486 RunStats,
487 },
488 },
489 };
490 use chrono::Utc;
491 use proptest::prelude::*;
492 use std::{
493 collections::{BTreeMap, btree_map},
494 convert::Infallible,
495 num::NonZero,
496 sync::OnceLock,
497 time::Duration,
498 };
499 use test_strategy::proptest;
500
501 #[proptest(cases = 200)]
507 fn sut_matches_oracle(#[strategy(arb_rerun_model())] model: RerunModel) {
508 let expected = model.compute_rerun_info_decision_table();
509 let actual = run_sut(&model);
510 prop_assert_eq!(actual, expected);
511 }
512
513 #[proptest(cases = 200)]
515 fn passing_and_outstanding_disjoint(#[strategy(arb_rerun_model())] model: RerunModel) {
516 let result = run_sut(&model);
517 for suite in result.iter() {
518 let intersection: BTreeSet<_> =
519 suite.passing.intersection(&suite.outstanding).collect();
520 prop_assert!(
521 intersection.is_empty(),
522 "passing and outstanding should be disjoint for {}: {:?}",
523 suite.binary_id,
524 intersection
525 );
526 }
527 }
528
529 #[proptest(cases = 200)]
535 fn matching_tests_with_outcomes_are_tracked(#[strategy(arb_rerun_model())] model: RerunModel) {
536 let result = run_sut(&model);
537
538 let final_step = model.reruns.last().unwrap_or(&model.initial);
540
541 for (binary_id, binary_model) in &final_step.test_list.binaries {
542 if let BinaryModel::Listed { tests } = binary_model {
543 let rust_binary_id = binary_id.rust_binary_id();
544
545 for (test_name, filter_match) in tests {
546 if matches!(filter_match, FilterMatch::Matches) {
547 let key = (*binary_id, *test_name);
548 let outcome = final_step.outcomes.get(&key);
549
550 let should_be_tracked = match outcome {
555 Some(TestOutcome::Passed)
556 | Some(TestOutcome::Failed)
557 | Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun))
558 | None => true,
559 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => false,
560 };
561
562 if should_be_tracked {
563 let tcn = test_name.test_case_name();
564 let suite = result.get(&rust_binary_id);
565 let in_passing = suite.is_some_and(|s| s.passing.contains(tcn));
566 let in_outstanding = suite.is_some_and(|s| s.outstanding.contains(tcn));
567 prop_assert!(
568 in_passing || in_outstanding,
569 "matching test {:?}::{:?} with outcome {:?} should be in passing or outstanding",
570 binary_id,
571 test_name,
572 outcome
573 );
574 }
575 }
576 }
577 }
578 }
579 }
580
581 #[test]
583 fn decide_test_outcome_truth_table() {
584 use Decision as D;
585 use FilterMatchResult as F;
586 use PrevStatus as P;
587
588 assert_eq!(
590 decide_test_outcome(P::Passing, F::BinaryNotPresent, None),
591 D::Passing
592 );
593 assert_eq!(
594 decide_test_outcome(P::Outstanding, F::BinaryNotPresent, None),
595 D::Outstanding
596 );
597 assert_eq!(
598 decide_test_outcome(P::Unknown, F::BinaryNotPresent, None),
599 D::NotTracked
600 );
601
602 assert_eq!(
604 decide_test_outcome(P::Passing, F::BinarySkipped, None),
605 D::Passing
606 );
607 assert_eq!(
608 decide_test_outcome(P::Outstanding, F::BinarySkipped, None),
609 D::Outstanding
610 );
611 assert_eq!(
612 decide_test_outcome(P::Unknown, F::BinarySkipped, None),
613 D::NotTracked
614 );
615
616 assert_eq!(
618 decide_test_outcome(P::Passing, F::TestNotInList, None),
619 D::NotTracked
620 );
621 assert_eq!(
622 decide_test_outcome(P::Outstanding, F::TestNotInList, None),
623 D::Outstanding
624 );
625 assert_eq!(
626 decide_test_outcome(P::Unknown, F::TestNotInList, None),
627 D::NotTracked
628 );
629
630 let matches = F::HasMatch(FilterMatch::Matches);
632
633 assert_eq!(
635 decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Passed)),
636 D::Passing
637 );
638 assert_eq!(
639 decide_test_outcome(P::Passing, matches, Some(TestOutcome::Passed)),
640 D::Passing
641 );
642 assert_eq!(
643 decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Passed)),
644 D::Passing
645 );
646
647 assert_eq!(
649 decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Failed)),
650 D::Outstanding
651 );
652 assert_eq!(
653 decide_test_outcome(P::Passing, matches, Some(TestOutcome::Failed)),
654 D::Outstanding
655 );
656 assert_eq!(
657 decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Failed)),
658 D::Outstanding
659 );
660
661 assert_eq!(
663 decide_test_outcome(P::Unknown, matches, None),
664 D::Outstanding
665 );
666 assert_eq!(
667 decide_test_outcome(P::Passing, matches, None),
668 D::Outstanding
669 );
670 assert_eq!(
671 decide_test_outcome(P::Outstanding, matches, None),
672 D::Outstanding
673 );
674
675 let rerun_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun));
677 assert_eq!(
678 decide_test_outcome(P::Unknown, matches, rerun_skipped),
679 D::Passing
680 );
681 assert_eq!(
682 decide_test_outcome(P::Passing, matches, rerun_skipped),
683 D::Passing
684 );
685 assert_eq!(
686 decide_test_outcome(P::Outstanding, matches, rerun_skipped),
687 D::Passing
688 );
689
690 let explicit_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit));
692 assert_eq!(
693 decide_test_outcome(P::Unknown, matches, explicit_skipped),
694 D::NotTracked
695 );
696 assert_eq!(
697 decide_test_outcome(P::Passing, matches, explicit_skipped),
698 D::Passing
699 );
700 assert_eq!(
701 decide_test_outcome(P::Outstanding, matches, explicit_skipped),
702 D::Outstanding
703 );
704
705 let rerun_mismatch = F::HasMatch(FilterMatch::Mismatch {
707 reason: MismatchReason::RerunAlreadyPassed,
708 });
709 assert_eq!(
710 decide_test_outcome(P::Unknown, rerun_mismatch, None),
711 D::Passing
712 );
713 assert_eq!(
714 decide_test_outcome(P::Passing, rerun_mismatch, None),
715 D::Passing
716 );
717 assert_eq!(
718 decide_test_outcome(P::Outstanding, rerun_mismatch, None),
719 D::Passing
720 );
721
722 let explicit_mismatch = F::HasMatch(FilterMatch::Mismatch {
724 reason: MismatchReason::Ignored,
725 });
726 assert_eq!(
727 decide_test_outcome(P::Unknown, explicit_mismatch, None),
728 D::NotTracked
729 );
730 assert_eq!(
731 decide_test_outcome(P::Passing, explicit_mismatch, None),
732 D::Passing
733 );
734 assert_eq!(
735 decide_test_outcome(P::Outstanding, explicit_mismatch, None),
736 D::Outstanding
737 );
738 }
739
740 const ALL_PREV_STATUSES: [PrevStatus; 3] = [
749 PrevStatus::Passing,
750 PrevStatus::Outstanding,
751 PrevStatus::Unknown,
752 ];
753
754 fn all_outcomes() -> [Option<TestOutcome>; 5] {
756 [
757 None,
758 Some(TestOutcome::Passed),
759 Some(TestOutcome::Failed),
760 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
761 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
762 ]
763 }
764
765 fn all_in_list_filter_results() -> Vec<FilterMatchResult> {
767 let mut results = vec![FilterMatchResult::HasMatch(FilterMatch::Matches)];
768 for &reason in MismatchReason::ALL_VARIANTS {
769 results.push(FilterMatchResult::HasMatch(FilterMatch::Mismatch {
770 reason,
771 }));
772 }
773 results
774 }
775
776 #[test]
784 fn spec_property_passing_monotonicity() {
785 let non_regressing_outcomes = [
786 Some(TestOutcome::Passed),
787 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
788 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
789 ];
790
791 for filter in all_in_list_filter_results() {
792 for outcome in non_regressing_outcomes {
793 let decision = decide_test_outcome(PrevStatus::Passing, filter, outcome);
794 assert_eq!(
795 decision,
796 Decision::Passing,
797 "monotonicity violated: Passing + {:?} + {:?} -> {:?}",
798 filter,
799 outcome,
800 decision
801 );
802 }
803 }
804 }
805
806 #[test]
811 fn spec_property_outstanding_to_passing_on_pass() {
812 let passing_outcomes = [
813 Some(TestOutcome::Passed),
814 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
815 ];
816
817 for outcome in passing_outcomes {
818 let decision = decide_test_outcome(
819 PrevStatus::Outstanding,
820 FilterMatchResult::HasMatch(FilterMatch::Matches),
821 outcome,
822 );
823 assert_eq!(
824 decision,
825 Decision::Passing,
826 "convergence violated: Outstanding + Matches + {:?} -> {:?}",
827 outcome,
828 decision
829 );
830 }
831 }
832
833 #[test]
837 fn spec_property_failed_becomes_outstanding() {
838 let failing_outcomes = [None, Some(TestOutcome::Failed)];
839
840 for prev in ALL_PREV_STATUSES {
841 for outcome in failing_outcomes {
842 let decision = decide_test_outcome(
843 prev,
844 FilterMatchResult::HasMatch(FilterMatch::Matches),
845 outcome,
846 );
847 assert_eq!(
848 decision,
849 Decision::Outstanding,
850 "FAILED->OUTSTANDING VIOLATED: {:?} + Matches + {:?} -> {:?}",
851 prev,
852 outcome,
853 decision
854 );
855 }
856 }
857 }
858
859 #[test]
866 fn spec_property_test_not_in_list_behavior() {
867 for outcome in all_outcomes() {
868 assert_eq!(
870 decide_test_outcome(
871 PrevStatus::Outstanding,
872 FilterMatchResult::TestNotInList,
873 outcome
874 ),
875 Decision::Outstanding,
876 );
877 assert_eq!(
879 decide_test_outcome(
880 PrevStatus::Passing,
881 FilterMatchResult::TestNotInList,
882 outcome
883 ),
884 Decision::NotTracked,
885 );
886 assert_eq!(
888 decide_test_outcome(
889 PrevStatus::Unknown,
890 FilterMatchResult::TestNotInList,
891 outcome
892 ),
893 Decision::NotTracked,
894 );
895 }
896 }
897
898 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
906 enum ModelBinaryId {
907 A,
908 B,
909 C,
910 D,
911 }
912
913 impl ModelBinaryId {
914 fn rust_binary_id(self) -> &'static RustBinaryId {
915 match self {
916 Self::A => {
917 static ID: OnceLock<RustBinaryId> = OnceLock::new();
918 ID.get_or_init(|| RustBinaryId::new("binary-a"))
919 }
920 Self::B => {
921 static ID: OnceLock<RustBinaryId> = OnceLock::new();
922 ID.get_or_init(|| RustBinaryId::new("binary-b"))
923 }
924 Self::C => {
925 static ID: OnceLock<RustBinaryId> = OnceLock::new();
926 ID.get_or_init(|| RustBinaryId::new("binary-c"))
927 }
928 Self::D => {
929 static ID: OnceLock<RustBinaryId> = OnceLock::new();
930 ID.get_or_init(|| RustBinaryId::new("binary-d"))
931 }
932 }
933 }
934 }
935
936 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
938 enum ModelTestName {
939 Test1,
940 Test2,
941 Test3,
942 Test4,
943 Test5,
944 }
945
946 impl ModelTestName {
947 fn test_case_name(self) -> &'static TestCaseName {
948 match self {
949 Self::Test1 => {
950 static NAME: OnceLock<TestCaseName> = OnceLock::new();
951 NAME.get_or_init(|| TestCaseName::new("test_1"))
952 }
953 Self::Test2 => {
954 static NAME: OnceLock<TestCaseName> = OnceLock::new();
955 NAME.get_or_init(|| TestCaseName::new("test_2"))
956 }
957 Self::Test3 => {
958 static NAME: OnceLock<TestCaseName> = OnceLock::new();
959 NAME.get_or_init(|| TestCaseName::new("test_3"))
960 }
961 Self::Test4 => {
962 static NAME: OnceLock<TestCaseName> = OnceLock::new();
963 NAME.get_or_init(|| TestCaseName::new("test_4"))
964 }
965 Self::Test5 => {
966 static NAME: OnceLock<TestCaseName> = OnceLock::new();
967 NAME.get_or_init(|| TestCaseName::new("test_5"))
968 }
969 }
970 }
971 }
972
973 #[derive(Clone, Debug)]
975 enum BinaryModel {
976 Listed {
978 tests: BTreeMap<ModelTestName, FilterMatch>,
979 },
980 Skipped,
982 }
983
984 #[derive(Clone, Debug)]
986 struct TestListModel {
987 binaries: BTreeMap<ModelBinaryId, BinaryModel>,
988 }
989
990 #[derive(Clone, Debug)]
992 struct RunStep {
993 test_list: TestListModel,
995 outcomes: BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
997 }
998
999 #[derive(Clone, Debug)]
1001 struct RerunModel {
1002 initial: RunStep,
1004 reruns: Vec<RunStep>,
1006 }
1007
1008 impl TestListInfo for TestListModel {
1009 type BinaryIter<'a> = TestListModelBinaryIter<'a>;
1010
1011 fn binaries(&self) -> Self::BinaryIter<'_> {
1012 TestListModelBinaryIter {
1013 inner: self.binaries.iter(),
1014 }
1015 }
1016 }
1017
1018 struct TestListModelBinaryIter<'a> {
1020 inner: btree_map::Iter<'a, ModelBinaryId, BinaryModel>,
1021 }
1022
1023 impl<'a> Iterator for TestListModelBinaryIter<'a> {
1024 type Item = (&'a RustBinaryId, BinaryInfo<'a>);
1025
1026 fn next(&mut self) -> Option<Self::Item> {
1027 self.inner.next().map(|(model_id, binary_model)| {
1028 let rust_id = model_id.rust_binary_id();
1029 let info = match binary_model {
1030 BinaryModel::Listed { tests } => BinaryInfo::Listed {
1031 test_cases: Box::new(
1032 tests.iter().map(|(name, fm)| (name.test_case_name(), *fm)),
1033 ),
1034 },
1035 BinaryModel::Skipped => BinaryInfo::Skipped,
1036 };
1037 (rust_id, info)
1038 })
1039 }
1040 }
1041
1042 fn arb_model_binary_id() -> impl Strategy<Value = ModelBinaryId> {
1047 prop_oneof![
1048 Just(ModelBinaryId::A),
1049 Just(ModelBinaryId::B),
1050 Just(ModelBinaryId::C),
1051 Just(ModelBinaryId::D),
1052 ]
1053 }
1054
1055 fn arb_model_test_name() -> impl Strategy<Value = ModelTestName> {
1056 prop_oneof![
1057 Just(ModelTestName::Test1),
1058 Just(ModelTestName::Test2),
1059 Just(ModelTestName::Test3),
1060 Just(ModelTestName::Test4),
1061 Just(ModelTestName::Test5),
1062 ]
1063 }
1064
1065 fn arb_filter_match() -> impl Strategy<Value = FilterMatch> {
1066 prop_oneof![
1067 4 => Just(FilterMatch::Matches),
1068 1 => any::<MismatchReason>().prop_map(|reason| FilterMatch::Mismatch { reason }),
1069 ]
1070 }
1071
1072 fn arb_test_outcome() -> impl Strategy<Value = TestOutcome> {
1073 prop_oneof![
1074 4 => Just(TestOutcome::Passed),
1075 2 => Just(TestOutcome::Failed),
1076 1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
1077 1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
1078 ]
1079 }
1080
1081 fn arb_test_map() -> impl Strategy<Value = BTreeMap<ModelTestName, FilterMatch>> {
1082 proptest::collection::btree_map(arb_model_test_name(), arb_filter_match(), 0..5)
1083 }
1084
1085 fn arb_binary_model() -> impl Strategy<Value = BinaryModel> {
1086 prop_oneof![
1087 8 => arb_test_map().prop_map(|tests| BinaryModel::Listed { tests }),
1088 2 => Just(BinaryModel::Skipped),
1089 ]
1090 }
1091
1092 fn arb_test_list_model() -> impl Strategy<Value = TestListModel> {
1093 proptest::collection::btree_map(arb_model_binary_id(), arb_binary_model(), 0..4)
1094 .prop_map(|binaries| TestListModel { binaries })
1095 }
1096
1097 fn arb_outcomes_for_matching_tests(
1102 matching_tests: Vec<(ModelBinaryId, ModelTestName)>,
1103 ) -> BoxedStrategy<BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>> {
1104 if matching_tests.is_empty() {
1105 Just(BTreeMap::new()).boxed()
1106 } else {
1107 let len = matching_tests.len();
1108 proptest::collection::btree_map(
1109 proptest::sample::select(matching_tests),
1110 arb_test_outcome(),
1111 0..=len,
1112 )
1113 .boxed()
1114 }
1115 }
1116
1117 fn extract_matching_tests(test_list: &TestListModel) -> Vec<(ModelBinaryId, ModelTestName)> {
1119 test_list
1120 .binaries
1121 .iter()
1122 .filter_map(|(binary_id, model)| match model {
1123 BinaryModel::Listed { tests } => Some(
1124 tests
1125 .iter()
1126 .filter(|(_, fm)| matches!(fm, FilterMatch::Matches))
1127 .map(move |(tn, _)| (*binary_id, *tn)),
1128 ),
1129 BinaryModel::Skipped => None,
1130 })
1131 .flatten()
1132 .collect()
1133 }
1134
1135 fn arb_run_step() -> impl Strategy<Value = RunStep> {
1136 arb_test_list_model().prop_flat_map(|test_list| {
1137 let matching_tests = extract_matching_tests(&test_list);
1138 arb_outcomes_for_matching_tests(matching_tests).prop_map(move |outcomes| RunStep {
1139 test_list: test_list.clone(),
1140 outcomes,
1141 })
1142 })
1143 }
1144
1145 fn arb_rerun_model() -> impl Strategy<Value = RerunModel> {
1146 (
1147 arb_run_step(),
1148 proptest::collection::vec(arb_run_step(), 0..5),
1149 )
1150 .prop_map(|(initial, reruns)| RerunModel { initial, reruns })
1151 }
1152
1153 fn model_outcomes_to_hashmap(
1158 outcomes: &BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
1159 ) -> HashMap<OwnedTestInstanceId, TestOutcome> {
1160 outcomes
1161 .iter()
1162 .map(|((binary_id, test_name), outcome)| {
1163 let id = OwnedTestInstanceId {
1164 binary_id: binary_id.rust_binary_id().clone(),
1165 test_name: test_name.test_case_name().clone(),
1166 };
1167 (id, *outcome)
1168 })
1169 .collect()
1170 }
1171
1172 fn run_sut(model: &RerunModel) -> IdOrdMap<RerunTestSuiteInfo> {
1178 let outcomes = model_outcomes_to_hashmap(&model.initial.outcomes);
1179 let mut result = compute_outstanding_pure(None, &model.initial.test_list, &outcomes);
1180
1181 for rerun in &model.reruns {
1182 let outcomes = model_outcomes_to_hashmap(&rerun.outcomes);
1183 result = compute_outstanding_pure(Some(&result), &rerun.test_list, &outcomes);
1184 }
1185
1186 result
1187 }
1188
1189 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1199 enum PrevStatus {
1200 Passing,
1202 Outstanding,
1204 Unknown,
1206 }
1207
1208 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1210 enum Decision {
1211 Passing,
1213 Outstanding,
1215 NotTracked,
1217 }
1218
1219 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1224 enum FilterMatchResult {
1225 BinaryNotPresent,
1228 BinarySkipped,
1231 TestNotInList,
1234 HasMatch(FilterMatch),
1236 }
1237
1238 fn decide_test_outcome(
1243 prev: PrevStatus,
1244 filter_result: FilterMatchResult,
1245 outcome: Option<TestOutcome>,
1246 ) -> Decision {
1247 match filter_result {
1248 FilterMatchResult::BinaryNotPresent | FilterMatchResult::BinarySkipped => {
1249 match prev {
1251 PrevStatus::Passing => Decision::Passing,
1252 PrevStatus::Outstanding => Decision::Outstanding,
1253 PrevStatus::Unknown => Decision::NotTracked,
1254 }
1255 }
1256 FilterMatchResult::TestNotInList => {
1257 match prev {
1262 PrevStatus::Outstanding => Decision::Outstanding,
1263 PrevStatus::Passing | PrevStatus::Unknown => Decision::NotTracked,
1264 }
1265 }
1266 FilterMatchResult::HasMatch(FilterMatch::Matches) => {
1267 match outcome {
1268 Some(TestOutcome::Passed) => Decision::Passing,
1269 Some(TestOutcome::Failed) => Decision::Outstanding,
1270 None => {
1271 Decision::Outstanding
1273 }
1274 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)) => Decision::Passing,
1275 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => {
1276 match prev {
1278 PrevStatus::Passing => Decision::Passing,
1279 PrevStatus::Outstanding => Decision::Outstanding,
1280 PrevStatus::Unknown => Decision::NotTracked,
1281 }
1282 }
1283 }
1284 }
1285 FilterMatchResult::HasMatch(FilterMatch::Mismatch { reason }) => {
1286 match TestOutcomeSkipped::from_mismatch_reason(reason) {
1287 TestOutcomeSkipped::Rerun => Decision::Passing,
1288 TestOutcomeSkipped::Explicit => {
1289 match prev {
1291 PrevStatus::Passing => Decision::Passing,
1292 PrevStatus::Outstanding => Decision::Outstanding,
1293 PrevStatus::Unknown => Decision::NotTracked,
1294 }
1295 }
1296 }
1297 }
1298 }
1299 }
1300
1301 impl RerunModel {
1302 fn compute_rerun_info_decision_table(&self) -> IdOrdMap<RerunTestSuiteInfo> {
1308 let mut prev_state: HashMap<(ModelBinaryId, ModelTestName), PrevStatus> =
1310 HashMap::new();
1311
1312 self.update_state_from_step(&mut prev_state, &self.initial);
1314
1315 for rerun in &self.reruns {
1317 self.update_state_from_step(&mut prev_state, rerun);
1318 }
1319
1320 self.collect_final_state(&prev_state)
1322 }
1323
1324 fn update_state_from_step(
1325 &self,
1326 state: &mut HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1327 step: &RunStep,
1328 ) {
1329 let all_tests = self.enumerate_all_tests(state, step);
1333
1334 for (binary_id, test_name) in all_tests {
1335 let prev = state
1336 .get(&(binary_id, test_name))
1337 .copied()
1338 .unwrap_or(PrevStatus::Unknown);
1339
1340 let filter_result = self.get_filter_match_result(step, binary_id, test_name);
1341 let outcome = step.outcomes.get(&(binary_id, test_name)).copied();
1342
1343 let decision = decide_test_outcome(prev, filter_result, outcome);
1344
1345 match decision {
1347 Decision::Passing => {
1348 state.insert((binary_id, test_name), PrevStatus::Passing);
1349 }
1350 Decision::Outstanding => {
1351 state.insert((binary_id, test_name), PrevStatus::Outstanding);
1352 }
1353 Decision::NotTracked => {
1354 state.remove(&(binary_id, test_name));
1355 }
1356 }
1357 }
1358 }
1359
1360 fn get_filter_match_result(
1365 &self,
1366 step: &RunStep,
1367 binary_id: ModelBinaryId,
1368 test_name: ModelTestName,
1369 ) -> FilterMatchResult {
1370 match step.test_list.binaries.get(&binary_id) {
1371 None => FilterMatchResult::BinaryNotPresent,
1372 Some(BinaryModel::Skipped) => FilterMatchResult::BinarySkipped,
1373 Some(BinaryModel::Listed { tests }) => match tests.get(&test_name) {
1374 Some(filter_match) => FilterMatchResult::HasMatch(*filter_match),
1375 None => FilterMatchResult::TestNotInList,
1376 },
1377 }
1378 }
1379
1380 fn enumerate_all_tests(
1385 &self,
1386 prev_state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1387 step: &RunStep,
1388 ) -> BTreeSet<(ModelBinaryId, ModelTestName)> {
1389 let mut tests = BTreeSet::new();
1390
1391 for (binary_id, binary_model) in &step.test_list.binaries {
1393 if let BinaryModel::Listed { tests: test_map } = binary_model {
1394 for test_name in test_map.keys() {
1395 tests.insert((*binary_id, *test_name));
1396 }
1397 }
1398 }
1399
1400 for (binary_id, test_name) in prev_state.keys() {
1402 tests.insert((*binary_id, *test_name));
1403 }
1404
1405 tests
1406 }
1407
1408 fn collect_final_state(
1410 &self,
1411 state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1412 ) -> IdOrdMap<RerunTestSuiteInfo> {
1413 let mut result: BTreeMap<ModelBinaryId, RerunTestSuiteInfo> = BTreeMap::new();
1414
1415 for ((binary_id, test_name), status) in state {
1416 let suite = result
1417 .entry(*binary_id)
1418 .or_insert_with(|| RerunTestSuiteInfo::new(binary_id.rust_binary_id().clone()));
1419
1420 match status {
1421 PrevStatus::Passing => {
1422 suite.passing.insert(test_name.test_case_name().clone());
1423 }
1424 PrevStatus::Outstanding => {
1425 suite.outstanding.insert(test_name.test_case_name().clone());
1426 }
1427 PrevStatus::Unknown => {
1428 }
1430 }
1431 }
1432
1433 let mut id_map = IdOrdMap::new();
1434 for (_, suite) in result {
1435 id_map.insert_unique(suite).expect("unique binaries");
1436 }
1437 id_map
1438 }
1439 }
1440
1441 fn make_test_finished(
1450 test_instance: OwnedTestInstanceId,
1451 stress_index: Option<(u32, Option<u32>)>,
1452 passed: bool,
1453 ) -> TestEventKindSummary<()> {
1454 let result = if passed {
1455 ExecutionResultDescription::Pass
1456 } else {
1457 ExecutionResultDescription::Fail {
1458 failure: FailureDescription::ExitCode { code: 1 },
1459 leaked: false,
1460 }
1461 };
1462
1463 let execute_status = ExecuteStatus {
1464 retry_data: RetryData {
1465 attempt: 1,
1466 total_attempts: 1,
1467 },
1468 output: ChildExecutionOutputDescription::Output {
1469 result: Some(result.clone()),
1470 output: ChildOutputDescription::Split {
1471 stdout: None,
1472 stderr: None,
1473 },
1474 errors: None,
1475 },
1476 result,
1477 start_time: Utc::now().into(),
1478 time_taken: Duration::from_millis(100),
1479 is_slow: false,
1480 delay_before_start: Duration::ZERO,
1481 error_summary: None,
1482 output_error_slice: None,
1483 };
1484
1485 TestEventKindSummary::Output(OutputEventKind::TestFinished {
1486 stress_index: stress_index.map(|(current, total)| StressIndexSummary {
1487 current,
1488 total: total.and_then(NonZero::new),
1489 }),
1490 test_instance,
1491 success_output: TestOutputDisplay::Never,
1492 failure_output: TestOutputDisplay::Never,
1493 junit_store_success_output: false,
1494 junit_store_failure_output: false,
1495 run_statuses: ExecutionStatuses::new(vec![execute_status]),
1496 current_stats: RunStats::default(),
1497 running: 0,
1498 })
1499 }
1500
1501 #[test]
1507 fn stress_run_accumulation() {
1508 let test_pass_fail_pass = OwnedTestInstanceId {
1510 binary_id: RustBinaryId::new("test-binary"),
1511 test_name: TestCaseName::new("pass_fail_pass"),
1512 };
1513
1514 let test_all_pass = OwnedTestInstanceId {
1516 binary_id: RustBinaryId::new("test-binary"),
1517 test_name: TestCaseName::new("all_pass"),
1518 };
1519
1520 let test_all_fail = OwnedTestInstanceId {
1522 binary_id: RustBinaryId::new("test-binary"),
1523 test_name: TestCaseName::new("all_fail"),
1524 };
1525
1526 let test_fail_first = OwnedTestInstanceId {
1528 binary_id: RustBinaryId::new("test-binary"),
1529 test_name: TestCaseName::new("fail_first"),
1530 };
1531
1532 let test_regular_pass = OwnedTestInstanceId {
1534 binary_id: RustBinaryId::new("test-binary"),
1535 test_name: TestCaseName::new("regular_pass"),
1536 };
1537
1538 let test_regular_fail = OwnedTestInstanceId {
1540 binary_id: RustBinaryId::new("test-binary"),
1541 test_name: TestCaseName::new("regular_fail"),
1542 };
1543
1544 let events = [
1546 make_test_finished(test_pass_fail_pass.clone(), Some((0, Some(3))), true),
1548 make_test_finished(test_pass_fail_pass.clone(), Some((1, Some(3))), false),
1549 make_test_finished(test_pass_fail_pass.clone(), Some((2, Some(3))), true),
1550 make_test_finished(test_all_pass.clone(), Some((0, Some(3))), true),
1552 make_test_finished(test_all_pass.clone(), Some((1, Some(3))), true),
1553 make_test_finished(test_all_pass.clone(), Some((2, Some(3))), true),
1554 make_test_finished(test_all_fail.clone(), Some((0, Some(3))), false),
1556 make_test_finished(test_all_fail.clone(), Some((1, Some(3))), false),
1557 make_test_finished(test_all_fail.clone(), Some((2, Some(3))), false),
1558 make_test_finished(test_fail_first.clone(), Some((0, Some(3))), false),
1560 make_test_finished(test_fail_first.clone(), Some((1, Some(3))), true),
1561 make_test_finished(test_fail_first.clone(), Some((2, Some(3))), true),
1562 make_test_finished(test_regular_pass.clone(), None, true),
1564 make_test_finished(test_regular_fail.clone(), None, false),
1566 ];
1567
1568 let outcomes = collect_from_events(events.iter().map(Ok::<_, Infallible>)).unwrap();
1569
1570 assert_eq!(
1571 outcomes.get(&test_pass_fail_pass),
1572 Some(&TestOutcome::Failed),
1573 "[Pass, Fail, Pass] should be Failed"
1574 );
1575 assert_eq!(
1576 outcomes.get(&test_all_pass),
1577 Some(&TestOutcome::Passed),
1578 "[Pass, Pass, Pass] should be Passed"
1579 );
1580 assert_eq!(
1581 outcomes.get(&test_all_fail),
1582 Some(&TestOutcome::Failed),
1583 "[Fail, Fail, Fail] should be Failed"
1584 );
1585 assert_eq!(
1586 outcomes.get(&test_fail_first),
1587 Some(&TestOutcome::Failed),
1588 "[Fail, Pass, Pass] should be Failed"
1589 );
1590 assert_eq!(
1591 outcomes.get(&test_regular_pass),
1592 Some(&TestOutcome::Passed),
1593 "regular pass should be Passed"
1594 );
1595 assert_eq!(
1596 outcomes.get(&test_regular_fail),
1597 Some(&TestOutcome::Failed),
1598 "regular fail should be Failed"
1599 );
1600 }
1601
1602 #[test]
1607 fn stress_run_multiple_tests_independent() {
1608 let test_a = OwnedTestInstanceId {
1609 binary_id: RustBinaryId::new("test-binary"),
1610 test_name: TestCaseName::new("test_a"),
1611 };
1612 let test_b = OwnedTestInstanceId {
1613 binary_id: RustBinaryId::new("test-binary"),
1614 test_name: TestCaseName::new("test_b"),
1615 };
1616
1617 let events = [
1621 make_test_finished(test_a.clone(), Some((0, Some(2))), true),
1622 make_test_finished(test_b.clone(), Some((0, Some(2))), true),
1623 make_test_finished(test_a.clone(), Some((1, Some(2))), true),
1624 make_test_finished(test_b.clone(), Some((1, Some(2))), false),
1625 ];
1626
1627 let outcomes = collect_from_events(events.iter().map(Ok::<_, Infallible>)).unwrap();
1628
1629 assert_eq!(
1630 outcomes.get(&test_a),
1631 Some(&TestOutcome::Passed),
1632 "test_a [Pass, Pass] should be Passed"
1633 );
1634 assert_eq!(
1635 outcomes.get(&test_b),
1636 Some(&TestOutcome::Failed),
1637 "test_b [Pass, Fail] should be Failed"
1638 );
1639 }
1640}