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 self.test_pattern =
79 self.test_pattern.or_else(|| config.test_pattern.clone().map(Into::into));
80 self.test_pattern_inverse = self
81 .test_pattern_inverse
82 .or_else(|| config.test_pattern_inverse.clone().map(Into::into));
83 self.contract_pattern =
84 self.contract_pattern.or_else(|| config.contract_pattern.clone().map(Into::into));
85 self.contract_pattern_inverse = self
86 .contract_pattern_inverse
87 .or_else(|| config.contract_pattern_inverse.clone().map(Into::into));
88 self.path_pattern =
89 self.path_pattern.or_else(|| config.path_pattern.clone().map(Into::into));
90 self.path_pattern_inverse = self
91 .path_pattern_inverse
92 .or_else(|| config.path_pattern_inverse.clone().map(Into::into));
93 self.coverage_pattern_inverse = self
94 .coverage_pattern_inverse
95 .or_else(|| config.coverage_pattern_inverse.clone().map(Into::into));
96 ProjectPathsAwareFilter {
97 args_filter: self,
98 paths: config.project_paths(),
99 rerun_failures: None,
100 }
101 }
102
103 fn patterns(&self) -> [(&'static str, Option<&str>); 7] {
105 [
106 ("match-test", self.test_pattern.as_ref().map(|r| r.as_str())),
107 ("no-match-test", self.test_pattern_inverse.as_ref().map(|r| r.as_str())),
108 ("match-contract", self.contract_pattern.as_ref().map(|r| r.as_str())),
109 ("no-match-contract", self.contract_pattern_inverse.as_ref().map(|r| r.as_str())),
110 ("match-path", self.path_pattern.as_ref().map(|g| g.as_str())),
111 ("no-match-path", self.path_pattern_inverse.as_ref().map(|g| g.as_str())),
112 ("no-match-coverage", self.coverage_pattern_inverse.as_ref().map(|r| r.as_str())),
113 ]
114 }
115}
116
117impl fmt::Debug for FilterArgs {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 let mut s = f.debug_struct("FilterArgs");
120 for (name, pattern) in self.patterns() {
121 s.field(name, &pattern);
122 }
123 s.finish_non_exhaustive()
124 }
125}
126
127impl FileFilter for FilterArgs {
128 fn is_match(&self, file: &Path) -> bool {
132 self.matches_path(file)
133 }
134}
135
136impl TestFilter for FilterArgs {
137 fn matches_test(&self, test_signature: &str) -> bool {
138 self.test_pattern.as_ref().is_none_or(|re| re.is_match(test_signature))
139 && self.test_pattern_inverse.as_ref().is_none_or(|re| !re.is_match(test_signature))
140 }
141
142 fn matches_contract(&self, contract_name: &str) -> bool {
143 self.contract_pattern.as_ref().is_none_or(|re| re.is_match(contract_name))
144 && self.contract_pattern_inverse.as_ref().is_none_or(|re| !re.is_match(contract_name))
145 }
146
147 fn matches_path(&self, path: &Path) -> bool {
148 self.path_pattern.as_ref().is_none_or(|g| g.is_match(path))
149 && self.path_pattern_inverse.as_ref().is_none_or(|g| !g.is_match(path))
150 }
151}
152
153impl fmt::Display for FilterArgs {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 for (name, pattern) in self.patterns() {
156 if let Some(pattern) = pattern {
157 writeln!(f, "\t{name}: `{pattern}`")?;
158 }
159 }
160 Ok(())
161 }
162}
163
164#[derive(Clone, Debug)]
166pub struct ProjectPathsAwareFilter {
167 args_filter: FilterArgs,
168 paths: ProjectPathsConfig,
169 rerun_failures: Option<Vec<RerunFailure>>,
170}
171
172impl ProjectPathsAwareFilter {
173 pub const fn is_empty(&self) -> bool {
175 self.args_filter.is_empty()
176 }
177
178 pub const fn args(&self) -> &FilterArgs {
180 &self.args_filter
181 }
182
183 pub const fn args_mut(&mut self) -> &mut FilterArgs {
185 &mut self.args_filter
186 }
187
188 pub const fn paths(&self) -> &ProjectPathsConfig {
190 &self.paths
191 }
192
193 pub fn set_rerun_failures(&mut self, failures: Vec<RerunFailure>) {
195 self.rerun_failures = Some(failures);
196 }
197
198 pub fn rerun_failures(&self) -> Option<&[RerunFailure]> {
200 self.rerun_failures.as_deref()
201 }
202
203 fn matches_rerun_contract(&self, failure_contract: &str, contract_id: &str) -> bool {
204 if failure_contract == contract_id {
205 return true;
206 }
207 let (Some((failure_path, failure_name)), Some((contract_path, contract_name))) =
208 (failure_contract.rsplit_once(':'), contract_id.rsplit_once(':'))
209 else {
210 return false;
211 };
212 if failure_name != contract_name {
213 return false;
214 }
215
216 let normalize = |path: &str| {
217 let path = Path::new(path);
218 if let Ok(path) = path.strip_prefix(&self.paths.root) {
219 return path.to_path_buf();
220 }
221 if path.is_absolute()
222 && let Ok(root) = dunce::canonicalize(&self.paths.root)
223 && let Ok(path) = dunce::canonicalize(path)
224 && let Ok(path) = path.strip_prefix(root)
225 {
226 return path.to_path_buf();
227 }
228 path.to_path_buf()
229 };
230 normalize(failure_path) == normalize(contract_path)
231 }
232}
233
234impl FileFilter for ProjectPathsAwareFilter {
235 fn is_match(&self, mut file: &Path) -> bool {
239 file = file.strip_prefix(&self.paths.root).unwrap_or(file);
240 self.args_filter.is_match(file)
241 }
242}
243
244impl TestFilter for ProjectPathsAwareFilter {
245 fn matches_test(&self, test_signature: &str) -> bool {
246 self.args_filter.matches_test(test_signature)
247 }
248
249 fn matches_contract(&self, contract_name: &str) -> bool {
250 self.args_filter.matches_contract(contract_name)
251 }
252
253 fn matches_path(&self, mut path: &Path) -> bool {
254 path = path.strip_prefix(&self.paths.root).unwrap_or(path);
256 self.args_filter.matches_path(path) && !self.paths.has_library_ancestor(path)
257 }
258
259 fn matches_test_function_kind_in_contract(
260 &self,
261 contract_id: &str,
262 func: &Function,
263 kind: TestFunctionKind,
264 ) -> bool {
265 let signature = func.signature();
266 if !kind.is_any_test() || !self.args_filter.matches_test(&signature) {
267 return false;
268 }
269 let Some(failures) = &self.rerun_failures else { return true };
270 let name = signature.split('(').next().unwrap_or(&signature);
271 failures.iter().any(|failure| {
272 self.matches_rerun_contract(&failure.contract, contract_id)
273 && (failure.test == signature || failure.test == name)
274 })
275 }
276}
277
278impl fmt::Display for ProjectPathsAwareFilter {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 self.args_filter.fmt(f)
281 }
282}