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