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