Skip to main content

fixture_data/
models.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Data models for fixture information.
5
6use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
7use nextest_metadata::{BuildPlatform, FilterMatch, RustBinaryId, TestCaseName};
8
9/// The expected result for a test execution, including both the outcome and the
10/// expected rerun behavior.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct ExpectedTestResult {
13    /// The expected outcome.
14    pub result: CheckResult,
15    /// The expected rerun behavior.
16    pub expected_reruns: ExpectedReruns,
17}
18
19/// The expected outcome of a test execution.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum CheckResult {
22    Pass,
23    Leak,
24    LeakFail,
25    Fail,
26    FlakyFail,
27    /// The test is a flaky-fail (counts as a run failure), but is configured
28    /// with `junit.flaky-fail-status = "success"` so it appears as a success
29    /// in JUnit XML output.
30    FlakyFailJunitSuccess,
31    FailLeak,
32    Abort,
33    AbortLeak,
34    Timeout,
35}
36
37impl CheckResult {
38    /// Returns true if this result represents a test failure of any kind.
39    ///
40    /// `Leak` is not a failure: the test passed but leaked subprocess handles.
41    /// `LeakFail` is a failure: the test was marked as failed due to leaked
42    /// handles.
43    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    /// Returns the leaky variant for this check result, if one exists.
58    ///
59    /// Leak detection is timing-sensitive, so a test not expected to leak may
60    /// still be reported leaky.
61    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            // These variants are already leaky.
67            CheckResult::Leak
68            | CheckResult::LeakFail
69            | CheckResult::FailLeak
70            | CheckResult::AbortLeak => None,
71            // Flaky status lines and the "flaky failure" JUnit type are written
72            // without consulting the leak flag, and Timeout doesn't track leaks
73            // at all. Flaky leaks do show up in the summary's leaky count,
74            // which is checked as a lower bound.
75            CheckResult::FlakyFail | CheckResult::FlakyFailJunitSuccess | CheckResult::Timeout => {
76                None
77            }
78        }
79    }
80
81    /// Converts this result to its terminal representation.
82    ///
83    /// Terminal output cannot distinguish between `FlakyFail` and
84    /// `FlakyFailJunitSuccess` — both display as `FLKY-FL`.
85    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            // The status column ignores the leak flag for aborts, so both
96            // show up as ABORT in the UI.
97            CheckResult::Abort | CheckResult::AbortLeak => TerminalCheckResult::Abort,
98            CheckResult::Timeout => TerminalCheckResult::Timeout,
99        }
100    }
101}
102
103/// The result of a test as it appears in terminal output.
104///
105/// This is separate from [`CheckResult`] because some model-level distinctions
106/// (e.g., `FlakyFailJunitSuccess` vs `FlakyFail`) are invisible in terminal
107/// output.
108#[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/// What rerun behavior to expect for a test case.
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum ExpectedReruns {
123    /// No reruns expected (no retries configured, or test doesn't retry).
124    None,
125    /// Exactly N flaky runs expected (test passed on attempt N+1).
126    FlakyRunCount(usize),
127    /// Some reruns expected but the exact count is unknown (failing test with
128    /// retries, where the count depends on per-test profile overrides that the
129    /// fixture data model doesn't track).
130    SomeReruns,
131}
132
133/// The reason a test case is expected to be skipped.
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub enum SkipReason {
136    /// The test is ignored, and the run doesn't include ignored tests.
137    Ignored,
138    /// The test is filtered out by the run's filterset, or isn't in the set of
139    /// test names a verifier was asked to check.
140    Filtered,
141    /// The run is benchmarks-only, and this test isn't a benchmark.
142    NotBenchmark,
143    /// The test's entire suite is excluded from the run.
144    SuiteNotInRun,
145    /// The test passed in the initial run of a rerun sequence.
146    RerunAlreadyPassed,
147}
148
149impl SkipReason {
150    /// Returns true if this test contributes to the "skipped" count in nextest's
151    /// summary line.
152    ///
153    /// Tests filtered out by filtersets, ignored tests, and tests that already
154    /// passed in a rerun all show up in nextest's skip count. Tests outside the
155    /// run's scope entirely (suites excluded from the run, and non-benchmark
156    /// tests in benchmark runs) don't appear in the counts at all.
157    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    /// Properties that control which tests should be run in integration test invocations.
167    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
168    pub struct RunProperties: u64 {
169        const RELOCATED = 0x1;
170        const WITH_DEFAULT_FILTER = 0x2;
171        // --skip cdylib
172        const WITH_SKIP_CDYLIB_FILTER = 0x4;
173        // --exact test_multiply_two tests::test_multiply_two_cdylib
174        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        /// Run ignored benchmarks with the `with-bench-override` profile.
180        const BENCH_OVERRIDE_TIMEOUT = 0x100;
181        /// Run ignored benchmarks with the `with-bench-termination` profile.
182        const BENCH_TERMINATION = 0x200;
183        /// Run benchmarks with the `with-test-termination-only` profile.
184        const BENCH_IGNORES_TEST_TIMEOUT = 0x400;
185        /// Run ignored tests only (--run-ignored only), excluding slow timeout tests.
186        const RUN_IGNORED_ONLY = 0x800;
187        /// Run with with-timeout-retries-success profile, slow_timeout tests only.
188        /// These tests time out but pass due to on-timeout=pass.
189        const TIMEOUT_RETRIES_PASS = 0x1000;
190        /// Run with with-timeout-retries-success profile, flaky slow timeout test only.
191        /// This test fails twice then times out (passes) on the 3rd attempt.
192        const TIMEOUT_RETRIES_FLAKY = 0x2000;
193        /// Run with the with-retries profile. Flaky tests should pass after retries.
194        const WITH_RETRIES = 0x4000;
195        /// Run with a target runner set. On Unix, segfaults are reported as regular
196        /// failures because the passthrough runner doesn't propagate signal info.
197        const WITH_TARGET_RUNNER = 0x8000;
198        /// Run with the with-termination profile. Tests should time out.
199        const WITH_TERMINATION = 0x10000;
200        /// Run with the with-timeout-success profile. test_slow_timeout passes
201        /// (on-timeout = "pass"), others fail.
202        const WITH_TIMEOUT_SUCCESS = 0x20000;
203        /// Allow skipped test names to appear in output (e.g., for replay which shows SKIP lines).
204        /// Without this flag, verification fails if any skipped test name appears in the output.
205        const ALLOW_SKIPPED_NAMES_IN_OUTPUT = 0x40000;
206        /// Run with the with-retries-flaky-fail profile. Flaky tests with
207        /// `flaky-result = "fail"` should count as failures.
208        const WITH_RETRIES_FLAKY_FAIL = 0x80000;
209        /// Run with `--flaky-result fail` CLI flag. All flaky tests should
210        /// count as failures, regardless of per-test config.
211        const WITH_CLI_FLAKY_RESULT_FAIL = 0x100000;
212        /// Run with `--flaky-result pass` CLI flag. No flaky tests should
213        /// count as failures, even if config has `flaky-result = "fail"`.
214        const WITH_CLI_FLAKY_RESULT_PASS = 0x200000;
215        /// Run with `--profile with-retries --retries 2` on the CLI.
216        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    /// Determines the expected test result based on test status and run
336    /// properties.
337    ///
338    /// Returns both the expected outcome and the expected rerun behavior.
339    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        // BenchOverrideTimeout - the benchmark times out due to override.
350        if self.has_property(TestCaseFixtureProperties::BENCH_OVERRIDE_TIMEOUT)
351            && properties.contains(RunProperties::BENCH_OVERRIDE_TIMEOUT)
352        {
353            return CheckResult::Timeout;
354        }
355
356        // BenchTermination - the benchmark times out due to bench.slow-timeout.
357        if self.has_property(TestCaseFixtureProperties::BENCH_TERMINATION)
358            && properties.contains(RunProperties::BENCH_TERMINATION)
359        {
360            return CheckResult::Timeout;
361        }
362
363        // BenchIgnoresTestTimeout - the benchmark passes because it uses
364        // bench.slow-timeout (30 years default) instead of slow-timeout.
365        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        // TIMEOUT_RETRIES_PASS - tests time out but pass due to on-timeout=pass.
372        // The output shows PASS, not TIMEOUT.
373        if self.has_property(TestCaseFixtureProperties::SLOW_TIMEOUT_SUBSTRING)
374            && properties.contains(RunProperties::TIMEOUT_RETRIES_PASS)
375        {
376            return CheckResult::Pass;
377        }
378
379        // WITH_TERMINATION - all test_slow_timeout* tests time out.
380        if self.has_property(TestCaseFixtureProperties::TEST_SLOW_TIMEOUT_SUBSTRING)
381            && properties.contains(RunProperties::WITH_TERMINATION)
382        {
383            return CheckResult::Timeout;
384        }
385
386        // WITH_TIMEOUT_SUCCESS - test_slow_timeout passes (on-timeout = "pass"),
387        // while other test_slow_timeout* tests fail.
388        if properties.contains(RunProperties::WITH_TIMEOUT_SUCCESS) {
389            if self.has_property(TestCaseFixtureProperties::EXACT_TEST_SLOW_TIMEOUT) {
390                // test_slow_timeout has on-timeout = "pass" override.
391                return CheckResult::Pass;
392            }
393            if self.has_property(TestCaseFixtureProperties::TEST_SLOW_TIMEOUT_SUBSTRING) {
394                // Other test_slow_timeout* tests time out normally.
395                return CheckResult::Timeout;
396            }
397        }
398
399        match self.status {
400            TestCaseFixtureStatus::Pass => {
401                // NeedsSameCwd tests fail when relocated.
402                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                // A global `--retries N` on the CLI replaces per-test config
415                // retry overrides entirely.
416                if properties.contains(RunProperties::WITH_CLI_RETRIES_2) {
417                    // `--retries 2` => 3 attempts total.
418                    return if pass_attempt <= 3 {
419                        CheckResult::Pass
420                    } else {
421                        CheckResult::Fail
422                    };
423                }
424                // CLI --flaky-result overrides all config-level settings.
425                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                // With retries and flaky-result = "fail", flaky tests that eventually
432                // pass are still counted as failures.
433                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                // With retries, flaky tests eventually pass. (Retries are
444                // configured in a way which ensures that all tests eventually
445                // pass.)
446                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                // On Unix, segfaults aren't passed through by the passthrough runner.
455                // They show as regular failures instead of aborts.
456                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                // TIMEOUT_RETRIES_FLAKY: the test fails several times, then
478                // times out and passes due to on-timeout=pass.
479                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    /// Computes the expected rerun behavior for a test case based on its
491    /// fixture status, the check result, and the run properties.
492    fn expected_reruns(&self, result: CheckResult, properties: RunProperties) -> ExpectedReruns {
493        // Flaky tests that eventually pass have a known rerun count.
494        // This applies both to flaky-pass (CheckResult::Pass) and
495        // flaky-fail (CheckResult::FlakyFail) — either way, the test ran
496        // pass_attempt - 1 failing attempts before the passing one.
497        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        // Failing tests with retries configured will have reruns, but the exact
511        // count depends on per-test profile overrides which the fixture data
512        // model doesn't track.
513        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
528/// Determines the reason a test should be skipped.
529///
530/// The general algorithm is:
531///
532/// * If the suite is not part of the run, produce `SuiteNotInRun`.
533/// * If this is a benchmark run and the test is not a benchmark, produce
534///   `NotBenchmark`.
535/// * Otherwise, check that the test is filtered out.
536/// * Otherwise, check that the test is ignored.
537/// * Otherwise, produce `None`.
538///
539/// This assigns every skip reason that is derivable from the fixture model
540/// and the run properties. The verifier in the integration-tests crate
541/// assigns the rest: `RerunAlreadyPassed` depends on the outcome of a prior
542/// run, and the not-in-name-list `Filtered` case depends on the name set a
543/// specific check was asked to verify.
544pub 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    // The production checks the ignore filter before the filterset. The
564    // !test.status.is_ignored() guards below defer such tests to the trailing
565    // ignore check.
566    //
567    // ---
568    //
569    // NotInDefaultSet filter.
570    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    // NotInDefaultSetUnix filter (Unix-specific).
578    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    // MatchesCdylib + WithSkipCdylibFilter.
587    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    // WithMultiplyTwoExactFilter - skip tests that don't match.
595    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    // CdyLibExamplePackageFilter - only run test_multiply_two_cdylib.
603    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    // ExpectNoBinaries - all tests should be skipped.
611    if properties.contains(RunProperties::EXPECT_NO_BINARIES) {
612        return Some(SkipReason::Filtered);
613    }
614
615    // BenchOverrideTimeout - only run the specific benchmark that times out.
616    if properties.contains(RunProperties::BENCH_OVERRIDE_TIMEOUT) {
617        return (!test.has_property(TestCaseFixtureProperties::BENCH_OVERRIDE_TIMEOUT))
618            .then_some(SkipReason::Filtered);
619    }
620
621    // BenchTermination - only run the specific benchmark that times out.
622    if properties.contains(RunProperties::BENCH_TERMINATION) {
623        return (!test.has_property(TestCaseFixtureProperties::BENCH_TERMINATION))
624            .then_some(SkipReason::Filtered);
625    }
626
627    // BenchIgnoresTestTimeout - only run the specific benchmark that passes.
628    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    // TIMEOUT_RETRIES_PASS - only run tests with the
634    // TEST_SLOW_TIMEOUT_SUBSTRING property (not benchmarks). These are the
635    // test_slow_timeout* tests that time out but pass.
636    if properties.contains(RunProperties::TIMEOUT_RETRIES_PASS) {
637        // Skip if not SLOW_TIMEOUT or if it's a benchmark.
638        return (!test.has_property(TestCaseFixtureProperties::TEST_SLOW_TIMEOUT_SUBSTRING)
639            || test.has_property(TestCaseFixtureProperties::IS_BENCHMARK))
640        .then_some(SkipReason::Filtered);
641    }
642
643    // TIMEOUT_RETRIES_FLAKY - only run the flaky slow timeout test.
644    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    // WITH_TERMINATION - only run test_slow_timeout* tests (they time out).
650    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    // WITH_TIMEOUT_SUCCESS - only run test_slow_timeout* tests.
657    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    // RUN_IGNORED_ONLY: run only ignored tests, excluding slow timeout
664    // tests.
665    if properties.contains(RunProperties::RUN_IGNORED_ONLY) {
666        // Skip slow timeout tests (filtered out in the test).
667        if test.has_property(TestCaseFixtureProperties::SLOW_TIMEOUT_SUBSTRING) {
668            return Some(SkipReason::Filtered);
669        }
670        // Skip non-ignored tests.
671        if !test.status.is_ignored() {
672            return Some(SkipReason::Filtered);
673        }
674        // Run other ignored tests.
675        return None;
676    }
677
678    // Ignored tests are skipped by this test suite.
679    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
703// This isn't great, but it is the easiest way to compare an IdOrdMap of
704// TestFixture with an IdOrdMap of TestNameAndFilterMatch.
705impl 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    /// An ignored test that is flaky: it fails `pass_attempt - 1` times, then
725    /// passes on attempt `pass_attempt`.
726    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        /// Benchmark that times out with the with-bench-override profile.
758        const BENCH_OVERRIDE_TIMEOUT = 0x40;
759        /// Benchmark that times out with the with-bench-termination profile.
760        const BENCH_TERMINATION = 0x80;
761        /// Benchmark that passes with the with-test-termination-only profile.
762        const BENCH_IGNORES_TEST_TIMEOUT = 0x100;
763        /// Test with "slow_timeout" as a substring.
764        const SLOW_TIMEOUT_SUBSTRING = 0x200;
765        /// Test with "test_slow_timeout" as a substring.
766        const TEST_SLOW_TIMEOUT_SUBSTRING = 0x400;
767        /// Test with "flaky_slow_timeout" as a substring.
768        const FLAKY_SLOW_TIMEOUT_SUBSTRING = 0x800;
769        /// Exactly test_slow_timeout (not test_slow_timeout_2 or test_slow_timeout_subprocess).
770        const EXACT_TEST_SLOW_TIMEOUT = 0x1000;
771        /// Flaky test configured with `flaky-result = "fail"`.
772        const FLAKY_RESULT_FAIL = 0x2000;
773        /// Flaky test configured with `flaky-result = "fail"` and
774        /// `junit.flaky-fail-status = "success"`.
775        const FLAKY_RESULT_FAIL_JUNIT_SUCCESS = 0x4000;
776    }
777}