forge/cmd/test/
filter.rs

1use clap::Parser;
2use foundry_common::TestFilter;
3use foundry_compilers::{FileFilter, ProjectPathsConfig};
4use foundry_config::{filter::GlobMatcher, Config};
5use std::{fmt, path::Path};
6
7/// The filter to use during testing.
8///
9/// See also `FileFilter`.
10#[derive(Clone, Parser)]
11#[command(next_help_heading = "Test filtering")]
12pub struct FilterArgs {
13    /// Only run test functions matching the specified regex pattern.
14    #[arg(long = "match-test", visible_alias = "mt", value_name = "REGEX")]
15    pub test_pattern: Option<regex::Regex>,
16
17    /// Only run test functions that do not match the specified regex pattern.
18    #[arg(long = "no-match-test", visible_alias = "nmt", value_name = "REGEX")]
19    pub test_pattern_inverse: Option<regex::Regex>,
20
21    /// Only run tests in contracts matching the specified regex pattern.
22    #[arg(long = "match-contract", visible_alias = "mc", value_name = "REGEX")]
23    pub contract_pattern: Option<regex::Regex>,
24
25    /// Only run tests in contracts that do not match the specified regex pattern.
26    #[arg(long = "no-match-contract", visible_alias = "nmc", value_name = "REGEX")]
27    pub contract_pattern_inverse: Option<regex::Regex>,
28
29    /// Only run tests in source files matching the specified glob pattern.
30    #[arg(long = "match-path", visible_alias = "mp", value_name = "GLOB")]
31    pub path_pattern: Option<GlobMatcher>,
32
33    /// Only run tests in source files that do not match the specified glob pattern.
34    #[arg(
35        id = "no-match-path",
36        long = "no-match-path",
37        visible_alias = "nmp",
38        value_name = "GLOB"
39    )]
40    pub path_pattern_inverse: Option<GlobMatcher>,
41
42    /// Only show coverage for files that do not match the specified regex pattern.
43    #[arg(long = "no-match-coverage", visible_alias = "nmco", value_name = "REGEX")]
44    pub coverage_pattern_inverse: Option<regex::Regex>,
45}
46
47impl FilterArgs {
48    /// Returns true if the filter is empty.
49    pub fn is_empty(&self) -> bool {
50        self.test_pattern.is_none() &&
51            self.test_pattern_inverse.is_none() &&
52            self.contract_pattern.is_none() &&
53            self.contract_pattern_inverse.is_none() &&
54            self.path_pattern.is_none() &&
55            self.path_pattern_inverse.is_none()
56    }
57
58    /// Merges the set filter globs with the config's values
59    pub fn merge_with_config(mut self, config: &Config) -> ProjectPathsAwareFilter {
60        if self.test_pattern.is_none() {
61            self.test_pattern = config.test_pattern.clone().map(Into::into);
62        }
63        if self.test_pattern_inverse.is_none() {
64            self.test_pattern_inverse = config.test_pattern_inverse.clone().map(Into::into);
65        }
66        if self.contract_pattern.is_none() {
67            self.contract_pattern = config.contract_pattern.clone().map(Into::into);
68        }
69        if self.contract_pattern_inverse.is_none() {
70            self.contract_pattern_inverse = config.contract_pattern_inverse.clone().map(Into::into);
71        }
72        if self.path_pattern.is_none() {
73            self.path_pattern = config.path_pattern.clone().map(Into::into);
74        }
75        if self.path_pattern_inverse.is_none() {
76            self.path_pattern_inverse = config.path_pattern_inverse.clone().map(Into::into);
77        }
78        if self.coverage_pattern_inverse.is_none() {
79            self.coverage_pattern_inverse = config.coverage_pattern_inverse.clone().map(Into::into);
80        }
81        ProjectPathsAwareFilter { args_filter: self, paths: config.project_paths() }
82    }
83}
84
85impl fmt::Debug for FilterArgs {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.debug_struct("FilterArgs")
88            .field("match-test", &self.test_pattern.as_ref().map(|r| r.as_str()))
89            .field("no-match-test", &self.test_pattern_inverse.as_ref().map(|r| r.as_str()))
90            .field("match-contract", &self.contract_pattern.as_ref().map(|r| r.as_str()))
91            .field("no-match-contract", &self.contract_pattern_inverse.as_ref().map(|r| r.as_str()))
92            .field("match-path", &self.path_pattern.as_ref().map(|g| g.as_str()))
93            .field("no-match-path", &self.path_pattern_inverse.as_ref().map(|g| g.as_str()))
94            .field("no-match-coverage", &self.coverage_pattern_inverse.as_ref().map(|g| g.as_str()))
95            .finish_non_exhaustive()
96    }
97}
98
99impl FileFilter for FilterArgs {
100    /// Returns true if the file regex pattern match the `file`
101    ///
102    /// If no file regex is set this returns true by default
103    fn is_match(&self, file: &Path) -> bool {
104        self.matches_path(file)
105    }
106}
107
108impl TestFilter for FilterArgs {
109    fn matches_test(&self, test_name: &str) -> bool {
110        let mut ok = true;
111        if let Some(re) = &self.test_pattern {
112            ok = ok && re.is_match(test_name);
113        }
114        if let Some(re) = &self.test_pattern_inverse {
115            ok = ok && !re.is_match(test_name);
116        }
117        ok
118    }
119
120    fn matches_contract(&self, contract_name: &str) -> bool {
121        let mut ok = true;
122        if let Some(re) = &self.contract_pattern {
123            ok = ok && re.is_match(contract_name);
124        }
125        if let Some(re) = &self.contract_pattern_inverse {
126            ok = ok && !re.is_match(contract_name);
127        }
128        ok
129    }
130
131    fn matches_path(&self, path: &Path) -> bool {
132        let mut ok = true;
133        if let Some(re) = &self.path_pattern {
134            ok = ok && re.is_match(path);
135        }
136        if let Some(re) = &self.path_pattern_inverse {
137            ok = ok && !re.is_match(path);
138        }
139        ok
140    }
141}
142
143impl fmt::Display for FilterArgs {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        if let Some(p) = &self.test_pattern {
146            writeln!(f, "\tmatch-test: `{}`", p.as_str())?;
147        }
148        if let Some(p) = &self.test_pattern_inverse {
149            writeln!(f, "\tno-match-test: `{}`", p.as_str())?;
150        }
151        if let Some(p) = &self.contract_pattern {
152            writeln!(f, "\tmatch-contract: `{}`", p.as_str())?;
153        }
154        if let Some(p) = &self.contract_pattern_inverse {
155            writeln!(f, "\tno-match-contract: `{}`", p.as_str())?;
156        }
157        if let Some(p) = &self.path_pattern {
158            writeln!(f, "\tmatch-path: `{}`", p.as_str())?;
159        }
160        if let Some(p) = &self.path_pattern_inverse {
161            writeln!(f, "\tno-match-path: `{}`", p.as_str())?;
162        }
163        if let Some(p) = &self.coverage_pattern_inverse {
164            writeln!(f, "\tno-match-coverage: `{}`", p.as_str())?;
165        }
166        Ok(())
167    }
168}
169
170/// A filter that combines all command line arguments and the paths of the current projects
171#[derive(Clone, Debug)]
172pub struct ProjectPathsAwareFilter {
173    args_filter: FilterArgs,
174    paths: ProjectPathsConfig,
175}
176
177impl ProjectPathsAwareFilter {
178    /// Returns true if the filter is empty.
179    pub fn is_empty(&self) -> bool {
180        self.args_filter.is_empty()
181    }
182
183    /// Returns the CLI arguments.
184    pub fn args(&self) -> &FilterArgs {
185        &self.args_filter
186    }
187
188    /// Returns the CLI arguments mutably.
189    pub fn args_mut(&mut self) -> &mut FilterArgs {
190        &mut self.args_filter
191    }
192
193    /// Returns the project paths.
194    pub fn paths(&self) -> &ProjectPathsConfig {
195        &self.paths
196    }
197}
198
199impl FileFilter for ProjectPathsAwareFilter {
200    /// Returns true if the file regex pattern match the `file`
201    ///
202    /// If no file regex is set this returns true by default
203    fn is_match(&self, mut file: &Path) -> bool {
204        file = file.strip_prefix(&self.paths.root).unwrap_or(file);
205        self.args_filter.is_match(file)
206    }
207}
208
209impl TestFilter for ProjectPathsAwareFilter {
210    fn matches_test(&self, test_name: &str) -> bool {
211        self.args_filter.matches_test(test_name)
212    }
213
214    fn matches_contract(&self, contract_name: &str) -> bool {
215        self.args_filter.matches_contract(contract_name)
216    }
217
218    fn matches_path(&self, mut path: &Path) -> bool {
219        // we don't want to test files that belong to a library
220        path = path.strip_prefix(&self.paths.root).unwrap_or(path);
221        self.args_filter.matches_path(path) && !self.paths.has_library_ancestor(path)
222    }
223}
224
225impl fmt::Display for ProjectPathsAwareFilter {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        self.args_filter.fmt(f)
228    }
229}