Skip to content

Commit 6aedaea

Browse files
authored
refactor: get_sources_to_compile (#11464)
* refactor: test filter * refactor: handling of empty test filters * dontfail * fix
1 parent a02c14b commit 6aedaea

File tree

8 files changed

+144
-138
lines changed

8 files changed

+144
-138
lines changed

crates/common/src/traits.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::{fmt, path::Path};
88
/// Test filter.
99
pub trait TestFilter: Send + Sync {
1010
/// Returns whether the test should be included.
11-
fn matches_test(&self, test_name: &str) -> bool;
11+
fn matches_test(&self, test_signature: &str) -> bool;
1212

1313
/// Returns whether the contract should be included.
1414
fn matches_contract(&self, contract_name: &str) -> bool;
@@ -17,6 +17,30 @@ pub trait TestFilter: Send + Sync {
1717
fn matches_path(&self, path: &Path) -> bool;
1818
}
1919

20+
impl<'a> dyn TestFilter + 'a {
21+
/// Returns `true` if the function is a test function that matches the given filter.
22+
pub fn matches_test_function(&self, func: &Function) -> bool {
23+
func.is_any_test() && self.matches_test(&func.signature())
24+
}
25+
}
26+
27+
/// A test filter that filters out nothing.
28+
#[derive(Clone, Debug, Default)]
29+
pub struct EmptyTestFilter(());
30+
impl TestFilter for EmptyTestFilter {
31+
fn matches_test(&self, _test_signature: &str) -> bool {
32+
true
33+
}
34+
35+
fn matches_contract(&self, _contract_name: &str) -> bool {
36+
true
37+
}
38+
39+
fn matches_path(&self, _path: &Path) -> bool {
40+
true
41+
}
42+
}
43+
2044
/// Extension trait for `Function`.
2145
pub trait TestFunctionExt {
2246
/// Returns the kind of test function.

crates/forge/src/cmd/test/filter.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,13 @@ impl FileFilter for FilterArgs {
106106
}
107107

108108
impl TestFilter for FilterArgs {
109-
fn matches_test(&self, test_name: &str) -> bool {
109+
fn matches_test(&self, test_signature: &str) -> bool {
110110
let mut ok = true;
111111
if let Some(re) = &self.test_pattern {
112-
ok = ok && re.is_match(test_name);
112+
ok = ok && re.is_match(test_signature);
113113
}
114114
if let Some(re) = &self.test_pattern_inverse {
115-
ok = ok && !re.is_match(test_name);
115+
ok = ok && !re.is_match(test_signature);
116116
}
117117
ok
118118
}
@@ -207,8 +207,8 @@ impl FileFilter for ProjectPathsAwareFilter {
207207
}
208208

209209
impl TestFilter for ProjectPathsAwareFilter {
210-
fn matches_test(&self, test_name: &str) -> bool {
211-
self.args_filter.matches_test(test_name)
210+
fn matches_test(&self, test_signature: &str) -> bool {
211+
self.args_filter.matches_test(test_signature)
212212
}
213213

214214
fn matches_contract(&self, contract_name: &str) -> bool {

crates/forge/src/cmd/test/mod.rs

Lines changed: 65 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use super::{install, test::filter::ProjectPathsAwareFilter, watch::WatchArgs};
22
use crate::{
3-
MultiContractRunner, MultiContractRunnerBuilder, TestFilter,
3+
MultiContractRunner, MultiContractRunnerBuilder,
44
decode::decode_console_logs,
55
gas_report::GasReport,
6-
multi_runner::matches_contract,
6+
multi_runner::{is_test_contract, matches_artifact},
77
result::{SuiteResult, TestOutcome, TestStatus},
88
traces::{
99
CallTraceDecoderBuilder, InternalTraceMode, TraceKind,
@@ -21,15 +21,12 @@ use foundry_cli::{
2121
opts::{BuildOpts, GlobalArgs},
2222
utils::{self, LoadConfig},
2323
};
24-
use foundry_common::{TestFunctionExt, compile::ProjectCompiler, evm::EvmArgs, fs, shell};
24+
use foundry_common::{
25+
EmptyTestFilter, TestFunctionExt, compile::ProjectCompiler, evm::EvmArgs, fs, shell,
26+
};
2527
use foundry_compilers::{
26-
ProjectCompileOutput,
27-
artifacts::output_selection::OutputSelection,
28-
compilers::{
29-
Language,
30-
multi::{MultiCompiler, MultiCompilerLanguage},
31-
},
32-
utils::source_files_iter,
28+
ProjectCompileOutput, artifacts::output_selection::OutputSelection,
29+
compilers::multi::MultiCompiler,
3330
};
3431
use foundry_config::{
3532
Config, figment,
@@ -209,76 +206,44 @@ impl TestArgs {
209206
self.execute_tests().await
210207
}
211208

212-
/// Returns sources which include any tests to be executed.
213-
/// If no filters are provided, sources are filtered by existence of test/invariant methods in
214-
/// them, If filters are provided, sources are additionally filtered by them.
209+
/// Returns a list of files that need to be compiled in order to run all the tests that match
210+
/// the given filter.
211+
///
212+
/// This means that it will return all sources that are not test contracts or that match the
213+
/// filter. We want to compile all non-test sources always because tests might depend on them
214+
/// dynamically through cheatcodes.
215+
///
216+
/// Returns `None` if all sources should be compiled.
215217
#[instrument(target = "forge::test", skip_all)]
216218
pub fn get_sources_to_compile(
217219
&self,
218220
config: &Config,
219-
filter: &ProjectPathsAwareFilter,
220-
) -> Result<BTreeSet<PathBuf>> {
221+
test_filter: &ProjectPathsAwareFilter,
222+
) -> Result<Option<BTreeSet<PathBuf>>> {
223+
// An empty filter doesn't filter out anything.
224+
if test_filter.is_empty() {
225+
return Ok(None);
226+
}
227+
221228
let mut project = config.create_project(true, true)?;
222229
project.update_output_selection(|selection| {
223230
*selection = OutputSelection::common_output_selection(["abi".to_string()]);
224231
});
225-
226232
let output = project.compile()?;
227-
228233
if output.has_compiler_errors() {
229234
sh_println!("{output}")?;
230235
eyre::bail!("Compilation failed");
231236
}
232237

233-
// ABIs of all sources
234-
let abis = output
235-
.into_artifacts()
236-
.filter_map(|(id, artifact)| artifact.abi.map(|abi| (id, abi)))
237-
.collect::<BTreeMap<_, _>>();
238-
239-
// Filter sources by their abis and contract names.
240-
let mut test_sources = abis
241-
.iter()
242-
.filter(|(id, abi)| matches_contract(id, abi, filter))
243-
.map(|(id, _)| id.source.clone())
238+
let sources = output
239+
.artifact_ids()
240+
.filter_map(|(id, artifact)| artifact.abi.as_ref().map(|abi| (id, abi)))
241+
.filter(|(id, abi)| {
242+
!is_test_contract(abi.functions()) || matches_artifact(test_filter, id, abi)
243+
})
244+
.map(|(id, _)| id.source)
244245
.collect::<BTreeSet<_>>();
245-
246-
if test_sources.is_empty() {
247-
if filter.is_empty() {
248-
sh_println!(
249-
"No tests found in project! \
250-
Forge looks for functions that starts with `test`."
251-
)?;
252-
} else {
253-
sh_println!("No tests match the provided pattern:")?;
254-
sh_print!("{filter}")?;
255-
256-
// Try to suggest a test when there's no match
257-
if let Some(test_pattern) = &filter.args().test_pattern {
258-
let test_name = test_pattern.as_str();
259-
let candidates = abis
260-
.into_iter()
261-
.filter(|(id, _)| {
262-
filter.matches_path(&id.source) && filter.matches_contract(&id.name)
263-
})
264-
.flat_map(|(_, abi)| abi.functions.into_keys())
265-
.collect::<Vec<_>>();
266-
if let Some(suggestion) = utils::did_you_mean(test_name, candidates).pop() {
267-
sh_println!("\nDid you mean `{suggestion}`?")?;
268-
}
269-
}
270-
}
271-
272-
eyre::bail!("No tests to run");
273-
}
274-
275-
// Always recompile all sources to ensure that `getCode` cheatcode can use any artifact.
276-
test_sources.extend(source_files_iter(
277-
&project.paths.sources,
278-
MultiCompilerLanguage::FILE_EXTENSIONS,
279-
));
280-
281-
Ok(test_sources)
246+
Ok(Some(sources))
282247
}
283248

284249
/// Executes all the tests in the project.
@@ -312,13 +277,10 @@ impl TestArgs {
312277
let filter = self.filter(&config)?;
313278
trace!(target: "forge::test", ?filter, "using filter");
314279

315-
let sources_to_compile = self.get_sources_to_compile(&config, &filter)?;
316-
317280
let compiler = ProjectCompiler::new()
318281
.dynamic_test_linking(config.dynamic_test_linking)
319282
.quiet(shell::is_json() || self.junit)
320-
.files(sources_to_compile);
321-
283+
.files(self.get_sources_to_compile(&config, &filter)?.unwrap_or_default());
322284
let output = compiler.compile(&project)?;
323285

324286
// Create test options from general project settings and compiler output.
@@ -457,6 +419,32 @@ impl TestArgs {
457419
let silent = self.gas_report && shell::is_json() || self.summary && shell::is_json();
458420

459421
let num_filtered = runner.matching_test_functions(filter).count();
422+
423+
if num_filtered == 0 {
424+
let mut total_tests = num_filtered;
425+
if !filter.is_empty() {
426+
total_tests = runner.matching_test_functions(&EmptyTestFilter::default()).count();
427+
}
428+
if total_tests == 0 {
429+
sh_println!(
430+
"No tests found in project! Forge looks for functions that start with `test`"
431+
)?;
432+
} else {
433+
let mut msg = format!("no tests match the provided pattern:\n{filter}");
434+
// Try to suggest a test when there's no match.
435+
if let Some(test_pattern) = &filter.args().test_pattern {
436+
let test_name = test_pattern.as_str();
437+
// Filter contracts but not test functions.
438+
let candidates = runner.all_test_functions(filter).map(|f| &f.name);
439+
if let Some(suggestion) = utils::did_you_mean(test_name, candidates).pop() {
440+
write!(msg, "\nDid you mean `{suggestion}`?")?;
441+
}
442+
}
443+
sh_warn!("{msg}")?;
444+
}
445+
return Ok(TestOutcome::empty(false));
446+
}
447+
460448
if num_filtered != 1 && (self.debug || self.flamegraph || self.flamechart) {
461449
let action = if self.flamegraph {
462450
"generate a flamegraph"
@@ -915,7 +903,14 @@ fn list(runner: MultiContractRunner, filter: &ProjectPathsAwareFilter) -> Result
915903
/// Load persisted filter (with last test run failures) from file.
916904
fn last_run_failures(config: &Config) -> Option<regex::Regex> {
917905
match fs::read_to_string(&config.test_failures_file) {
918-
Ok(filter) => Some(Regex::new(&filter).unwrap()),
906+
Ok(filter) => Regex::new(&filter)
907+
.inspect_err(|e| {
908+
_ = sh_warn!(
909+
"failed to parse test filter from {:?}: {e}",
910+
config.test_failures_file
911+
)
912+
})
913+
.ok(),
919914
Err(_) => None,
920915
}
921916
}

crates/forge/src/multi_runner.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ impl MultiContractRunner {
8989
&'a self,
9090
filter: &'b dyn TestFilter,
9191
) -> impl Iterator<Item = (&'a ArtifactId, &'a TestContract)> + 'b {
92-
self.contracts.iter().filter(|&(id, c)| matches_contract(id, &c.abi, filter))
92+
self.contracts.iter().filter(|&(id, c)| matches_artifact(filter, id, &c.abi))
9393
}
9494

9595
/// Returns an iterator over all test functions that match the filter.
@@ -99,7 +99,7 @@ impl MultiContractRunner {
9999
) -> impl Iterator<Item = &'a Function> + 'b {
100100
self.matching_contracts(filter)
101101
.flat_map(|(_, c)| c.abi.functions())
102-
.filter(|func| is_matching_test(func, filter))
102+
.filter(|func| filter.matches_test_function(func))
103103
}
104104

105105
/// Returns an iterator over all test functions in contracts that match the filter.
@@ -123,7 +123,7 @@ impl MultiContractRunner {
123123
let tests = c
124124
.abi
125125
.functions()
126-
.filter(|func| is_matching_test(func, filter))
126+
.filter(|func| filter.matches_test_function(func))
127127
.map(|func| func.name.clone())
128128
.collect::<Vec<_>>();
129129
(source, name, tests)
@@ -566,12 +566,23 @@ impl MultiContractRunnerBuilder {
566566
}
567567
}
568568

569-
pub fn matches_contract(id: &ArtifactId, abi: &JsonAbi, filter: &dyn TestFilter) -> bool {
570-
(filter.matches_path(&id.source) && filter.matches_contract(&id.name))
571-
&& abi.functions().any(|func| is_matching_test(func, filter))
569+
pub fn matches_artifact(filter: &dyn TestFilter, id: &ArtifactId, abi: &JsonAbi) -> bool {
570+
matches_contract(filter, &id.source, &id.name, abi.functions())
572571
}
573572

574-
/// Returns `true` if the function is a test function that matches the given filter.
575-
pub(crate) fn is_matching_test(func: &Function, filter: &dyn TestFilter) -> bool {
576-
func.is_any_test() && filter.matches_test(&func.signature())
573+
pub(crate) fn matches_contract(
574+
filter: &dyn TestFilter,
575+
path: &Path,
576+
contract_name: &str,
577+
functions: impl IntoIterator<Item = impl std::borrow::Borrow<Function>>,
578+
) -> bool {
579+
(filter.matches_path(path) && filter.matches_contract(contract_name))
580+
&& functions.into_iter().any(|func| filter.matches_test_function(func.borrow()))
581+
}
582+
583+
/// Returns `true` if the given contract is a test contract.
584+
pub(crate) fn is_test_contract(
585+
functions: impl IntoIterator<Item = impl std::borrow::Borrow<Function>>,
586+
) -> bool {
587+
functions.into_iter().any(|func| func.borrow().is_any_test())
577588
}

crates/forge/src/runner.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::{
44
MultiContractRunner, TestFilter,
55
fuzz::BaseCounterExample,
6-
multi_runner::{TestContract, TestRunnerConfig, is_matching_test},
6+
multi_runner::{TestContract, TestRunnerConfig},
77
progress::{TestsProgress, start_fuzz_progress},
88
result::{SuiteResult, TestResult, TestSetup},
99
};
@@ -369,7 +369,7 @@ impl<'a> ContractRunner<'a> {
369369
.contract
370370
.abi
371371
.functions()
372-
.filter(|func| is_matching_test(func, filter))
372+
.filter(|func| filter.matches_test_function(func))
373373
.collect::<Vec<_>>();
374374
debug!(
375375
"Found {} test functions out of {} in {:?}",

0 commit comments

Comments
 (0)