1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use alloy_primitives::{
12 Bytes,
13 map::{
14 B256HashMap, HashMap,
15 rustc_hash::{FxHashMap, FxHashSet},
16 },
17};
18use analysis::SourceAnalysis;
19use eyre::Result;
20use foundry_compilers::artifacts::sourcemap::SourceMap;
21use semver::Version;
22use std::{
23 collections::BTreeMap,
24 fmt,
25 num::NonZeroU32,
26 ops::{Deref, DerefMut, Range},
27 path::{Path, PathBuf},
28 sync::Arc,
29};
30
31pub mod analysis;
32pub mod anchors;
33
34mod inspector;
35pub use inspector::LineCoverageCollector;
36
37#[derive(Clone, Debug, Default)]
42pub struct CoverageReport {
43 pub source_paths: HashMap<String, HashMap<usize, PathBuf>>,
45 pub source_paths_to_ids: HashMap<String, HashMap<PathBuf, usize>>,
47 pub analyses: HashMap<String, SourceAnalysis>,
49 pub anchors: HashMap<ContractId, (ItemAnchors, ItemAnchors)>,
53 execution_anchors: HashMap<ContractId, ContractExecutionAnchors>,
55 pub bytecode_hits: HashMap<ContractId, HitMap>,
57 pub source_maps: HashMap<ContractId, (SourceMap, SourceMap)>,
59}
60
61impl CoverageReport {
62 pub fn add_source(&mut self, build_id: String, source_id: usize, path: PathBuf) {
64 self.source_paths.entry(build_id.clone()).or_default().insert(source_id, path.clone());
65 self.source_paths_to_ids.entry(build_id).or_default().insert(path, source_id);
66 }
67
68 pub fn get_source_id(&self, build_id: &str, path: &Path) -> Option<usize> {
70 self.source_paths_to_ids.get(build_id)?.get(path).copied()
71 }
72
73 pub fn get_source_path(&self, build_id: &str, source_id: usize) -> Option<&Path> {
75 self.source_paths.get(build_id)?.get(&source_id).map(PathBuf::as_path)
76 }
77
78 pub fn add_source_maps(
80 &mut self,
81 source_maps: impl IntoIterator<Item = (ContractId, (SourceMap, SourceMap))>,
82 ) {
83 self.source_maps.extend(source_maps);
84 }
85
86 pub fn add_analysis(&mut self, build_id: String, analysis: SourceAnalysis) {
88 self.analyses.insert(build_id, analysis);
89 }
90
91 pub fn add_anchors(
95 &mut self,
96 anchors: impl IntoIterator<Item = (ContractId, (ItemAnchors, ItemAnchors))>,
97 ) {
98 self.anchors.extend(anchors);
99 }
100
101 pub fn add_execution_anchors(
103 &mut self,
104 contract_id: ContractId,
105 anchors: Vec<ExecutionAnchor>,
106 function_selectors: impl IntoIterator<Item = [u8; 4]>,
107 has_receive: bool,
108 fallback_payable: bool,
109 ) {
110 if anchors.is_empty() {
111 return;
112 }
113 self.execution_anchors.insert(
114 contract_id,
115 ContractExecutionAnchors {
116 anchors,
117 function_selectors: function_selectors.into_iter().collect(),
118 has_receive,
119 fallback_payable,
120 },
121 );
122 }
123
124 pub fn summary_by_file(&self) -> impl Iterator<Item = (&Path, CoverageSummary)> {
126 self.items_by_file().map(|(path, items)| {
127 let summary = CoverageSummary::from_items(&items);
128 (path, summary)
129 })
130 }
131
132 pub fn items_by_file(&self) -> impl Iterator<Item = (&Path, Vec<CoverageItem>)> {
134 let mut by_file = BTreeMap::<&Path, BTreeMap<CoverageItemKey<'_>, CoverageItem>>::new();
135 for (build_id, items) in &self.analyses {
136 for source_items in
137 items.all_items().chunk_by(|a, b| a.loc.source_id == b.loc.source_id)
138 {
139 if let Some(path) = self.get_source_path(build_id, source_items[0].loc.source_id) {
140 let file_items = by_file.entry(path).or_default();
141 for item in source_items {
142 file_items
143 .entry(CoverageItemKey::new(item))
144 .and_modify(|merged| {
145 merged.hits = merged.hits.saturating_add(item.hits)
146 })
147 .or_insert_with(|| item.clone());
148 }
149 }
150 }
151 }
152 by_file.into_iter().map(|(path, items)| (path, items.into_values().collect()))
153 }
154
155 pub fn add_hit_map(
161 &mut self,
162 contract_id: &ContractId,
163 hit_map: &HitMap,
164 is_deployed_code: bool,
165 ) -> Result<()> {
166 self.bytecode_hits
168 .entry(contract_id.clone())
169 .and_modify(|m| m.merge(hit_map))
170 .or_insert_with(|| hit_map.clone());
171
172 if let Some(anchors) = self.anchors.get(contract_id) {
174 let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
175 for (anchor, hits) in anchors.hits(hit_map) {
176 self.analyses
177 .get_mut(&contract_id.build_id)
178 .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
179 .expect("Anchor refers to non-existent coverage item")
180 .hits += hits.get();
181 }
182 }
183 if let Some(anchors) = self.execution_anchors.get(contract_id) {
184 for anchor in &anchors.anchors {
185 let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
186 self.analyses
187 .get_mut(&contract_id.build_id)
188 .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
189 .expect("Anchor refers to non-existent coverage item")
190 .hits += hits;
191 }
192 }
193
194 Ok(())
195 }
196
197 pub fn hit_items_for_hit_map<'a>(
199 &'a self,
200 contract_id: &ContractId,
201 hit_map: &HitMap,
202 is_deployed_code: bool,
203 ) -> Vec<(&'a CoverageItem, u32)> {
204 let Some(anchors) = self.anchors.get(contract_id) else { return Vec::new() };
205 let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
206
207 let mut hits_by_item = BTreeMap::<u32, u32>::new();
208 for (anchor, hits) in anchors.hits(hit_map) {
209 *hits_by_item.entry(anchor.item_id).or_default() += hits.get();
210 }
211 if let Some(anchors) = self.execution_anchors.get(contract_id) {
212 for anchor in &anchors.anchors {
213 let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
214 if hits > 0 {
215 *hits_by_item.entry(anchor.item_id).or_default() += hits;
216 }
217 }
218 }
219
220 let Some(items) = self.analyses.get(&contract_id.build_id) else {
221 return Vec::new();
222 };
223 hits_by_item
224 .into_iter()
225 .filter_map(|(item_id, hits)| {
226 let item = items.get(item_id)?;
227 Some((item, hits))
228 })
229 .collect()
230 }
231
232 pub fn retain_sources(&mut self, mut predicate: impl FnMut(&Path) -> bool) {
237 self.source_paths.retain(|_, paths| {
238 paths.retain(|_, path| predicate(path));
239 !paths.is_empty()
240 });
241
242 let source_paths = &self.source_paths;
243 self.source_paths_to_ids.retain(|build_id, paths| {
244 paths.retain(|_, source_id| {
245 source_paths.get(build_id).is_some_and(|paths| paths.contains_key(source_id))
246 });
247 !paths.is_empty()
248 });
249 }
250}
251
252#[derive(Clone, Debug, Default)]
254pub struct HitMaps(pub B256HashMap<HitMap>);
255
256impl HitMaps {
257 pub fn merge_opt(a: &mut Option<Self>, b: Option<Self>) {
259 match (a, b) {
260 (_, None) => {}
261 (a @ None, Some(b)) => *a = Some(b),
262 (Some(a), Some(b)) => a.merge(b),
263 }
264 }
265
266 pub fn merge(&mut self, other: Self) {
268 self.reserve(other.len());
269 for (code_hash, other) in other.0 {
270 self.entry(code_hash).and_modify(|e| e.merge(&other)).or_insert(other);
271 }
272 }
273
274 pub fn merged(mut self, other: Self) -> Self {
276 self.merge(other);
277 self
278 }
279}
280
281impl Deref for HitMaps {
282 type Target = B256HashMap<HitMap>;
283
284 fn deref(&self) -> &Self::Target {
285 &self.0
286 }
287}
288
289impl DerefMut for HitMaps {
290 fn deref_mut(&mut self) -> &mut Self::Target {
291 &mut self.0
292 }
293}
294
295#[derive(Clone, Copy, Debug)]
296enum CallData {
297 Empty,
298 Short,
299 Selector([u8; 4]),
300}
301
302impl CallData {
303 fn new(input: &[u8]) -> Self {
304 if input.is_empty() {
305 Self::Empty
306 } else if let Some(selector) = input.get(..4) {
307 Self::Selector(selector.try_into().unwrap())
308 } else {
309 Self::Short
310 }
311 }
312}
313
314#[derive(Clone, Copy, Debug, Default)]
315struct CallHits {
316 without_value: u32,
317 with_value: u32,
318}
319
320impl CallHits {
321 const fn hit(&mut self, with_value: bool) {
322 if with_value {
323 self.with_value += 1;
324 } else {
325 self.without_value += 1;
326 }
327 }
328
329 const fn merge(&mut self, other: Self) {
330 self.without_value += other.without_value;
331 self.with_value += other.with_value;
332 }
333
334 const fn total(self, payable: bool) -> u32 {
335 self.without_value + if payable { self.with_value } else { 0 }
336 }
337}
338
339#[derive(Clone, Debug)]
343pub struct HitMap {
344 hits: FxHashMap<u32, u32>,
345 pending: Vec<u32>,
347 pcs: Vec<u32>,
349 bytecode: Bytes,
350 creations: u32,
351 empty_calls: CallHits,
352 short_calls: CallHits,
353 selector_calls: FxHashMap<[u8; 4], CallHits>,
354}
355
356impl HitMap {
357 #[inline]
359 pub fn new(bytecode: Bytes) -> Self {
360 Self {
361 bytecode,
362 hits: Default::default(),
363 pending: Vec::new(),
364 pcs: Vec::new(),
365 creations: 0,
366 empty_calls: Default::default(),
367 short_calls: Default::default(),
368 selector_calls: Default::default(),
369 }
370 }
371
372 #[inline]
374 pub const fn bytecode(&self) -> &Bytes {
375 &self.bytecode
376 }
377
378 #[inline]
380 pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
381 NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
382 }
383
384 #[inline]
386 pub fn hit(&mut self, pc: u32) {
387 self.hits(pc, 1)
388 }
389
390 #[inline]
392 pub fn hits(&mut self, pc: u32, hits: u32) {
393 *self.hits.entry(pc).or_default() += hits;
394 }
395
396 #[inline]
400 pub(crate) fn record(&mut self, pc: usize) {
401 if let Some(hits) = self.pending.get_mut(pc) {
402 if *hits == 0 {
403 self.pcs.push(pc as u32);
404 }
405 *hits = hits.saturating_add(1);
406 } else {
407 self.grow_pending(pc);
408 }
409 }
410
411 #[cold]
413 #[inline(never)]
414 fn grow_pending(&mut self, pc: usize) {
415 self.pending.resize(self.bytecode.len().max(pc + 1), 0);
416 self.record(pc);
417 }
418
419 pub(crate) fn flush(&mut self) {
421 let pending = std::mem::take(&mut self.pending);
422 let pcs = std::mem::take(&mut self.pcs);
423 self.reserve(pcs.len());
424 for pc in pcs {
425 self.hits(pc, pending[pc as usize]);
426 }
427 }
428
429 fn call(&mut self, call: CallData, with_value: bool) {
430 let hits = match call {
431 CallData::Empty => &mut self.empty_calls,
432 CallData::Short => &mut self.short_calls,
433 CallData::Selector(selector) => self.selector_calls.entry(selector).or_default(),
434 };
435 hits.hit(with_value);
436 }
437
438 const fn creation(&mut self) {
439 self.creations += 1;
440 }
441
442 #[inline]
444 pub fn reserve(&mut self, additional: usize) {
445 self.hits.reserve(additional);
446 }
447
448 pub fn merge(&mut self, other: &Self) {
450 self.reserve(other.len());
451 for (pc, hits) in other.iter() {
452 self.hits(pc, hits);
453 }
454 self.creations += other.creations;
455 self.empty_calls.merge(other.empty_calls);
456 self.short_calls.merge(other.short_calls);
457 for (&selector, &hits) in &other.selector_calls {
458 self.selector_calls.entry(selector).or_default().merge(hits);
459 }
460 }
461
462 #[inline]
464 pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
465 self.hits.iter().map(|(&pc, &hits)| (pc, hits))
466 }
467
468 #[inline]
470 pub fn len(&self) -> usize {
471 self.hits.len()
472 }
473
474 #[inline]
476 pub fn is_empty(&self) -> bool {
477 self.hits.is_empty()
478 }
479}
480
481#[derive(Clone, Debug, PartialEq, Eq, Hash)]
483pub struct ContractId {
484 pub version: Version,
485 pub build_id: String,
486 pub source_id: usize,
487 pub contract_name: Arc<str>,
488}
489
490impl fmt::Display for ContractId {
491 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492 write!(
493 f,
494 "Contract \"{}\" (solc {}, source ID {})",
495 self.contract_name, self.version, self.source_id
496 )
497 }
498}
499
500#[derive(Clone, Debug)]
502pub struct ItemAnchor {
503 pub instruction: u32,
505 pub item_id: u32,
507}
508
509const _: () = assert!(std::mem::size_of::<ItemAnchor>() == 8);
510
511#[derive(Clone, Debug, Default)]
513pub struct ItemAnchors {
514 pub anchors: Vec<ItemAnchor>,
516 pub jumps: FxHashMap<usize, u32>,
519}
520
521impl ItemAnchors {
522 fn hits<'a>(
523 &'a self,
524 hit_map: &'a HitMap,
525 ) -> impl Iterator<Item = (&'a ItemAnchor, NonZeroU32)> {
526 self.anchors.iter().enumerate().filter_map(|(index, anchor)| {
527 let hits = if let Some(&jump) = self.jumps.get(&index) {
528 let hits = hit_map.get(jump)?.get();
531 NonZeroU32::new(
532 hits.saturating_sub(hit_map.get(jump + 1).map_or(0, NonZeroU32::get)),
533 )?
534 } else {
535 hit_map.get(anchor.instruction)?
536 };
537 Some((anchor, hits))
538 })
539 }
540}
541
542impl fmt::Display for ItemAnchor {
543 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544 write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
545 }
546}
547
548#[derive(Clone, Copy, Debug)]
550pub struct ExecutionAnchor {
551 pub item_id: u32,
553 pub kind: ExecutionAnchorKind,
555}
556
557#[derive(Clone, Copy, Debug, PartialEq, Eq)]
559pub enum ExecutionAnchorKind {
560 Constructor,
562 Receive,
564 Fallback,
566}
567
568#[derive(Clone, Debug)]
569struct ContractExecutionAnchors {
570 anchors: Vec<ExecutionAnchor>,
571 function_selectors: FxHashSet<[u8; 4]>,
572 has_receive: bool,
573 fallback_payable: bool,
574}
575
576impl ContractExecutionAnchors {
577 fn hits(&self, hit_map: &HitMap, kind: ExecutionAnchorKind, is_deployed_code: bool) -> u32 {
578 match (kind, is_deployed_code) {
579 (ExecutionAnchorKind::Constructor, false) => hit_map.creations,
580 (ExecutionAnchorKind::Receive, true) => hit_map.empty_calls.total(true),
581 (ExecutionAnchorKind::Fallback, true) => {
582 let empty_calls = if self.has_receive {
583 0
584 } else {
585 hit_map.empty_calls.total(self.fallback_payable)
586 };
587 empty_calls
588 + hit_map.short_calls.total(self.fallback_payable)
589 + hit_map
590 .selector_calls
591 .iter()
592 .filter(|(selector, _)| !self.function_selectors.contains(*selector))
593 .map(|(_, hits)| hits.total(self.fallback_payable))
594 .sum::<u32>()
595 }
596 _ => 0,
597 }
598 }
599}
600
601#[derive(Clone, Debug)]
602pub enum CoverageItemKind {
603 Line,
605 Statement,
607 Branch {
609 branch_id: u32,
614 path_id: u32,
618 is_first_opcode: bool,
620 },
621 Function {
623 name: Box<str>,
625 },
626}
627
628impl PartialEq for CoverageItemKind {
629 fn eq(&self, other: &Self) -> bool {
630 self.ord_key() == other.ord_key()
631 }
632}
633
634impl Eq for CoverageItemKind {}
635
636impl PartialOrd for CoverageItemKind {
637 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
638 Some(self.cmp(other))
639 }
640}
641
642impl Ord for CoverageItemKind {
643 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
644 self.ord_key().cmp(&other.ord_key())
645 }
646}
647
648impl CoverageItemKind {
649 fn ord_key(&self) -> impl Ord + use<> {
650 match *self {
651 Self::Line => 0,
652 Self::Statement => 1,
653 Self::Branch { .. } => 2,
654 Self::Function { .. } => 3,
655 }
656 }
657}
658
659#[derive(Clone, Debug)]
660pub struct CoverageItem {
661 pub kind: CoverageItemKind,
663 pub loc: SourceLocation,
665 pub anchor_loc: Option<SourceLocation>,
667 pub hits: u32,
669}
670
671#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
672enum CoverageItemKindKey<'a> {
673 Line,
674 Statement,
675 Branch { branch_id: u32, path_id: u32 },
676 Function(&'a str),
677}
678
679#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
680struct CoverageItemKey<'a> {
681 line_start: u32,
682 line_end: u32,
683 kind_order: u8,
684 byte_start: u32,
685 byte_end: u32,
686 contract_name: &'a str,
687 kind: CoverageItemKindKey<'a>,
688}
689
690impl<'a> CoverageItemKey<'a> {
691 fn new(item: &'a CoverageItem) -> Self {
692 let (kind_order, kind) = match &item.kind {
693 CoverageItemKind::Line => (0, CoverageItemKindKey::Line),
694 CoverageItemKind::Statement => (1, CoverageItemKindKey::Statement),
695 CoverageItemKind::Branch { branch_id, path_id, .. } => {
696 (2, CoverageItemKindKey::Branch { branch_id: *branch_id, path_id: *path_id })
697 }
698 CoverageItemKind::Function { name } => {
699 (3, CoverageItemKindKey::Function(name.as_ref()))
700 }
701 };
702
703 Self {
704 line_start: item.loc.lines.start,
705 line_end: item.loc.lines.end,
706 kind_order,
707 byte_start: item.loc.bytes.start,
708 byte_end: item.loc.bytes.end,
709 contract_name: item.loc.contract_name.as_ref(),
710 kind,
711 }
712 }
713}
714
715impl PartialEq for CoverageItem {
716 fn eq(&self, other: &Self) -> bool {
717 self.ord_key() == other.ord_key()
718 }
719}
720
721impl Eq for CoverageItem {}
722
723impl PartialOrd for CoverageItem {
724 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
725 Some(self.cmp(other))
726 }
727}
728
729impl Ord for CoverageItem {
730 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
731 self.ord_key().cmp(&other.ord_key())
732 }
733}
734
735impl fmt::Display for CoverageItem {
736 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
737 self.fmt_with_source(None).fmt(f)
738 }
739}
740
741impl CoverageItem {
742 fn ord_key(&self) -> impl Ord + use<> {
743 (
744 self.loc.source_id,
745 self.loc.lines.start,
746 self.loc.lines.end,
747 self.kind.ord_key(),
748 self.loc.bytes.start,
749 self.loc.bytes.end,
750 )
751 }
752
753 pub fn fmt_with_source(&self, src: Option<&str>) -> impl fmt::Display {
754 solar::data_structures::fmt::from_fn(move |f| {
755 match &self.kind {
756 CoverageItemKind::Line => {
757 write!(f, "Line")?;
758 }
759 CoverageItemKind::Statement => {
760 write!(f, "Statement")?;
761 }
762 CoverageItemKind::Branch { branch_id, path_id, .. } => {
763 write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
764 }
765 CoverageItemKind::Function { name } => {
766 write!(f, r#"Function "{name}""#)?;
767 }
768 }
769 write!(f, " (location: ({}), hits: {})", self.loc, self.hits)?;
770
771 if let Some(src) = src
772 && let Some(src) = src.get(self.loc.bytes())
773 {
774 write!(f, " -> ")?;
775
776 let max_len = 64;
777 let max_half = max_len / 2;
778
779 if src.len() > max_len {
780 write!(f, "\"{}", src[..max_half].escape_debug())?;
781 write!(f, "...")?;
782 write!(f, "{}\"", src[src.len() - max_half..].escape_debug())?;
783 } else {
784 write!(f, "{src:?}")?;
785 }
786 }
787
788 Ok(())
789 })
790 }
791}
792
793#[derive(Clone, Debug)]
795pub struct SourceLocation {
796 pub source_id: usize,
798 pub contract_name: Arc<str>,
800 pub bytes: Range<u32>,
802 pub lines: Range<u32>,
804}
805
806impl fmt::Display for SourceLocation {
807 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
808 write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
809 }
810}
811
812impl SourceLocation {
813 pub const fn bytes(&self) -> Range<usize> {
815 self.bytes.start as usize..self.bytes.end as usize
816 }
817
818 pub fn len(&self) -> u32 {
820 self.bytes.len() as u32
821 }
822
823 pub fn is_empty(&self) -> bool {
825 self.len() == 0
826 }
827}
828
829#[derive(Clone, Debug, Default)]
831pub struct CoverageSummary {
832 pub line_count: usize,
834 pub line_hits: usize,
836 pub statement_count: usize,
838 pub statement_hits: usize,
840 pub branch_count: usize,
842 pub branch_hits: usize,
844 pub function_count: usize,
846 pub function_hits: usize,
848}
849
850impl CoverageSummary {
851 pub fn new() -> Self {
853 Self::default()
854 }
855
856 pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
858 let mut summary = Self::new();
859 summary.add_items(items);
860 summary
861 }
862
863 pub const fn merge(&mut self, other: &Self) {
865 let Self {
866 line_count,
867 line_hits,
868 statement_count,
869 statement_hits,
870 branch_count,
871 branch_hits,
872 function_count,
873 function_hits,
874 } = self;
875 *line_count += other.line_count;
876 *line_hits += other.line_hits;
877 *statement_count += other.statement_count;
878 *statement_hits += other.statement_hits;
879 *branch_count += other.branch_count;
880 *branch_hits += other.branch_hits;
881 *function_count += other.function_count;
882 *function_hits += other.function_hits;
883 }
884
885 pub const fn add_item(&mut self, item: &CoverageItem) {
887 match item.kind {
888 CoverageItemKind::Line => {
889 self.line_count += 1;
890 if item.hits > 0 {
891 self.line_hits += 1;
892 }
893 }
894 CoverageItemKind::Statement => {
895 self.statement_count += 1;
896 if item.hits > 0 {
897 self.statement_hits += 1;
898 }
899 }
900 CoverageItemKind::Branch { .. } => {
901 self.branch_count += 1;
902 if item.hits > 0 {
903 self.branch_hits += 1;
904 }
905 }
906 CoverageItemKind::Function { .. } => {
907 self.function_count += 1;
908 if item.hits > 0 {
909 self.function_hits += 1;
910 }
911 }
912 }
913 }
914
915 pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
917 for item in items {
918 self.add_item(item);
919 }
920 }
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926
927 #[test]
928 fn sparse_jump_hits_distinguish_shared_items_and_destinations() {
929 let anchors = ItemAnchors {
930 anchors: vec![
931 ItemAnchor { instruction: 20, item_id: 0 },
932 ItemAnchor { instruction: 20, item_id: 0 },
933 ItemAnchor { instruction: 20, item_id: 0 },
934 ],
935 jumps: [(1, 4), (2, 10)].into_iter().collect(),
936 };
937 for (jump_hits, fallthrough_hits, taken_hits) in
938 [(3, 2, 1), (3, 0, 3), (3, 3, 0), (0, 0, 0)]
939 {
940 let mut hit_map = HitMap::new(Bytes::new());
941 hit_map.hits(20, 5);
942 hit_map.hits(4, jump_hits);
943 hit_map.hits(5, fallthrough_hits);
944 hit_map.hits(10, 2);
945 hit_map.hits(11, 2);
946
947 let hits = anchors.hits(&hit_map).map(|(_, hits)| hits.get()).collect::<Vec<_>>();
948 let expected = if taken_hits == 0 { vec![5] } else { vec![5, taken_hits] };
949 assert_eq!(hits, expected);
950 }
951 }
952
953 #[test]
954 fn ordinary_anchors_do_not_allocate_jump_metadata() {
955 let anchors = ItemAnchors {
956 anchors: vec![ItemAnchor { instruction: 20, item_id: 0 }],
957 ..Default::default()
958 };
959 assert_eq!(anchors.jumps.capacity(), 0);
960 let mut hit_map = HitMap::new(Bytes::new());
961 assert_eq!(anchors.hits(&hit_map).count(), 0);
962 hit_map.hits(20, 3);
963 assert_eq!(anchors.hits(&hit_map).next().unwrap().1.get(), 3);
964 }
965
966 #[test]
967 fn saturated_pending_hits_flush_once() {
968 let mut hit_map = HitMap::new(Bytes::from_static(&[0]));
969 hit_map.record(0);
970 hit_map.pending[0] = u32::MAX;
971 hit_map.record(0);
972 hit_map.record(0);
973 hit_map.flush();
974 assert_eq!(hit_map.iter().collect::<Vec<_>>(), [(0, u32::MAX)]);
975 }
976}