1use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
7use nextest_metadata::{BuildPlatform, FilterMatch, RustBinaryId, TestCaseName};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct ExpectedTestResult {
13 pub result: CheckResult,
15 pub expected_reruns: ExpectedReruns,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum CheckResult {
22 Pass,
23 Leak,
24 LeakFail,
25 Fail,
26 FlakyFail,
27 FlakyFailJunitSuccess,
31 FailLeak,
32 Abort,
33 AbortLeak,
34 Timeout,
35}
36
37impl CheckResult {
38 pub fn is_failure(self) -> bool {
44 match self {
45 CheckResult::Pass | CheckResult::Leak => false,
46 CheckResult::LeakFail
47 | CheckResult::Fail
48 | CheckResult::FlakyFail
49 | CheckResult::FlakyFailJunitSuccess
50 | CheckResult::FailLeak
51 | CheckResult::Abort
52 | CheckResult::AbortLeak
53 | CheckResult::Timeout => true,
54 }
55 }
56
57 pub fn leaky_variant(self) -> Option<Self> {
62 match self {
63 CheckResult::Pass => Some(CheckResult::Leak),
64 CheckResult::Fail => Some(CheckResult::FailLeak),
65 CheckResult::Abort => Some(CheckResult::AbortLeak),
66 CheckResult::Leak
68 | CheckResult::LeakFail
69 | CheckResult::FailLeak
70 | CheckResult::AbortLeak => None,
71 CheckResult::FlakyFail | CheckResult::FlakyFailJunitSuccess | CheckResult::Timeout => {
76 None
77 }
78 }
79 }
80
81 pub fn to_terminal(self) -> TerminalCheckResult {
86 match self {
87 CheckResult::Pass => TerminalCheckResult::Pass,
88 CheckResult::Leak => TerminalCheckResult::Leak,
89 CheckResult::LeakFail => TerminalCheckResult::LeakFail,
90 CheckResult::Fail => TerminalCheckResult::Fail,
91 CheckResult::FlakyFail | CheckResult::FlakyFailJunitSuccess => {
92 TerminalCheckResult::FlakyFail
93 }
94 CheckResult::FailLeak => TerminalCheckResult::FailLeak,
95 CheckResult::Abort | CheckResult::AbortLeak => TerminalCheckResult::Abort,
98 CheckResult::Timeout => TerminalCheckResult::Timeout,
99 }
100 }
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub enum TerminalCheckResult {
110 Pass,
111 Leak,
112 LeakFail,
113 Fail,
114 FlakyFail,
115 FailLeak,
116 Abort,
117 Timeout,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum ExpectedReruns {
123 None,
125 FlakyRunCount(usize),
127 SomeReruns,
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub enum SkipReason {
136 Ignored,
138 Filtered,
141 NotBenchmark,
143 SuiteNotInRun,
145 RerunAlreadyPassed,
147}
148
149impl SkipReason {
150 pub fn counted_in_skip_summary(self) -> bool {
158 match self {
159 SkipReason::Ignored | SkipReason::Filtered | SkipReason::RerunAlreadyPassed => true,
160 SkipReason::NotBenchmark | SkipReason::SuiteNotInRun => false,
161 }
162 }
163}
164
165bitflags::bitflags! {
166 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
168 pub struct RunProperties: u64 {
169 const RELOCATED = 0x1;
170 const WITH_DEFAULT_FILTER = 0x2;
171 const WITH_SKIP_CDYLIB_FILTER = 0x4;
173 const WITH_MULTIPLY_TWO_EXACT_FILTER = 0x8;
175 const CDYLIB_EXAMPLE_PACKAGE_FILTER = 0x10;
176 const SKIP_SUMMARY_CHECK = 0x20;
177 const EXPECT_NO_BINARIES = 0x40;
178 const BENCHMARKS = 0x80;
179 const BENCH_OVERRIDE_TIMEOUT = 0x100;
181 const BENCH_TERMINATION = 0x200;
183 const BENCH_IGNORES_TEST_TIMEOUT = 0x400;
185 const RUN_IGNORED_ONLY = 0x800;
187 const TIMEOUT_RETRIES_PASS = 0x1000;
190 const TIMEOUT_RETRIES_FLAKY = 0x2000;
193 const WITH_RETRIES = 0x4000;
195 const WITH_TARGET_RUNNER = 0x8000;
198 const WITH_TERMINATION = 0x10000;
200 const WITH_TIMEOUT_SUCCESS = 0x20000;
203 const ALLOW_SKIPPED_NAMES_IN_OUTPUT = 0x40000;
206 const WITH_RETRIES_FLAKY_FAIL = 0x80000;
209 const WITH_CLI_FLAKY_RESULT_FAIL = 0x100000;
212 const WITH_CLI_FLAKY_RESULT_PASS = 0x200000;
215 const WITH_CLI_RETRIES_2 = 0x400000;
217 }
218}
219
220#[derive(Clone, Debug)]
221pub struct TestSuiteFixture {
222 pub binary_id: RustBinaryId,
223 pub binary_name: &'static str,
224 pub build_platform: BuildPlatform,
225 pub test_cases: IdOrdMap<TestCaseFixture>,
226 properties: TestSuiteFixtureProperties,
227}
228
229impl IdOrdItem for TestSuiteFixture {
230 type Key<'a> = &'a RustBinaryId;
231 fn key(&self) -> Self::Key<'_> {
232 &self.binary_id
233 }
234 id_upcast!();
235}
236
237impl TestSuiteFixture {
238 pub fn new(
239 binary_id: &'static str,
240 binary_name: &'static str,
241 build_platform: BuildPlatform,
242 test_cases: IdOrdMap<TestCaseFixture>,
243 ) -> Self {
244 Self {
245 binary_id: binary_id.into(),
246 binary_name,
247 build_platform,
248 test_cases,
249 properties: TestSuiteFixtureProperties::empty(),
250 }
251 }
252
253 pub fn with_property(mut self, property: TestSuiteFixtureProperties) -> Self {
254 self.properties |= property;
255 self
256 }
257
258 pub fn has_property(&self, property: TestSuiteFixtureProperties) -> bool {
259 self.properties.contains(property)
260 }
261
262 pub fn assert_test_cases_match(&self, other: &IdOrdMap<TestNameAndFilterMatch<'_>>) {
263 if self.test_cases.len() != other.len() {
264 panic!(
265 "test cases mismatch: expected {} test cases, found {}; \
266 expected: {self:#?}, actual: {other:#?}",
267 self.test_cases.len(),
268 other.len(),
269 );
270 }
271
272 for name_and_filter_match in other {
273 if let Some(test_case) = self.test_cases.get(name_and_filter_match.name) {
274 if test_case.status.is_ignored() == name_and_filter_match.filter_match.is_match() {
275 panic!(
276 "test case status mismatch for '{}': expected {:?}, found {:?}; \
277 expected: {self:#?}, actual: {other:#?}",
278 name_and_filter_match.name,
279 test_case.status,
280 name_and_filter_match.filter_match,
281 );
282 }
283 } else {
284 panic!(
285 "test case '{}' not found in test suite '{}'; \
286 expected: {self:#?}, actual: {other:#?}",
287 name_and_filter_match.name, self.binary_name,
288 );
289 }
290 }
291 }
292}
293
294bitflags::bitflags! {
295 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
296 pub struct TestSuiteFixtureProperties: u64 {
297 const NOT_IN_DEFAULT_SET = 0x1;
298 const MATCHES_CDYLIB_EXAMPLE = 0x2;
299 }
300}
301
302#[derive(Clone, Debug)]
303pub struct TestCaseFixture {
304 pub name: TestCaseName,
305 pub status: TestCaseFixtureStatus,
306 properties: TestCaseFixtureProperties,
307}
308
309impl IdOrdItem for TestCaseFixture {
310 type Key<'a> = &'a TestCaseName;
311 fn key(&self) -> Self::Key<'_> {
312 &self.name
313 }
314 id_upcast!();
315}
316
317impl TestCaseFixture {
318 pub fn new(name: &str, status: TestCaseFixtureStatus) -> Self {
319 Self {
320 name: TestCaseName::new(name),
321 status,
322 properties: TestCaseFixtureProperties::empty(),
323 }
324 }
325
326 pub fn with_property(mut self, property: TestCaseFixtureProperties) -> Self {
327 self.properties |= property;
328 self
329 }
330
331 pub fn has_property(&self, property: TestCaseFixtureProperties) -> bool {
332 self.properties.contains(property)
333 }
334
335 pub fn expected_result(&self, properties: RunProperties) -> ExpectedTestResult {
340 let result = self.expected_check_result(properties);
341 let expected_reruns = self.expected_reruns(result, properties);
342 ExpectedTestResult {
343 result,
344 expected_reruns,
345 }
346 }
347
348 fn expected_check_result(&self, properties: RunProperties) -> CheckResult {
349 if self.has_property(TestCaseFixtureProperties::BENCH_OVERRIDE_TIMEOUT)
351 && properties.contains(RunProperties::BENCH_OVERRIDE_TIMEOUT)
352 {
353 return CheckResult::Timeout;
354 }
355
356 if self.has_property(TestCaseFixtureProperties::BENCH_TERMINATION)
358 && properties.contains(RunProperties::BENCH_TERMINATION)
359 {
360 return CheckResult::Timeout;
361 }
362
363 if self.has_property(TestCaseFixtureProperties::BENCH_IGNORES_TEST_TIMEOUT)
366 && properties.contains(RunProperties::BENCH_IGNORES_TEST_TIMEOUT)
367 {
368 return CheckResult::Pass;
369 }
370
371 if self.has_property(TestCaseFixtureProperties::SLOW_TIMEOUT_SUBSTRING)
374 && properties.contains(RunProperties::TIMEOUT_RETRIES_PASS)
375 {
376 return CheckResult::Pass;
377 }
378
379 if self.has_property(TestCaseFixtureProperties::TEST_SLOW_TIMEOUT_SUBSTRING)
381 && properties.contains(RunProperties::WITH_TERMINATION)
382 {
383 return CheckResult::Timeout;
384 }
385
386 if properties.contains(RunProperties::WITH_TIMEOUT_SUCCESS) {
389 if self.has_property(TestCaseFixtureProperties::EXACT_TEST_SLOW_TIMEOUT) {
390 return CheckResult::Pass;
392 }
393 if self.has_property(TestCaseFixtureProperties::TEST_SLOW_TIMEOUT_SUBSTRING) {
394 return CheckResult::Timeout;
396 }
397 }
398
399 match self.status {
400 TestCaseFixtureStatus::Pass => {
401 if self.has_property(TestCaseFixtureProperties::NEEDS_SAME_CWD)
403 && properties.contains(RunProperties::RELOCATED)
404 {
405 CheckResult::Fail
406 } else {
407 CheckResult::Pass
408 }
409 }
410 TestCaseFixtureStatus::Leak => CheckResult::Leak,
411 TestCaseFixtureStatus::LeakFail => CheckResult::LeakFail,
412 TestCaseFixtureStatus::Fail => CheckResult::Fail,
413 TestCaseFixtureStatus::Flaky { pass_attempt } => {
414 if properties.contains(RunProperties::WITH_CLI_RETRIES_2) {
417 return if pass_attempt <= 3 {
419 CheckResult::Pass
420 } else {
421 CheckResult::Fail
422 };
423 }
424 if properties.contains(RunProperties::WITH_CLI_FLAKY_RESULT_FAIL) {
426 return CheckResult::FlakyFail;
427 }
428 if properties.contains(RunProperties::WITH_CLI_FLAKY_RESULT_PASS) {
429 return CheckResult::Pass;
430 }
431 if properties.contains(RunProperties::WITH_RETRIES_FLAKY_FAIL) {
434 if self.has_property(TestCaseFixtureProperties::FLAKY_RESULT_FAIL_JUNIT_SUCCESS)
435 {
436 return CheckResult::FlakyFailJunitSuccess;
437 } else if self.has_property(TestCaseFixtureProperties::FLAKY_RESULT_FAIL) {
438 return CheckResult::FlakyFail;
439 } else {
440 return CheckResult::Pass;
441 }
442 }
443 if properties.contains(RunProperties::WITH_RETRIES) {
447 CheckResult::Pass
448 } else {
449 CheckResult::Fail
450 }
451 }
452 TestCaseFixtureStatus::FailLeak => CheckResult::FailLeak,
453 TestCaseFixtureStatus::Segfault => {
454 if cfg!(unix) && properties.contains(RunProperties::WITH_TARGET_RUNNER) {
457 CheckResult::Fail
458 } else {
459 CheckResult::Abort
460 }
461 }
462 TestCaseFixtureStatus::IgnoredPass => {
463 if properties.contains(RunProperties::RUN_IGNORED_ONLY) {
464 CheckResult::Pass
465 } else {
466 unreachable!("ignored tests should be filtered out")
467 }
468 }
469 TestCaseFixtureStatus::IgnoredFail => {
470 if properties.contains(RunProperties::RUN_IGNORED_ONLY) {
471 CheckResult::Fail
472 } else {
473 unreachable!("ignored tests should be filtered out")
474 }
475 }
476 TestCaseFixtureStatus::IgnoredFlaky { .. } => {
477 if properties.contains(RunProperties::TIMEOUT_RETRIES_FLAKY) {
480 CheckResult::Pass
481 } else if properties.contains(RunProperties::RUN_IGNORED_ONLY) {
482 CheckResult::Fail
483 } else {
484 unreachable!("ignored tests should be filtered out")
485 }
486 }
487 }
488 }
489
490 fn expected_reruns(&self, result: CheckResult, properties: RunProperties) -> ExpectedReruns {
493 if let TestCaseFixtureStatus::Flaky { pass_attempt }
498 | TestCaseFixtureStatus::IgnoredFlaky { pass_attempt } = self.status
499 && (result == CheckResult::Pass
500 || result == CheckResult::FlakyFail
501 || result == CheckResult::FlakyFailJunitSuccess)
502 {
503 debug_assert!(
504 pass_attempt >= 2,
505 "pass_attempt must be >= 2 for a flaky test"
506 );
507 return ExpectedReruns::FlakyRunCount((pass_attempt - 1) as usize);
508 }
509
510 let has_retries = properties.intersects(
514 RunProperties::WITH_RETRIES
515 | RunProperties::WITH_RETRIES_FLAKY_FAIL
516 | RunProperties::WITH_CLI_FLAKY_RESULT_FAIL
517 | RunProperties::WITH_CLI_FLAKY_RESULT_PASS
518 | RunProperties::WITH_CLI_RETRIES_2,
519 );
520 if has_retries && result.is_failure() {
521 return ExpectedReruns::SomeReruns;
522 }
523
524 ExpectedReruns::None
525 }
526}
527
528pub fn expected_skip_reason(
545 suite: &TestSuiteFixture,
546 test: &TestCaseFixture,
547 properties: RunProperties,
548) -> Option<SkipReason> {
549 if (suite.has_property(TestSuiteFixtureProperties::NOT_IN_DEFAULT_SET)
550 && properties.contains(RunProperties::WITH_DEFAULT_FILTER))
551 || (!suite.has_property(TestSuiteFixtureProperties::MATCHES_CDYLIB_EXAMPLE)
552 && properties.contains(RunProperties::CDYLIB_EXAMPLE_PACKAGE_FILTER))
553 {
554 return Some(SkipReason::SuiteNotInRun);
555 }
556
557 if properties.contains(RunProperties::BENCHMARKS)
558 && !test.has_property(TestCaseFixtureProperties::IS_BENCHMARK)
559 {
560 return Some(SkipReason::NotBenchmark);
561 }
562
563 if !test.status.is_ignored()
571 && test.has_property(TestCaseFixtureProperties::NOT_IN_DEFAULT_SET)
572 && properties.contains(RunProperties::WITH_DEFAULT_FILTER)
573 {
574 return Some(SkipReason::Filtered);
575 }
576
577 if !test.status.is_ignored()
579 && cfg!(unix)
580 && test.has_property(TestCaseFixtureProperties::NOT_IN_DEFAULT_SET_UNIX)
581 && properties.contains(RunProperties::WITH_DEFAULT_FILTER)
582 {
583 return Some(SkipReason::Filtered);
584 }
585
586 if !test.status.is_ignored()
588 && test.has_property(TestCaseFixtureProperties::MATCHES_CDYLIB)
589 && properties.contains(RunProperties::WITH_SKIP_CDYLIB_FILTER)
590 {
591 return Some(SkipReason::Filtered);
592 }
593
594 if !test.status.is_ignored()
596 && !test.has_property(TestCaseFixtureProperties::MATCHES_TEST_MULTIPLY_TWO)
597 && properties.contains(RunProperties::WITH_MULTIPLY_TWO_EXACT_FILTER)
598 {
599 return Some(SkipReason::Filtered);
600 }
601
602 if !test.status.is_ignored()
604 && properties.contains(RunProperties::CDYLIB_EXAMPLE_PACKAGE_FILTER)
605 && test.name != TestCaseName::new("tests::test_multiply_two_cdylib")
606 {
607 return Some(SkipReason::Filtered);
608 }
609
610 if properties.contains(RunProperties::EXPECT_NO_BINARIES) {
612 return Some(SkipReason::Filtered);
613 }
614
615 if properties.contains(RunProperties::BENCH_OVERRIDE_TIMEOUT) {
617 return (!test.has_property(TestCaseFixtureProperties::BENCH_OVERRIDE_TIMEOUT))
618 .then_some(SkipReason::Filtered);
619 }
620
621 if properties.contains(RunProperties::BENCH_TERMINATION) {
623 return (!test.has_property(TestCaseFixtureProperties::BENCH_TERMINATION))
624 .then_some(SkipReason::Filtered);
625 }
626
627 if properties.contains(RunProperties::BENCH_IGNORES_TEST_TIMEOUT) {
629 return (!test.has_property(TestCaseFixtureProperties::BENCH_IGNORES_TEST_TIMEOUT))
630 .then_some(SkipReason::Filtered);
631 }
632
633 if properties.contains(RunProperties::TIMEOUT_RETRIES_PASS) {
637 return (!test.has_property(TestCaseFixtureProperties::TEST_SLOW_TIMEOUT_SUBSTRING)
639 || test.has_property(TestCaseFixtureProperties::IS_BENCHMARK))
640 .then_some(SkipReason::Filtered);
641 }
642
643 if properties.contains(RunProperties::TIMEOUT_RETRIES_FLAKY) {
645 return (!test.has_property(TestCaseFixtureProperties::FLAKY_SLOW_TIMEOUT_SUBSTRING))
646 .then_some(SkipReason::Filtered);
647 }
648
649 if properties.contains(RunProperties::WITH_TERMINATION) {
651 return (!test.has_property(TestCaseFixtureProperties::TEST_SLOW_TIMEOUT_SUBSTRING)
652 || test.has_property(TestCaseFixtureProperties::IS_BENCHMARK))
653 .then_some(SkipReason::Filtered);
654 }
655
656 if properties.contains(RunProperties::WITH_TIMEOUT_SUCCESS) {
658 return (!test.has_property(TestCaseFixtureProperties::TEST_SLOW_TIMEOUT_SUBSTRING)
659 || test.has_property(TestCaseFixtureProperties::IS_BENCHMARK))
660 .then_some(SkipReason::Filtered);
661 }
662
663 if properties.contains(RunProperties::RUN_IGNORED_ONLY) {
666 if test.has_property(TestCaseFixtureProperties::SLOW_TIMEOUT_SUBSTRING) {
668 return Some(SkipReason::Filtered);
669 }
670 if !test.status.is_ignored() {
672 return Some(SkipReason::Filtered);
673 }
674 return None;
676 }
677
678 if test.status.is_ignored() {
680 return Some(SkipReason::Ignored);
681 }
682
683 None
684}
685
686#[derive(Clone, Debug)]
687pub struct TestNameAndFilterMatch<'a> {
688 pub name: &'a TestCaseName,
689 pub filter_match: FilterMatch,
690}
691
692impl<'a> IdOrdItem for TestNameAndFilterMatch<'a> {
693 type Key<'k>
694 = &'a TestCaseName
695 where
696 Self: 'k;
697 fn key(&self) -> Self::Key<'_> {
698 self.name
699 }
700 id_upcast!();
701}
702
703impl PartialEq<TestNameAndFilterMatch<'_>> for TestCaseFixture {
706 fn eq(&self, other: &TestNameAndFilterMatch<'_>) -> bool {
707 self.name == *other.name && self.status.is_ignored() != other.filter_match.is_match()
708 }
709}
710
711#[derive(Copy, Clone, Debug, Eq, PartialEq)]
712pub enum TestCaseFixtureStatus {
713 Pass,
714 Fail,
715 Flaky {
716 pass_attempt: u32,
717 },
718 Leak,
719 LeakFail,
720 FailLeak,
721 Segfault,
722 IgnoredPass,
723 IgnoredFail,
724 IgnoredFlaky {
727 pass_attempt: u32,
728 },
729}
730
731impl TestCaseFixtureStatus {
732 pub fn is_ignored(self) -> bool {
733 match self {
734 TestCaseFixtureStatus::IgnoredPass
735 | TestCaseFixtureStatus::IgnoredFail
736 | TestCaseFixtureStatus::IgnoredFlaky { .. } => true,
737 TestCaseFixtureStatus::Pass
738 | TestCaseFixtureStatus::Fail
739 | TestCaseFixtureStatus::Flaky { .. }
740 | TestCaseFixtureStatus::Leak
741 | TestCaseFixtureStatus::LeakFail
742 | TestCaseFixtureStatus::FailLeak
743 | TestCaseFixtureStatus::Segfault => false,
744 }
745 }
746}
747
748bitflags::bitflags! {
749 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
750 pub struct TestCaseFixtureProperties: u64 {
751 const NEEDS_SAME_CWD = 0x1;
752 const NOT_IN_DEFAULT_SET = 0x2;
753 const MATCHES_CDYLIB = 0x4;
754 const MATCHES_TEST_MULTIPLY_TWO = 0x8;
755 const NOT_IN_DEFAULT_SET_UNIX = 0x10;
756 const IS_BENCHMARK = 0x20;
757 const BENCH_OVERRIDE_TIMEOUT = 0x40;
759 const BENCH_TERMINATION = 0x80;
761 const BENCH_IGNORES_TEST_TIMEOUT = 0x100;
763 const SLOW_TIMEOUT_SUBSTRING = 0x200;
765 const TEST_SLOW_TIMEOUT_SUBSTRING = 0x400;
767 const FLAKY_SLOW_TIMEOUT_SUBSTRING = 0x800;
769 const EXACT_TEST_SLOW_TIMEOUT = 0x1000;
771 const FLAKY_RESULT_FAIL = 0x2000;
773 const FLAKY_RESULT_FAIL_JUNIT_SUCCESS = 0x4000;
776 }
777}