1use alloy_json_abi::Function;
2use clap::Parser;
3use foundry_common::{TestFilter, TestFunctionExt};
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, 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
227impl FileFilter for ProjectPathsAwareFilter {
228 fn is_match(&self, mut file: &Path) -> bool {
232 file = file.strip_prefix(&self.paths.root).unwrap_or(file);
233 self.args_filter.is_match(file)
234 }
235}
236
237impl TestFilter for ProjectPathsAwareFilter {
238 fn matches_test(&self, test_signature: &str) -> bool {
239 self.args_filter.matches_test(test_signature)
240 }
241
242 fn matches_contract(&self, contract_name: &str) -> bool {
243 self.args_filter.matches_contract(contract_name)
244 }
245
246 fn matches_path(&self, mut path: &Path) -> bool {
247 path = path.strip_prefix(&self.paths.root).unwrap_or(path);
249 self.args_filter.matches_path(path) && !self.paths.has_library_ancestor(path)
250 }
251
252 fn matches_test_function_in_contract(&self, contract_id: &str, func: &Function) -> bool {
253 if let Some(failures) = &self.rerun_failures {
254 if !func.is_any_test() || !self.args_filter.matches_test(&func.signature()) {
255 return false;
256 }
257 let signature = func.signature();
258 let name = signature.split('(').next().unwrap_or(&signature);
259 failures.iter().any(|failure| {
260 failure.contract == contract_id
261 && (failure.test == signature || failure.test == name)
262 })
263 } else {
264 func.is_any_test() && self.args_filter.matches_test(&func.signature())
265 }
266 }
267}
268
269impl fmt::Display for ProjectPathsAwareFilter {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 self.args_filter.fmt(f)
272 }
273}