1use alloy_json_abi::Function;
2use clap::Parser;
3use foundry_common::{TestFilter, TestFunctionKind};
4use foundry_compilers::{FileFilter, ProjectPathsConfig};
5use foundry_config::{Config, filter::GlobMatcher};
6use serde::{Deserialize, Serialize};
7use std::{fmt, path::Path};
8
9#[derive(Clone, Debug, Deserialize, Serialize)]
11pub struct RerunFailure {
12 pub contract: String,
14 pub test: String,
16}
17
18#[derive(Clone, Debug, Deserialize, Serialize)]
20pub struct RerunFailures {
21 pub version: u8,
22 pub failures: Vec<RerunFailure>,
23}
24
25#[derive(Clone, Default, Parser)]
29#[command(next_help_heading = "Test filtering")]
30pub struct FilterArgs {
31 #[arg(long = "match-test", visible_alias = "mt", value_name = "REGEX")]
33 pub test_pattern: Option<regex::Regex>,
34
35 #[arg(long = "no-match-test", visible_alias = "nmt", value_name = "REGEX")]
37 pub test_pattern_inverse: Option<regex::Regex>,
38
39 #[arg(long = "match-contract", visible_alias = "mc", value_name = "REGEX")]
41 pub contract_pattern: Option<regex::Regex>,
42
43 #[arg(long = "no-match-contract", visible_alias = "nmc", value_name = "REGEX")]
45 pub contract_pattern_inverse: Option<regex::Regex>,
46
47 #[arg(long = "match-path", visible_alias = "mp", value_name = "GLOB")]
49 pub path_pattern: Option<GlobMatcher>,
50
51 #[arg(
53 id = "no-match-path",
54 long = "no-match-path",
55 visible_alias = "nmp",
56 value_name = "GLOB"
57 )]
58 pub path_pattern_inverse: Option<GlobMatcher>,
59
60 #[arg(long = "no-match-coverage", visible_alias = "nmco", value_name = "REGEX")]
62 pub coverage_pattern_inverse: Option<regex::Regex>,
63}
64
65impl FilterArgs {
66 pub const fn is_empty(&self) -> bool {
68 self.test_pattern.is_none()
69 && self.test_pattern_inverse.is_none()
70 && self.contract_pattern.is_none()
71 && self.contract_pattern_inverse.is_none()
72 && self.path_pattern.is_none()
73 && self.path_pattern_inverse.is_none()
74 }
75
76 pub fn merge_with_config(mut self, config: &Config) -> ProjectPathsAwareFilter {
78 if self.test_pattern.is_none() {
79 self.test_pattern = config.test_pattern.clone().map(Into::into);
80 }
81 if self.test_pattern_inverse.is_none() {
82 self.test_pattern_inverse = config.test_pattern_inverse.clone().map(Into::into);
83 }
84 if self.contract_pattern.is_none() {
85 self.contract_pattern = config.contract_pattern.clone().map(Into::into);
86 }
87 if self.contract_pattern_inverse.is_none() {
88 self.contract_pattern_inverse = config.contract_pattern_inverse.clone().map(Into::into);
89 }
90 if self.path_pattern.is_none() {
91 self.path_pattern = config.path_pattern.clone().map(Into::into);
92 }
93 if self.path_pattern_inverse.is_none() {
94 self.path_pattern_inverse = config.path_pattern_inverse.clone().map(Into::into);
95 }
96 if self.coverage_pattern_inverse.is_none() {
97 self.coverage_pattern_inverse = config.coverage_pattern_inverse.clone().map(Into::into);
98 }
99 ProjectPathsAwareFilter {
100 args_filter: self,
101 paths: config.project_paths(),
102 rerun_failures: None,
103 }
104 }
105}
106
107impl fmt::Debug for FilterArgs {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 f.debug_struct("FilterArgs")
110 .field("match-test", &self.test_pattern.as_ref().map(|r| r.as_str()))
111 .field("no-match-test", &self.test_pattern_inverse.as_ref().map(|r| r.as_str()))
112 .field("match-contract", &self.contract_pattern.as_ref().map(|r| r.as_str()))
113 .field("no-match-contract", &self.contract_pattern_inverse.as_ref().map(|r| r.as_str()))
114 .field("match-path", &self.path_pattern.as_ref().map(|g| g.as_str()))
115 .field("no-match-path", &self.path_pattern_inverse.as_ref().map(|g| g.as_str()))
116 .field("no-match-coverage", &self.coverage_pattern_inverse.as_ref().map(|g| g.as_str()))
117 .finish_non_exhaustive()
118 }
119}
120
121impl FileFilter for FilterArgs {
122 fn is_match(&self, file: &Path) -> bool {
126 self.matches_path(file)
127 }
128}
129
130impl TestFilter for FilterArgs {
131 fn matches_test(&self, test_signature: &str) -> bool {
132 let mut ok = true;
133 if let Some(re) = &self.test_pattern {
134 ok = ok && re.is_match(test_signature);
135 }
136 if let Some(re) = &self.test_pattern_inverse {
137 ok = ok && !re.is_match(test_signature);
138 }
139 ok
140 }
141
142 fn matches_contract(&self, contract_name: &str) -> bool {
143 let mut ok = true;
144 if let Some(re) = &self.contract_pattern {
145 ok = ok && re.is_match(contract_name);
146 }
147 if let Some(re) = &self.contract_pattern_inverse {
148 ok = ok && !re.is_match(contract_name);
149 }
150 ok
151 }
152
153 fn matches_path(&self, path: &Path) -> bool {
154 let mut ok = true;
155 if let Some(re) = &self.path_pattern {
156 ok = ok && re.is_match(path);
157 }
158 if let Some(re) = &self.path_pattern_inverse {
159 ok = ok && !re.is_match(path);
160 }
161 ok
162 }
163}
164
165impl fmt::Display for FilterArgs {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 if let Some(p) = &self.test_pattern {
168 writeln!(f, "\tmatch-test: `{}`", p.as_str())?;
169 }
170 if let Some(p) = &self.test_pattern_inverse {
171 writeln!(f, "\tno-match-test: `{}`", p.as_str())?;
172 }
173 if let Some(p) = &self.contract_pattern {
174 writeln!(f, "\tmatch-contract: `{}`", p.as_str())?;
175 }
176 if let Some(p) = &self.contract_pattern_inverse {
177 writeln!(f, "\tno-match-contract: `{}`", p.as_str())?;
178 }
179 if let Some(p) = &self.path_pattern {
180 writeln!(f, "\tmatch-path: `{}`", p.as_str())?;
181 }
182 if let Some(p) = &self.path_pattern_inverse {
183 writeln!(f, "\tno-match-path: `{}`", p.as_str())?;
184 }
185 if let Some(p) = &self.coverage_pattern_inverse {
186 writeln!(f, "\tno-match-coverage: `{}`", p.as_str())?;
187 }
188 Ok(())
189 }
190}
191
192#[derive(Clone, Debug)]
194pub struct ProjectPathsAwareFilter {
195 args_filter: FilterArgs,
196 paths: ProjectPathsConfig,
197 rerun_failures: Option<Vec<RerunFailure>>,
198}
199
200impl ProjectPathsAwareFilter {
201 pub const fn is_empty(&self) -> bool {
203 self.args_filter.is_empty()
204 }
205
206 pub const fn args(&self) -> &FilterArgs {
208 &self.args_filter
209 }
210
211 pub const fn args_mut(&mut self) -> &mut FilterArgs {
213 &mut self.args_filter
214 }
215
216 pub const fn paths(&self) -> &ProjectPathsConfig {
218 &self.paths
219 }
220
221 pub fn set_rerun_failures(&mut self, failures: Vec<RerunFailure>) {
223 self.rerun_failures = Some(failures);
224 }
225
226 pub fn rerun_failures(&self) -> Option<&[RerunFailure]> {
228 self.rerun_failures.as_deref()
229 }
230
231 fn matches_rerun_contract(&self, failure_contract: &str, contract_id: &str) -> bool {
232 if failure_contract == contract_id {
233 return true;
234 }
235
236 let Some((failure_path, failure_name)) = failure_contract.rsplit_once(':') else {
237 return false;
238 };
239 let Some((contract_path, contract_name)) = contract_id.rsplit_once(':') else {
240 return false;
241 };
242 if failure_name != contract_name {
243 return false;
244 }
245
246 let normalize = |path: &str| {
247 let path = Path::new(path);
248 if let Ok(path) = path.strip_prefix(&self.paths.root) {
249 return path.to_path_buf();
250 }
251
252 if path.is_absolute()
253 && let Ok(root) = dunce::canonicalize(&self.paths.root)
254 && let Ok(path) = dunce::canonicalize(path)
255 && let Ok(path) = path.strip_prefix(root)
256 {
257 return path.to_path_buf();
258 }
259
260 path.to_path_buf()
261 };
262
263 normalize(failure_path) == normalize(contract_path)
264 }
265}
266
267impl FileFilter for ProjectPathsAwareFilter {
268 fn is_match(&self, mut file: &Path) -> bool {
272 file = file.strip_prefix(&self.paths.root).unwrap_or(file);
273 self.args_filter.is_match(file)
274 }
275}
276
277impl TestFilter for ProjectPathsAwareFilter {
278 fn matches_test(&self, test_signature: &str) -> bool {
279 self.args_filter.matches_test(test_signature)
280 }
281
282 fn matches_contract(&self, contract_name: &str) -> bool {
283 self.args_filter.matches_contract(contract_name)
284 }
285
286 fn matches_path(&self, mut path: &Path) -> bool {
287 path = path.strip_prefix(&self.paths.root).unwrap_or(path);
289 self.args_filter.matches_path(path) && !self.paths.has_library_ancestor(path)
290 }
291
292 fn matches_test_function_kind_in_contract(
293 &self,
294 contract_id: &str,
295 func: &Function,
296 kind: TestFunctionKind,
297 ) -> bool {
298 if let Some(failures) = &self.rerun_failures {
299 if !kind.is_any_test() || !self.args_filter.matches_test(&func.signature()) {
300 return false;
301 }
302 let signature = func.signature();
303 let name = signature.split('(').next().unwrap_or(&signature);
304 failures.iter().any(|failure| {
305 self.matches_rerun_contract(&failure.contract, contract_id)
306 && (failure.test == signature || failure.test == name)
307 })
308 } else {
309 kind.is_any_test() && self.args_filter.matches_test(&func.signature())
310 }
311 }
312}
313
314impl fmt::Display for ProjectPathsAwareFilter {
315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316 self.args_filter.fmt(f)
317 }
318}