#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#[macro_use]
extern crate tracing;
use alloy_primitives::{
map::{B256HashMap, HashMap},
Bytes,
};
use eyre::Result;
use foundry_compilers::artifacts::sourcemap::SourceMap;
use semver::Version;
use std::{
collections::BTreeMap,
fmt::Display,
num::NonZeroU32,
ops::{Deref, DerefMut, Range},
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 = (&Path, CoverageSummary)> {
self.by_file(|summary: &mut CoverageSummary, item| summary.add_item(item))
}
pub fn items_by_file(&self) -> impl Iterator<Item = (&Path, Vec<&CoverageItem>)> {
self.by_file(|list: &mut Vec<_>, item| list.push(item))
}
fn by_file<'a, T: Default>(
&'a self,
mut f: impl FnMut(&mut T, &'a CoverageItem),
) -> impl Iterator<Item = (&'a Path, T)> {
let mut by_file: BTreeMap<&Path, T> = BTreeMap::new();
for (version, items) in &self.items {
for item in items {
let key = (version.clone(), item.loc.source_id);
let Some(path) = self.source_paths.get(&key) else { continue };
f(by_file.entry(path).or_default(), item);
}
}
by_file.into_iter()
}
pub fn add_hit_map(
&mut self,
contract_id: &ContractId,
hit_map: &HitMap,
is_deployed_code: bool,
) -> Result<()> {
self.bytecode_hits
.entry(contract_id.clone())
.and_modify(|m| m.merge(hit_map))
.or_insert_with(|| hit_map.clone());
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.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.get();
}
}
}
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 B256HashMap<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) {
self.reserve(other.len());
for (code_hash, other) in other.0 {
self.entry(code_hash).and_modify(|e| e.merge(&other)).or_insert(other);
}
}
pub fn merged(mut self, other: Self) -> Self {
self.merge(other);
self
}
}
impl Deref for HitMaps {
type Target = B256HashMap<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 {
bytecode: Bytes,
hits: HashMap<u32, u32>,
}
impl HitMap {
#[inline]
pub fn new(bytecode: Bytes) -> Self {
Self { bytecode, hits: HashMap::with_capacity_and_hasher(1024, Default::default()) }
}
#[inline]
pub fn bytecode(&self) -> &Bytes {
&self.bytecode
}
#[inline]
pub fn get(&self, pc: usize) -> Option<NonZeroU32> {
NonZeroU32::new(self.hits.get(&Self::cvt_pc(pc)).copied().unwrap_or(0))
}
#[inline]
pub fn hit(&mut self, pc: usize) {
self.hits(pc, 1)
}
#[inline]
pub fn hits(&mut self, pc: usize, hits: u32) {
*self.hits.entry(Self::cvt_pc(pc)).or_default() += hits;
}
pub fn merge(&mut self, other: &Self) {
self.hits.reserve(other.len());
for (pc, hits) in other.iter() {
self.hits(pc, hits);
}
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = (usize, u32)> + '_ {
self.hits.iter().map(|(&pc, &hits)| (pc as usize, hits))
}
#[inline]
pub fn len(&self) -> usize {
self.hits.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.hits.is_empty()
}
#[inline]
fn cvt_pc(pc: usize) -> u32 {
pc.try_into().expect("4GiB bytecode")
}
}
#[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: u32,
}
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 bytes: Range<u32>,
pub lines: Range<u32>,
}
impl Display for SourceLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "source ID {}, lines {:?}, bytes {:?}", self.source_id, self.lines, self.bytes)
}
}
impl SourceLocation {
pub fn len(&self) -> u32 {
self.bytes.len() as u32
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[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 CoverageSummary {
pub fn new() -> Self {
Self::default()
}
pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
let mut summary = Self::default();
summary.add_items(items);
summary
}
pub fn merge(&mut self, other: &Self) {
let Self {
line_count,
line_hits,
statement_count,
statement_hits,
branch_count,
branch_hits,
function_count,
function_hits,
} = self;
*line_count += other.line_count;
*line_hits += other.line_hits;
*statement_count += other.statement_count;
*statement_hits += other.statement_hits;
*branch_count += other.branch_count;
*branch_hits += other.branch_hits;
*function_count += other.function_count;
*function_hits += other.function_hits;
}
pub fn add_item(&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;
}
}
}
}
pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
for item in items {
self.add_item(item);
}
}
}