#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#[macro_use]
extern crate foundry_common;
#[macro_use]
extern crate tracing;
use alloy_primitives::{map::HashMap, Bytes, B256};
use eyre::{Context, Result};
use foundry_compilers::artifacts::sourcemap::SourceMap;
use semver::Version;
use std::{
collections::BTreeMap,
fmt::Display,
ops::{AddAssign, Deref, DerefMut},
path::{Path, PathBuf},
sync::Arc,
};
pub mod analysis;
pub mod anchors;
mod inspector;
pub use inspector::CoverageCollector;
#[derive(Clone, Debug, Default)]
pub struct CoverageReport {
pub source_paths: HashMap<(Version, usize), PathBuf>,
pub source_paths_to_ids: HashMap<(Version, PathBuf), usize>,
pub items: HashMap<Version, Vec<CoverageItem>>,
pub anchors: HashMap<ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>)>,
pub bytecode_hits: HashMap<ContractId, HitMap>,
pub source_maps: HashMap<ContractId, (SourceMap, SourceMap)>,
}
impl CoverageReport {
pub fn add_source(&mut self, version: Version, source_id: usize, path: PathBuf) {
self.source_paths.insert((version.clone(), source_id), path.clone());
self.source_paths_to_ids.insert((version, path), source_id);
}
pub fn get_source_id(&self, version: Version, path: PathBuf) -> Option<usize> {
self.source_paths_to_ids.get(&(version, path)).copied()
}
pub fn add_source_maps(
&mut self,
source_maps: impl IntoIterator<Item = (ContractId, (SourceMap, SourceMap))>,
) {
self.source_maps.extend(source_maps);
}
pub fn add_items(&mut self, version: Version, items: impl IntoIterator<Item = CoverageItem>) {
self.items.entry(version).or_default().extend(items);
}
pub fn add_anchors(
&mut self,
anchors: impl IntoIterator<Item = (ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>))>,
) {
self.anchors.extend(anchors);
}
pub fn summary_by_file(&self) -> impl Iterator<Item = (PathBuf, CoverageSummary)> {
let mut summaries = BTreeMap::new();
for (version, items) in self.items.iter() {
for item in items {
let Some(path) =
self.source_paths.get(&(version.clone(), item.loc.source_id)).cloned()
else {
continue;
};
*summaries.entry(path).or_default() += item;
}
}
summaries.into_iter()
}
pub fn items_by_source(&self) -> impl Iterator<Item = (PathBuf, Vec<CoverageItem>)> {
let mut items_by_source: BTreeMap<_, Vec<_>> = BTreeMap::new();
for (version, items) in self.items.iter() {
for item in items {
let Some(path) =
self.source_paths.get(&(version.clone(), item.loc.source_id)).cloned()
else {
continue;
};
items_by_source.entry(path).or_default().push(item.clone());
}
}
items_by_source.into_iter()
}
pub fn add_hit_map(
&mut self,
contract_id: &ContractId,
hit_map: &HitMap,
is_deployed_code: bool,
) -> Result<()> {
let e = self
.bytecode_hits
.entry(contract_id.clone())
.or_insert_with(|| HitMap::new(hit_map.bytecode.clone()));
e.merge(hit_map).wrap_err_with(|| format!("{contract_id:?}"))?;
if let Some(anchors) = self.anchors.get(contract_id) {
let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
for anchor in anchors {
if let Some(&hits) = hit_map.hits.get(&anchor.instruction) {
self.items
.get_mut(&contract_id.version)
.and_then(|items| items.get_mut(anchor.item_id))
.expect("Anchor refers to non-existent coverage item")
.hits += hits;
}
}
}
Ok(())
}
pub fn filter_out_ignored_sources(&mut self, filter: impl Fn(&Path) -> bool) {
self.items.retain(|version, items| {
items.retain(|item| {
self.source_paths
.get(&(version.clone(), item.loc.source_id))
.map(|path| filter(path))
.unwrap_or(false)
});
!items.is_empty()
});
}
}
#[derive(Clone, Debug, Default)]
pub struct HitMaps(pub HashMap<B256, HitMap>);
impl HitMaps {
pub fn merge_opt(a: &mut Option<Self>, b: Option<Self>) {
match (a, b) {
(_, None) => {}
(a @ None, Some(b)) => *a = Some(b),
(Some(a), Some(b)) => a.merge(b),
}
}
pub fn merge(&mut self, other: Self) {
for (code_hash, hit_map) in other.0 {
if let Some(HitMap { hits: extra_hits, .. }) = self.insert(code_hash, hit_map) {
for (pc, hits) in extra_hits {
self.entry(code_hash)
.and_modify(|map| *map.hits.entry(pc).or_default() += hits);
}
}
}
}
pub fn merged(mut self, other: Self) -> Self {
self.merge(other);
self
}
}
impl Deref for HitMaps {
type Target = HashMap<B256, HitMap>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for HitMaps {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Clone, Debug)]
pub struct HitMap {
pub bytecode: Bytes,
pub hits: BTreeMap<usize, u64>,
}
impl HitMap {
pub fn new(bytecode: Bytes) -> Self {
Self { bytecode, hits: BTreeMap::new() }
}
pub fn hit(&mut self, pc: usize) {
*self.hits.entry(pc).or_default() += 1;
}
pub fn merge(&mut self, other: &Self) -> Result<(), eyre::Report> {
for (pc, hits) in &other.hits {
*self.hits.entry(*pc).or_default() += hits;
}
Ok(())
}
pub fn consistent_bytecode(&self, hm1: &Self, hm2: &Self) -> bool {
let len1 = hm1.hits.last_key_value();
let len2 = hm2.hits.last_key_value();
if let (Some(len1), Some(len2)) = (len1, len2) {
let len = std::cmp::max(len1.0, len2.0);
let ok = hm1.bytecode.0[..*len] == hm2.bytecode.0[..*len];
let _ = sh_println!("consistent_bytecode: {}, {}, {}, {}", ok, len1.0, len2.0, len);
return ok;
}
true
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ContractId {
pub version: Version,
pub source_id: usize,
pub contract_name: Arc<str>,
}
impl Display for ContractId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Contract \"{}\" (solc {}, source ID {})",
self.contract_name, self.version, self.source_id
)
}
}
#[derive(Clone, Debug)]
pub struct ItemAnchor {
pub instruction: usize,
pub item_id: usize,
}
impl Display for ItemAnchor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
}
}
#[derive(Clone, Debug)]
pub enum CoverageItemKind {
Line,
Statement,
Branch {
branch_id: usize,
path_id: usize,
is_first_opcode: bool,
},
Function {
name: String,
},
}
#[derive(Clone, Debug)]
pub struct CoverageItem {
pub kind: CoverageItemKind,
pub loc: SourceLocation,
pub hits: u64,
}
impl Display for CoverageItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind {
CoverageItemKind::Line => {
write!(f, "Line")?;
}
CoverageItemKind::Statement => {
write!(f, "Statement")?;
}
CoverageItemKind::Branch { branch_id, path_id, .. } => {
write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
}
CoverageItemKind::Function { name } => {
write!(f, r#"Function "{name}""#)?;
}
}
write!(f, " (location: {}, hits: {})", self.loc, self.hits)
}
}
#[derive(Clone, Debug)]
pub struct SourceLocation {
pub source_id: usize,
pub contract_name: Arc<str>,
pub start: u32,
pub length: Option<u32>,
pub line: usize,
}
impl Display for SourceLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"source ID {}, line {}, chars {}-{}",
self.source_id,
self.line,
self.start,
self.length.map_or(self.start, |length| self.start + length)
)
}
}
#[derive(Clone, Debug, Default)]
pub struct CoverageSummary {
pub line_count: usize,
pub line_hits: usize,
pub statement_count: usize,
pub statement_hits: usize,
pub branch_count: usize,
pub branch_hits: usize,
pub function_count: usize,
pub function_hits: usize,
}
impl AddAssign<&Self> for CoverageSummary {
fn add_assign(&mut self, other: &Self) {
self.line_count += other.line_count;
self.line_hits += other.line_hits;
self.statement_count += other.statement_count;
self.statement_hits += other.statement_hits;
self.branch_count += other.branch_count;
self.branch_hits += other.branch_hits;
self.function_count += other.function_count;
self.function_hits += other.function_hits;
}
}
impl AddAssign<&CoverageItem> for CoverageSummary {
fn add_assign(&mut self, item: &CoverageItem) {
match item.kind {
CoverageItemKind::Line => {
self.line_count += 1;
if item.hits > 0 {
self.line_hits += 1;
}
}
CoverageItemKind::Statement => {
self.statement_count += 1;
if item.hits > 0 {
self.statement_hits += 1;
}
}
CoverageItemKind::Branch { .. } => {
self.branch_count += 1;
if item.hits > 0 {
self.branch_hits += 1;
}
}
CoverageItemKind::Function { .. } => {
self.function_count += 1;
if item.hits > 0 {
self.function_hits += 1;
}
}
}
}
}