Skip to content

Commit ca57995

Browse files
authored
fix(coverage): sync implementation with forge test (#11801)
Coverage was calling a low level function of forge test args, which does not setup stuff like config.fuzz.gas_report_samples, EvmArgs, etc., as well as missing options in the test runner builder etc. This means that `forge coverage` falls out of sync gradually with `forge test`. One of the options which was thus missed, `gas_report_samples`, controls how many traces are saved from fuzz and invariant tests, which with the recent addition of tracing for backtraces means we are storing a lot of traces unnecessarily, massively increasing memory usage.
1 parent 76afa69 commit ca57995

File tree

6 files changed

+80
-71
lines changed

6 files changed

+80
-71
lines changed

crates/evm/evm/src/executors/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1131,7 +1131,7 @@ impl FuzzTestTimer {
11311131
}
11321132

11331133
/// Helper struct to enable fail fast behavior: when one test fails, all other tests stop early.
1134-
#[derive(Clone)]
1134+
#[derive(Clone, Debug)]
11351135
pub struct FailFast {
11361136
/// Shared atomic flag set to `true` when a failure occurs.
11371137
/// None if fail-fast is disabled.

crates/forge/src/cmd/coverage.rs

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
use super::{install, test::TestArgs, watch::WatchArgs};
2-
use crate::{
3-
MultiContractRunnerBuilder,
4-
coverage::{
5-
BytecodeReporter, ContractId, CoverageReport, CoverageReporter, CoverageSummaryReporter,
6-
DebugReporter, ItemAnchor, LcovReporter,
7-
analysis::{SourceAnalysis, SourceFiles},
8-
anchors::find_anchors,
9-
},
2+
use crate::coverage::{
3+
BytecodeReporter, ContractId, CoverageReport, CoverageReporter, CoverageSummaryReporter,
4+
DebugReporter, ItemAnchor, LcovReporter,
5+
analysis::{SourceAnalysis, SourceFiles},
6+
anchors::find_anchors,
107
};
118
use alloy_primitives::{Address, Bytes, U256, map::HashMap};
129
use clap::{Parser, ValueEnum, ValueHint};
@@ -16,17 +13,13 @@ use foundry_common::{compile::ProjectCompiler, errors::convert_solar_errors};
1613
use foundry_compilers::{
1714
Artifact, ArtifactId, Project, ProjectCompileOutput, ProjectPathsConfig,
1815
artifacts::{CompactBytecode, CompactDeployedBytecode, SolcLanguage, sourcemap::SourceMap},
19-
compilers::multi::MultiCompiler,
2016
};
2117
use foundry_config::Config;
2218
use foundry_evm::opts::EvmOpts;
2319
use foundry_evm_core::ic::IcPcMap;
2420
use rayon::prelude::*;
2521
use semver::{Version, VersionReq};
26-
use std::{
27-
path::{Path, PathBuf},
28-
sync::Arc,
29-
};
22+
use std::path::{Path, PathBuf};
3023

3124
// Loads project's figment and merges the build cli arguments into it
3225
foundry_config::impl_figment_convert!(CoverageArgs, test);
@@ -109,7 +102,7 @@ impl CoverageArgs {
109102
let report = self.prepare(&paths, &mut output)?;
110103

111104
sh_println!("Running tests...")?;
112-
self.collect(&paths.root, &output, report, Arc::new(config), evm_opts).await
105+
self.collect(&paths.root, &output, report, config, evm_opts).await
113106
}
114107

115108
fn populate_reporters(&mut self, root: &Path) {
@@ -255,31 +248,19 @@ impl CoverageArgs {
255248
#[instrument(name = "Coverage::collect", skip_all)]
256249
async fn collect(
257250
mut self,
258-
root: &Path,
251+
project_root: &Path,
259252
output: &ProjectCompileOutput,
260253
mut report: CoverageReport,
261-
config: Arc<Config>,
254+
config: Config,
262255
evm_opts: EvmOpts,
263256
) -> Result<()> {
264-
let verbosity = evm_opts.verbosity;
265-
266-
// Build the contract runner
267-
let env = evm_opts.evm_env().await?;
268-
let runner = MultiContractRunnerBuilder::new(config.clone())
269-
.initial_balance(evm_opts.initial_balance)
270-
.evm_spec(config.evm_spec_id())
271-
.sender(evm_opts.sender)
272-
.with_fork(evm_opts.get_fork(&config, env.clone()))
273-
.set_coverage(true)
274-
.build::<MultiCompiler>(root, output, env, evm_opts)?;
275-
276-
let known_contracts = runner.known_contracts.clone();
277-
278257
let filter = self.test.filter(&config)?;
279-
let outcome = self.test.run_tests(runner, config, verbosity, &filter, output).await?;
280-
258+
let outcome =
259+
self.test.run_tests(project_root, config, evm_opts, output, &filter, true).await?;
281260
outcome.ensure_ok(false)?;
282261

262+
let known_contracts = outcome.runner.as_ref().unwrap().known_contracts.clone();
263+
283264
// Add hit data to the coverage report
284265
let data = outcome.results.iter().flat_map(|(_, suite)| {
285266
let mut hits = Vec::new();

crates/forge/src/cmd/snapshot.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ impl GasSnapshotArgs {
9494
// Set fuzz seed so gas snapshots are deterministic
9595
self.test.fuzz_seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED));
9696

97-
let outcome = self.test.execute_tests().await?;
97+
let outcome = self.test.compile_and_run().await?;
9898
outcome.ensure_ok(false)?;
9999
let tests = self.config.apply(outcome);
100100

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

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,15 @@ use foundry_config::{
3939
filter::GlobMatcher,
4040
};
4141
use foundry_debugger::Debugger;
42-
use foundry_evm::traces::{backtrace::BacktraceBuilder, identifier::TraceIdentifiers};
42+
use foundry_evm::{
43+
opts::EvmOpts,
44+
traces::{backtrace::BacktraceBuilder, identifier::TraceIdentifiers},
45+
};
4346
use regex::Regex;
4447
use std::{
4548
collections::{BTreeMap, BTreeSet},
4649
fmt::Write,
47-
path::PathBuf,
50+
path::{Path, PathBuf},
4851
sync::{Arc, mpsc::channel},
4952
time::{Duration, Instant},
5053
};
@@ -199,9 +202,9 @@ pub struct TestArgs {
199202
}
200203

201204
impl TestArgs {
202-
pub async fn run(self) -> Result<TestOutcome> {
205+
pub async fn run(mut self) -> Result<TestOutcome> {
203206
trace!(target: "forge::test", "executing test command");
204-
self.execute_tests().await
207+
self.compile_and_run().await
205208
}
206209

207210
/// Returns a list of files that need to be compiled in order to run all the tests that match
@@ -250,18 +253,9 @@ impl TestArgs {
250253
/// configured filter will be executed
251254
///
252255
/// Returns the test results for all matching tests.
253-
pub async fn execute_tests(mut self) -> Result<TestOutcome> {
256+
pub async fn compile_and_run(&mut self) -> Result<TestOutcome> {
254257
// Merge all configs.
255-
let (mut config, mut evm_opts) = self.load_config_and_evm_opts()?;
256-
257-
// Explicitly enable isolation for gas reports for more correct gas accounting.
258-
if self.gas_report {
259-
evm_opts.isolate = true;
260-
} else {
261-
// Do not collect gas report traces if gas report is not enabled.
262-
config.fuzz.gas_report_samples = 0;
263-
config.invariant.gas_report_samples = 0;
264-
}
258+
let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
265259

266260
// Install missing dependencies.
267261
if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings {
@@ -281,9 +275,31 @@ impl TestArgs {
281275
.files(self.get_sources_to_compile(&config, &filter)?);
282276
let output = compiler.compile(&project)?;
283277

284-
// Create test options from general project settings and compiler output.
285-
let project_root = &project.paths.root;
278+
self.run_tests(&project.paths.root, config, evm_opts, &output, &filter, false).await
279+
}
286280

281+
/// Executes all the tests in the project.
282+
///
283+
/// See [`Self::compile_and_run`] for more details.
284+
pub async fn run_tests(
285+
&mut self,
286+
project_root: &Path,
287+
mut config: Config,
288+
mut evm_opts: EvmOpts,
289+
output: &ProjectCompileOutput,
290+
filter: &ProjectPathsAwareFilter,
291+
coverage: bool,
292+
) -> Result<TestOutcome> {
293+
// Explicitly enable isolation for gas reports for more correct gas accounting.
294+
if self.gas_report {
295+
evm_opts.isolate = true;
296+
} else {
297+
// Do not collect gas report traces if gas report is not enabled.
298+
config.fuzz.gas_report_samples = 0;
299+
config.invariant.gas_report_samples = 0;
300+
}
301+
302+
// Create test options from general project settings and compiler output.
287303
let should_debug = self.debug;
288304
let should_draw = self.flamegraph || self.flamechart;
289305

@@ -321,10 +337,11 @@ impl TestArgs {
321337
.enable_isolation(evm_opts.isolate)
322338
.networks(evm_opts.networks)
323339
.fail_fast(self.fail_fast)
324-
.build::<MultiCompiler>(project_root, &output, env, evm_opts)?;
340+
.set_coverage(coverage)
341+
.build::<MultiCompiler>(project_root, output, env, evm_opts)?;
325342

326343
let libraries = runner.libraries.clone();
327-
let mut outcome = self.run_tests(runner, config, verbosity, &filter, &output).await?;
344+
let mut outcome = self.run_tests_inner(runner, config, verbosity, filter, output).await?;
328345

329346
if should_draw {
330347
let (suite_name, test_name, mut test_result) =
@@ -373,7 +390,7 @@ impl TestArgs {
373390
outcome.remove_first().ok_or_eyre("no tests were executed")?;
374391

375392
let sources =
376-
ContractSources::from_project_output(&output, project.root(), Some(&libraries))?;
393+
ContractSources::from_project_output(output, project_root, Some(&libraries))?;
377394

378395
// Run the debugger.
379396
let mut builder = Debugger::builder()
@@ -388,8 +405,8 @@ impl TestArgs {
388405
}
389406

390407
let mut debugger = builder.build();
391-
if let Some(dump_path) = self.dump {
392-
debugger.dump_to_file(&dump_path)?;
408+
if let Some(dump_path) = &self.dump {
409+
debugger.dump_to_file(dump_path)?;
393410
} else {
394411
debugger.try_run_tui()?;
395412
}
@@ -399,7 +416,7 @@ impl TestArgs {
399416
}
400417

401418
/// Run all tests that matches the filter predicate from a test runner
402-
pub async fn run_tests(
419+
async fn run_tests_inner(
403420
&self,
404421
mut runner: MultiContractRunner,
405422
config: Arc<Config>,
@@ -440,7 +457,7 @@ impl TestArgs {
440457
}
441458
sh_warn!("{msg}")?;
442459
}
443-
return Ok(TestOutcome::empty(false));
460+
return Ok(TestOutcome::empty(Some(runner), false));
444461
}
445462

446463
if num_filtered != 1 && (self.debug || self.flamegraph || self.flamechart) {
@@ -482,13 +499,13 @@ impl TestArgs {
482499
}
483500
});
484501
sh_println!("{}", serde_json::to_string(&results)?)?;
485-
return Ok(TestOutcome::new(results, self.allow_failure));
502+
return Ok(TestOutcome::new(Some(runner), results, self.allow_failure));
486503
}
487504

488505
if self.junit {
489506
let results = runner.test_collect(filter)?;
490507
sh_println!("{}", junit_xml_report(&results, verbosity).to_string()?)?;
491-
return Ok(TestOutcome::new(results, self.allow_failure));
508+
return Ok(TestOutcome::new(Some(runner), results, self.allow_failure));
492509
}
493510

494511
let remote_chain_id = runner.evm_opts.get_remote_chain_id().await;
@@ -502,7 +519,7 @@ impl TestArgs {
502519
let show_progress = config.show_progress;
503520
let handle = tokio::task::spawn_blocking({
504521
let filter = filter.clone();
505-
move || runner.test(&filter, tx, show_progress)
522+
move || runner.test(&filter, tx, show_progress).map(|()| runner)
506523
});
507524

508525
// Set up trace identifiers.
@@ -542,7 +559,7 @@ impl TestArgs {
542559

543560
let mut gas_snapshots = BTreeMap::<String, BTreeMap<String, String>>::new();
544561

545-
let mut outcome = TestOutcome::empty(self.allow_failure);
562+
let mut outcome = TestOutcome::empty(None, self.allow_failure);
546563

547564
let mut any_test_failed = false;
548565
let mut backtrace_builder = None;
@@ -823,11 +840,12 @@ impl TestArgs {
823840
}
824841

825842
// Reattach the task.
826-
if let Err(e) = handle.await {
827-
match e.try_into_panic() {
843+
match handle.await {
844+
Ok(result) => outcome.runner = Some(result?),
845+
Err(e) => match e.try_into_panic() {
828846
Ok(payload) => std::panic::resume_unwind(payload),
829847
Err(e) => return Err(e.into()),
830-
}
848+
},
831849
}
832850

833851
// Persist test run failures to enable replaying.
@@ -919,7 +937,7 @@ fn list(runner: MultiContractRunner, filter: &ProjectPathsAwareFilter) -> Result
919937
}
920938
}
921939
}
922-
Ok(TestOutcome::empty(false))
940+
Ok(TestOutcome::empty(Some(runner), false))
923941
}
924942

925943
/// Load persisted filter (with last test run failures) from file.

crates/forge/src/multi_runner.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub type DeployableContracts = BTreeMap<ArtifactId, TestContract>;
5050

5151
/// A multi contract runner receives a set of contracts deployed in an EVM instance and proceeds
5252
/// to run all test functions in these contracts.
53+
#[derive(Clone, Debug)]
5354
pub struct MultiContractRunner {
5455
/// Mapping of contract name to JsonAbi, creation bytecode and library bytecode which
5556
/// needs to be deployed & linked against
@@ -274,7 +275,7 @@ impl MultiContractRunner {
274275
/// Configuration for the test runner.
275276
///
276277
/// This is modified after instantiation through inline config.
277-
#[derive(Clone)]
278+
#[derive(Clone, Debug)]
278279
pub struct TestRunnerConfig {
279280
/// Project config.
280281
pub config: Arc<Config>,

crates/forge/src/result.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Test outcomes.
22
33
use crate::{
4+
MultiContractRunner,
45
fuzz::{BaseCounterExample, FuzzedCases},
56
gas_report::GasReport,
67
};
@@ -42,17 +43,23 @@ pub struct TestOutcome {
4243
pub last_run_decoder: Option<CallTraceDecoder>,
4344
/// The gas report, if requested.
4445
pub gas_report: Option<GasReport>,
46+
/// The runner used to execute the tests.
47+
pub runner: Option<MultiContractRunner>,
4548
}
4649

4750
impl TestOutcome {
4851
/// Creates a new test outcome with the given results.
49-
pub fn new(results: BTreeMap<String, SuiteResult>, allow_failure: bool) -> Self {
50-
Self { results, allow_failure, last_run_decoder: None, gas_report: None }
52+
pub fn new(
53+
runner: Option<MultiContractRunner>,
54+
results: BTreeMap<String, SuiteResult>,
55+
allow_failure: bool,
56+
) -> Self {
57+
Self { results, allow_failure, last_run_decoder: None, gas_report: None, runner }
5158
}
5259

5360
/// Creates a new empty test outcome.
54-
pub fn empty(allow_failure: bool) -> Self {
55-
Self::new(BTreeMap::new(), allow_failure)
61+
pub fn empty(runner: Option<MultiContractRunner>, allow_failure: bool) -> Self {
62+
Self::new(runner, BTreeMap::new(), allow_failure)
5663
}
5764

5865
/// Returns an iterator over all individual succeeding tests and their names.
@@ -399,6 +406,8 @@ pub struct TestResult {
399406
pub traces: Traces,
400407

401408
/// Additional traces to use for gas report.
409+
///
410+
/// These are cleared after the gas report is analyzed.
402411
#[serde(skip)]
403412
pub gas_report_traces: Vec<Vec<CallTraceArena>>,
404413

0 commit comments

Comments
 (0)