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