foundry_evm_coverage/
lib.rs

1//! # foundry-evm-coverage
2//!
3//! EVM bytecode coverage analysis.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use alloy_primitives::{
12    Bytes,
13    map::{B256HashMap, HashMap, rustc_hash::FxHashMap},
14};
15use analysis::SourceAnalysis;
16use eyre::Result;
17use foundry_compilers::artifacts::sourcemap::SourceMap;
18use semver::Version;
19use std::{
20    collections::BTreeMap,
21    fmt::Display,
22    num::NonZeroU32,
23    ops::{Deref, DerefMut, Range},
24    path::{Path, PathBuf},
25    sync::Arc,
26};
27
28pub mod analysis;
29pub mod anchors;
30
31mod inspector;
32pub use inspector::LineCoverageCollector;
33
34/// A coverage report.
35///
36/// A coverage report contains coverage items and opcodes corresponding to those items (called
37/// "anchors"). A single coverage item may be referred to by multiple anchors.
38#[derive(Clone, Debug, Default)]
39pub struct CoverageReport {
40    /// A map of source IDs to the source path.
41    pub source_paths: HashMap<(Version, usize), PathBuf>,
42    /// A map of source paths to source IDs.
43    pub source_paths_to_ids: HashMap<(Version, PathBuf), usize>,
44    /// All coverage items for the codebase, keyed by the compiler version.
45    pub analyses: HashMap<Version, SourceAnalysis>,
46    /// All item anchors for the codebase, keyed by their contract ID.
47    ///
48    /// `(id, (creation, runtime))`
49    pub anchors: HashMap<ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>)>,
50    /// All the bytecode hits for the codebase.
51    pub bytecode_hits: HashMap<ContractId, HitMap>,
52    /// The bytecode -> source mappings.
53    pub source_maps: HashMap<ContractId, (SourceMap, SourceMap)>,
54}
55
56impl CoverageReport {
57    /// Add a source file path.
58    pub fn add_source(&mut self, version: Version, source_id: usize, path: PathBuf) {
59        self.source_paths.insert((version.clone(), source_id), path.clone());
60        self.source_paths_to_ids.insert((version, path), source_id);
61    }
62
63    /// Get the source ID for a specific source file path.
64    pub fn get_source_id(&self, version: Version, path: PathBuf) -> Option<usize> {
65        self.source_paths_to_ids.get(&(version, path)).copied()
66    }
67
68    /// Add the source maps.
69    pub fn add_source_maps(
70        &mut self,
71        source_maps: impl IntoIterator<Item = (ContractId, (SourceMap, SourceMap))>,
72    ) {
73        self.source_maps.extend(source_maps);
74    }
75
76    /// Add a [`SourceAnalysis`] to this report.
77    pub fn add_analysis(&mut self, version: Version, analysis: SourceAnalysis) {
78        self.analyses.insert(version, analysis);
79    }
80
81    /// Add anchors to this report.
82    ///
83    /// `(id, (creation, runtime))`
84    pub fn add_anchors(
85        &mut self,
86        anchors: impl IntoIterator<Item = (ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>))>,
87    ) {
88        self.anchors.extend(anchors);
89    }
90
91    /// Returns an iterator over coverage summaries by source file path.
92    pub fn summary_by_file(&self) -> impl Iterator<Item = (&Path, CoverageSummary)> {
93        self.by_file(|summary: &mut CoverageSummary, item| summary.add_item(item))
94    }
95
96    /// Returns an iterator over coverage items by source file path.
97    pub fn items_by_file(&self) -> impl Iterator<Item = (&Path, Vec<&CoverageItem>)> {
98        self.by_file(|list: &mut Vec<_>, item| list.push(item))
99    }
100
101    fn by_file<'a, T: Default>(
102        &'a self,
103        mut f: impl FnMut(&mut T, &'a CoverageItem),
104    ) -> impl Iterator<Item = (&'a Path, T)> {
105        let mut by_file: BTreeMap<&Path, T> = BTreeMap::new();
106        for (version, items) in &self.analyses {
107            for item in items.all_items() {
108                let key = (version.clone(), item.loc.source_id);
109                let Some(path) = self.source_paths.get(&key) else { continue };
110                f(by_file.entry(path).or_default(), item);
111            }
112        }
113        by_file.into_iter()
114    }
115
116    /// Processes data from a [`HitMap`] and sets hit counts for coverage items in this coverage
117    /// map.
118    ///
119    /// This function should only be called *after* all the relevant sources have been processed and
120    /// added to the map (see [`add_source`](Self::add_source)).
121    pub fn add_hit_map(
122        &mut self,
123        contract_id: &ContractId,
124        hit_map: &HitMap,
125        is_deployed_code: bool,
126    ) -> Result<()> {
127        // Add bytecode level hits.
128        self.bytecode_hits
129            .entry(contract_id.clone())
130            .and_modify(|m| m.merge(hit_map))
131            .or_insert_with(|| hit_map.clone());
132
133        // Add source level hits.
134        if let Some(anchors) = self.anchors.get(contract_id) {
135            let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
136            for anchor in anchors {
137                if let Some(hits) = hit_map.get(anchor.instruction) {
138                    self.analyses
139                        .get_mut(&contract_id.version)
140                        .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
141                        .expect("Anchor refers to non-existent coverage item")
142                        .hits += hits.get();
143                }
144            }
145        }
146
147        Ok(())
148    }
149
150    /// Retains all the coverage items specified by `predicate`.
151    ///
152    /// This function should only be called after all the sources were used, otherwise, the output
153    /// will be missing the ones that are dependent on them.
154    pub fn retain_sources(&mut self, mut predicate: impl FnMut(&Path) -> bool) {
155        self.analyses.retain(|version, analysis| {
156            analysis.all_items_mut().retain(|item| {
157                self.source_paths
158                    .get(&(version.clone(), item.loc.source_id))
159                    .map(|path| predicate(path))
160                    .unwrap_or(false)
161            });
162            !analysis.all_items().is_empty()
163        });
164    }
165}
166
167/// A collection of [`HitMap`]s.
168#[derive(Clone, Debug, Default)]
169pub struct HitMaps(pub B256HashMap<HitMap>);
170
171impl HitMaps {
172    /// Merges two `Option<HitMaps>`.
173    pub fn merge_opt(a: &mut Option<Self>, b: Option<Self>) {
174        match (a, b) {
175            (_, None) => {}
176            (a @ None, Some(b)) => *a = Some(b),
177            (Some(a), Some(b)) => a.merge(b),
178        }
179    }
180
181    /// Merges two `HitMaps`.
182    pub fn merge(&mut self, other: Self) {
183        self.reserve(other.len());
184        for (code_hash, other) in other.0 {
185            self.entry(code_hash).and_modify(|e| e.merge(&other)).or_insert(other);
186        }
187    }
188
189    /// Merges two `HitMaps`.
190    pub fn merged(mut self, other: Self) -> Self {
191        self.merge(other);
192        self
193    }
194}
195
196impl Deref for HitMaps {
197    type Target = B256HashMap<HitMap>;
198
199    fn deref(&self) -> &Self::Target {
200        &self.0
201    }
202}
203
204impl DerefMut for HitMaps {
205    fn deref_mut(&mut self) -> &mut Self::Target {
206        &mut self.0
207    }
208}
209
210/// Hit data for an address.
211///
212/// Contains low-level data about hit counters for the instructions in the bytecode of a contract.
213#[derive(Clone, Debug)]
214pub struct HitMap {
215    hits: FxHashMap<u32, u32>,
216    bytecode: Bytes,
217}
218
219impl HitMap {
220    /// Create a new hitmap with the given bytecode.
221    #[inline]
222    pub fn new(bytecode: Bytes) -> Self {
223        Self { bytecode, hits: HashMap::with_capacity_and_hasher(1024, Default::default()) }
224    }
225
226    /// Returns the bytecode.
227    #[inline]
228    pub fn bytecode(&self) -> &Bytes {
229        &self.bytecode
230    }
231
232    /// Returns the number of hits for the given program counter.
233    #[inline]
234    pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
235        NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
236    }
237
238    /// Increase the hit counter by 1 for the given program counter.
239    #[inline]
240    pub fn hit(&mut self, pc: u32) {
241        self.hits(pc, 1)
242    }
243
244    /// Increase the hit counter by `hits` for the given program counter.
245    #[inline]
246    pub fn hits(&mut self, pc: u32, hits: u32) {
247        *self.hits.entry(pc).or_default() += hits;
248    }
249
250    /// Merge another hitmap into this, assuming the bytecode is consistent
251    pub fn merge(&mut self, other: &Self) {
252        self.hits.reserve(other.len());
253        for (pc, hits) in other.iter() {
254            self.hits(pc, hits);
255        }
256    }
257
258    /// Returns an iterator over all the program counters and their hit counts.
259    #[inline]
260    pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
261        self.hits.iter().map(|(&pc, &hits)| (pc, hits))
262    }
263
264    /// Returns the number of program counters hit in the hitmap.
265    #[inline]
266    pub fn len(&self) -> usize {
267        self.hits.len()
268    }
269
270    /// Returns `true` if the hitmap is empty.
271    #[inline]
272    pub fn is_empty(&self) -> bool {
273        self.hits.is_empty()
274    }
275}
276
277/// A unique identifier for a contract
278#[derive(Clone, Debug, PartialEq, Eq, Hash)]
279pub struct ContractId {
280    pub version: Version,
281    pub source_id: usize,
282    pub contract_name: Arc<str>,
283}
284
285impl Display for ContractId {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        write!(
288            f,
289            "Contract \"{}\" (solc {}, source ID {})",
290            self.contract_name, self.version, self.source_id
291        )
292    }
293}
294
295/// An item anchor describes what instruction marks a [CoverageItem] as covered.
296#[derive(Clone, Debug)]
297pub struct ItemAnchor {
298    /// The program counter for the opcode of this anchor.
299    pub instruction: u32,
300    /// The item ID this anchor points to.
301    pub item_id: u32,
302}
303
304impl Display for ItemAnchor {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
307    }
308}
309
310#[derive(Clone, Debug)]
311pub enum CoverageItemKind {
312    /// An executable line in the code.
313    Line,
314    /// A statement in the code.
315    Statement,
316    /// A branch in the code.
317    Branch {
318        /// The ID that identifies the branch.
319        ///
320        /// There may be multiple items with the same branch ID - they belong to the same branch,
321        /// but represent different paths.
322        branch_id: u32,
323        /// The path ID for this branch.
324        ///
325        /// The first path has ID 0, the next ID 1, and so on.
326        path_id: u32,
327        /// If true, then the branch anchor is the first opcode within the branch source range.
328        is_first_opcode: bool,
329    },
330    /// A function in the code.
331    Function {
332        /// The name of the function.
333        name: Box<str>,
334    },
335}
336
337#[derive(Clone, Debug)]
338pub struct CoverageItem {
339    /// The coverage item kind.
340    pub kind: CoverageItemKind,
341    /// The location of the item in the source code.
342    pub loc: SourceLocation,
343    /// The number of times this item was hit.
344    pub hits: u32,
345}
346
347impl Display for CoverageItem {
348    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349        match &self.kind {
350            CoverageItemKind::Line => {
351                write!(f, "Line")?;
352            }
353            CoverageItemKind::Statement => {
354                write!(f, "Statement")?;
355            }
356            CoverageItemKind::Branch { branch_id, path_id, .. } => {
357                write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
358            }
359            CoverageItemKind::Function { name } => {
360                write!(f, r#"Function "{name}""#)?;
361            }
362        }
363        write!(f, " (location: ({}), hits: {})", self.loc, self.hits)
364    }
365}
366
367/// A source location.
368#[derive(Clone, Debug)]
369pub struct SourceLocation {
370    /// The source ID.
371    pub source_id: usize,
372    /// The contract this source range is in.
373    pub contract_name: Arc<str>,
374    /// Byte range.
375    pub bytes: Range<u32>,
376    /// Line range. Indices are 1-based.
377    pub lines: Range<u32>,
378}
379
380impl Display for SourceLocation {
381    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382        write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
383    }
384}
385
386impl SourceLocation {
387    /// Returns the length of the byte range.
388    pub fn len(&self) -> u32 {
389        self.bytes.len() as u32
390    }
391
392    /// Returns true if the byte range is empty.
393    pub fn is_empty(&self) -> bool {
394        self.len() == 0
395    }
396}
397
398/// Coverage summary for a source file.
399#[derive(Clone, Debug, Default)]
400pub struct CoverageSummary {
401    /// The number of executable lines in the source file.
402    pub line_count: usize,
403    /// The number of lines that were hit.
404    pub line_hits: usize,
405    /// The number of statements in the source file.
406    pub statement_count: usize,
407    /// The number of statements that were hit.
408    pub statement_hits: usize,
409    /// The number of branches in the source file.
410    pub branch_count: usize,
411    /// The number of branches that were hit.
412    pub branch_hits: usize,
413    /// The number of functions in the source file.
414    pub function_count: usize,
415    /// The number of functions hit.
416    pub function_hits: usize,
417}
418
419impl CoverageSummary {
420    /// Creates a new, empty coverage summary.
421    pub fn new() -> Self {
422        Self::default()
423    }
424
425    /// Creates a coverage summary from a collection of coverage items.
426    pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
427        let mut summary = Self::default();
428        summary.add_items(items);
429        summary
430    }
431
432    /// Adds another coverage summary to this one.
433    pub fn merge(&mut self, other: &Self) {
434        let Self {
435            line_count,
436            line_hits,
437            statement_count,
438            statement_hits,
439            branch_count,
440            branch_hits,
441            function_count,
442            function_hits,
443        } = self;
444        *line_count += other.line_count;
445        *line_hits += other.line_hits;
446        *statement_count += other.statement_count;
447        *statement_hits += other.statement_hits;
448        *branch_count += other.branch_count;
449        *branch_hits += other.branch_hits;
450        *function_count += other.function_count;
451        *function_hits += other.function_hits;
452    }
453
454    /// Adds a coverage item to this summary.
455    pub fn add_item(&mut self, item: &CoverageItem) {
456        match item.kind {
457            CoverageItemKind::Line => {
458                self.line_count += 1;
459                if item.hits > 0 {
460                    self.line_hits += 1;
461                }
462            }
463            CoverageItemKind::Statement => {
464                self.statement_count += 1;
465                if item.hits > 0 {
466                    self.statement_hits += 1;
467                }
468            }
469            CoverageItemKind::Branch { .. } => {
470                self.branch_count += 1;
471                if item.hits > 0 {
472                    self.branch_hits += 1;
473                }
474            }
475            CoverageItemKind::Function { .. } => {
476                self.function_count += 1;
477                if item.hits > 0 {
478                    self.function_hits += 1;
479                }
480            }
481        }
482    }
483
484    /// Adds multiple coverage items to this summary.
485    pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
486        for item in items {
487            self.add_item(item);
488        }
489    }
490}