11use super :: { install, test:: filter:: ProjectPathsAwareFilter , watch:: WatchArgs } ;
22use 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+ } ;
2527use 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} ;
3431use 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 ! ( "\n Did 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, "\n Did 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.
916904fn 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}
0 commit comments