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#[derive(Clone, Parser)]
11#[command(next_help_heading = "Test filtering")]
12pub struct FilterArgs {
13 #[arg(long = "match-test", visible_alias = "mt", value_name = "REGEX")]
15 pub test_pattern: Option<regex::Regex>,
16
17 #[arg(long = "no-match-test", visible_alias = "nmt", value_name = "REGEX")]
19 pub test_pattern_inverse: Option<regex::Regex>,
20
21 #[arg(long = "match-contract", visible_alias = "mc", value_name = "REGEX")]
23 pub contract_pattern: Option<regex::Regex>,
24
25 #[arg(long = "no-match-contract", visible_alias = "nmc", value_name = "REGEX")]
27 pub contract_pattern_inverse: Option<regex::Regex>,
28
29 #[arg(long = "match-path", visible_alias = "mp", value_name = "GLOB")]
31 pub path_pattern: Option<GlobMatcher>,
32
33 #[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 #[arg(long = "no-match-coverage", visible_alias = "nmco", value_name = "REGEX")]
44 pub coverage_pattern_inverse: Option<regex::Regex>,
45}
46
47impl FilterArgs {
48 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 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 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#[derive(Clone, Debug)]
172pub struct ProjectPathsAwareFilter {
173 args_filter: FilterArgs,
174 paths: ProjectPathsConfig,
175}
176
177impl ProjectPathsAwareFilter {
178 pub fn is_empty(&self) -> bool {
180 self.args_filter.is_empty()
181 }
182
183 pub fn args(&self) -> &FilterArgs {
185 &self.args_filter
186 }
187
188 pub fn args_mut(&mut self) -> &mut FilterArgs {
190 &mut self.args_filter
191 }
192
193 pub fn paths(&self) -> &ProjectPathsConfig {
195 &self.paths
196 }
197}
198
199impl FileFilter for ProjectPathsAwareFilter {
200 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 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}