1
1
use super :: { install, test:: filter:: ProjectPathsAwareFilter , watch:: WatchArgs } ;
2
2
use crate :: {
3
- MultiContractRunner , MultiContractRunnerBuilder , TestFilter ,
3
+ MultiContractRunner , MultiContractRunnerBuilder ,
4
4
decode:: decode_console_logs,
5
5
gas_report:: GasReport ,
6
- multi_runner:: matches_contract ,
6
+ multi_runner:: { is_test_contract , matches_artifact } ,
7
7
result:: { SuiteResult , TestOutcome , TestStatus } ,
8
8
traces:: {
9
9
CallTraceDecoderBuilder , InternalTraceMode , TraceKind ,
@@ -21,15 +21,12 @@ use foundry_cli::{
21
21
opts:: { BuildOpts , GlobalArgs } ,
22
22
utils:: { self , LoadConfig } ,
23
23
} ;
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
+ } ;
25
27
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 ,
33
30
} ;
34
31
use foundry_config:: {
35
32
Config , figment,
@@ -209,76 +206,44 @@ impl TestArgs {
209
206
self . execute_tests ( ) . await
210
207
}
211
208
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.
215
217
#[ instrument( target = "forge::test" , skip_all) ]
216
218
pub fn get_sources_to_compile (
217
219
& self ,
218
220
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
+
221
228
let mut project = config. create_project ( true , true ) ?;
222
229
project. update_output_selection ( |selection| {
223
230
* selection = OutputSelection :: common_output_selection ( [ "abi" . to_string ( ) ] ) ;
224
231
} ) ;
225
-
226
232
let output = project. compile ( ) ?;
227
-
228
233
if output. has_compiler_errors ( ) {
229
234
sh_println ! ( "{output}" ) ?;
230
235
eyre:: bail!( "Compilation failed" ) ;
231
236
}
232
237
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 )
244
245
. 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) )
282
247
}
283
248
284
249
/// Executes all the tests in the project.
@@ -312,13 +277,10 @@ impl TestArgs {
312
277
let filter = self . filter ( & config) ?;
313
278
trace ! ( target: "forge::test" , ?filter, "using filter" ) ;
314
279
315
- let sources_to_compile = self . get_sources_to_compile ( & config, & filter) ?;
316
-
317
280
let compiler = ProjectCompiler :: new ( )
318
281
. dynamic_test_linking ( config. dynamic_test_linking )
319
282
. quiet ( shell:: is_json ( ) || self . junit )
320
- . files ( sources_to_compile) ;
321
-
283
+ . files ( self . get_sources_to_compile ( & config, & filter) ?. unwrap_or_default ( ) ) ;
322
284
let output = compiler. compile ( & project) ?;
323
285
324
286
// Create test options from general project settings and compiler output.
@@ -457,6 +419,32 @@ impl TestArgs {
457
419
let silent = self . gas_report && shell:: is_json ( ) || self . summary && shell:: is_json ( ) ;
458
420
459
421
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
+
460
448
if num_filtered != 1 && ( self . debug || self . flamegraph || self . flamechart ) {
461
449
let action = if self . flamegraph {
462
450
"generate a flamegraph"
@@ -915,7 +903,14 @@ fn list(runner: MultiContractRunner, filter: &ProjectPathsAwareFilter) -> Result
915
903
/// Load persisted filter (with last test run failures) from file.
916
904
fn last_run_failures ( config : & Config ) -> Option < regex:: Regex > {
917
905
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 ( ) ,
919
914
Err ( _) => None ,
920
915
}
921
916
}
0 commit comments