1use crate::{
10 errors::RecordReadError,
11 list::OwnedTestInstanceId,
12 record::{
13 CoreEventKind, OutputEventKind, RecordReader, TestEventKindSummary,
14 format::{RerunInfo, RerunRootInfo, RerunTestSuiteInfo},
15 },
16};
17use iddqd::IdOrdMap;
18use nextest_metadata::{
19 FilterMatch, MismatchReason, RustBinaryId, RustTestSuiteStatusSummary, TestCaseName,
20 TestListSummary,
21};
22use quick_junit::ReportUuid;
23use std::collections::{BTreeSet, HashMap};
24
25pub(crate) trait TestListInfo {
30 type BinaryIter<'a>: Iterator<Item = (&'a RustBinaryId, BinaryInfo<'a>)>
32 where
33 Self: 'a;
34
35 fn binaries(&self) -> Self::BinaryIter<'_>;
37}
38
39pub(crate) enum BinaryInfo<'a> {
41 Listed {
43 test_cases: Box<dyn Iterator<Item = (&'a TestCaseName, FilterMatch)> + 'a>,
45 },
46 Skipped,
48}
49
50impl TestListInfo for TestListSummary {
51 type BinaryIter<'a> = TestListSummaryBinaryIter<'a>;
52
53 fn binaries(&self) -> Self::BinaryIter<'_> {
54 TestListSummaryBinaryIter {
55 inner: self.rust_suites.iter(),
56 }
57 }
58}
59
60pub(crate) struct TestListSummaryBinaryIter<'a> {
62 inner:
63 std::collections::btree_map::Iter<'a, RustBinaryId, nextest_metadata::RustTestSuiteSummary>,
64}
65
66impl<'a> Iterator for TestListSummaryBinaryIter<'a> {
67 type Item = (&'a RustBinaryId, BinaryInfo<'a>);
68
69 fn next(&mut self) -> Option<Self::Item> {
70 self.inner.next().map(|(binary_id, suite)| {
71 let info = if suite.status == RustTestSuiteStatusSummary::LISTED {
72 BinaryInfo::Listed {
73 test_cases: Box::new(
74 suite
75 .test_cases
76 .iter()
77 .map(|(name, tc)| (name, tc.filter_match)),
78 ),
79 }
80 } else {
81 BinaryInfo::Skipped
82 };
83 (binary_id, info)
84 })
85 }
86}
87
88pub(crate) fn compute_outstanding_pure(
90 prev_info: Option<&IdOrdMap<RerunTestSuiteInfo>>,
91 test_list: &impl TestListInfo,
92 outcomes: &HashMap<OwnedTestInstanceId, TestOutcome>,
93) -> IdOrdMap<RerunTestSuiteInfo> {
94 let mut new_outstanding = IdOrdMap::new();
95
96 let mut binaries_in_test_list = BTreeSet::new();
100
101 for (binary_id, binary_info) in test_list.binaries() {
102 binaries_in_test_list.insert(binary_id.clone());
103
104 match binary_info {
105 BinaryInfo::Listed { test_cases } => {
106 let prev = prev_info.and_then(|p| p.get(binary_id));
109
110 let mut curr = RerunTestSuiteInfo::new(binary_id.clone());
111 for (test_name, filter_match) in test_cases {
112 match filter_match {
113 FilterMatch::Matches => {
114 let key = OwnedTestInstanceId {
116 binary_id: binary_id.clone(),
117 test_name: test_name.clone(),
118 };
119 match outcomes.get(&key) {
120 Some(TestOutcome::Passed) => {
121 curr.passing.insert(test_name.clone());
123 }
124 Some(TestOutcome::Failed) => {
125 curr.outstanding.insert(test_name.clone());
127 }
128 Some(TestOutcome::Skipped(skipped)) => {
129 handle_skipped(test_name, *skipped, prev, &mut curr);
133 }
134 None => {
135 curr.outstanding.insert(test_name.clone());
138 }
139 }
140 }
141 FilterMatch::Mismatch { reason } => {
142 handle_skipped(
143 test_name,
144 TestOutcomeSkipped::from_mismatch_reason(reason),
145 prev,
146 &mut curr,
147 );
148 }
149 }
150 }
151
152 if let Some(prev) = prev {
156 for t in &prev.outstanding {
157 if !curr.passing.contains(t) && !curr.outstanding.contains(t) {
158 curr.outstanding.insert(t.clone());
159 }
160 }
161 }
162
163 if !curr.passing.is_empty() || !curr.outstanding.is_empty() {
170 new_outstanding
171 .insert_unique(curr)
172 .expect("binaries iterator should not yield duplicates");
173 }
174 }
175 BinaryInfo::Skipped => {
176 if let Some(prev_outstanding) = prev_info
186 && let Some(outstanding) = prev_outstanding.get(binary_id)
187 {
188 new_outstanding
190 .insert_unique(outstanding.clone())
191 .expect("binaries iterator should not yield duplicates");
192 }
193 }
198 }
199 }
200
201 if let Some(prev) = prev_info {
204 for prev_suite in prev.iter() {
205 if !binaries_in_test_list.contains(&prev_suite.binary_id) {
206 new_outstanding
207 .insert_unique(prev_suite.clone())
208 .expect("binary not in test list, so this should succeed");
209 }
210 }
211 }
212
213 new_outstanding
214}
215
216#[derive(Clone, Debug)]
218pub struct ComputedRerunInfo {
219 pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
223}
224
225impl ComputedRerunInfo {
226 pub fn expected_test_ids(&self) -> BTreeSet<OwnedTestInstanceId> {
230 self.test_suites
231 .iter()
232 .flat_map(|suite| {
233 suite.outstanding.iter().map(|name| OwnedTestInstanceId {
234 binary_id: suite.binary_id.clone(),
235 test_name: name.clone(),
236 })
237 })
238 .collect()
239 }
240
241 pub fn compute(
246 reader: &mut RecordReader,
247 ) -> Result<(Self, Option<RerunRootInfo>), RecordReadError> {
248 let rerun_info = reader.read_rerun_info()?;
249 let test_list = reader.read_test_list()?;
250 let outcomes = TestEventOutcomes::collect(reader)?;
251
252 let prev_test_suites = rerun_info.as_ref().map(|info| &info.test_suites);
253 let new_test_suites =
254 compute_outstanding_pure(prev_test_suites, &test_list, &outcomes.outcomes);
255
256 let root_info = rerun_info.map(|info| info.root_info);
257
258 Ok((
259 Self {
260 test_suites: new_test_suites,
261 },
262 root_info,
263 ))
264 }
265
266 pub fn into_rerun_info(self, parent_run_id: ReportUuid, root_info: RerunRootInfo) -> RerunInfo {
268 RerunInfo {
269 parent_run_id,
270 root_info,
271 test_suites: self.test_suites,
272 }
273 }
274}
275
276fn handle_skipped(
277 test_name: &TestCaseName,
278 skipped: TestOutcomeSkipped,
279 prev: Option<&RerunTestSuiteInfo>,
280 curr: &mut RerunTestSuiteInfo,
281) {
282 match skipped {
283 TestOutcomeSkipped::Rerun => {
284 curr.passing.insert(test_name.clone());
291 }
292 TestOutcomeSkipped::Explicit => {
293 if let Some(prev) = prev {
307 if prev.outstanding.contains(test_name) {
308 curr.outstanding.insert(test_name.clone());
309 } else if prev.passing.contains(test_name) {
310 curr.passing.insert(test_name.clone());
311 }
312 } else {
313 }
316 }
317 }
318}
319
320#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub(crate) enum TestOutcomeSkipped {
323 Explicit,
325
326 Rerun,
328}
329
330impl TestOutcomeSkipped {
331 fn from_mismatch_reason(reason: MismatchReason) -> Self {
333 match reason {
334 MismatchReason::NotBenchmark
335 | MismatchReason::Ignored
336 | MismatchReason::String
337 | MismatchReason::Expression
338 | MismatchReason::Partition
339 | MismatchReason::DefaultFilter => TestOutcomeSkipped::Explicit,
340 MismatchReason::RerunAlreadyPassed => TestOutcomeSkipped::Rerun,
341 other => unreachable!("all known match arms are covered, found {other:?}"),
342 }
343 }
344}
345
346#[derive(Clone, Copy, Debug, PartialEq, Eq)]
348pub(crate) enum TestOutcome {
349 Passed,
351
352 Skipped(TestOutcomeSkipped),
354
355 Failed,
357}
358
359#[derive(Clone, Debug)]
363struct TestEventOutcomes {
364 outcomes: HashMap<OwnedTestInstanceId, TestOutcome>,
366}
367
368impl TestEventOutcomes {
369 fn collect(reader: &mut RecordReader) -> Result<Self, RecordReadError> {
374 reader.load_dictionaries()?;
375
376 let events: Vec<_> = reader.events()?.collect::<Result<Vec<_>, _>>()?;
377 let outcomes = collect_from_events(events.iter().map(|e| &e.kind));
378
379 Ok(Self { outcomes })
380 }
381}
382
383fn collect_from_events<'a, O>(
388 events: impl Iterator<Item = &'a TestEventKindSummary<O>>,
389) -> HashMap<OwnedTestInstanceId, TestOutcome>
390where
391 O: 'a,
392{
393 let mut outcomes = HashMap::new();
394
395 for kind in events {
396 match kind {
397 TestEventKindSummary::Output(OutputEventKind::TestFinished {
398 test_instance,
399 run_statuses,
400 ..
401 }) => {
402 let outcome = if run_statuses.last_status().result.is_success() {
404 TestOutcome::Passed
405 } else {
406 TestOutcome::Failed
407 };
408
409 outcomes
416 .entry(test_instance.clone())
417 .and_modify(|existing| {
418 if outcome == TestOutcome::Failed {
419 *existing = TestOutcome::Failed;
420 }
421 })
422 .or_insert(outcome);
423 }
424 TestEventKindSummary::Core(CoreEventKind::TestSkipped {
425 test_instance,
426 reason,
427 ..
428 }) => {
429 let skipped_reason = TestOutcomeSkipped::from_mismatch_reason(*reason);
430 outcomes.insert(test_instance.clone(), TestOutcome::Skipped(skipped_reason));
431 }
432 _ => {}
433 }
434 }
435
436 outcomes
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use crate::{
443 record::{OutputEventKind, StressIndexSummary, TestEventKindSummary},
444 reporter::{
445 TestOutputDisplay,
446 events::{
447 ChildExecutionOutputDescription, ChildOutputDescription, ExecuteStatus,
448 ExecutionResultDescription, ExecutionStatuses, FailureDescription, RetryData,
449 RunStats,
450 },
451 },
452 };
453 use chrono::Utc;
454 use proptest::prelude::*;
455 use std::{
456 collections::{BTreeMap, btree_map},
457 num::NonZero,
458 sync::OnceLock,
459 time::Duration,
460 };
461 use test_strategy::proptest;
462
463 #[proptest(cases = 200)]
469 fn sut_matches_oracle(#[strategy(arb_rerun_model())] model: RerunModel) {
470 let expected = model.compute_rerun_info_decision_table();
471 let actual = run_sut(&model);
472 prop_assert_eq!(actual, expected);
473 }
474
475 #[proptest(cases = 200)]
477 fn passing_and_outstanding_disjoint(#[strategy(arb_rerun_model())] model: RerunModel) {
478 let result = run_sut(&model);
479 for suite in result.iter() {
480 let intersection: BTreeSet<_> =
481 suite.passing.intersection(&suite.outstanding).collect();
482 prop_assert!(
483 intersection.is_empty(),
484 "passing and outstanding should be disjoint for {}: {:?}",
485 suite.binary_id,
486 intersection
487 );
488 }
489 }
490
491 #[proptest(cases = 200)]
497 fn matching_tests_with_outcomes_are_tracked(#[strategy(arb_rerun_model())] model: RerunModel) {
498 let result = run_sut(&model);
499
500 let final_step = model.reruns.last().unwrap_or(&model.initial);
502
503 for (binary_id, binary_model) in &final_step.test_list.binaries {
504 if let BinaryModel::Listed { tests } = binary_model {
505 let rust_binary_id = binary_id.rust_binary_id();
506
507 for (test_name, filter_match) in tests {
508 if matches!(filter_match, FilterMatch::Matches) {
509 let key = (*binary_id, *test_name);
510 let outcome = final_step.outcomes.get(&key);
511
512 let should_be_tracked = match outcome {
517 Some(TestOutcome::Passed)
518 | Some(TestOutcome::Failed)
519 | Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun))
520 | None => true,
521 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => false,
522 };
523
524 if should_be_tracked {
525 let tcn = test_name.test_case_name();
526 let suite = result.get(&rust_binary_id);
527 let in_passing = suite.is_some_and(|s| s.passing.contains(tcn));
528 let in_outstanding = suite.is_some_and(|s| s.outstanding.contains(tcn));
529 prop_assert!(
530 in_passing || in_outstanding,
531 "matching test {:?}::{:?} with outcome {:?} should be in passing or outstanding",
532 binary_id,
533 test_name,
534 outcome
535 );
536 }
537 }
538 }
539 }
540 }
541 }
542
543 #[test]
545 fn decide_test_outcome_truth_table() {
546 use Decision as D;
547 use FilterMatchResult as F;
548 use PrevStatus as P;
549
550 assert_eq!(
552 decide_test_outcome(P::Passing, F::BinaryNotPresent, None),
553 D::Passing
554 );
555 assert_eq!(
556 decide_test_outcome(P::Outstanding, F::BinaryNotPresent, None),
557 D::Outstanding
558 );
559 assert_eq!(
560 decide_test_outcome(P::Unknown, F::BinaryNotPresent, None),
561 D::NotTracked
562 );
563
564 assert_eq!(
566 decide_test_outcome(P::Passing, F::BinarySkipped, None),
567 D::Passing
568 );
569 assert_eq!(
570 decide_test_outcome(P::Outstanding, F::BinarySkipped, None),
571 D::Outstanding
572 );
573 assert_eq!(
574 decide_test_outcome(P::Unknown, F::BinarySkipped, None),
575 D::NotTracked
576 );
577
578 assert_eq!(
580 decide_test_outcome(P::Passing, F::TestNotInList, None),
581 D::NotTracked
582 );
583 assert_eq!(
584 decide_test_outcome(P::Outstanding, F::TestNotInList, None),
585 D::Outstanding
586 );
587 assert_eq!(
588 decide_test_outcome(P::Unknown, F::TestNotInList, None),
589 D::NotTracked
590 );
591
592 let matches = F::HasMatch(FilterMatch::Matches);
594
595 assert_eq!(
597 decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Passed)),
598 D::Passing
599 );
600 assert_eq!(
601 decide_test_outcome(P::Passing, matches, Some(TestOutcome::Passed)),
602 D::Passing
603 );
604 assert_eq!(
605 decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Passed)),
606 D::Passing
607 );
608
609 assert_eq!(
611 decide_test_outcome(P::Unknown, matches, Some(TestOutcome::Failed)),
612 D::Outstanding
613 );
614 assert_eq!(
615 decide_test_outcome(P::Passing, matches, Some(TestOutcome::Failed)),
616 D::Outstanding
617 );
618 assert_eq!(
619 decide_test_outcome(P::Outstanding, matches, Some(TestOutcome::Failed)),
620 D::Outstanding
621 );
622
623 assert_eq!(
625 decide_test_outcome(P::Unknown, matches, None),
626 D::Outstanding
627 );
628 assert_eq!(
629 decide_test_outcome(P::Passing, matches, None),
630 D::Outstanding
631 );
632 assert_eq!(
633 decide_test_outcome(P::Outstanding, matches, None),
634 D::Outstanding
635 );
636
637 let rerun_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun));
639 assert_eq!(
640 decide_test_outcome(P::Unknown, matches, rerun_skipped),
641 D::Passing
642 );
643 assert_eq!(
644 decide_test_outcome(P::Passing, matches, rerun_skipped),
645 D::Passing
646 );
647 assert_eq!(
648 decide_test_outcome(P::Outstanding, matches, rerun_skipped),
649 D::Passing
650 );
651
652 let explicit_skipped = Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit));
654 assert_eq!(
655 decide_test_outcome(P::Unknown, matches, explicit_skipped),
656 D::NotTracked
657 );
658 assert_eq!(
659 decide_test_outcome(P::Passing, matches, explicit_skipped),
660 D::Passing
661 );
662 assert_eq!(
663 decide_test_outcome(P::Outstanding, matches, explicit_skipped),
664 D::Outstanding
665 );
666
667 let rerun_mismatch = F::HasMatch(FilterMatch::Mismatch {
669 reason: MismatchReason::RerunAlreadyPassed,
670 });
671 assert_eq!(
672 decide_test_outcome(P::Unknown, rerun_mismatch, None),
673 D::Passing
674 );
675 assert_eq!(
676 decide_test_outcome(P::Passing, rerun_mismatch, None),
677 D::Passing
678 );
679 assert_eq!(
680 decide_test_outcome(P::Outstanding, rerun_mismatch, None),
681 D::Passing
682 );
683
684 let explicit_mismatch = F::HasMatch(FilterMatch::Mismatch {
686 reason: MismatchReason::Ignored,
687 });
688 assert_eq!(
689 decide_test_outcome(P::Unknown, explicit_mismatch, None),
690 D::NotTracked
691 );
692 assert_eq!(
693 decide_test_outcome(P::Passing, explicit_mismatch, None),
694 D::Passing
695 );
696 assert_eq!(
697 decide_test_outcome(P::Outstanding, explicit_mismatch, None),
698 D::Outstanding
699 );
700 }
701
702 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
710 enum ModelBinaryId {
711 A,
712 B,
713 C,
714 D,
715 }
716
717 impl ModelBinaryId {
718 fn rust_binary_id(self) -> &'static RustBinaryId {
719 match self {
720 Self::A => {
721 static ID: OnceLock<RustBinaryId> = OnceLock::new();
722 ID.get_or_init(|| RustBinaryId::new("binary-a"))
723 }
724 Self::B => {
725 static ID: OnceLock<RustBinaryId> = OnceLock::new();
726 ID.get_or_init(|| RustBinaryId::new("binary-b"))
727 }
728 Self::C => {
729 static ID: OnceLock<RustBinaryId> = OnceLock::new();
730 ID.get_or_init(|| RustBinaryId::new("binary-c"))
731 }
732 Self::D => {
733 static ID: OnceLock<RustBinaryId> = OnceLock::new();
734 ID.get_or_init(|| RustBinaryId::new("binary-d"))
735 }
736 }
737 }
738 }
739
740 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
742 enum ModelTestName {
743 Test1,
744 Test2,
745 Test3,
746 Test4,
747 Test5,
748 }
749
750 impl ModelTestName {
751 fn test_case_name(self) -> &'static TestCaseName {
752 match self {
753 Self::Test1 => {
754 static NAME: OnceLock<TestCaseName> = OnceLock::new();
755 NAME.get_or_init(|| TestCaseName::new("test_1"))
756 }
757 Self::Test2 => {
758 static NAME: OnceLock<TestCaseName> = OnceLock::new();
759 NAME.get_or_init(|| TestCaseName::new("test_2"))
760 }
761 Self::Test3 => {
762 static NAME: OnceLock<TestCaseName> = OnceLock::new();
763 NAME.get_or_init(|| TestCaseName::new("test_3"))
764 }
765 Self::Test4 => {
766 static NAME: OnceLock<TestCaseName> = OnceLock::new();
767 NAME.get_or_init(|| TestCaseName::new("test_4"))
768 }
769 Self::Test5 => {
770 static NAME: OnceLock<TestCaseName> = OnceLock::new();
771 NAME.get_or_init(|| TestCaseName::new("test_5"))
772 }
773 }
774 }
775 }
776
777 #[derive(Clone, Debug)]
779 enum BinaryModel {
780 Listed {
782 tests: BTreeMap<ModelTestName, FilterMatch>,
783 },
784 Skipped,
786 }
787
788 #[derive(Clone, Debug)]
790 struct TestListModel {
791 binaries: BTreeMap<ModelBinaryId, BinaryModel>,
792 }
793
794 #[derive(Clone, Debug)]
796 struct RunStep {
797 test_list: TestListModel,
799 outcomes: BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
801 }
802
803 #[derive(Clone, Debug)]
805 struct RerunModel {
806 initial: RunStep,
808 reruns: Vec<RunStep>,
810 }
811
812 impl TestListInfo for TestListModel {
813 type BinaryIter<'a> = TestListModelBinaryIter<'a>;
814
815 fn binaries(&self) -> Self::BinaryIter<'_> {
816 TestListModelBinaryIter {
817 inner: self.binaries.iter(),
818 }
819 }
820 }
821
822 struct TestListModelBinaryIter<'a> {
824 inner: btree_map::Iter<'a, ModelBinaryId, BinaryModel>,
825 }
826
827 impl<'a> Iterator for TestListModelBinaryIter<'a> {
828 type Item = (&'a RustBinaryId, BinaryInfo<'a>);
829
830 fn next(&mut self) -> Option<Self::Item> {
831 self.inner.next().map(|(model_id, binary_model)| {
832 let rust_id = model_id.rust_binary_id();
833 let info = match binary_model {
834 BinaryModel::Listed { tests } => BinaryInfo::Listed {
835 test_cases: Box::new(
836 tests.iter().map(|(name, fm)| (name.test_case_name(), *fm)),
837 ),
838 },
839 BinaryModel::Skipped => BinaryInfo::Skipped,
840 };
841 (rust_id, info)
842 })
843 }
844 }
845
846 fn arb_model_binary_id() -> impl Strategy<Value = ModelBinaryId> {
851 prop_oneof![
852 Just(ModelBinaryId::A),
853 Just(ModelBinaryId::B),
854 Just(ModelBinaryId::C),
855 Just(ModelBinaryId::D),
856 ]
857 }
858
859 fn arb_model_test_name() -> impl Strategy<Value = ModelTestName> {
860 prop_oneof![
861 Just(ModelTestName::Test1),
862 Just(ModelTestName::Test2),
863 Just(ModelTestName::Test3),
864 Just(ModelTestName::Test4),
865 Just(ModelTestName::Test5),
866 ]
867 }
868
869 fn arb_filter_match() -> impl Strategy<Value = FilterMatch> {
870 prop_oneof![
871 4 => Just(FilterMatch::Matches),
872 1 => any::<MismatchReason>().prop_map(|reason| FilterMatch::Mismatch { reason }),
873 ]
874 }
875
876 fn arb_test_outcome() -> impl Strategy<Value = TestOutcome> {
877 prop_oneof![
878 4 => Just(TestOutcome::Passed),
879 2 => Just(TestOutcome::Failed),
880 1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)),
881 1 => Just(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)),
882 ]
883 }
884
885 fn arb_test_map() -> impl Strategy<Value = BTreeMap<ModelTestName, FilterMatch>> {
886 proptest::collection::btree_map(arb_model_test_name(), arb_filter_match(), 0..5)
887 }
888
889 fn arb_binary_model() -> impl Strategy<Value = BinaryModel> {
890 prop_oneof![
891 8 => arb_test_map().prop_map(|tests| BinaryModel::Listed { tests }),
892 2 => Just(BinaryModel::Skipped),
893 ]
894 }
895
896 fn arb_test_list_model() -> impl Strategy<Value = TestListModel> {
897 proptest::collection::btree_map(arb_model_binary_id(), arb_binary_model(), 0..4)
898 .prop_map(|binaries| TestListModel { binaries })
899 }
900
901 fn arb_outcomes_for_matching_tests(
906 matching_tests: Vec<(ModelBinaryId, ModelTestName)>,
907 ) -> BoxedStrategy<BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>> {
908 if matching_tests.is_empty() {
909 Just(BTreeMap::new()).boxed()
910 } else {
911 let len = matching_tests.len();
912 proptest::collection::btree_map(
913 proptest::sample::select(matching_tests),
914 arb_test_outcome(),
915 0..=len,
916 )
917 .boxed()
918 }
919 }
920
921 fn extract_matching_tests(test_list: &TestListModel) -> Vec<(ModelBinaryId, ModelTestName)> {
923 test_list
924 .binaries
925 .iter()
926 .filter_map(|(binary_id, model)| match model {
927 BinaryModel::Listed { tests } => Some(
928 tests
929 .iter()
930 .filter(|(_, fm)| matches!(fm, FilterMatch::Matches))
931 .map(move |(tn, _)| (*binary_id, *tn)),
932 ),
933 BinaryModel::Skipped => None,
934 })
935 .flatten()
936 .collect()
937 }
938
939 fn arb_run_step() -> impl Strategy<Value = RunStep> {
940 arb_test_list_model().prop_flat_map(|test_list| {
941 let matching_tests = extract_matching_tests(&test_list);
942 arb_outcomes_for_matching_tests(matching_tests).prop_map(move |outcomes| RunStep {
943 test_list: test_list.clone(),
944 outcomes,
945 })
946 })
947 }
948
949 fn arb_rerun_model() -> impl Strategy<Value = RerunModel> {
950 (
951 arb_run_step(),
952 proptest::collection::vec(arb_run_step(), 0..5),
953 )
954 .prop_map(|(initial, reruns)| RerunModel { initial, reruns })
955 }
956
957 fn model_outcomes_to_hashmap(
962 outcomes: &BTreeMap<(ModelBinaryId, ModelTestName), TestOutcome>,
963 ) -> HashMap<OwnedTestInstanceId, TestOutcome> {
964 outcomes
965 .iter()
966 .map(|((binary_id, test_name), outcome)| {
967 let id = OwnedTestInstanceId {
968 binary_id: binary_id.rust_binary_id().clone(),
969 test_name: test_name.test_case_name().clone(),
970 };
971 (id, *outcome)
972 })
973 .collect()
974 }
975
976 fn run_sut(model: &RerunModel) -> IdOrdMap<RerunTestSuiteInfo> {
982 let outcomes = model_outcomes_to_hashmap(&model.initial.outcomes);
983 let mut result = compute_outstanding_pure(None, &model.initial.test_list, &outcomes);
984
985 for rerun in &model.reruns {
986 let outcomes = model_outcomes_to_hashmap(&rerun.outcomes);
987 result = compute_outstanding_pure(Some(&result), &rerun.test_list, &outcomes);
988 }
989
990 result
991 }
992
993 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1003 enum PrevStatus {
1004 Passing,
1006 Outstanding,
1008 Unknown,
1010 }
1011
1012 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1014 enum Decision {
1015 Passing,
1017 Outstanding,
1019 NotTracked,
1021 }
1022
1023 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
1028 enum FilterMatchResult {
1029 BinaryNotPresent,
1032 BinarySkipped,
1035 TestNotInList,
1038 HasMatch(FilterMatch),
1040 }
1041
1042 fn decide_test_outcome(
1047 prev: PrevStatus,
1048 filter_result: FilterMatchResult,
1049 outcome: Option<TestOutcome>,
1050 ) -> Decision {
1051 match filter_result {
1052 FilterMatchResult::BinaryNotPresent | FilterMatchResult::BinarySkipped => {
1053 match prev {
1055 PrevStatus::Passing => Decision::Passing,
1056 PrevStatus::Outstanding => Decision::Outstanding,
1057 PrevStatus::Unknown => Decision::NotTracked,
1058 }
1059 }
1060 FilterMatchResult::TestNotInList => {
1061 match prev {
1066 PrevStatus::Outstanding => Decision::Outstanding,
1067 PrevStatus::Passing | PrevStatus::Unknown => Decision::NotTracked,
1068 }
1069 }
1070 FilterMatchResult::HasMatch(FilterMatch::Matches) => {
1071 match outcome {
1072 Some(TestOutcome::Passed) => Decision::Passing,
1073 Some(TestOutcome::Failed) => Decision::Outstanding,
1074 None => {
1075 Decision::Outstanding
1077 }
1078 Some(TestOutcome::Skipped(TestOutcomeSkipped::Rerun)) => Decision::Passing,
1079 Some(TestOutcome::Skipped(TestOutcomeSkipped::Explicit)) => {
1080 match prev {
1082 PrevStatus::Passing => Decision::Passing,
1083 PrevStatus::Outstanding => Decision::Outstanding,
1084 PrevStatus::Unknown => Decision::NotTracked,
1085 }
1086 }
1087 }
1088 }
1089 FilterMatchResult::HasMatch(FilterMatch::Mismatch { reason }) => {
1090 match TestOutcomeSkipped::from_mismatch_reason(reason) {
1091 TestOutcomeSkipped::Rerun => Decision::Passing,
1092 TestOutcomeSkipped::Explicit => {
1093 match prev {
1095 PrevStatus::Passing => Decision::Passing,
1096 PrevStatus::Outstanding => Decision::Outstanding,
1097 PrevStatus::Unknown => Decision::NotTracked,
1098 }
1099 }
1100 }
1101 }
1102 }
1103 }
1104
1105 impl RerunModel {
1106 fn compute_rerun_info_decision_table(&self) -> IdOrdMap<RerunTestSuiteInfo> {
1112 let mut prev_state: HashMap<(ModelBinaryId, ModelTestName), PrevStatus> =
1114 HashMap::new();
1115
1116 self.update_state_from_step(&mut prev_state, &self.initial);
1118
1119 for rerun in &self.reruns {
1121 self.update_state_from_step(&mut prev_state, rerun);
1122 }
1123
1124 self.collect_final_state(&prev_state)
1126 }
1127
1128 fn update_state_from_step(
1129 &self,
1130 state: &mut HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1131 step: &RunStep,
1132 ) {
1133 let all_tests = self.enumerate_all_tests(state, step);
1137
1138 for (binary_id, test_name) in all_tests {
1139 let prev = state
1140 .get(&(binary_id, test_name))
1141 .copied()
1142 .unwrap_or(PrevStatus::Unknown);
1143
1144 let filter_result = self.get_filter_match_result(step, binary_id, test_name);
1145 let outcome = step.outcomes.get(&(binary_id, test_name)).copied();
1146
1147 let decision = decide_test_outcome(prev, filter_result, outcome);
1148
1149 match decision {
1151 Decision::Passing => {
1152 state.insert((binary_id, test_name), PrevStatus::Passing);
1153 }
1154 Decision::Outstanding => {
1155 state.insert((binary_id, test_name), PrevStatus::Outstanding);
1156 }
1157 Decision::NotTracked => {
1158 state.remove(&(binary_id, test_name));
1159 }
1160 }
1161 }
1162 }
1163
1164 fn get_filter_match_result(
1169 &self,
1170 step: &RunStep,
1171 binary_id: ModelBinaryId,
1172 test_name: ModelTestName,
1173 ) -> FilterMatchResult {
1174 match step.test_list.binaries.get(&binary_id) {
1175 None => FilterMatchResult::BinaryNotPresent,
1176 Some(BinaryModel::Skipped) => FilterMatchResult::BinarySkipped,
1177 Some(BinaryModel::Listed { tests }) => match tests.get(&test_name) {
1178 Some(filter_match) => FilterMatchResult::HasMatch(*filter_match),
1179 None => FilterMatchResult::TestNotInList,
1180 },
1181 }
1182 }
1183
1184 fn enumerate_all_tests(
1189 &self,
1190 prev_state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1191 step: &RunStep,
1192 ) -> BTreeSet<(ModelBinaryId, ModelTestName)> {
1193 let mut tests = BTreeSet::new();
1194
1195 for (binary_id, binary_model) in &step.test_list.binaries {
1197 if let BinaryModel::Listed { tests: test_map } = binary_model {
1198 for test_name in test_map.keys() {
1199 tests.insert((*binary_id, *test_name));
1200 }
1201 }
1202 }
1203
1204 for (binary_id, test_name) in prev_state.keys() {
1206 tests.insert((*binary_id, *test_name));
1207 }
1208
1209 tests
1210 }
1211
1212 fn collect_final_state(
1214 &self,
1215 state: &HashMap<(ModelBinaryId, ModelTestName), PrevStatus>,
1216 ) -> IdOrdMap<RerunTestSuiteInfo> {
1217 let mut result: BTreeMap<ModelBinaryId, RerunTestSuiteInfo> = BTreeMap::new();
1218
1219 for ((binary_id, test_name), status) in state {
1220 let suite = result
1221 .entry(*binary_id)
1222 .or_insert_with(|| RerunTestSuiteInfo::new(binary_id.rust_binary_id().clone()));
1223
1224 match status {
1225 PrevStatus::Passing => {
1226 suite.passing.insert(test_name.test_case_name().clone());
1227 }
1228 PrevStatus::Outstanding => {
1229 suite.outstanding.insert(test_name.test_case_name().clone());
1230 }
1231 PrevStatus::Unknown => {
1232 }
1234 }
1235 }
1236
1237 let mut id_map = IdOrdMap::new();
1238 for (_, suite) in result {
1239 id_map.insert_unique(suite).expect("unique binaries");
1240 }
1241 id_map
1242 }
1243 }
1244
1245 fn make_test_finished(
1254 test_instance: OwnedTestInstanceId,
1255 stress_index: Option<(u32, Option<u32>)>,
1256 passed: bool,
1257 ) -> TestEventKindSummary<()> {
1258 let result = if passed {
1259 ExecutionResultDescription::Pass
1260 } else {
1261 ExecutionResultDescription::Fail {
1262 failure: FailureDescription::ExitCode { code: 1 },
1263 leaked: false,
1264 }
1265 };
1266
1267 let execute_status = ExecuteStatus {
1268 retry_data: RetryData {
1269 attempt: 1,
1270 total_attempts: 1,
1271 },
1272 output: ChildExecutionOutputDescription::Output {
1273 result: Some(result.clone()),
1274 output: ChildOutputDescription::Split {
1275 stdout: None,
1276 stderr: None,
1277 },
1278 errors: None,
1279 },
1280 result,
1281 start_time: Utc::now().into(),
1282 time_taken: Duration::from_millis(100),
1283 is_slow: false,
1284 delay_before_start: Duration::ZERO,
1285 error_summary: None,
1286 output_error_slice: None,
1287 };
1288
1289 TestEventKindSummary::Output(OutputEventKind::TestFinished {
1290 stress_index: stress_index.map(|(current, total)| StressIndexSummary {
1291 current,
1292 total: total.and_then(NonZero::new),
1293 }),
1294 test_instance,
1295 success_output: TestOutputDisplay::Never,
1296 failure_output: TestOutputDisplay::Never,
1297 junit_store_success_output: false,
1298 junit_store_failure_output: false,
1299 run_statuses: ExecutionStatuses::new(vec![execute_status]),
1300 current_stats: RunStats::default(),
1301 running: 0,
1302 })
1303 }
1304
1305 #[test]
1311 fn stress_run_accumulation() {
1312 let test_pass_fail_pass = OwnedTestInstanceId {
1314 binary_id: RustBinaryId::new("test-binary"),
1315 test_name: TestCaseName::new("pass_fail_pass"),
1316 };
1317
1318 let test_all_pass = OwnedTestInstanceId {
1320 binary_id: RustBinaryId::new("test-binary"),
1321 test_name: TestCaseName::new("all_pass"),
1322 };
1323
1324 let test_all_fail = OwnedTestInstanceId {
1326 binary_id: RustBinaryId::new("test-binary"),
1327 test_name: TestCaseName::new("all_fail"),
1328 };
1329
1330 let test_fail_first = OwnedTestInstanceId {
1332 binary_id: RustBinaryId::new("test-binary"),
1333 test_name: TestCaseName::new("fail_first"),
1334 };
1335
1336 let test_regular_pass = OwnedTestInstanceId {
1338 binary_id: RustBinaryId::new("test-binary"),
1339 test_name: TestCaseName::new("regular_pass"),
1340 };
1341
1342 let test_regular_fail = OwnedTestInstanceId {
1344 binary_id: RustBinaryId::new("test-binary"),
1345 test_name: TestCaseName::new("regular_fail"),
1346 };
1347
1348 let events = [
1350 make_test_finished(test_pass_fail_pass.clone(), Some((0, Some(3))), true),
1352 make_test_finished(test_pass_fail_pass.clone(), Some((1, Some(3))), false),
1353 make_test_finished(test_pass_fail_pass.clone(), Some((2, Some(3))), true),
1354 make_test_finished(test_all_pass.clone(), Some((0, Some(3))), true),
1356 make_test_finished(test_all_pass.clone(), Some((1, Some(3))), true),
1357 make_test_finished(test_all_pass.clone(), Some((2, Some(3))), true),
1358 make_test_finished(test_all_fail.clone(), Some((0, Some(3))), false),
1360 make_test_finished(test_all_fail.clone(), Some((1, Some(3))), false),
1361 make_test_finished(test_all_fail.clone(), Some((2, Some(3))), false),
1362 make_test_finished(test_fail_first.clone(), Some((0, Some(3))), false),
1364 make_test_finished(test_fail_first.clone(), Some((1, Some(3))), true),
1365 make_test_finished(test_fail_first.clone(), Some((2, Some(3))), true),
1366 make_test_finished(test_regular_pass.clone(), None, true),
1368 make_test_finished(test_regular_fail.clone(), None, false),
1370 ];
1371
1372 let outcomes = collect_from_events(events.iter());
1373
1374 assert_eq!(
1375 outcomes.get(&test_pass_fail_pass),
1376 Some(&TestOutcome::Failed),
1377 "[Pass, Fail, Pass] should be Failed"
1378 );
1379 assert_eq!(
1380 outcomes.get(&test_all_pass),
1381 Some(&TestOutcome::Passed),
1382 "[Pass, Pass, Pass] should be Passed"
1383 );
1384 assert_eq!(
1385 outcomes.get(&test_all_fail),
1386 Some(&TestOutcome::Failed),
1387 "[Fail, Fail, Fail] should be Failed"
1388 );
1389 assert_eq!(
1390 outcomes.get(&test_fail_first),
1391 Some(&TestOutcome::Failed),
1392 "[Fail, Pass, Pass] should be Failed"
1393 );
1394 assert_eq!(
1395 outcomes.get(&test_regular_pass),
1396 Some(&TestOutcome::Passed),
1397 "regular pass should be Passed"
1398 );
1399 assert_eq!(
1400 outcomes.get(&test_regular_fail),
1401 Some(&TestOutcome::Failed),
1402 "regular fail should be Failed"
1403 );
1404 }
1405
1406 #[test]
1411 fn stress_run_multiple_tests_independent() {
1412 let test_a = OwnedTestInstanceId {
1413 binary_id: RustBinaryId::new("test-binary"),
1414 test_name: TestCaseName::new("test_a"),
1415 };
1416 let test_b = OwnedTestInstanceId {
1417 binary_id: RustBinaryId::new("test-binary"),
1418 test_name: TestCaseName::new("test_b"),
1419 };
1420
1421 let events = [
1425 make_test_finished(test_a.clone(), Some((0, Some(2))), true),
1426 make_test_finished(test_b.clone(), Some((0, Some(2))), true),
1427 make_test_finished(test_a.clone(), Some((1, Some(2))), true),
1428 make_test_finished(test_b.clone(), Some((1, Some(2))), false),
1429 ];
1430
1431 let outcomes = collect_from_events(events.iter());
1432
1433 assert_eq!(
1434 outcomes.get(&test_a),
1435 Some(&TestOutcome::Passed),
1436 "test_a [Pass, Pass] should be Passed"
1437 );
1438 assert_eq!(
1439 outcomes.get(&test_b),
1440 Some(&TestOutcome::Failed),
1441 "test_b [Pass, Fail] should be Failed"
1442 );
1443 }
1444}