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