nextest_runner/
test_filter.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Filtering tests based on user-specified parameters.
5//!
6//! The main structure in this module is [`TestFilter`].
7
8use crate::{errors::TestFilterBuildError, record::ComputedRerunInfo, run_mode::NextestRunMode};
9use aho_corasick::AhoCorasick;
10use nextest_filtering::{BinaryQuery, EvalContext, Filterset, GroupLookup, TestQuery};
11use nextest_metadata::{FilterMatch, MismatchReason, RustBinaryId, RustTestKind, TestCaseName};
12use std::{collections::HashSet, fmt, mem};
13
14/// Whether to run ignored tests.
15#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
16pub enum RunIgnored {
17    /// Only run tests that aren't ignored.
18    ///
19    /// This is the default.
20    #[default]
21    Default,
22
23    /// Only run tests that are ignored.
24    Only,
25
26    /// Run both ignored and non-ignored tests.
27    All,
28}
29
30/// A higher-level filter.
31#[derive(Clone, Copy, Debug)]
32pub enum FilterBound {
33    /// Filter with the default set.
34    DefaultSet,
35
36    /// Do not perform any higher-level filtering.
37    All,
38}
39
40#[derive(Clone, Debug, Eq, PartialEq)]
41/// Filters Binaries based on `TestFilterExprs`.
42pub struct BinaryFilter {
43    exprs: TestFilterExprs,
44}
45
46impl BinaryFilter {
47    /// Creates a new `BinaryFilter` from `exprs`.
48    ///
49    /// If `exprs` is an empty slice, all binaries will match.
50    pub fn new(exprs: Vec<Filterset>) -> Self {
51        let exprs = if exprs.is_empty() {
52            TestFilterExprs::All
53        } else {
54            TestFilterExprs::Sets(exprs)
55        };
56        Self { exprs }
57    }
58
59    /// Returns a value indicating whether this binary should or should not be run to obtain the
60    /// list of tests within it.
61    pub fn check_match(
62        &self,
63        query: &BinaryQuery<'_>,
64        ecx: &EvalContext<'_>,
65        bound: FilterBound,
66    ) -> FilterBinaryMatch {
67        let expr_result = match &self.exprs {
68            TestFilterExprs::All => FilterBinaryMatch::Definite,
69            TestFilterExprs::Sets(exprs) => exprs.iter().fold(
70                FilterBinaryMatch::Mismatch {
71                    // Just use this as a placeholder as the lowest possible value.
72                    reason: BinaryMismatchReason::Expression,
73                },
74                |acc, expr| {
75                    acc.logic_or(FilterBinaryMatch::from_result(
76                        expr.matches_binary(query, ecx),
77                        BinaryMismatchReason::Expression,
78                    ))
79                },
80            ),
81        };
82
83        // If none of the expressions matched, then there's no need to check the default set.
84        if !expr_result.is_match() {
85            return expr_result;
86        }
87
88        match bound {
89            FilterBound::All => expr_result,
90            FilterBound::DefaultSet => expr_result.logic_and(FilterBinaryMatch::from_result(
91                ecx.default_filter.matches_binary(query, ecx),
92                BinaryMismatchReason::DefaultSet,
93            )),
94        }
95    }
96}
97
98/// Filter for selecting which tests to run.
99///
100/// Constructed via [`TestFilter::new`].
101#[derive(Clone, Debug)]
102pub struct TestFilter {
103    mode: NextestRunMode,
104    rerun_info: Option<ComputedRerunInfo>,
105    run_ignored: RunIgnored,
106    patterns: ResolvedFilterPatterns,
107    binary_filter: BinaryFilter,
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
111enum TestFilterExprs {
112    /// No filtersets specified to filter against -- match the default set of tests.
113    All,
114
115    /// Filtersets to match against. A match can be against any of the sets.
116    Sets(Vec<Filterset>),
117}
118
119/// A set of string-based patterns for test filters.
120#[derive(Clone, Debug, Eq, PartialEq)]
121pub enum TestFilterPatterns {
122    /// The only patterns specified (if any) are skip patterns: match the default set of tests minus
123    /// the skip patterns.
124    SkipOnly {
125        /// Skip patterns.
126        skip_patterns: Vec<String>,
127
128        /// Skip patterns to match exactly.
129        skip_exact_patterns: HashSet<String>,
130    },
131
132    /// At least one substring or exact pattern is specified.
133    ///
134    /// In other words, at least one of `patterns` or `exact_patterns` should be non-empty.
135    ///
136    /// A fully empty `Patterns` is logically sound (will match no tests), but never created by
137    /// nextest itself.
138    Patterns {
139        /// Substring patterns.
140        patterns: Vec<String>,
141
142        /// Patterns to match exactly.
143        exact_patterns: HashSet<String>,
144
145        /// Patterns passed in via `--skip`.
146        skip_patterns: Vec<String>,
147
148        /// Skip patterns to match exactly.
149        skip_exact_patterns: HashSet<String>,
150    },
151}
152
153impl Default for TestFilterPatterns {
154    fn default() -> Self {
155        Self::SkipOnly {
156            skip_patterns: Vec::new(),
157            skip_exact_patterns: HashSet::new(),
158        }
159    }
160}
161
162impl TestFilterPatterns {
163    /// Initializes a new `TestFilterPatterns` with a set of substring patterns specified before
164    /// `--`.
165    ///
166    /// An empty slice matches all tests.
167    pub fn new(substring_patterns: Vec<String>) -> Self {
168        if substring_patterns.is_empty() {
169            Self::default()
170        } else {
171            Self::Patterns {
172                patterns: substring_patterns,
173                exact_patterns: HashSet::new(),
174                skip_patterns: Vec::new(),
175                skip_exact_patterns: HashSet::new(),
176            }
177        }
178    }
179
180    /// Adds a regular pattern to the set of patterns.
181    pub fn add_substring_pattern(&mut self, pattern: String) {
182        match self {
183            Self::SkipOnly {
184                skip_patterns,
185                skip_exact_patterns,
186            } => {
187                *self = Self::Patterns {
188                    patterns: vec![pattern],
189                    exact_patterns: HashSet::new(),
190                    skip_patterns: mem::take(skip_patterns),
191                    skip_exact_patterns: mem::take(skip_exact_patterns),
192                };
193            }
194            Self::Patterns { patterns, .. } => {
195                patterns.push(pattern);
196            }
197        }
198    }
199
200    /// Adds an exact pattern to the set of patterns.
201    pub fn add_exact_pattern(&mut self, pattern: String) {
202        match self {
203            Self::SkipOnly {
204                skip_patterns,
205                skip_exact_patterns,
206            } => {
207                *self = Self::Patterns {
208                    patterns: Vec::new(),
209                    exact_patterns: [pattern].into_iter().collect(),
210                    skip_patterns: mem::take(skip_patterns),
211                    skip_exact_patterns: mem::take(skip_exact_patterns),
212                };
213            }
214            Self::Patterns { exact_patterns, .. } => {
215                exact_patterns.insert(pattern);
216            }
217        }
218    }
219
220    /// Adds a skip pattern to the set of patterns.
221    pub fn add_skip_pattern(&mut self, pattern: String) {
222        match self {
223            Self::SkipOnly { skip_patterns, .. } => {
224                skip_patterns.push(pattern);
225            }
226            Self::Patterns { skip_patterns, .. } => {
227                skip_patterns.push(pattern);
228            }
229        }
230    }
231
232    /// Adds a skip pattern to match exactly.
233    pub fn add_skip_exact_pattern(&mut self, pattern: String) {
234        match self {
235            Self::SkipOnly {
236                skip_exact_patterns,
237                ..
238            } => {
239                skip_exact_patterns.insert(pattern);
240            }
241            Self::Patterns {
242                skip_exact_patterns,
243                ..
244            } => {
245                skip_exact_patterns.insert(pattern);
246            }
247        }
248    }
249
250    fn resolve(self) -> Result<ResolvedFilterPatterns, TestFilterBuildError> {
251        match self {
252            Self::SkipOnly {
253                mut skip_patterns,
254                skip_exact_patterns,
255            } => {
256                if skip_patterns.is_empty() {
257                    Ok(ResolvedFilterPatterns::All)
258                } else {
259                    // sort_unstable allows the PartialEq implementation to work correctly.
260                    skip_patterns.sort_unstable();
261                    let skip_pattern_matcher = Box::new(AhoCorasick::new(&skip_patterns)?);
262                    Ok(ResolvedFilterPatterns::SkipOnly {
263                        skip_patterns,
264                        skip_pattern_matcher,
265                        skip_exact_patterns,
266                    })
267                }
268            }
269            Self::Patterns {
270                mut patterns,
271                exact_patterns,
272                mut skip_patterns,
273                skip_exact_patterns,
274            } => {
275                // sort_unstable allows the PartialEq implementation to work correctly.
276                patterns.sort_unstable();
277                skip_patterns.sort_unstable();
278
279                let pattern_matcher = Box::new(AhoCorasick::new(&patterns)?);
280                let skip_pattern_matcher = Box::new(AhoCorasick::new(&skip_patterns)?);
281
282                Ok(ResolvedFilterPatterns::Patterns {
283                    patterns,
284                    exact_patterns,
285                    skip_patterns,
286                    skip_exact_patterns,
287                    pattern_matcher,
288                    skip_pattern_matcher,
289                })
290            }
291        }
292    }
293}
294
295#[derive(Clone, Debug, Default)]
296enum ResolvedFilterPatterns {
297    /// Match all tests.
298    ///
299    /// This is mostly for convenience -- it's equivalent to `SkipOnly` with an empty set of skip
300    /// patterns.
301    #[default]
302    All,
303
304    /// Match all tests except those that match the skip patterns.
305    SkipOnly {
306        skip_patterns: Vec<String>,
307        skip_pattern_matcher: Box<AhoCorasick>,
308        skip_exact_patterns: HashSet<String>,
309    },
310
311    /// Match tests that match the patterns and don't match the skip patterns.
312    Patterns {
313        patterns: Vec<String>,
314        exact_patterns: HashSet<String>,
315        skip_patterns: Vec<String>,
316        skip_exact_patterns: HashSet<String>,
317        pattern_matcher: Box<AhoCorasick>,
318        skip_pattern_matcher: Box<AhoCorasick>,
319    },
320}
321
322impl ResolvedFilterPatterns {
323    fn name_match(&self, test_name: &TestCaseName) -> FilterNameMatch {
324        let test_name = test_name.as_str();
325        match self {
326            Self::All => FilterNameMatch::MatchEmptyPatterns,
327            Self::SkipOnly {
328                // skip_patterns is covered by the matcher.
329                skip_patterns: _,
330                skip_exact_patterns,
331                skip_pattern_matcher,
332            } => {
333                if skip_exact_patterns.contains(test_name)
334                    || skip_pattern_matcher.is_match(test_name)
335                {
336                    FilterNameMatch::Mismatch(MismatchReason::String)
337                } else {
338                    FilterNameMatch::MatchWithPatterns
339                }
340            }
341            Self::Patterns {
342                // patterns is covered by the matcher.
343                patterns: _,
344                exact_patterns,
345                // skip_patterns is covered by the matcher.
346                skip_patterns: _,
347                skip_exact_patterns,
348                pattern_matcher,
349                skip_pattern_matcher,
350            } => {
351                // skip overrides all other patterns.
352                if skip_exact_patterns.contains(test_name)
353                    || skip_pattern_matcher.is_match(test_name)
354                {
355                    FilterNameMatch::Mismatch(MismatchReason::String)
356                } else if exact_patterns.contains(test_name) || pattern_matcher.is_match(test_name)
357                {
358                    FilterNameMatch::MatchWithPatterns
359                } else {
360                    FilterNameMatch::Mismatch(MismatchReason::String)
361                }
362            }
363        }
364    }
365}
366
367impl PartialEq for ResolvedFilterPatterns {
368    fn eq(&self, other: &Self) -> bool {
369        match (self, other) {
370            (Self::All, Self::All) => true,
371            (
372                Self::SkipOnly {
373                    skip_patterns,
374                    skip_exact_patterns,
375                    // The matcher is derived from `skip_patterns`, so it can be ignored.
376                    skip_pattern_matcher: _,
377                },
378                Self::SkipOnly {
379                    skip_patterns: other_skip_patterns,
380                    skip_exact_patterns: other_skip_exact_patterns,
381                    skip_pattern_matcher: _,
382                },
383            ) => {
384                skip_patterns == other_skip_patterns
385                    && skip_exact_patterns == other_skip_exact_patterns
386            }
387            (
388                Self::Patterns {
389                    patterns,
390                    exact_patterns,
391                    skip_patterns,
392                    skip_exact_patterns,
393                    // The matchers are derived from `patterns` and `skip_patterns`, so they can be
394                    // ignored.
395                    pattern_matcher: _,
396                    skip_pattern_matcher: _,
397                },
398                Self::Patterns {
399                    patterns: other_patterns,
400                    exact_patterns: other_exact_patterns,
401                    skip_patterns: other_skip_patterns,
402                    skip_exact_patterns: other_skip_exact_patterns,
403                    pattern_matcher: _,
404                    skip_pattern_matcher: _,
405                },
406            ) => {
407                patterns == other_patterns
408                    && exact_patterns == other_exact_patterns
409                    && skip_patterns == other_skip_patterns
410                    && skip_exact_patterns == other_skip_exact_patterns
411            }
412            _ => false,
413        }
414    }
415}
416
417impl Eq for ResolvedFilterPatterns {}
418
419impl TestFilter {
420    /// Creates a new `TestFilter` from the given patterns.
421    ///
422    /// If an empty slice is passed, the test filter matches all possible test names.
423    pub fn new(
424        mode: NextestRunMode,
425        run_ignored: RunIgnored,
426        patterns: TestFilterPatterns,
427        exprs: Vec<Filterset>,
428    ) -> Result<Self, TestFilterBuildError> {
429        let patterns = patterns.resolve()?;
430
431        let binary_filter = BinaryFilter::new(exprs);
432
433        Ok(Self {
434            mode,
435            rerun_info: None,
436            run_ignored,
437            patterns,
438            binary_filter,
439        })
440    }
441
442    /// Returns a value indicating whether this binary should or should not be run to obtain the
443    /// list of tests within it.
444    pub fn filter_binary_match(
445        &self,
446        query: &BinaryQuery<'_>,
447        ecx: &EvalContext<'_>,
448        bound: FilterBound,
449    ) -> FilterBinaryMatch {
450        self.binary_filter.check_match(query, ecx, bound)
451    }
452
453    /// Creates a new `TestFilter` that matches the default set of tests.
454    pub fn default_set(mode: NextestRunMode, run_ignored: RunIgnored) -> Self {
455        let binary_filter = BinaryFilter::new(Vec::new());
456        Self {
457            mode,
458            rerun_info: None,
459            run_ignored,
460            patterns: ResolvedFilterPatterns::default(),
461            binary_filter,
462        }
463    }
464
465    /// Sets the list of outstanding tests, if this is a rerun.
466    pub fn set_outstanding_tests(&mut self, rerun_info: ComputedRerunInfo) {
467        self.rerun_info = Some(rerun_info);
468    }
469
470    /// Returns the nextest execution mode.
471    pub fn mode(&self) -> NextestRunMode {
472        self.mode
473    }
474
475    /// Compares the patterns between two `TestFilter`s.
476    pub fn patterns_eq(&self, other: &Self) -> bool {
477        self.patterns == other.patterns
478    }
479
480    /// Returns true if the filter expressions contain any `group()` predicates.
481    /// When true, test list creation must precompute group memberships.
482    pub fn has_group_predicates(&self) -> bool {
483        match &self.binary_filter.exprs {
484            TestFilterExprs::All => false,
485            TestFilterExprs::Sets(exprs) => {
486                exprs.iter().any(|expr| expr.compiled.has_group_matchers())
487            }
488        }
489    }
490
491    /// Returns true if the given test matches the expression filters
492    /// with the given evaluation context (or there are no expression
493    /// filters).
494    ///
495    /// This evaluates only expression filters (`-E` arguments), not
496    /// name patterns or the default filter.
497    fn matches_expression(
498        &self,
499        query: &TestQuery<'_>,
500        ecx: &EvalContext<'_>,
501        groups: Option<&dyn GroupLookup>,
502    ) -> bool {
503        match &self.binary_filter.exprs {
504            TestFilterExprs::All => true,
505            TestFilterExprs::Sets(exprs) => exprs.iter().any(|expr| match groups {
506                Some(groups) => expr.matches_test_with_groups(query, ecx, groups),
507                None => expr.matches_test(query, ecx),
508            }),
509        }
510    }
511
512    /// Consumes self, returning the underlying [`ComputedRerunInfo`] if any.
513    pub fn into_rerun_info(self) -> Option<ComputedRerunInfo> {
514        self.rerun_info
515    }
516
517    /// Returns an enum describing the match status of this filter.
518    ///
519    /// `groups` should be `Some` when the CLI filter contains `group()`
520    /// predicates, and `None` otherwise. When `None`, encountering a
521    /// `group()` predicate panics (it indicates a bug in filterset
522    /// compilation).
523    #[expect(clippy::too_many_arguments)]
524    pub fn filter_match(
525        &self,
526        binary_query: BinaryQuery<'_>,
527        test_name: &TestCaseName,
528        test_kind: &RustTestKind,
529        ecx: &EvalContext<'_>,
530        bound: FilterBound,
531        ignored: bool,
532        groups: Option<&dyn GroupLookup>,
533    ) -> FilterMatch {
534        // Handle benchmark mismatches first.
535        if let Some(mismatch) = self.filter_benchmark_mismatch(test_kind) {
536            return mismatch;
537        }
538
539        // Check if this test already passed in a prior rerun.
540        if self.is_rerun_already_passed(binary_query.binary_id, test_name) {
541            return FilterMatch::Mismatch {
542                reason: MismatchReason::RerunAlreadyPassed,
543            };
544        }
545
546        self.filter_match_base(binary_query, test_name, ecx, bound, ignored, groups)
547    }
548
549    /// Core filter matching logic, used by `filter_match`.
550    fn filter_match_base(
551        &self,
552        binary_query: BinaryQuery<'_>,
553        test_name: &TestCaseName,
554        ecx: &EvalContext<'_>,
555        bound: FilterBound,
556        ignored: bool,
557        groups: Option<&dyn GroupLookup>,
558    ) -> FilterMatch {
559        if let Some(mismatch) = self.filter_ignored_mismatch(ignored) {
560            return mismatch;
561        }
562
563        {
564            // ---
565            // NOTE
566            // ---
567            //
568            // Previously, if either expression OR string filters matched, we'd run the tests.
569            // The current (stable) implementation is that *both* the expression AND the string
570            // filters should match.
571            //
572            // This is because we try and skip running test binaries which don't match
573            // expression filters. So for example:
574            //
575            //     cargo nextest run -E 'binary(foo)' test_bar
576            //
577            // would not even get to the point of enumerating the tests not in binary(foo), thus
578            // not running any test_bars in the workspace. But, with the OR semantics:
579            //
580            //     cargo nextest run -E 'binary(foo) or test(test_foo)' test_bar
581            //
582            // would run all the test_bars in the repo. This is inconsistent, so nextest must
583            // use AND semantics.
584            use FilterNameMatch::*;
585            match (
586                self.filter_name_match(test_name),
587                self.filter_expression_match(binary_query, test_name, ecx, bound, groups),
588            ) {
589                // Tests must be accepted by both expressions and filters.
590                (
591                    MatchEmptyPatterns | MatchWithPatterns,
592                    MatchEmptyPatterns | MatchWithPatterns,
593                ) => {}
594                // If rejected by at least one of the filtering strategies, the test is
595                // rejected. Note we use the _name_ mismatch reason first. That's because
596                // expression-based matches can also match against the default set. If a test
597                // fails both name and expression matches, then the name reason is more directly
598                // relevant.
599                (Mismatch(reason), _) | (_, Mismatch(reason)) => {
600                    return FilterMatch::Mismatch { reason };
601                }
602            }
603        }
604
605        FilterMatch::Matches
606    }
607
608    /// Returns true if this test already passed in a prior rerun.
609    fn is_rerun_already_passed(&self, binary_id: &RustBinaryId, test_name: &TestCaseName) -> bool {
610        if let Some(rerun_info) = &self.rerun_info
611            && let Some(suite) = rerun_info.test_suites.get(binary_id)
612        {
613            return suite.passing.contains(test_name);
614        }
615        false
616    }
617
618    fn filter_benchmark_mismatch(&self, test_kind: &RustTestKind) -> Option<FilterMatch> {
619        if self.mode == NextestRunMode::Benchmark && test_kind != &RustTestKind::BENCH {
620            Some(FilterMatch::Mismatch {
621                reason: MismatchReason::NotBenchmark,
622            })
623        } else {
624            None
625        }
626    }
627
628    fn filter_ignored_mismatch(&self, ignored: bool) -> Option<FilterMatch> {
629        let dominated = match self.run_ignored {
630            RunIgnored::Only => !ignored,
631            RunIgnored::Default => ignored,
632            RunIgnored::All => false,
633        };
634        dominated.then_some(FilterMatch::Mismatch {
635            reason: MismatchReason::Ignored,
636        })
637    }
638
639    fn filter_name_match(&self, test_name: &TestCaseName) -> FilterNameMatch {
640        self.patterns.name_match(test_name)
641    }
642
643    fn filter_expression_match(
644        &self,
645        binary_query: BinaryQuery<'_>,
646        test_name: &TestCaseName,
647        ecx: &EvalContext<'_>,
648        bound: FilterBound,
649        groups: Option<&dyn GroupLookup>,
650    ) -> FilterNameMatch {
651        let query = TestQuery {
652            binary_query,
653            test_name,
654        };
655
656        let expr_result = if self.matches_expression(&query, ecx, groups) {
657            match &self.binary_filter.exprs {
658                TestFilterExprs::All => FilterNameMatch::MatchEmptyPatterns,
659                TestFilterExprs::Sets(_) => FilterNameMatch::MatchWithPatterns,
660            }
661        } else {
662            return FilterNameMatch::Mismatch(MismatchReason::Expression);
663        };
664
665        match bound {
666            FilterBound::All => expr_result,
667            FilterBound::DefaultSet => {
668                // The default filter is always group-free (group() is
669                // banned in DefaultFilter filtersets).
670                if ecx.default_filter.matches_test(&query, ecx) {
671                    expr_result
672                } else {
673                    FilterNameMatch::Mismatch(MismatchReason::DefaultFilter)
674                }
675            }
676        }
677    }
678}
679
680/// Whether a binary matched filters and should be run to obtain the list of tests within.
681///
682/// The result of [`TestFilter::filter_binary_match`].
683#[derive(Copy, Clone, Debug)]
684pub enum FilterBinaryMatch {
685    /// This is a definite match -- binaries should be run.
686    Definite,
687
688    /// We don't know for sure -- binaries should be run.
689    Possible,
690
691    /// This is a definite mismatch -- binaries should not be run.
692    Mismatch {
693        /// The reason for the mismatch.
694        reason: BinaryMismatchReason,
695    },
696}
697
698impl FilterBinaryMatch {
699    fn from_result(result: Option<bool>, reason: BinaryMismatchReason) -> Self {
700        match result {
701            Some(true) => Self::Definite,
702            None => Self::Possible,
703            Some(false) => Self::Mismatch { reason },
704        }
705    }
706
707    fn is_match(self) -> bool {
708        match self {
709            Self::Definite | Self::Possible => true,
710            Self::Mismatch { .. } => false,
711        }
712    }
713
714    fn logic_or(self, other: Self) -> Self {
715        match (self, other) {
716            (Self::Definite, _) | (_, Self::Definite) => Self::Definite,
717            (Self::Possible, _) | (_, Self::Possible) => Self::Possible,
718            (Self::Mismatch { reason: r1 }, Self::Mismatch { reason: r2 }) => Self::Mismatch {
719                reason: r1.prefer_expression(r2),
720            },
721        }
722    }
723
724    fn logic_and(self, other: Self) -> Self {
725        match (self, other) {
726            (Self::Definite, Self::Definite) => Self::Definite,
727            (Self::Definite, Self::Possible)
728            | (Self::Possible, Self::Definite)
729            | (Self::Possible, Self::Possible) => Self::Possible,
730            (Self::Mismatch { reason: r1 }, Self::Mismatch { reason: r2 }) => {
731                // If one of the mismatch reasons is `Expression` and the other is `DefaultSet`, we
732                // return Expression.
733                Self::Mismatch {
734                    reason: r1.prefer_expression(r2),
735                }
736            }
737            (Self::Mismatch { reason }, _) | (_, Self::Mismatch { reason }) => {
738                Self::Mismatch { reason }
739            }
740        }
741    }
742}
743
744/// The reason for a binary mismatch.
745///
746/// Part of [`FilterBinaryMatch`], as returned by [`TestFilter::filter_binary_match`].
747#[derive(Copy, Clone, Debug, Eq, PartialEq)]
748pub enum BinaryMismatchReason {
749    /// The binary doesn't match any of the provided filtersets.
750    Expression,
751
752    /// No filtersets were specified and the binary doesn't match the default set.
753    DefaultSet,
754}
755
756impl BinaryMismatchReason {
757    fn prefer_expression(self, other: Self) -> Self {
758        match (self, other) {
759            (Self::Expression, _) | (_, Self::Expression) => Self::Expression,
760            (Self::DefaultSet, Self::DefaultSet) => Self::DefaultSet,
761        }
762    }
763}
764
765impl fmt::Display for BinaryMismatchReason {
766    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
767        match self {
768            Self::Expression => write!(f, "didn't match filtersets"),
769            Self::DefaultSet => write!(f, "didn't match the default set"),
770        }
771    }
772}
773
774#[derive(Clone, Debug, Eq, PartialEq)]
775enum FilterNameMatch {
776    /// Match because there are no patterns.
777    MatchEmptyPatterns,
778    /// Matches with non-empty patterns.
779    MatchWithPatterns,
780    /// Mismatch.
781    Mismatch(MismatchReason),
782}
783
784impl FilterNameMatch {
785    #[cfg(test)]
786    fn is_match(&self) -> bool {
787        match self {
788            Self::MatchEmptyPatterns | Self::MatchWithPatterns => true,
789            Self::Mismatch(_) => false,
790        }
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797    use proptest::{collection::vec, prelude::*};
798    use test_strategy::proptest;
799
800    #[proptest(cases = 50)]
801    fn proptest_empty(#[strategy(vec(any::<String>(), 0..16))] test_names: Vec<String>) {
802        let patterns = TestFilterPatterns::default();
803        let test_filter = TestFilter::new(
804            NextestRunMode::Test,
805            RunIgnored::Default,
806            patterns,
807            Vec::new(),
808        )
809        .unwrap();
810        for test_name in test_names {
811            let test_name = TestCaseName::new(&test_name);
812            prop_assert!(test_filter.filter_name_match(&test_name).is_match());
813        }
814    }
815
816    // Test that exact names match.
817    #[proptest(cases = 50)]
818    fn proptest_exact(#[strategy(vec(any::<String>(), 0..16))] test_names: Vec<String>) {
819        // Test with the default matcher.
820        let patterns = TestFilterPatterns::new(test_names.clone());
821        let test_filter = TestFilter::new(
822            NextestRunMode::Test,
823            RunIgnored::Default,
824            patterns,
825            Vec::new(),
826        )
827        .unwrap();
828        for test_name in &test_names {
829            let test_name = TestCaseName::new(test_name);
830            prop_assert!(test_filter.filter_name_match(&test_name).is_match());
831        }
832
833        // Test with the exact matcher.
834        let mut patterns = TestFilterPatterns::default();
835        for test_name in &test_names {
836            patterns.add_exact_pattern(test_name.clone());
837        }
838        let test_filter = TestFilter::new(
839            NextestRunMode::Test,
840            RunIgnored::Default,
841            patterns,
842            Vec::new(),
843        )
844        .unwrap();
845        for test_name in &test_names {
846            let test_name = TestCaseName::new(test_name);
847            prop_assert!(test_filter.filter_name_match(&test_name).is_match());
848        }
849    }
850
851    // Test that substrings match.
852    #[proptest(cases = 50)]
853    fn proptest_substring(
854        #[strategy(vec([any::<String>(); 3], 0..16))] substring_prefix_suffixes: Vec<[String; 3]>,
855    ) {
856        let mut patterns = TestFilterPatterns::default();
857        let mut test_names = Vec::with_capacity(substring_prefix_suffixes.len());
858        for [substring, prefix, suffix] in substring_prefix_suffixes {
859            test_names.push(prefix + &substring + &suffix);
860            patterns.add_substring_pattern(substring);
861        }
862
863        let test_filter = TestFilter::new(
864            NextestRunMode::Test,
865            RunIgnored::Default,
866            patterns,
867            Vec::new(),
868        )
869        .unwrap();
870        for test_name in test_names {
871            let test_name = TestCaseName::new(&test_name);
872            prop_assert!(test_filter.filter_name_match(&test_name).is_match());
873        }
874    }
875
876    // Test that dropping a character from a string doesn't match.
877    #[proptest(cases = 50)]
878    fn proptest_no_match(substring: String, prefix: String, suffix: String) {
879        prop_assume!(!substring.is_empty() && !prefix.is_empty() && !suffix.is_empty());
880        let pattern = prefix + &substring + &suffix;
881        let patterns = TestFilterPatterns::new(vec![pattern]);
882        let test_filter = TestFilter::new(
883            NextestRunMode::Test,
884            RunIgnored::Default,
885            patterns,
886            Vec::new(),
887        )
888        .unwrap();
889        let substring = TestCaseName::new(&substring);
890        prop_assert!(!test_filter.filter_name_match(&substring).is_match());
891    }
892
893    fn test_name(s: &str) -> TestCaseName {
894        TestCaseName::new(s)
895    }
896
897    #[test]
898    fn pattern_examples() {
899        let mut patterns = TestFilterPatterns::new(vec!["foo".to_string()]);
900        patterns.add_substring_pattern("bar".to_string());
901        patterns.add_exact_pattern("baz".to_string());
902        patterns.add_skip_pattern("quux".to_string());
903        patterns.add_skip_exact_pattern("quuz".to_string());
904
905        let resolved = patterns.clone().resolve().unwrap();
906
907        // Test substring matches.
908        assert_eq!(
909            resolved.name_match(&test_name("foo")),
910            FilterNameMatch::MatchWithPatterns,
911        );
912        assert_eq!(
913            resolved.name_match(&test_name("1foo2")),
914            FilterNameMatch::MatchWithPatterns,
915        );
916        assert_eq!(
917            resolved.name_match(&test_name("bar")),
918            FilterNameMatch::MatchWithPatterns,
919        );
920        assert_eq!(
921            resolved.name_match(&test_name("x_bar_y")),
922            FilterNameMatch::MatchWithPatterns,
923        );
924
925        // Test exact matches.
926        assert_eq!(
927            resolved.name_match(&test_name("baz")),
928            FilterNameMatch::MatchWithPatterns,
929        );
930        assert_eq!(
931            resolved.name_match(&test_name("abazb")),
932            FilterNameMatch::Mismatch(MismatchReason::String),
933        );
934
935        // Both substring and exact matches.
936        assert_eq!(
937            resolved.name_match(&test_name("bazfoo")),
938            FilterNameMatch::MatchWithPatterns,
939        );
940
941        // Skip patterns.
942        assert_eq!(
943            resolved.name_match(&test_name("quux")),
944            FilterNameMatch::Mismatch(MismatchReason::String),
945        );
946        assert_eq!(
947            resolved.name_match(&test_name("1quux2")),
948            FilterNameMatch::Mismatch(MismatchReason::String),
949        );
950
951        // Skip and substring patterns.
952        assert_eq!(
953            resolved.name_match(&test_name("quuxbar")),
954            FilterNameMatch::Mismatch(MismatchReason::String),
955        );
956
957        // Skip-exact patterns.
958        assert_eq!(
959            resolved.name_match(&test_name("quuz")),
960            FilterNameMatch::Mismatch(MismatchReason::String),
961        );
962
963        // Skip overrides regular patterns -- in this case, add `baz` to the skip list.
964        patterns.add_skip_pattern("baz".to_string());
965        let resolved = patterns.resolve().unwrap();
966        assert_eq!(
967            resolved.name_match(&test_name("quuxbaz")),
968            FilterNameMatch::Mismatch(MismatchReason::String),
969        );
970    }
971
972    #[test]
973    fn skip_only_pattern_examples() {
974        let mut patterns = TestFilterPatterns::default();
975        patterns.add_skip_pattern("foo".to_string());
976        patterns.add_skip_pattern("bar".to_string());
977        patterns.add_skip_exact_pattern("baz".to_string());
978
979        let resolved = patterns.clone().resolve().unwrap();
980
981        // Test substring matches.
982        assert_eq!(
983            resolved.name_match(&test_name("foo")),
984            FilterNameMatch::Mismatch(MismatchReason::String),
985        );
986        assert_eq!(
987            resolved.name_match(&test_name("1foo2")),
988            FilterNameMatch::Mismatch(MismatchReason::String),
989        );
990        assert_eq!(
991            resolved.name_match(&test_name("bar")),
992            FilterNameMatch::Mismatch(MismatchReason::String),
993        );
994        assert_eq!(
995            resolved.name_match(&test_name("x_bar_y")),
996            FilterNameMatch::Mismatch(MismatchReason::String),
997        );
998
999        // Test exact matches.
1000        assert_eq!(
1001            resolved.name_match(&test_name("baz")),
1002            FilterNameMatch::Mismatch(MismatchReason::String),
1003        );
1004        assert_eq!(
1005            resolved.name_match(&test_name("abazb")),
1006            FilterNameMatch::MatchWithPatterns,
1007        );
1008
1009        // Anything that doesn't match the skip filter should match.
1010        assert_eq!(
1011            resolved.name_match(&test_name("quux")),
1012            FilterNameMatch::MatchWithPatterns,
1013        );
1014    }
1015
1016    #[test]
1017    fn empty_pattern_examples() {
1018        let patterns = TestFilterPatterns::default();
1019        let resolved = patterns.resolve().unwrap();
1020        assert_eq!(resolved, ResolvedFilterPatterns::All);
1021
1022        // Anything matches.
1023        assert_eq!(
1024            resolved.name_match(&test_name("foo")),
1025            FilterNameMatch::MatchEmptyPatterns,
1026        );
1027        assert_eq!(
1028            resolved.name_match(&test_name("1foo2")),
1029            FilterNameMatch::MatchEmptyPatterns,
1030        );
1031        assert_eq!(
1032            resolved.name_match(&test_name("bar")),
1033            FilterNameMatch::MatchEmptyPatterns,
1034        );
1035        assert_eq!(
1036            resolved.name_match(&test_name("x_bar_y")),
1037            FilterNameMatch::MatchEmptyPatterns,
1038        );
1039        assert_eq!(
1040            resolved.name_match(&test_name("baz")),
1041            FilterNameMatch::MatchEmptyPatterns,
1042        );
1043        assert_eq!(
1044            resolved.name_match(&test_name("abazb")),
1045            FilterNameMatch::MatchEmptyPatterns,
1046        );
1047    }
1048}