1use super::{install, test::TestArgs, watch::WatchArgs};
2use crate::{
3 MultiContractRunnerBuilder,
4 coverage::{
5 BytecodeReporter, ContractId, CoverageReport, CoverageReporter, CoverageSummaryReporter,
6 DebugReporter, ItemAnchor, LcovReporter,
7 analysis::{SourceAnalysis, SourceFile, SourceFiles},
8 anchors::find_anchors,
9 },
10};
11use alloy_primitives::{Address, Bytes, U256, map::HashMap};
12use clap::{Parser, ValueEnum, ValueHint};
13use eyre::{Context, Result};
14use foundry_cli::utils::{LoadConfig, STATIC_FUZZ_SEED};
15use foundry_common::compile::ProjectCompiler;
16use foundry_compilers::{
17 Artifact, ArtifactId, Project, ProjectCompileOutput, ProjectPathsConfig,
18 artifacts::{
19 CompactBytecode, CompactDeployedBytecode, SolcLanguage, Source, sourcemap::SourceMap,
20 },
21 compilers::multi::MultiCompiler,
22};
23use foundry_config::Config;
24use foundry_evm::opts::EvmOpts;
25use foundry_evm_core::ic::IcPcMap;
26use rayon::prelude::*;
27use semver::{Version, VersionReq};
28use std::{
29 path::{Path, PathBuf},
30 sync::Arc,
31};
32
33foundry_config::impl_figment_convert!(CoverageArgs, test);
35
36#[derive(Parser)]
38pub struct CoverageArgs {
39 #[arg(long, value_enum, default_value = "summary")]
43 report: Vec<CoverageReportKind>,
44
45 #[arg(long, default_value = "1", value_parser = parse_lcov_version)]
54 lcov_version: Version,
55
56 #[arg(long)]
61 ir_minimum: bool,
62
63 #[arg(
67 long,
68 short,
69 value_hint = ValueHint::FilePath,
70 value_name = "PATH"
71 )]
72 report_file: Option<PathBuf>,
73
74 #[arg(long)]
76 include_libs: bool,
77
78 #[arg(long)]
80 exclude_tests: bool,
81
82 #[arg(skip)]
84 reporters: Vec<Box<dyn CoverageReporter>>,
85
86 #[command(flatten)]
87 test: TestArgs,
88}
89
90impl CoverageArgs {
91 pub async fn run(mut self) -> Result<()> {
92 let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
93
94 if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings {
96 config = self.load_config()?;
98 }
99
100 config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED));
102
103 config.ast = true;
105
106 let (paths, output) = {
107 let (project, output) = self.build(&config)?;
108 (project.paths, output)
109 };
110
111 self.populate_reporters(&paths.root);
112
113 sh_println!("Analysing contracts...")?;
114 let report = self.prepare(&paths, &output)?;
115
116 sh_println!("Running tests...")?;
117 self.collect(&paths.root, &output, report, Arc::new(config), evm_opts).await
118 }
119
120 fn populate_reporters(&mut self, root: &Path) {
121 self.reporters = self
122 .report
123 .iter()
124 .map(|report_kind| match report_kind {
125 CoverageReportKind::Summary => {
126 Box::<CoverageSummaryReporter>::default() as Box<dyn CoverageReporter>
127 }
128 CoverageReportKind::Lcov => {
129 let path =
130 root.join(self.report_file.as_deref().unwrap_or("lcov.info".as_ref()));
131 Box::new(LcovReporter::new(path, self.lcov_version.clone()))
132 }
133 CoverageReportKind::Bytecode => Box::new(BytecodeReporter::new(
134 root.to_path_buf(),
135 root.join("bytecode-coverage"),
136 )),
137 CoverageReportKind::Debug => Box::new(DebugReporter),
138 })
139 .collect::<Vec<_>>();
140 }
141
142 fn build(&self, config: &Config) -> Result<(Project, ProjectCompileOutput)> {
144 let mut project = config.ephemeral_project()?;
145
146 if self.ir_minimum {
147 sh_warn!(
149 "`--ir-minimum` enables `viaIR` with minimum optimization, \
150 which can result in inaccurate source mappings.\n\
151 Only use this flag as a workaround if you are experiencing \"stack too deep\" errors.\n\
152 Note that `viaIR` is production ready since Solidity 0.8.13 and above.\n\
153 See more: https://github.com/foundry-rs/foundry/issues/3357"
154 )?;
155
156 project.settings.solc.settings =
159 project.settings.solc.settings.with_via_ir_minimum_optimization();
160
161 let evm_version = project.settings.solc.evm_version;
164 let version = config.solc_version().unwrap_or_else(|| Version::new(0, 8, 4));
165 project.settings.solc.settings.sanitize(&version, SolcLanguage::Solidity);
166 project.settings.solc.evm_version = evm_version;
167 } else {
168 sh_warn!(
169 "optimizer settings and `viaIR` have been disabled for accurate coverage reports.\n\
170 If you encounter \"stack too deep\" errors, consider using `--ir-minimum` which \
171 enables `viaIR` with minimum optimization resolving most of the errors"
172 )?;
173
174 project.settings.solc.optimizer.disable();
175 project.settings.solc.optimizer.runs = None;
176 project.settings.solc.optimizer.details = None;
177 project.settings.solc.via_ir = None;
178 }
179
180 let output = ProjectCompiler::default()
181 .compile(&project)?
182 .with_stripped_file_prefixes(project.root());
183
184 Ok((project, output))
185 }
186
187 #[instrument(name = "Coverage::prepare", skip_all)]
189 fn prepare(
190 &self,
191 project_paths: &ProjectPathsConfig,
192 output: &ProjectCompileOutput,
193 ) -> Result<CoverageReport> {
194 let mut report = CoverageReport::default();
195
196 let mut versioned_sources = HashMap::<Version, SourceFiles<'_>>::default();
198 for (path, source_file, version) in output.output().sources.sources_with_version() {
199 report.add_source(version.clone(), source_file.id as usize, path.clone());
200
201 if (!self.include_libs && project_paths.has_library_ancestor(path))
203 || (self.exclude_tests && project_paths.is_test(path))
204 {
205 continue;
206 }
207
208 if let Some(ast) = &source_file.ast {
209 let file = project_paths.root.join(path);
210 trace!(root=?project_paths.root, ?file, "reading source file");
211
212 let source = SourceFile {
213 ast,
214 source: Source::read(&file)
215 .wrap_err("Could not read source code for analysis")?,
216 };
217 versioned_sources
218 .entry(version.clone())
219 .or_default()
220 .sources
221 .insert(source_file.id as usize, source);
222 }
223 }
224
225 let artifacts: Vec<ArtifactData> = output
227 .artifact_ids()
228 .par_bridge() .filter_map(|(id, artifact)| {
230 let source_id = report.get_source_id(id.version.clone(), id.source.clone())?;
231 ArtifactData::new(&id, source_id, artifact)
232 })
233 .collect();
234
235 for (version, sources) in &versioned_sources {
237 let source_analysis = SourceAnalysis::new(sources)?;
238 let anchors = artifacts
239 .par_iter()
240 .filter(|artifact| artifact.contract_id.version == *version)
241 .map(|artifact| {
242 let creation_code_anchors = artifact.creation.find_anchors(&source_analysis);
243 let deployed_code_anchors = artifact.deployed.find_anchors(&source_analysis);
244 (artifact.contract_id.clone(), (creation_code_anchors, deployed_code_anchors))
245 })
246 .collect_vec_list();
247 report.add_anchors(anchors.into_iter().flatten());
248 report.add_analysis(version.clone(), source_analysis);
249 }
250
251 if self.reporters.iter().any(|reporter| reporter.needs_source_maps()) {
252 report.add_source_maps(artifacts.into_iter().map(|artifact| {
253 (artifact.contract_id, (artifact.creation.source_map, artifact.deployed.source_map))
254 }));
255 }
256
257 Ok(report)
258 }
259
260 #[instrument(name = "Coverage::collect", skip_all)]
262 async fn collect(
263 mut self,
264 root: &Path,
265 output: &ProjectCompileOutput,
266 mut report: CoverageReport,
267 config: Arc<Config>,
268 evm_opts: EvmOpts,
269 ) -> Result<()> {
270 let verbosity = evm_opts.verbosity;
271
272 let env = evm_opts.evm_env().await?;
274 let runner = MultiContractRunnerBuilder::new(config.clone())
275 .initial_balance(evm_opts.initial_balance)
276 .evm_spec(config.evm_spec_id())
277 .sender(evm_opts.sender)
278 .with_fork(evm_opts.get_fork(&config, env.clone()))
279 .set_coverage(true)
280 .build::<MultiCompiler>(root, output, env, evm_opts)?;
281
282 let known_contracts = runner.known_contracts.clone();
283
284 let filter = self.test.filter(&config)?;
285 let outcome = self.test.run_tests(runner, config, verbosity, &filter, output).await?;
286
287 outcome.ensure_ok(false)?;
288
289 let data = outcome.results.iter().flat_map(|(_, suite)| {
291 let mut hits = Vec::new();
292 for result in suite.test_results.values() {
293 let Some(hit_maps) = result.line_coverage.as_ref() else { continue };
294 for map in hit_maps.0.values() {
295 if let Some((id, _)) = known_contracts.find_by_deployed_code(map.bytecode()) {
296 hits.push((id, map, true));
297 } else if let Some((id, _)) =
298 known_contracts.find_by_creation_code(map.bytecode())
299 {
300 hits.push((id, map, false));
301 }
302 }
303 }
304 hits
305 });
306
307 for (artifact_id, map, is_deployed_code) in data {
308 if let Some(source_id) =
309 report.get_source_id(artifact_id.version.clone(), artifact_id.source.clone())
310 {
311 report.add_hit_map(
312 &ContractId {
313 version: artifact_id.version.clone(),
314 source_id,
315 contract_name: artifact_id.name.as_str().into(),
316 },
317 map,
318 is_deployed_code,
319 )?;
320 }
321 }
322
323 if let Some(not_re) = &filter.args().coverage_pattern_inverse {
325 let file_root = filter.paths().root.as_path();
326 report.retain_sources(|path: &Path| {
327 let path = path.strip_prefix(file_root).unwrap_or(path);
328 !not_re.is_match(&path.to_string_lossy())
329 });
330 }
331
332 self.report(&report)?;
334
335 Ok(())
336 }
337
338 #[instrument(name = "Coverage::report", skip_all)]
339 fn report(&mut self, report: &CoverageReport) -> Result<()> {
340 for reporter in &mut self.reporters {
341 let _guard = debug_span!("reporter.report", kind=%reporter.name()).entered();
342 reporter.report(report)?;
343 }
344 Ok(())
345 }
346
347 pub fn is_watch(&self) -> bool {
348 self.test.is_watch()
349 }
350
351 pub fn watch(&self) -> &WatchArgs {
352 &self.test.watch
353 }
354}
355
356#[derive(Clone, Debug, Default, ValueEnum)]
358pub enum CoverageReportKind {
359 #[default]
360 Summary,
361 Lcov,
362 Debug,
363 Bytecode,
364}
365
366fn dummy_link_bytecode(mut obj: CompactBytecode) -> Option<Bytes> {
370 let link_references = obj.link_references.clone();
371 for (file, libraries) in link_references {
372 for library in libraries.keys() {
373 obj.link(&file, library, Address::ZERO);
374 }
375 }
376
377 obj.object.resolve();
378 obj.object.into_bytes()
379}
380
381fn dummy_link_deployed_bytecode(obj: CompactDeployedBytecode) -> Option<Bytes> {
385 obj.bytecode.and_then(dummy_link_bytecode)
386}
387
388pub struct ArtifactData {
389 pub contract_id: ContractId,
390 pub creation: BytecodeData,
391 pub deployed: BytecodeData,
392}
393
394impl ArtifactData {
395 pub fn new(id: &ArtifactId, source_id: usize, artifact: &impl Artifact) -> Option<Self> {
396 Some(Self {
397 contract_id: ContractId {
398 version: id.version.clone(),
399 source_id,
400 contract_name: id.name.as_str().into(),
401 },
402 creation: BytecodeData::new(
403 artifact.get_source_map()?.ok()?,
404 artifact
405 .get_bytecode()
406 .and_then(|bytecode| dummy_link_bytecode(bytecode.into_owned()))?,
407 ),
408 deployed: BytecodeData::new(
409 artifact.get_source_map_deployed()?.ok()?,
410 artifact
411 .get_deployed_bytecode()
412 .and_then(|bytecode| dummy_link_deployed_bytecode(bytecode.into_owned()))?,
413 ),
414 })
415 }
416}
417
418pub struct BytecodeData {
419 source_map: SourceMap,
420 bytecode: Bytes,
421 ic_pc_map: IcPcMap,
429}
430
431impl BytecodeData {
432 fn new(source_map: SourceMap, bytecode: Bytes) -> Self {
433 let ic_pc_map = IcPcMap::new(&bytecode);
434 Self { source_map, bytecode, ic_pc_map }
435 }
436
437 pub fn find_anchors(&self, source_analysis: &SourceAnalysis) -> Vec<ItemAnchor> {
438 find_anchors(&self.bytecode, &self.source_map, &self.ic_pc_map, source_analysis)
439 }
440}
441
442fn parse_lcov_version(s: &str) -> Result<Version, String> {
443 let vr = VersionReq::parse(&format!("={s}")).map_err(|e| e.to_string())?;
444 let [c] = &vr.comparators[..] else {
445 return Err("invalid version".to_string());
446 };
447 if c.op != semver::Op::Exact {
448 return Err("invalid version".to_string());
449 }
450 if !c.pre.is_empty() {
451 return Err("pre-releases are not supported".to_string());
452 }
453 Ok(Version::new(c.major, c.minor.unwrap_or(0), c.patch.unwrap_or(0)))
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn lcov_version() {
462 assert_eq!(parse_lcov_version("0").unwrap(), Version::new(0, 0, 0));
463 assert_eq!(parse_lcov_version("1").unwrap(), Version::new(1, 0, 0));
464 assert_eq!(parse_lcov_version("1.0").unwrap(), Version::new(1, 0, 0));
465 assert_eq!(parse_lcov_version("1.1").unwrap(), Version::new(1, 1, 0));
466 assert_eq!(parse_lcov_version("1.11").unwrap(), Version::new(1, 11, 0));
467 }
468}