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, Default)]
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    #[default]
304    All,
305
306    /// Match all tests except those that match the skip patterns.
307    SkipOnly {
308        skip_patterns: Vec<String>,
309        skip_pattern_matcher: Box<AhoCorasick>,
310        skip_exact_patterns: HashSet<String>,
311    },
312
313    /// Match tests that match the patterns and don't match the skip patterns.
314    Patterns {
315        patterns: Vec<String>,
316        exact_patterns: HashSet<String>,
317        skip_patterns: Vec<String>,
318        skip_exact_patterns: HashSet<String>,
319        pattern_matcher: Box<AhoCorasick>,
320        skip_pattern_matcher: Box<AhoCorasick>,
321    },
322}
323
324impl ResolvedFilterPatterns {
325    fn name_match(&self, test_name: &str) -> FilterNameMatch {
326        match self {
327            Self::All => FilterNameMatch::MatchEmptyPatterns,
328            Self::SkipOnly {
329                // skip_patterns is covered by the matcher.
330                skip_patterns: _,
331                skip_exact_patterns,
332                skip_pattern_matcher,
333            } => {
334                if skip_exact_patterns.contains(test_name)
335                    || skip_pattern_matcher.is_match(test_name)
336                {
337                    FilterNameMatch::Mismatch(MismatchReason::String)
338                } else {
339                    FilterNameMatch::MatchWithPatterns
340                }
341            }
342            Self::Patterns {
343                // patterns is covered by the matcher.
344                patterns: _,
345                exact_patterns,
346                // skip_patterns is covered by the matcher.
347                skip_patterns: _,
348                skip_exact_patterns,
349                pattern_matcher,
350                skip_pattern_matcher,
351            } => {
352                // skip overrides all other patterns.
353                if skip_exact_patterns.contains(test_name)
354                    || skip_pattern_matcher.is_match(test_name)
355                {
356                    FilterNameMatch::Mismatch(MismatchReason::String)
357                } else if exact_patterns.contains(test_name) || pattern_matcher.is_match(test_name)
358                {
359                    FilterNameMatch::MatchWithPatterns
360                } else {
361                    FilterNameMatch::Mismatch(MismatchReason::String)
362                }
363            }
364        }
365    }
366}
367
368impl PartialEq for ResolvedFilterPatterns {
369    fn eq(&self, other: &Self) -> bool {
370        match (self, other) {
371            (Self::All, Self::All) => true,
372            (
373                Self::SkipOnly {
374                    skip_patterns,
375                    skip_exact_patterns,
376                    // The matcher is derived from `skip_patterns`, so it can be ignored.
377                    skip_pattern_matcher: _,
378                },
379                Self::SkipOnly {
380                    skip_patterns: other_skip_patterns,
381                    skip_exact_patterns: other_skip_exact_patterns,
382                    skip_pattern_matcher: _,
383                },
384            ) => {
385                skip_patterns == other_skip_patterns
386                    && skip_exact_patterns == other_skip_exact_patterns
387            }
388            (
389                Self::Patterns {
390                    patterns,
391                    exact_patterns,
392                    skip_patterns,
393                    skip_exact_patterns,
394                    // The matchers are derived from `patterns` and `skip_patterns`, so they can be
395                    // ignored.
396                    pattern_matcher: _,
397                    skip_pattern_matcher: _,
398                },
399                Self::Patterns {
400                    patterns: other_patterns,
401                    exact_patterns: other_exact_patterns,
402                    skip_patterns: other_skip_patterns,
403                    skip_exact_patterns: other_skip_exact_patterns,
404                    pattern_matcher: _,
405                    skip_pattern_matcher: _,
406                },
407            ) => {
408                patterns == other_patterns
409                    && exact_patterns == other_exact_patterns
410                    && skip_patterns == other_skip_patterns
411                    && skip_exact_patterns == other_skip_exact_patterns
412            }
413            _ => false,
414        }
415    }
416}
417
418impl Eq for ResolvedFilterPatterns {}
419
420impl TestFilterBuilder {
421    /// Creates a new `TestFilterBuilder` from the given patterns.
422    ///
423    /// If an empty slice is passed, the test filter matches all possible test names.
424    pub fn new(
425        run_ignored: RunIgnored,
426        partitioner_builder: Option<PartitionerBuilder>,
427        patterns: TestFilterPatterns,
428        exprs: Vec<Filterset>,
429    ) -> Result<Self, TestFilterBuilderError> {
430        let patterns = patterns.resolve()?;
431
432        let binary_filter = BinaryFilter::new(exprs);
433
434        Ok(Self {
435            run_ignored,
436            partitioner_builder,
437            patterns,
438            binary_filter,
439        })
440    }
441
442    /// Returns a value indicating whether this binary should or should not be run to obtain the
443    /// list of tests within it.
444    ///
445    /// This method is implemented directly on `TestFilterBuilder`. The statefulness of `TestFilter`
446    /// is only used for counted test partitioning, and is not currently relevant for binaries.
447    pub fn filter_binary_match(
448        &self,
449        test_binary: &RustTestArtifact<'_>,
450        ecx: &EvalContext<'_>,
451        bound: FilterBound,
452    ) -> FilterBinaryMatch {
453        self.binary_filter.check_match(test_binary, ecx, bound)
454    }
455
456    /// Creates a new `TestFilterBuilder` that matches the default set of tests.
457    pub fn default_set(run_ignored: RunIgnored) -> Self {
458        let binary_filter = BinaryFilter::new(Vec::new());
459        Self {
460            run_ignored,
461            partitioner_builder: None,
462            patterns: ResolvedFilterPatterns::default(),
463            binary_filter,
464        }
465    }
466
467    /// Creates a new test filter scoped to a single binary.
468    ///
469    /// This test filter may be stateful.
470    pub fn build(&self) -> TestFilter<'_> {
471        let partitioner = self
472            .partitioner_builder
473            .as_ref()
474            .map(|partitioner_builder| partitioner_builder.build());
475        TestFilter {
476            builder: self,
477            partitioner,
478        }
479    }
480}
481
482/// Whether a binary matched filters and should be run to obtain the list of tests within.
483///
484/// The result of [`TestFilterBuilder::filter_binary_match`].
485#[derive(Copy, Clone, Debug)]
486pub enum FilterBinaryMatch {
487    /// This is a definite match -- binaries should be run.
488    Definite,
489
490    /// We don't know for sure -- binaries should be run.
491    Possible,
492
493    /// This is a definite mismatch -- binaries should not be run.
494    Mismatch {
495        /// The reason for the mismatch.
496        reason: BinaryMismatchReason,
497    },
498}
499
500impl FilterBinaryMatch {
501    fn from_result(result: Option<bool>, reason: BinaryMismatchReason) -> Self {
502        match result {
503            Some(true) => Self::Definite,
504            None => Self::Possible,
505            Some(false) => Self::Mismatch { reason },
506        }
507    }
508
509    fn is_match(self) -> bool {
510        match self {
511            Self::Definite | Self::Possible => true,
512            Self::Mismatch { .. } => false,
513        }
514    }
515
516    fn logic_or(self, other: Self) -> Self {
517        match (self, other) {
518            (Self::Definite, _) | (_, Self::Definite) => Self::Definite,
519            (Self::Possible, _) | (_, Self::Possible) => Self::Possible,
520            (Self::Mismatch { reason: r1 }, Self::Mismatch { reason: r2 }) => Self::Mismatch {
521                reason: r1.prefer_expression(r2),
522            },
523        }
524    }
525
526    fn logic_and(self, other: Self) -> Self {
527        match (self, other) {
528            (Self::Definite, Self::Definite) => Self::Definite,
529            (Self::Definite, Self::Possible)
530            | (Self::Possible, Self::Definite)
531            | (Self::Possible, Self::Possible) => Self::Possible,
532            (Self::Mismatch { reason: r1 }, Self::Mismatch { reason: r2 }) => {
533                // If one of the mismatch reasons is `Expression` and the other is `DefaultSet`, we
534                // return Expression.
535                Self::Mismatch {
536                    reason: r1.prefer_expression(r2),
537                }
538            }
539            (Self::Mismatch { reason }, _) | (_, Self::Mismatch { reason }) => {
540                Self::Mismatch { reason }
541            }
542        }
543    }
544}
545
546/// The reason for a binary mismatch.
547///
548/// Part of [`FilterBinaryMatch`], as returned by [`TestFilterBuilder::filter_binary_match`].
549#[derive(Copy, Clone, Debug, Eq, PartialEq)]
550pub enum BinaryMismatchReason {
551    /// The binary doesn't match any of the provided filtersets.
552    Expression,
553
554    /// No filtersets were specified and the binary doesn't match the default set.
555    DefaultSet,
556}
557
558impl BinaryMismatchReason {
559    fn prefer_expression(self, other: Self) -> Self {
560        match (self, other) {
561            (Self::Expression, _) | (_, Self::Expression) => Self::Expression,
562            (Self::DefaultSet, Self::DefaultSet) => Self::DefaultSet,
563        }
564    }
565}
566
567impl fmt::Display for BinaryMismatchReason {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        match self {
570            Self::Expression => write!(f, "didn't match filtersets"),
571            Self::DefaultSet => write!(f, "didn't match the default set"),
572        }
573    }
574}
575
576/// Test filter, scoped to a single binary.
577#[derive(Debug)]
578pub struct TestFilter<'builder> {
579    builder: &'builder TestFilterBuilder,
580    partitioner: Option<Box<dyn Partitioner>>,
581}
582
583impl TestFilter<'_> {
584    /// Returns an enum describing the match status of this filter.
585    pub fn filter_match(
586        &mut self,
587        test_binary: &RustTestArtifact<'_>,
588        test_name: &str,
589        ecx: &EvalContext<'_>,
590        bound: FilterBound,
591        ignored: bool,
592    ) -> FilterMatch {
593        self.filter_ignored_mismatch(ignored)
594            .or_else(|| {
595                // ---
596                // NOTE
597                // ---
598                //
599                // Previously, if either expression OR string filters matched, we'd run the tests.
600                // The current (stable) implementation is that *both* the expression AND the string
601                // filters should match.
602                //
603                // This is because we try and skip running test binaries which don't match
604                // expression filters. So for example:
605                //
606                //     cargo nextest run -E 'binary(foo)' test_bar
607                //
608                // would not even get to the point of enumerating the tests not in binary(foo), thus
609                // not running any test_bars in the workspace. But, with the OR semantics:
610                //
611                //     cargo nextest run -E 'binary(foo) or test(test_foo)' test_bar
612                //
613                // would run all the test_bars in the repo. This is inconsistent, so nextest must
614                // use AND semantics.
615                use FilterNameMatch::*;
616                match (
617                    self.filter_name_match(test_name),
618                    self.filter_expression_match(test_binary, test_name, ecx, bound),
619                ) {
620                    // Tests must be accepted by both expressions and filters.
621                    (
622                        MatchEmptyPatterns | MatchWithPatterns,
623                        MatchEmptyPatterns | MatchWithPatterns,
624                    ) => None,
625                    // If rejected by at least one of the filtering strategies, the test is
626                    // rejected. Note we use the _name_ mismatch reason first. That's because
627                    // expression-based matches can also match against the default set. If a test
628                    // fails both name and expression matches, then the name reason is more directly
629                    // relevant.
630                    (Mismatch(reason), _) | (_, Mismatch(reason)) => {
631                        Some(FilterMatch::Mismatch { reason })
632                    }
633                }
634            })
635            // Note that partition-based filtering MUST come after all other kinds of filtering,
636            // so that count-based bucketing applies after ignored, name and expression matching.
637            // This also means that mutable count state must be maintained by the partitioner.
638            .or_else(|| self.filter_partition_mismatch(test_name))
639            .unwrap_or(FilterMatch::Matches)
640    }
641
642    fn filter_ignored_mismatch(&self, ignored: bool) -> Option<FilterMatch> {
643        match self.builder.run_ignored {
644            RunIgnored::Only => {
645                if !ignored {
646                    return Some(FilterMatch::Mismatch {
647                        reason: MismatchReason::Ignored,
648                    });
649                }
650            }
651            RunIgnored::Default => {
652                if ignored {
653                    return Some(FilterMatch::Mismatch {
654                        reason: MismatchReason::Ignored,
655                    });
656                }
657            }
658            _ => {}
659        }
660        None
661    }
662
663    fn filter_name_match(&self, test_name: &str) -> FilterNameMatch {
664        self.builder.patterns.name_match(test_name)
665    }
666
667    fn filter_expression_match(
668        &self,
669        test_binary: &RustTestArtifact<'_>,
670        test_name: &str,
671        ecx: &EvalContext<'_>,
672        bound: FilterBound,
673    ) -> FilterNameMatch {
674        let query = TestQuery {
675            binary_query: test_binary.to_binary_query(),
676            test_name,
677        };
678
679        let expr_result = match &self.builder.binary_filter.exprs {
680            TestFilterExprs::All => FilterNameMatch::MatchEmptyPatterns,
681            TestFilterExprs::Sets(exprs) => {
682                if exprs.iter().any(|expr| expr.matches_test(&query, ecx)) {
683                    FilterNameMatch::MatchWithPatterns
684                } else {
685                    return FilterNameMatch::Mismatch(MismatchReason::Expression);
686                }
687            }
688        };
689
690        match bound {
691            FilterBound::All => expr_result,
692            FilterBound::DefaultSet => {
693                if ecx.default_filter.matches_test(&query, ecx) {
694                    expr_result
695                } else {
696                    FilterNameMatch::Mismatch(MismatchReason::DefaultFilter)
697                }
698            }
699        }
700    }
701
702    fn filter_partition_mismatch(&mut self, test_name: &str) -> Option<FilterMatch> {
703        let partition_match = match &mut self.partitioner {
704            Some(partitioner) => partitioner.test_matches(test_name),
705            None => true,
706        };
707        if partition_match {
708            None
709        } else {
710            Some(FilterMatch::Mismatch {
711                reason: MismatchReason::Partition,
712            })
713        }
714    }
715}
716
717#[derive(Clone, Debug, Eq, PartialEq)]
718enum FilterNameMatch {
719    /// Match because there are no patterns.
720    MatchEmptyPatterns,
721    /// Matches with non-empty patterns.
722    MatchWithPatterns,
723    /// Mismatch.
724    Mismatch(MismatchReason),
725}
726
727impl FilterNameMatch {
728    #[cfg(test)]
729    fn is_match(&self) -> bool {
730        match self {
731            Self::MatchEmptyPatterns | Self::MatchWithPatterns => true,
732            Self::Mismatch(_) => false,
733        }
734    }
735}
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740    use proptest::{collection::vec, prelude::*};
741    use test_strategy::proptest;
742
743    #[proptest(cases = 50)]
744    fn proptest_empty(#[strategy(vec(any::<String>(), 0..16))] test_names: Vec<String>) {
745        let patterns = TestFilterPatterns::default();
746        let test_filter =
747            TestFilterBuilder::new(RunIgnored::Default, None, patterns, Vec::new()).unwrap();
748        let single_filter = test_filter.build();
749        for test_name in test_names {
750            prop_assert!(single_filter.filter_name_match(&test_name).is_match());
751        }
752    }
753
754    // Test that exact names match.
755    #[proptest(cases = 50)]
756    fn proptest_exact(#[strategy(vec(any::<String>(), 0..16))] test_names: Vec<String>) {
757        // Test with the default matcher.
758        let patterns = TestFilterPatterns::new(test_names.clone());
759        let test_filter =
760            TestFilterBuilder::new(RunIgnored::Default, None, patterns, Vec::new()).unwrap();
761        let single_filter = test_filter.build();
762        for test_name in &test_names {
763            prop_assert!(single_filter.filter_name_match(test_name).is_match());
764        }
765
766        // Test with the exact matcher.
767        let mut patterns = TestFilterPatterns::default();
768        for test_name in &test_names {
769            patterns.add_exact_pattern(test_name.clone());
770        }
771        let test_filter =
772            TestFilterBuilder::new(RunIgnored::Default, None, patterns, Vec::new()).unwrap();
773        let single_filter = test_filter.build();
774        for test_name in &test_names {
775            prop_assert!(single_filter.filter_name_match(test_name).is_match());
776        }
777    }
778
779    // Test that substrings match.
780    #[proptest(cases = 50)]
781    fn proptest_substring(
782        #[strategy(vec([any::<String>(); 3], 0..16))] substring_prefix_suffixes: Vec<[String; 3]>,
783    ) {
784        let mut patterns = TestFilterPatterns::default();
785        let mut test_names = Vec::with_capacity(substring_prefix_suffixes.len());
786        for [substring, prefix, suffix] in substring_prefix_suffixes {
787            test_names.push(prefix + &substring + &suffix);
788            patterns.add_substring_pattern(substring);
789        }
790
791        let test_filter =
792            TestFilterBuilder::new(RunIgnored::Default, None, patterns, Vec::new()).unwrap();
793        let single_filter = test_filter.build();
794        for test_name in test_names {
795            prop_assert!(single_filter.filter_name_match(&test_name).is_match());
796        }
797    }
798
799    // Test that dropping a character from a string doesn't match.
800    #[proptest(cases = 50)]
801    fn proptest_no_match(substring: String, prefix: String, suffix: String) {
802        prop_assume!(!substring.is_empty() && !prefix.is_empty() && !suffix.is_empty());
803        let pattern = prefix + &substring + &suffix;
804        let patterns = TestFilterPatterns::new(vec![pattern]);
805        let test_filter =
806            TestFilterBuilder::new(RunIgnored::Default, None, patterns, Vec::new()).unwrap();
807        let single_filter = test_filter.build();
808        prop_assert!(!single_filter.filter_name_match(&substring).is_match());
809    }
810
811    #[test]
812    fn pattern_examples() {
813        let mut patterns = TestFilterPatterns::new(vec!["foo".to_string()]);
814        patterns.add_substring_pattern("bar".to_string());
815        patterns.add_exact_pattern("baz".to_string());
816        patterns.add_skip_pattern("quux".to_string());
817        patterns.add_skip_exact_pattern("quuz".to_string());
818
819        let resolved = patterns.clone().resolve().unwrap();
820
821        // Test substring matches.
822        assert_eq!(
823            resolved.name_match("foo"),
824            FilterNameMatch::MatchWithPatterns,
825        );
826        assert_eq!(
827            resolved.name_match("1foo2"),
828            FilterNameMatch::MatchWithPatterns,
829        );
830        assert_eq!(
831            resolved.name_match("bar"),
832            FilterNameMatch::MatchWithPatterns,
833        );
834        assert_eq!(
835            resolved.name_match("x_bar_y"),
836            FilterNameMatch::MatchWithPatterns,
837        );
838
839        // Test exact matches.
840        assert_eq!(
841            resolved.name_match("baz"),
842            FilterNameMatch::MatchWithPatterns,
843        );
844        assert_eq!(
845            resolved.name_match("abazb"),
846            FilterNameMatch::Mismatch(MismatchReason::String),
847        );
848
849        // Both substring and exact matches.
850        assert_eq!(
851            resolved.name_match("bazfoo"),
852            FilterNameMatch::MatchWithPatterns,
853        );
854
855        // Skip patterns.
856        assert_eq!(
857            resolved.name_match("quux"),
858            FilterNameMatch::Mismatch(MismatchReason::String),
859        );
860        assert_eq!(
861            resolved.name_match("1quux2"),
862            FilterNameMatch::Mismatch(MismatchReason::String),
863        );
864
865        // Skip and substring patterns.
866        assert_eq!(
867            resolved.name_match("quuxbar"),
868            FilterNameMatch::Mismatch(MismatchReason::String),
869        );
870
871        // Skip-exact patterns.
872        assert_eq!(
873            resolved.name_match("quuz"),
874            FilterNameMatch::Mismatch(MismatchReason::String),
875        );
876
877        // Skip overrides regular patterns -- in this case, add `baz` to the skip list.
878        patterns.add_skip_pattern("baz".to_string());
879        let resolved = patterns.resolve().unwrap();
880        assert_eq!(
881            resolved.name_match("quuxbaz"),
882            FilterNameMatch::Mismatch(MismatchReason::String),
883        );
884    }
885
886    #[test]
887    fn skip_only_pattern_examples() {
888        let mut patterns = TestFilterPatterns::default();
889        patterns.add_skip_pattern("foo".to_string());
890        patterns.add_skip_pattern("bar".to_string());
891        patterns.add_skip_exact_pattern("baz".to_string());
892
893        let resolved = patterns.clone().resolve().unwrap();
894
895        // Test substring matches.
896        assert_eq!(
897            resolved.name_match("foo"),
898            FilterNameMatch::Mismatch(MismatchReason::String),
899        );
900        assert_eq!(
901            resolved.name_match("1foo2"),
902            FilterNameMatch::Mismatch(MismatchReason::String),
903        );
904        assert_eq!(
905            resolved.name_match("bar"),
906            FilterNameMatch::Mismatch(MismatchReason::String),
907        );
908        assert_eq!(
909            resolved.name_match("x_bar_y"),
910            FilterNameMatch::Mismatch(MismatchReason::String),
911        );
912
913        // Test exact matches.
914        assert_eq!(
915            resolved.name_match("baz"),
916            FilterNameMatch::Mismatch(MismatchReason::String),
917        );
918        assert_eq!(
919            resolved.name_match("abazb"),
920            FilterNameMatch::MatchWithPatterns,
921        );
922
923        // Anything that doesn't match the skip filter should match.
924        assert_eq!(
925            resolved.name_match("quux"),
926            FilterNameMatch::MatchWithPatterns,
927        );
928    }
929
930    #[test]
931    fn empty_pattern_examples() {
932        let patterns = TestFilterPatterns::default();
933        let resolved = patterns.resolve().unwrap();
934        assert_eq!(resolved, ResolvedFilterPatterns::All);
935
936        // Anything matches.
937        assert_eq!(
938            resolved.name_match("foo"),
939            FilterNameMatch::MatchEmptyPatterns,
940        );
941        assert_eq!(
942            resolved.name_match("1foo2"),
943            FilterNameMatch::MatchEmptyPatterns,
944        );
945        assert_eq!(
946            resolved.name_match("bar"),
947            FilterNameMatch::MatchEmptyPatterns,
948        );
949        assert_eq!(
950            resolved.name_match("x_bar_y"),
951            FilterNameMatch::MatchEmptyPatterns,
952        );
953        assert_eq!(
954            resolved.name_match("baz"),
955            FilterNameMatch::MatchEmptyPatterns,
956        );
957        assert_eq!(
958            resolved.name_match("abazb"),
959            FilterNameMatch::MatchEmptyPatterns,
960        );
961    }
962}