1#![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,
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#[derive(Clone, Debug, Default)]
39pub struct CoverageReport {
40 pub source_paths: HashMap<(Version, usize), PathBuf>,
42 pub source_paths_to_ids: HashMap<(Version, PathBuf), usize>,
44 pub analyses: HashMap<Version, SourceAnalysis>,
46 pub anchors: HashMap<ContractId, (Vec<ItemAnchor>, Vec<ItemAnchor>)>,
50 pub bytecode_hits: HashMap<ContractId, HitMap>,
52 pub source_maps: HashMap<ContractId, (SourceMap, SourceMap)>,
54}
55
56impl CoverageReport {
57 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 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 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 pub fn add_analysis(&mut self, version: Version, analysis: SourceAnalysis) {
78 self.analyses.insert(version, analysis);
79 }
80
81 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 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 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 pub fn add_hit_map(
122 &mut self,
123 contract_id: &ContractId,
124 hit_map: &HitMap,
125 is_deployed_code: bool,
126 ) -> Result<()> {
127 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 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 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#[derive(Clone, Debug, Default)]
169pub struct HitMaps(pub B256HashMap<HitMap>);
170
171impl HitMaps {
172 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 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 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#[derive(Clone, Debug)]
214pub struct HitMap {
215 hits: FxHashMap<u32, u32>,
216 bytecode: Bytes,
217}
218
219impl HitMap {
220 #[inline]
222 pub fn new(bytecode: Bytes) -> Self {
223 Self { bytecode, hits: HashMap::with_capacity_and_hasher(1024, Default::default()) }
224 }
225
226 #[inline]
228 pub fn bytecode(&self) -> &Bytes {
229 &self.bytecode
230 }
231
232 #[inline]
234 pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
235 NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
236 }
237
238 #[inline]
240 pub fn hit(&mut self, pc: u32) {
241 self.hits(pc, 1)
242 }
243
244 #[inline]
246 pub fn hits(&mut self, pc: u32, hits: u32) {
247 *self.hits.entry(pc).or_default() += hits;
248 }
249
250 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 #[inline]
260 pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
261 self.hits.iter().map(|(&pc, &hits)| (pc, hits))
262 }
263
264 #[inline]
266 pub fn len(&self) -> usize {
267 self.hits.len()
268 }
269
270 #[inline]
272 pub fn is_empty(&self) -> bool {
273 self.hits.is_empty()
274 }
275}
276
277#[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 fmt::Display for ContractId {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> 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#[derive(Clone, Debug)]
297pub struct ItemAnchor {
298 pub instruction: u32,
300 pub item_id: u32,
302}
303
304impl fmt::Display for ItemAnchor {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
307 }
308}
309
310#[derive(Clone, Debug)]
311pub enum CoverageItemKind {
312 Line,
314 Statement,
316 Branch {
318 branch_id: u32,
323 path_id: u32,
327 is_first_opcode: bool,
329 },
330 Function {
332 name: Box<str>,
334 },
335}
336
337impl PartialEq for CoverageItemKind {
338 fn eq(&self, other: &Self) -> bool {
339 self.ord_key() == other.ord_key()
340 }
341}
342
343impl Eq for CoverageItemKind {}
344
345impl PartialOrd for CoverageItemKind {
346 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
347 Some(self.cmp(other))
348 }
349}
350
351impl Ord for CoverageItemKind {
352 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
353 self.ord_key().cmp(&other.ord_key())
354 }
355}
356
357impl CoverageItemKind {
358 fn ord_key(&self) -> impl Ord + use<> {
359 match *self {
360 Self::Line => 0,
361 Self::Statement => 1,
362 Self::Branch { .. } => 2,
363 Self::Function { .. } => 3,
364 }
365 }
366}
367
368#[derive(Clone, Debug)]
369pub struct CoverageItem {
370 pub kind: CoverageItemKind,
372 pub loc: SourceLocation,
374 pub hits: u32,
376}
377
378impl PartialEq for CoverageItem {
379 fn eq(&self, other: &Self) -> bool {
380 self.ord_key() == other.ord_key()
381 }
382}
383
384impl Eq for CoverageItem {}
385
386impl PartialOrd for CoverageItem {
387 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
388 Some(self.cmp(other))
389 }
390}
391
392impl Ord for CoverageItem {
393 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
394 self.ord_key().cmp(&other.ord_key())
395 }
396}
397
398impl fmt::Display for CoverageItem {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 self.fmt_with_source(None).fmt(f)
401 }
402}
403
404impl CoverageItem {
405 fn ord_key(&self) -> impl Ord + use<> {
406 (
407 self.loc.source_id,
408 self.loc.lines.start,
409 self.loc.lines.end,
410 self.kind.ord_key(),
411 self.loc.bytes.start,
412 self.loc.bytes.end,
413 )
414 }
415
416 pub fn fmt_with_source(&self, src: Option<&str>) -> impl fmt::Display {
417 solar::data_structures::fmt::from_fn(move |f| {
418 match &self.kind {
419 CoverageItemKind::Line => {
420 write!(f, "Line")?;
421 }
422 CoverageItemKind::Statement => {
423 write!(f, "Statement")?;
424 }
425 CoverageItemKind::Branch { branch_id, path_id, .. } => {
426 write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
427 }
428 CoverageItemKind::Function { name } => {
429 write!(f, r#"Function "{name}""#)?;
430 }
431 }
432 write!(f, " (location: ({}), hits: {})", self.loc, self.hits)?;
433
434 if let Some(src) = src
435 && let Some(src) = src.get(self.loc.bytes())
436 {
437 write!(f, " -> ")?;
438
439 let max_len = 64;
440 let max_half = max_len / 2;
441
442 if src.len() > max_len {
443 write!(f, "\"{}", src[..max_half].escape_debug())?;
444 write!(f, "...")?;
445 write!(f, "{}\"", src[src.len() - max_half..].escape_debug())?;
446 } else {
447 write!(f, "{src:?}")?;
448 }
449 }
450
451 Ok(())
452 })
453 }
454}
455
456#[derive(Clone, Debug)]
458pub struct SourceLocation {
459 pub source_id: usize,
461 pub contract_name: Arc<str>,
463 pub bytes: Range<u32>,
465 pub lines: Range<u32>,
467}
468
469impl fmt::Display for SourceLocation {
470 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471 write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
472 }
473}
474
475impl SourceLocation {
476 pub fn bytes(&self) -> Range<usize> {
478 self.bytes.start as usize..self.bytes.end as usize
479 }
480
481 pub fn len(&self) -> u32 {
483 self.bytes.len() as u32
484 }
485
486 pub fn is_empty(&self) -> bool {
488 self.len() == 0
489 }
490}
491
492#[derive(Clone, Debug, Default)]
494pub struct CoverageSummary {
495 pub line_count: usize,
497 pub line_hits: usize,
499 pub statement_count: usize,
501 pub statement_hits: usize,
503 pub branch_count: usize,
505 pub branch_hits: usize,
507 pub function_count: usize,
509 pub function_hits: usize,
511}
512
513impl CoverageSummary {
514 pub fn new() -> Self {
516 Self::default()
517 }
518
519 pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
521 let mut summary = Self::default();
522 summary.add_items(items);
523 summary
524 }
525
526 pub fn merge(&mut self, other: &Self) {
528 let Self {
529 line_count,
530 line_hits,
531 statement_count,
532 statement_hits,
533 branch_count,
534 branch_hits,
535 function_count,
536 function_hits,
537 } = self;
538 *line_count += other.line_count;
539 *line_hits += other.line_hits;
540 *statement_count += other.statement_count;
541 *statement_hits += other.statement_hits;
542 *branch_count += other.branch_count;
543 *branch_hits += other.branch_hits;
544 *function_count += other.function_count;
545 *function_hits += other.function_hits;
546 }
547
548 pub fn add_item(&mut self, item: &CoverageItem) {
550 match item.kind {
551 CoverageItemKind::Line => {
552 self.line_count += 1;
553 if item.hits > 0 {
554 self.line_hits += 1;
555 }
556 }
557 CoverageItemKind::Statement => {
558 self.statement_count += 1;
559 if item.hits > 0 {
560 self.statement_hits += 1;
561 }
562 }
563 CoverageItemKind::Branch { .. } => {
564 self.branch_count += 1;
565 if item.hits > 0 {
566 self.branch_hits += 1;
567 }
568 }
569 CoverageItemKind::Function { .. } => {
570 self.function_count += 1;
571 if item.hits > 0 {
572 self.function_hits += 1;
573 }
574 }
575 }
576 }
577
578 pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
580 for item in items {
581 self.add_item(item);
582 }
583 }
584}