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, SourceFiles},
8 anchors::find_anchors,
9 },
10};
11use alloy_primitives::{Address, Bytes, U256, map::HashMap};
12use clap::{Parser, ValueEnum, ValueHint};
13use eyre::Result;
14use foundry_cli::utils::{LoadConfig, STATIC_FUZZ_SEED};
15use foundry_common::{compile::ProjectCompiler, errors::convert_solar_errors};
16use foundry_compilers::{
17 Artifact, ArtifactId, Project, ProjectCompileOutput, ProjectPathsConfig,
18 artifacts::{CompactBytecode, CompactDeployedBytecode, SolcLanguage, sourcemap::SourceMap},
19 compilers::multi::MultiCompiler,
20};
21use foundry_config::Config;
22use foundry_evm::opts::EvmOpts;
23use foundry_evm_core::ic::IcPcMap;
24use rayon::prelude::*;
25use semver::{Version, VersionReq};
26use std::{
27 path::{Path, PathBuf},
28 sync::Arc,
29};
30
31foundry_config::impl_figment_convert!(CoverageArgs, test);
33
34#[derive(Parser)]
36pub struct CoverageArgs {
37 #[arg(long, value_enum, default_value = "summary")]
41 report: Vec<CoverageReportKind>,
42
43 #[arg(long, default_value = "1", value_parser = parse_lcov_version)]
52 lcov_version: Version,
53
54 #[arg(long)]
59 ir_minimum: bool,
60
61 #[arg(
65 long,
66 short,
67 value_hint = ValueHint::FilePath,
68 value_name = "PATH"
69 )]
70 report_file: Option<PathBuf>,
71
72 #[arg(long)]
74 include_libs: bool,
75
76 #[arg(long)]
78 exclude_tests: bool,
79
80 #[arg(skip)]
82 reporters: Vec<Box<dyn CoverageReporter>>,
83
84 #[command(flatten)]
85 test: TestArgs,
86}
87
88impl CoverageArgs {
89 pub async fn run(mut self) -> Result<()> {
90 let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
91
92 if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings {
94 config = self.load_config()?;
96 }
97
98 config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED));
100
101 let (paths, mut output) = {
102 let (project, output) = self.build(&config)?;
103 (project.paths, output)
104 };
105
106 self.populate_reporters(&paths.root);
107
108 sh_println!("Analysing contracts...")?;
109 let report = self.prepare(&paths, &mut output)?;
110
111 sh_println!("Running tests...")?;
112 self.collect(&paths.root, &output, report, Arc::new(config), evm_opts).await
113 }
114
115 fn populate_reporters(&mut self, root: &Path) {
116 self.reporters = self
117 .report
118 .iter()
119 .map(|report_kind| match report_kind {
120 CoverageReportKind::Summary => {
121 Box::<CoverageSummaryReporter>::default() as Box<dyn CoverageReporter>
122 }
123 CoverageReportKind::Lcov => {
124 let path =
125 root.join(self.report_file.as_deref().unwrap_or("lcov.info".as_ref()));
126 Box::new(LcovReporter::new(path, self.lcov_version.clone()))
127 }
128 CoverageReportKind::Bytecode => Box::new(BytecodeReporter::new(
129 root.to_path_buf(),
130 root.join("bytecode-coverage"),
131 )),
132 CoverageReportKind::Debug => Box::new(DebugReporter),
133 })
134 .collect::<Vec<_>>();
135 }
136
137 fn build(&self, config: &Config) -> Result<(Project, ProjectCompileOutput)> {
139 let mut project = config.ephemeral_project()?;
140
141 if self.ir_minimum {
142 sh_warn!(
144 "`--ir-minimum` enables `viaIR` with minimum optimization, \
145 which can result in inaccurate source mappings.\n\
146 Only use this flag as a workaround if you are experiencing \"stack too deep\" errors.\n\
147 Note that `viaIR` is production ready since Solidity 0.8.13 and above.\n\
148 See more: https://github.com/foundry-rs/foundry/issues/3357"
149 )?;
150
151 project.settings.solc.settings =
154 project.settings.solc.settings.with_via_ir_minimum_optimization();
155
156 let evm_version = project.settings.solc.evm_version;
159 let version = config.solc_version().unwrap_or_else(|| Version::new(0, 8, 4));
160 project.settings.solc.settings.sanitize(&version, SolcLanguage::Solidity);
161 project.settings.solc.evm_version = evm_version;
162 } else {
163 sh_warn!(
164 "optimizer settings and `viaIR` have been disabled for accurate coverage reports.\n\
165 If you encounter \"stack too deep\" errors, consider using `--ir-minimum` which \
166 enables `viaIR` with minimum optimization resolving most of the errors"
167 )?;
168
169 project.settings.solc.optimizer.disable();
170 project.settings.solc.optimizer.runs = None;
171 project.settings.solc.optimizer.details = None;
172 project.settings.solc.via_ir = None;
173 }
174
175 let output = ProjectCompiler::default()
176 .compile(&project)?
177 .with_stripped_file_prefixes(project.root());
178
179 Ok((project, output))
180 }
181
182 #[instrument(name = "Coverage::prepare", skip_all)]
184 fn prepare(
185 &self,
186 project_paths: &ProjectPathsConfig,
187 output: &mut ProjectCompileOutput,
188 ) -> Result<CoverageReport> {
189 let mut report = CoverageReport::default();
190
191 output.parser_mut().solc_mut().compiler_mut().enter_mut(|compiler| {
192 if compiler.gcx().stage() < Some(solar::config::CompilerStage::Lowering) {
193 let _ = compiler.lower_asts();
194 }
195 convert_solar_errors(compiler.dcx())
196 })?;
197 let output = &*output;
198
199 let mut versioned_sources = HashMap::<Version, SourceFiles>::default();
201 for (path, source_file, version) in output.output().sources.sources_with_version() {
202 report.add_source(version.clone(), source_file.id as usize, path.clone());
203
204 if (!self.include_libs && project_paths.has_library_ancestor(path))
206 || (self.exclude_tests && project_paths.is_test(path))
207 {
208 continue;
209 }
210
211 let path = project_paths.root.join(path);
212 versioned_sources
213 .entry(version.clone())
214 .or_default()
215 .sources
216 .insert(source_file.id, path);
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, output)?;
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 #[instrument(name = "Coverage::collect", skip_all)]
256 async fn collect(
257 mut self,
258 root: &Path,
259 output: &ProjectCompileOutput,
260 mut report: CoverageReport,
261 config: Arc<Config>,
262 evm_opts: EvmOpts,
263 ) -> Result<()> {
264 let verbosity = evm_opts.verbosity;
265
266 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
278 let filter = self.test.filter(&config)?;
279 let outcome = self.test.run_tests(runner, config, verbosity, &filter, output).await?;
280
281 outcome.ensure_ok(false)?;
282
283 let data = outcome.results.iter().flat_map(|(_, suite)| {
285 let mut hits = Vec::new();
286 for result in suite.test_results.values() {
287 let Some(hit_maps) = result.line_coverage.as_ref() else { continue };
288 for map in hit_maps.0.values() {
289 if let Some((id, _)) = known_contracts.find_by_deployed_code(map.bytecode()) {
290 hits.push((id, map, true));
291 } else if let Some((id, _)) =
292 known_contracts.find_by_creation_code(map.bytecode())
293 {
294 hits.push((id, map, false));
295 }
296 }
297 }
298 hits
299 });
300
301 for (artifact_id, map, is_deployed_code) in data {
302 if let Some(source_id) =
303 report.get_source_id(artifact_id.version.clone(), artifact_id.source.clone())
304 {
305 report.add_hit_map(
306 &ContractId {
307 version: artifact_id.version.clone(),
308 source_id,
309 contract_name: artifact_id.name.as_str().into(),
310 },
311 map,
312 is_deployed_code,
313 )?;
314 }
315 }
316
317 if let Some(not_re) = &filter.args().coverage_pattern_inverse {
319 let file_root = filter.paths().root.as_path();
320 report.retain_sources(|path: &Path| {
321 let path = path.strip_prefix(file_root).unwrap_or(path);
322 !not_re.is_match(&path.to_string_lossy())
323 });
324 }
325
326 self.report(&report)?;
328
329 Ok(())
330 }
331
332 #[instrument(name = "Coverage::report", skip_all)]
333 fn report(&mut self, report: &CoverageReport) -> Result<()> {
334 for reporter in &mut self.reporters {
335 let _guard = debug_span!("reporter.report", kind=%reporter.name()).entered();
336 reporter.report(report)?;
337 }
338 Ok(())
339 }
340
341 pub fn is_watch(&self) -> bool {
342 self.test.is_watch()
343 }
344
345 pub fn watch(&self) -> &WatchArgs {
346 &self.test.watch
347 }
348}
349
350#[derive(Clone, Debug, Default, ValueEnum)]
352pub enum CoverageReportKind {
353 #[default]
354 Summary,
355 Lcov,
356 Debug,
357 Bytecode,
358}
359
360fn dummy_link_bytecode(mut obj: CompactBytecode) -> Option<Bytes> {
364 let link_references = obj.link_references.clone();
365 for (file, libraries) in link_references {
366 for library in libraries.keys() {
367 obj.link(&file, library, Address::ZERO);
368 }
369 }
370
371 obj.object.resolve();
372 obj.object.into_bytes()
373}
374
375fn dummy_link_deployed_bytecode(obj: CompactDeployedBytecode) -> Option<Bytes> {
379 obj.bytecode.and_then(dummy_link_bytecode)
380}
381
382pub struct ArtifactData {
383 pub contract_id: ContractId,
384 pub creation: BytecodeData,
385 pub deployed: BytecodeData,
386}
387
388impl ArtifactData {
389 pub fn new(id: &ArtifactId, source_id: usize, artifact: &impl Artifact) -> Option<Self> {
390 Some(Self {
391 contract_id: ContractId {
392 version: id.version.clone(),
393 source_id,
394 contract_name: id.name.as_str().into(),
395 },
396 creation: BytecodeData::new(
397 artifact.get_source_map()?.ok()?,
398 artifact
399 .get_bytecode()
400 .and_then(|bytecode| dummy_link_bytecode(bytecode.into_owned()))?,
401 ),
402 deployed: BytecodeData::new(
403 artifact.get_source_map_deployed()?.ok()?,
404 artifact
405 .get_deployed_bytecode()
406 .and_then(|bytecode| dummy_link_deployed_bytecode(bytecode.into_owned()))?,
407 ),
408 })
409 }
410}
411
412pub struct BytecodeData {
413 source_map: SourceMap,
414 bytecode: Bytes,
415 ic_pc_map: IcPcMap,
423}
424
425impl BytecodeData {
426 fn new(source_map: SourceMap, bytecode: Bytes) -> Self {
427 let ic_pc_map = IcPcMap::new(&bytecode);
428 Self { source_map, bytecode, ic_pc_map }
429 }
430
431 pub fn find_anchors(&self, source_analysis: &SourceAnalysis) -> Vec<ItemAnchor> {
432 find_anchors(&self.bytecode, &self.source_map, &self.ic_pc_map, source_analysis)
433 }
434}
435
436fn parse_lcov_version(s: &str) -> Result<Version, String> {
437 let vr = VersionReq::parse(&format!("={s}")).map_err(|e| e.to_string())?;
438 let [c] = &vr.comparators[..] else {
439 return Err("invalid version".to_string());
440 };
441 if c.op != semver::Op::Exact {
442 return Err("invalid version".to_string());
443 }
444 if !c.pre.is_empty() {
445 return Err("pre-releases are not supported".to_string());
446 }
447 Ok(Version::new(c.major, c.minor.unwrap_or(0), c.patch.unwrap_or(0)))
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453
454 #[test]
455 fn lcov_version() {
456 assert_eq!(parse_lcov_version("0").unwrap(), Version::new(0, 0, 0));
457 assert_eq!(parse_lcov_version("1").unwrap(), Version::new(1, 0, 0));
458 assert_eq!(parse_lcov_version("1.0").unwrap(), Version::new(1, 0, 0));
459 assert_eq!(parse_lcov_version("1.1").unwrap(), Version::new(1, 1, 0));
460 assert_eq!(parse_lcov_version("1.11").unwrap(), Version::new(1, 11, 0));
461 }
462}