1use super::{
2 test::{ProjectPathsAwareFilter, TestArgs, TestExecutionOptions},
3 watch::WatchArgs,
4};
5use crate::coverage::{
6 BytecodeReporter, ContractId, CoverageAttributionReporter, CoverageReport, CoverageReporter,
7 CoverageSummaryReporter, DebugReporter, GapsReporter, ItemAnchors, LcovReporter,
8 ResolvedHitMap, ResolvedHitMaps,
9 analysis::{SourceAnalysis, SourceFiles},
10 anchors::{find_anchors, find_execution_anchors},
11};
12use alloy_json_abi::StateMutability;
13use alloy_primitives::{
14 Address, B256, Bytes, U256, keccak256,
15 map::{HashMap, HashSet},
16};
17use clap::{Parser, ValueHint};
18use eyre::Result;
19use foundry_cli::utils::{FoundryPathExt, LoadConfig, STATIC_FUZZ_SEED};
20use foundry_common::{
21 ContractsByArtifactBuilder, TestFilter, compile::ProjectCompiler, errors::convert_solar_errors,
22 version::SHORT_VERSION,
23};
24use foundry_compilers::{
25 Artifact, ArtifactId, Graph, Project, ProjectCompileOutput, ProjectPathsConfig,
26 VYPER_EXTENSIONS,
27 artifacts::{CompactBytecode, CompactDeployedBytecode, Source, sourcemap::SourceMap},
28 cache::SOLIDITY_FILES_CACHE_FILENAME,
29 compilers::{
30 Language,
31 multi::{MultiCompilerLanguage, MultiCompilerParser},
32 },
33 error::SolcError,
34 utils::source_files_iter,
35};
36use foundry_config::{
37 Config, CoverageConfig, CoverageReportKind, InlineConfig, parse_lcov_version,
38};
39use foundry_evm::{core::ic::IcPcMap, opts::EvmOpts};
40use globset::{Glob, GlobSetBuilder};
41use rayon::prelude::*;
42use semver::Version;
43use std::{
44 collections::{BTreeMap, BTreeSet},
45 fs,
46 path::{Path, PathBuf},
47 sync::Arc,
48};
49
50foundry_config::impl_figment_convert!(CoverageArgs, test);
52
53#[derive(Parser)]
59#[command(after_long_help = r#"Source attribution:
60 Coverage follows compiler source maps. Inherited modifier code is reported under the
61 source where the modifier is declared. Dependency sources are excluded by default;
62 use `--include-libs` to include their coverage.
63
64Compatibility:
65 `forge coverage` supports test filters and `--watch`, but not test-only output or
66 execution modes such as `--json`, `--junit`, `--list`, `--debug`, flame profiles,
67 symbolic artifact replay, showmap replay, brutalization, or mutation testing. Use
68 `--report lcov` for interoperable coverage data, `--report attribution` for
69 Foundry's per-test JSON attribution report, or `--report gaps` for a JSON list of
70 uncovered source items."#)]
71pub struct CoverageArgs {
72 #[arg(long, value_enum)]
78 report: Vec<CoverageReportKind>,
79
80 #[arg(long = "lcov-version", value_parser = parse_lcov_version)]
92 lcov_version_cli: Option<Version>,
93
94 #[arg(skip = Version::new(1, 0, 0))]
96 lcov_version: Version,
97
98 #[arg(long)]
103 ir_minimum: bool,
104
105 #[arg(
110 long,
111 value_hint = ValueHint::FilePath,
112 value_name = "PATH"
113 )]
114 report_file: Option<PathBuf>,
115
116 #[arg(long)]
118 include_libs: bool,
119
120 #[arg(long)]
122 exclude_tests: bool,
123
124 #[arg(skip)]
126 reporters: Vec<Box<dyn CoverageReporter>>,
127
128 #[arg(skip)]
132 skip_files: Vec<String>,
133
134 #[command(flatten)]
135 test: TestArgs,
136}
137
138impl CoverageArgs {
139 fn report_path(&self, root: &Path, default_file_name: &str) -> PathBuf {
140 let report_file =
141 (self.file_report_count() == 1).then_some(self.report_file.as_deref()).flatten();
142 root.join(report_file.unwrap_or_else(|| Path::new(default_file_name)))
143 }
144
145 fn file_report_count(&self) -> usize {
146 let has_lcov = self.report.iter().any(|kind| matches!(kind, CoverageReportKind::Lcov));
147 let has_attribution =
148 self.report.iter().any(|kind| matches!(kind, CoverageReportKind::Attribution));
149 let has_gaps = self.report.iter().any(|kind| matches!(kind, CoverageReportKind::Gaps));
150 usize::from(has_lcov) + usize::from(has_attribution) + usize::from(has_gaps)
151 }
152
153 pub(crate) fn ensure_mode_compatible(&self) -> Result<()> {
154 self.test.ensure_coverage_mode_compatible()
155 }
156
157 pub async fn run(mut self) -> Result<()> {
158 self.ensure_mode_compatible()?;
159
160 let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
161
162 self.install_missing_dependencies(&mut config)?;
164
165 if config.fuzz.seed.is_none() {
168 config.fuzz.seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED));
169 }
170
171 self.resolve_with(&config.coverage);
174 let filter = self.test.filter(&config)?;
175
176 let (paths, mut output) = {
177 let (project, output) = self.build(&config, &filter)?;
178 (project.paths, output)
179 };
180
181 if self.report_file.is_some() && self.file_report_count() > 1 {
182 sh_warn!(
183 "`--report-file` is ignored when multiple file reports are requested; \
184 each report will use its default output path"
185 )?;
186 }
187
188 self.populate_reporters(&paths.root);
189
190 sh_println!("Analysing contracts...")?;
191 let report = self.prepare(&paths, &mut output)?;
192
193 sh_println!("Running tests...")?;
194 self.collect(&paths.root, &output, report, config, evm_opts, filter).await
195 }
196
197 fn resolve_with(&mut self, config: &CoverageConfig) {
206 if self.report.is_empty() {
207 self.report.clone_from(&config.report);
208 }
209 self.lcov_version =
210 self.lcov_version_cli.clone().unwrap_or_else(|| config.lcov_version.clone());
211 if !self.ir_minimum {
212 self.ir_minimum = config.ir_minimum;
213 }
214 if self.report_file.is_none() {
215 self.report_file.clone_from(&config.report_file);
216 }
217 if !self.include_libs {
218 self.include_libs = config.include_libs;
219 }
220 if !self.exclude_tests {
221 self.exclude_tests = config.exclude_tests;
222 }
223 self.skip_files.clone_from(&config.skip_files);
226 }
227
228 fn populate_reporters(&mut self, root: &Path) {
229 self.reporters = self
230 .report
231 .iter()
232 .filter_map(|report_kind| match report_kind {
233 CoverageReportKind::Summary => {
234 Some(Box::<CoverageSummaryReporter>::default() as Box<dyn CoverageReporter>)
235 }
236 CoverageReportKind::Lcov => {
237 let path = self.report_path(root, "lcov.info");
238 Some(Box::new(LcovReporter::new(path, self.lcov_version.clone())))
239 }
240 CoverageReportKind::Bytecode => Some(Box::new(BytecodeReporter::new(
241 root.to_path_buf(),
242 root.join("bytecode-coverage"),
243 ))),
244 CoverageReportKind::Debug => Some(Box::new(DebugReporter)),
245 CoverageReportKind::Attribution => None,
246 CoverageReportKind::Gaps => {
247 let path = self.report_path(root, "coverage-gaps.json");
248 Some(Box::new(GapsReporter::new(root.to_path_buf(), path)))
249 }
250 })
251 .collect::<Vec<_>>();
252 }
253
254 fn build(
256 &self,
257 config: &Config,
258 filter: &ProjectPathsAwareFilter,
259 ) -> Result<(Project, ProjectCompileOutput)> {
260 let mut project = config.ephemeral_project()?;
261 let cache = (config.cache && !config.deny.warnings())
264 .then(|| config.coverage_cache_path())
265 .flatten()
266 .and_then(|path| {
267 fs::create_dir_all(path.parent()?).ok()?;
268 let lock = Config::lock_coverage_cache(&path).ok()?;
269 match fs::create_dir(&path) {
270 Ok(()) => {
271 if fs::File::create_new(path.join(Config::COVERAGE_CACHE_MARKER)).is_err() {
272 let _ = fs::remove_dir(&path);
273 return None;
274 }
275 }
276 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
277 if !path.join(Config::COVERAGE_CACHE_MARKER).is_file() {
278 return None;
279 }
280 }
281 Err(_) => return None,
282 }
283 Some((path, lock))
284 });
285 let original_paths = project.paths.clone();
286 if let Some((path, _)) = &cache {
287 project.cached = true;
288 project.no_artifacts = false;
289 project.paths.cache = path.join(SOLIDITY_FILES_CACHE_FILENAME);
290 project.paths.artifacts = path.join("artifacts");
291 project.paths.build_infos = path.join("build-info");
292 project.paths.slash_paths();
293 }
294
295 if self.ir_minimum {
296 sh_warn!(
297 "`--ir-minimum` enables `viaIR` with minimum optimization, \
298 which can result in inaccurate source mappings.\n\
299 Only use this flag as a workaround if you are experiencing \"stack too deep\" errors.\n\
300 Note that `viaIR` is production ready since Solidity 0.8.13 and above.\n\
301 See more: https://book.getfoundry.sh/guides/best-practices/stack-too-deep"
302 )?;
303 } else {
304 sh_warn!(
305 "optimizer settings and `viaIR` have been disabled for accurate coverage reports.\n\
306 If you encounter \"stack too deep\" errors, consider using `--ir-minimum` which \
307 enables `viaIR` with minimum optimization resolving most of the errors.\n\
308 See more: https://book.getfoundry.sh/guides/best-practices/stack-too-deep"
309 )?;
310 }
311
312 config.disable_optimizations(&mut project, self.ir_minimum);
313
314 let files = (filter.args().path_pattern.is_some()
315 || filter.args().path_pattern_inverse.is_some())
316 .then(|| {
317 source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
318 .chain(
319 source_files_iter(&config.test, MultiCompilerLanguage::FILE_EXTENSIONS)
320 .filter(|path| !path.is_sol_test() || filter.matches_path(path)),
323 )
324 .chain(source_files_iter(&config.script, MultiCompilerLanguage::FILE_EXTENSIONS))
326 .collect::<BTreeSet<_>>()
327 });
328 let compile = |project: &Project| {
329 let mut compiler = ProjectCompiler::new()
330 .allow_linked_libraries(config.allow_linked_libraries)
331 .external_compilers(config)
332 .external_artifacts(false)
333 .dynamic_test_linking(config.dynamic_test_linking);
334 if let Some(files) = &files {
335 compiler = compiler.files(files.iter().cloned());
336 }
337 compiler.compile(project)
338 };
339 let cached_output = (|| -> Result<Option<ProjectCompileOutput>> {
343 let Some((path, _)) = &cache else { return Ok(None) };
344 let mut sources = if let Some(files) = &files
345 && !files.is_empty()
346 {
347 Source::read_all(files)?
348 } else {
349 project.paths.read_input_files()?
350 };
351 if let Some(filter) = &project.sparse_output {
352 sources.retain(|path, _| filter.is_match(path));
353 }
354 let graph = Graph::<MultiCompilerParser>::resolve_sources(&project.paths, sources)?;
355 let resolved = graph.into_sources_by_version(&project)?;
356 let mut jobs = resolved
357 .sources
358 .iter()
359 .flat_map(|(language, jobs)| {
360 jobs.iter().map(move |(version, sources, (profile, settings))| {
361 let sources = sources
362 .iter()
363 .map(|(path, source)| {
364 (path, source.content_hash(), source.kind.is_dirty())
365 })
366 .collect::<Vec<_>>();
367 serde_json::to_value((language, version, profile, settings, sources))
368 })
369 })
370 .collect::<Result<Vec<_>, _>>()?;
371 jobs.sort_unstable_by_key(serde_json::Value::to_string);
372 let primary_profiles = resolved.primary_profiles.iter().collect::<BTreeMap<_, _>>();
373 let mut identity = serde_json::to_value((
374 SHORT_VERSION,
375 &project.paths,
376 (&config.solc, &config.vyper, &config.extra_args),
377 (&config.extra_output, &config.extra_output_files),
378 config.dynamic_test_linking,
379 jobs,
380 primary_profiles,
381 ))?;
382 identity.sort_all_objects();
383 let fingerprint = keccak256(serde_json::to_vec(&identity)?).to_string();
384 let marker = path.join(Config::COVERAGE_CACHE_MARKER);
385 if fs::read_to_string(&marker).ok().as_deref() != Some(&fingerprint) {
386 fs::write(&marker, "")?;
388 for entry in fs::read_dir(path)? {
389 let entry = entry?;
390 if entry.file_name() == Config::COVERAGE_CACHE_MARKER {
391 continue;
392 }
393 if entry.file_type()?.is_dir() {
394 fs::remove_dir_all(entry.path())?;
395 } else {
396 fs::remove_file(entry.path())?;
397 }
398 }
399 }
400 let output = compile(&project)?;
401 let mapped_sources = output
404 .builds()
405 .flat_map(|(_, build)| build.source_id_to_path.values().map(PathBuf::as_path))
406 .collect::<HashSet<_>>();
407 if output
408 .graph()
409 .files()
410 .any(|idx| !mapped_sources.contains(output.graph().node_path(idx)))
411 {
412 return Ok(None);
413 }
414 if let Err(err) = fs::write(marker, fingerprint) {
416 debug!(%err, "failed to publish coverage cache fingerprint");
417 }
418 Ok(Some(output))
419 })();
420 let output = match cached_output {
421 Ok(Some(output)) => output,
422 Err(err)
423 if !err.chain().any(|cause| {
424 cause.is::<std::io::Error>()
425 || matches!(cause.downcast_ref(), Some(SolcError::Io(_)))
426 }) =>
427 {
428 return Err(err);
429 }
430 result => {
431 if let Err(err) = result {
432 debug!(%err, "coverage cache unavailable; compiling without persistence");
433 }
434 project.cached = false;
435 project.no_artifacts = true;
436 project.paths = original_paths;
437 compile(&project)?
438 }
439 };
440 let output = output.with_stripped_file_prefixes(project.root());
441
442 Ok((project, output))
443 }
444
445 #[instrument(name = "Coverage::prepare", skip_all)]
447 fn prepare(
448 &self,
449 project_paths: &ProjectPathsConfig,
450 output: &mut ProjectCompileOutput,
451 ) -> Result<CoverageReport> {
452 let mut report = CoverageReport::default();
453
454 output.parser_mut().solc_mut().compiler_mut().enter_mut(|compiler| {
455 if compiler.gcx().stage() < Some(solar::config::CompilerStage::Lowering) {
456 let _ = compiler.lower_asts();
457 }
458 convert_solar_errors(compiler.dcx())
459 })?;
460 let output = &*output;
461
462 let mut sources_by_build = HashMap::<String, SourceFiles>::default();
464 for (build_id, build) in output.builds() {
465 for (source_id, path) in &build.source_id_to_path {
466 if output.graph().get_parsed_source(path).is_none()
467 || path
468 .extension()
469 .and_then(|s| s.to_str())
470 .is_some_and(|ext| VYPER_EXTENSIONS.contains(&ext))
471 {
472 continue;
473 }
474 let path = path.strip_prefix(&project_paths.root).unwrap_or(path);
475 report.add_source(build_id.clone(), *source_id as usize, path.to_path_buf());
476
477 if (!self.include_libs && project_paths.has_library_ancestor(path))
478 || (self.exclude_tests && project_paths.is_test(path))
479 {
480 continue;
481 }
482
483 sources_by_build
484 .entry(build_id.clone())
485 .or_default()
486 .sources
487 .insert(*source_id, project_paths.root.join(path));
488 }
489 }
490
491 let artifacts: Vec<ArtifactData> = output
493 .artifact_ids()
494 .par_bridge() .filter_map(|(id, artifact)| {
496 let source_id = report.get_source_id(&id.build_id, &id.source)?;
497 ArtifactData::new(&id, source_id, artifact)
498 })
499 .collect();
500
501 for (build_id, sources) in &sources_by_build {
503 let source_analysis = SourceAnalysis::new(sources, output)?;
504 let anchors = artifacts
505 .par_iter()
506 .filter(|artifact| artifact.contract_id.build_id == *build_id)
507 .map(|artifact| {
508 let creation_code_anchors = artifact.creation.find_anchors(&source_analysis);
509 let deployed_code_anchors = artifact.deployed.find_anchors(&source_analysis);
510 (artifact.contract_id.clone(), (creation_code_anchors, deployed_code_anchors))
511 })
512 .collect_vec_list();
513 report.add_anchors(anchors.into_iter().flatten());
514 for artifact in
515 artifacts.iter().filter(|artifact| artifact.contract_id.build_id == *build_id)
516 {
517 let execution_anchors = find_execution_anchors(
518 artifact.contract_id.source_id as u32,
519 &artifact.contract_id.contract_name,
520 &source_analysis,
521 );
522 report.add_execution_anchors(
523 artifact.contract_id.clone(),
524 execution_anchors,
525 artifact.function_selectors.iter().copied(),
526 artifact.has_receive,
527 artifact.fallback_payable,
528 );
529 }
530 report.add_analysis(build_id.clone(), source_analysis);
531 }
532
533 if self.reporters.iter().any(|reporter| reporter.needs_source_maps()) {
534 report.add_source_maps(artifacts.into_iter().map(|artifact| {
535 (artifact.contract_id, (artifact.creation.source_map, artifact.deployed.source_map))
536 }));
537 }
538
539 Ok(report)
540 }
541
542 #[instrument(name = "Coverage::collect", skip_all)]
544 async fn collect(
545 mut self,
546 project_root: &Path,
547 output: &ProjectCompileOutput,
548 mut report: CoverageReport,
549 config: Config,
550 evm_opts: EvmOpts,
551 filter: ProjectPathsAwareFilter,
552 ) -> Result<()> {
553 let inline_config = Arc::new(InlineConfig::new_parsed(output, &config)?);
554 let outcome = self
555 .test
556 .run_tests(
557 project_root,
558 config,
559 evm_opts,
560 output,
561 &filter,
562 TestExecutionOptions::coverage(inline_config),
563 )
564 .await?;
565
566 let known_contracts = outcome.known_contracts.as_ref().unwrap();
567 let runtime_contracts = ContractsByArtifactBuilder::new(
570 output.artifact_ids().map(|(id, artifact)| (id, artifact.into())),
571 )
572 .build();
573 let mut resolved_hit_maps = ResolvedHitMaps::default();
574 let mut unresolved_hit_maps = HashSet::<B256>::default();
575
576 for suite in outcome.results.values() {
578 for result in suite.test_results.values() {
579 let Some(hit_maps) = result.line_coverage.as_ref() else { continue };
580
581 for (code_hash, map) in &hit_maps.0 {
582 if let Some(resolved) = resolved_hit_maps.get(code_hash) {
583 report.add_hit_map(
584 &resolved.contract_id,
585 map,
586 resolved.is_deployed_code,
587 )?;
588 continue;
589 }
590 if unresolved_hit_maps.contains(code_hash) {
591 continue;
592 }
593
594 let Some((artifact_id, is_deployed_code)) = runtime_contracts
598 .find_by_deployed_code_exact(map.bytecode())
599 .map(|(id, _)| (id, true))
600 .or_else(|| {
601 known_contracts
602 .iter()
603 .find(|(_, contract)| {
604 contract
605 .bytecode()
606 .is_some_and(|code| map.bytecode().starts_with(code))
607 })
608 .map(|(id, _)| (id, false))
609 })
610 else {
611 if let Some((similar, kind)) = known_contracts
612 .find_by_deployed_code(map.bytecode())
613 .map(|(id, _)| (id, "runtime"))
614 .or_else(|| {
615 known_contracts
616 .find_by_creation_code(map.bytecode())
617 .map(|(id, _)| (id, "creation"))
618 })
619 {
620 sh_warn!(
621 "excluding executed {kind} code from coverage: it resembles `{}` but does not match it exactly",
622 similar.identifier()
623 )?;
624 }
625 unresolved_hit_maps.insert(*code_hash);
626 continue;
627 };
628
629 let Some(source_id) =
630 report.get_source_id(&artifact_id.build_id, &artifact_id.source)
631 else {
632 continue;
633 };
634 let contract_id = ContractId {
635 version: artifact_id.version.clone(),
636 build_id: artifact_id.build_id.clone(),
637 source_id,
638 contract_name: artifact_id.name.as_str().into(),
639 };
640
641 report.add_hit_map(&contract_id, map, is_deployed_code)?;
642
643 resolved_hit_maps
644 .entry(*code_hash)
645 .or_insert(ResolvedHitMap { contract_id, is_deployed_code });
646 }
647 }
648 }
649
650 let file_root = filter.paths().root.as_path();
652 if let Some(not_re) = &filter.args().coverage_pattern_inverse {
653 report.retain_sources(|path: &Path| {
654 let path = path.strip_prefix(file_root).unwrap_or(path);
655 !not_re.is_match(&path.to_string_lossy())
656 });
657 }
658 if !self.skip_files.is_empty() {
659 let mut builder = GlobSetBuilder::new();
660 for pattern in &self.skip_files {
661 let glob = Glob::new(pattern).map_err(|e| {
662 eyre::eyre!("invalid glob in coverage.skip_files: '{pattern}': {e}")
663 })?;
664 builder.add(glob);
665 }
666 let set = builder
667 .build()
668 .map_err(|e| eyre::eyre!("failed to build coverage.skip_files glob set: {e}"))?;
669 report.retain_sources(|path: &Path| {
670 let path = path.strip_prefix(file_root).unwrap_or(path);
671 !set.is_match(path)
672 });
673 }
674
675 self.report(&report)?;
677
678 if self.report.iter().any(|kind| matches!(kind, CoverageReportKind::Attribution)) {
679 let reporter = CoverageAttributionReporter::new(
680 self.report_path(project_root, "coverage-attribution.json"),
681 );
682 reporter.report(&report, &outcome, &resolved_hit_maps)?;
683 }
684
685 outcome.ensure_ok(false)?;
688
689 Ok(())
690 }
691
692 #[instrument(name = "Coverage::report", skip_all)]
693 fn report(&mut self, report: &CoverageReport) -> Result<()> {
694 for reporter in &mut self.reporters {
695 let _guard = debug_span!("reporter.report", kind=%reporter.name()).entered();
696 reporter.report(report)?;
697 }
698 Ok(())
699 }
700
701 pub const fn is_watch(&self) -> bool {
702 self.test.is_watch()
703 }
704
705 pub const fn watch(&self) -> &WatchArgs {
706 &self.test.watch
707 }
708}
709
710fn dummy_link_bytecode(mut obj: CompactBytecode) -> Option<Bytes> {
714 let link_references = obj.link_references.clone();
715 for (file, libraries) in link_references {
716 for library in libraries.keys() {
717 obj.link(&file, library, Address::ZERO);
718 }
719 }
720
721 obj.object.resolve();
722 obj.object.into_bytes()
723}
724
725fn dummy_link_deployed_bytecode(obj: CompactDeployedBytecode) -> Option<Bytes> {
729 obj.bytecode.and_then(dummy_link_bytecode)
730}
731
732pub struct ArtifactData {
733 pub contract_id: ContractId,
734 pub creation: BytecodeData,
735 pub deployed: BytecodeData,
736 pub function_selectors: Vec<[u8; 4]>,
737 pub has_receive: bool,
738 pub fallback_payable: bool,
739}
740
741impl ArtifactData {
742 pub fn new(id: &ArtifactId, source_id: usize, artifact: &impl Artifact) -> Option<Self> {
743 let abi = artifact.get_abi();
744 let function_selectors = abi
745 .as_ref()
746 .map(|abi| abi.functions().map(|function| function.selector().into()).collect())
747 .unwrap_or_default();
748 let has_receive = abi.as_ref().is_some_and(|abi| abi.receive.is_some());
749 let fallback_payable = abi
750 .as_ref()
751 .and_then(|abi| abi.fallback)
752 .is_some_and(|fallback| fallback.state_mutability == StateMutability::Payable);
753 Some(Self {
754 contract_id: ContractId {
755 version: id.version.clone(),
756 build_id: id.build_id.clone(),
757 source_id,
758 contract_name: id.name.as_str().into(),
759 },
760 creation: BytecodeData::new(
761 artifact.get_source_map()?.ok()?,
762 artifact
763 .get_bytecode()
764 .and_then(|bytecode| dummy_link_bytecode(bytecode.into_owned()))?,
765 ),
766 deployed: BytecodeData::new(
767 artifact.get_source_map_deployed()?.ok()?,
768 artifact
769 .get_deployed_bytecode()
770 .and_then(|bytecode| dummy_link_deployed_bytecode(bytecode.into_owned()))?,
771 ),
772 function_selectors,
773 has_receive,
774 fallback_payable,
775 })
776 }
777}
778
779pub struct BytecodeData {
780 source_map: SourceMap,
781 bytecode: Bytes,
782 ic_pc_map: IcPcMap,
790}
791
792impl BytecodeData {
793 fn new(source_map: SourceMap, bytecode: Bytes) -> Self {
794 let ic_pc_map = IcPcMap::new(&bytecode);
795 Self { source_map, bytecode, ic_pc_map }
796 }
797
798 pub fn find_anchors(&self, source_analysis: &SourceAnalysis) -> ItemAnchors {
799 find_anchors(&self.bytecode, &self.source_map, &self.ic_pc_map, source_analysis)
800 }
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806
807 #[test]
808 fn lcov_version() {
809 assert_eq!(parse_lcov_version("0").unwrap(), Version::new(0, 0, 0));
810 assert_eq!(parse_lcov_version("1").unwrap(), Version::new(1, 0, 0));
811 assert_eq!(parse_lcov_version("1.0").unwrap(), Version::new(1, 0, 0));
812 assert_eq!(parse_lcov_version("1.1").unwrap(), Version::new(1, 1, 0));
813 assert_eq!(parse_lcov_version("1.11").unwrap(), Version::new(1, 11, 0));
814 }
815
816 #[test]
817 fn resolve_lcov_version_uses_config_when_cli_absent() {
818 let mut args = CoverageArgs::parse_from(["coverage"]);
819 let config = CoverageConfig { lcov_version: Version::new(2, 2, 0), ..Default::default() };
820
821 args.resolve_with(&config);
822
823 assert_eq!(args.lcov_version, Version::new(2, 2, 0));
824 }
825
826 #[test]
827 fn resolve_lcov_version_keeps_explicit_cli_default() {
828 let mut args = CoverageArgs::parse_from(["coverage", "--lcov-version", "1"]);
829 let config = CoverageConfig { lcov_version: Version::new(2, 2, 0), ..Default::default() };
830
831 args.resolve_with(&config);
832
833 assert_eq!(args.lcov_version, Version::new(1, 0, 0));
834 }
835
836 #[test]
837 fn resolve_lcov_version_keeps_explicit_cli_value() {
838 let mut args = CoverageArgs::parse_from(["coverage", "--lcov-version", "2"]);
839 let config = CoverageConfig { lcov_version: Version::new(2, 2, 0), ..Default::default() };
840
841 args.resolve_with(&config);
842
843 assert_eq!(args.lcov_version, Version::new(2, 0, 0));
844 }
845}