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 item in items.all_items() {
137 let Some(path) = self.get_source_path(build_id, item.loc.source_id) else {
138 continue;
139 };
140 by_file
141 .entry(path)
142 .or_default()
143 .entry(CoverageItemKey::new(item))
144 .and_modify(|merged| merged.hits = merged.hits.saturating_add(item.hits))
145 .or_insert_with(|| item.clone());
146 }
147 }
148 by_file.into_iter().map(|(path, items)| (path, items.into_values().collect()))
149 }
150
151 pub fn add_hit_map(
157 &mut self,
158 contract_id: &ContractId,
159 hit_map: &HitMap,
160 is_deployed_code: bool,
161 ) -> Result<()> {
162 self.bytecode_hits
164 .entry(contract_id.clone())
165 .and_modify(|m| m.merge(hit_map))
166 .or_insert_with(|| hit_map.clone());
167
168 if let Some(anchors) = self.anchors.get(contract_id) {
170 let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
171 for (anchor, hits) in anchors.hits(hit_map) {
172 self.analyses
173 .get_mut(&contract_id.build_id)
174 .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
175 .expect("Anchor refers to non-existent coverage item")
176 .hits += hits.get();
177 }
178 }
179 if let Some(anchors) = self.execution_anchors.get(contract_id) {
180 for anchor in &anchors.anchors {
181 let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
182 self.analyses
183 .get_mut(&contract_id.build_id)
184 .and_then(|items| items.all_items_mut().get_mut(anchor.item_id as usize))
185 .expect("Anchor refers to non-existent coverage item")
186 .hits += hits;
187 }
188 }
189
190 Ok(())
191 }
192
193 pub fn hit_items_for_hit_map<'a>(
195 &'a self,
196 contract_id: &ContractId,
197 hit_map: &HitMap,
198 is_deployed_code: bool,
199 ) -> Vec<(&'a CoverageItem, u32)> {
200 let Some(anchors) = self.anchors.get(contract_id) else { return Vec::new() };
201 let anchors = if is_deployed_code { &anchors.1 } else { &anchors.0 };
202
203 let mut hits_by_item = BTreeMap::<u32, u32>::new();
204 for (anchor, hits) in anchors.hits(hit_map) {
205 *hits_by_item.entry(anchor.item_id).or_default() += hits.get();
206 }
207 if let Some(anchors) = self.execution_anchors.get(contract_id) {
208 for anchor in &anchors.anchors {
209 let hits = anchors.hits(hit_map, anchor.kind, is_deployed_code);
210 if hits > 0 {
211 *hits_by_item.entry(anchor.item_id).or_default() += hits;
212 }
213 }
214 }
215
216 let Some(items) = self.analyses.get(&contract_id.build_id) else {
217 return Vec::new();
218 };
219 hits_by_item
220 .into_iter()
221 .filter_map(|(item_id, hits)| {
222 let item = items.get(item_id)?;
223 Some((item, hits))
224 })
225 .collect()
226 }
227
228 pub fn retain_sources(&mut self, mut predicate: impl FnMut(&Path) -> bool) {
233 self.source_paths.retain(|_, paths| {
234 paths.retain(|_, path| predicate(path));
235 !paths.is_empty()
236 });
237
238 let source_paths = &self.source_paths;
239 self.source_paths_to_ids.retain(|build_id, paths| {
240 paths.retain(|_, source_id| {
241 source_paths.get(build_id).is_some_and(|paths| paths.contains_key(source_id))
242 });
243 !paths.is_empty()
244 });
245 }
246}
247
248#[derive(Clone, Debug, Default)]
250pub struct HitMaps(pub B256HashMap<HitMap>);
251
252impl HitMaps {
253 pub fn merge_opt(a: &mut Option<Self>, b: Option<Self>) {
255 match (a, b) {
256 (_, None) => {}
257 (a @ None, Some(b)) => *a = Some(b),
258 (Some(a), Some(b)) => a.merge(b),
259 }
260 }
261
262 pub fn merge(&mut self, other: Self) {
264 self.reserve(other.len());
265 for (code_hash, other) in other.0 {
266 self.entry(code_hash).and_modify(|e| e.merge(&other)).or_insert(other);
267 }
268 }
269
270 pub fn merged(mut self, other: Self) -> Self {
272 self.merge(other);
273 self
274 }
275}
276
277impl Deref for HitMaps {
278 type Target = B256HashMap<HitMap>;
279
280 fn deref(&self) -> &Self::Target {
281 &self.0
282 }
283}
284
285impl DerefMut for HitMaps {
286 fn deref_mut(&mut self) -> &mut Self::Target {
287 &mut self.0
288 }
289}
290
291#[derive(Clone, Copy, Debug)]
292enum CallData {
293 Empty,
294 Short,
295 Selector([u8; 4]),
296}
297
298impl CallData {
299 fn new(input: &[u8]) -> Self {
300 if input.is_empty() {
301 Self::Empty
302 } else if let Some(selector) = input.get(..4) {
303 Self::Selector(selector.try_into().unwrap())
304 } else {
305 Self::Short
306 }
307 }
308}
309
310#[derive(Clone, Copy, Debug, Default)]
311struct CallHits {
312 without_value: u32,
313 with_value: u32,
314}
315
316impl CallHits {
317 const fn hit(&mut self, with_value: bool) {
318 if with_value {
319 self.with_value += 1;
320 } else {
321 self.without_value += 1;
322 }
323 }
324
325 const fn merge(&mut self, other: Self) {
326 self.without_value += other.without_value;
327 self.with_value += other.with_value;
328 }
329
330 const fn total(self, payable: bool) -> u32 {
331 self.without_value + if payable { self.with_value } else { 0 }
332 }
333}
334
335#[derive(Clone, Debug)]
339pub struct HitMap {
340 hits: FxHashMap<u32, u32>,
341 bytecode: Bytes,
342 creations: u32,
343 empty_calls: CallHits,
344 short_calls: CallHits,
345 selector_calls: FxHashMap<[u8; 4], CallHits>,
346}
347
348impl HitMap {
349 #[inline]
351 pub fn new(bytecode: Bytes) -> Self {
352 Self {
353 bytecode,
354 hits: HashMap::with_capacity_and_hasher(1024, Default::default()),
355 creations: 0,
356 empty_calls: Default::default(),
357 short_calls: Default::default(),
358 selector_calls: Default::default(),
359 }
360 }
361
362 #[inline]
364 pub const fn bytecode(&self) -> &Bytes {
365 &self.bytecode
366 }
367
368 #[inline]
370 pub fn get(&self, pc: u32) -> Option<NonZeroU32> {
371 NonZeroU32::new(self.hits.get(&pc).copied().unwrap_or(0))
372 }
373
374 #[inline]
376 pub fn hit(&mut self, pc: u32) {
377 self.hits(pc, 1)
378 }
379
380 #[inline]
382 pub fn hits(&mut self, pc: u32, hits: u32) {
383 *self.hits.entry(pc).or_default() += hits;
384 }
385
386 fn call(&mut self, call: CallData, with_value: bool) {
387 let hits = match call {
388 CallData::Empty => &mut self.empty_calls,
389 CallData::Short => &mut self.short_calls,
390 CallData::Selector(selector) => self.selector_calls.entry(selector).or_default(),
391 };
392 hits.hit(with_value);
393 }
394
395 const fn creation(&mut self) {
396 self.creations += 1;
397 }
398
399 #[inline]
401 pub fn reserve(&mut self, additional: usize) {
402 self.hits.reserve(additional);
403 }
404
405 pub fn merge(&mut self, other: &Self) {
407 self.reserve(other.len());
408 for (pc, hits) in other.iter() {
409 self.hits(pc, hits);
410 }
411 self.creations += other.creations;
412 self.empty_calls.merge(other.empty_calls);
413 self.short_calls.merge(other.short_calls);
414 for (&selector, &hits) in &other.selector_calls {
415 self.selector_calls.entry(selector).or_default().merge(hits);
416 }
417 }
418
419 #[inline]
421 pub fn iter(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
422 self.hits.iter().map(|(&pc, &hits)| (pc, hits))
423 }
424
425 #[inline]
427 pub fn len(&self) -> usize {
428 self.hits.len()
429 }
430
431 #[inline]
433 pub fn is_empty(&self) -> bool {
434 self.hits.is_empty()
435 }
436}
437
438#[derive(Clone, Debug, PartialEq, Eq, Hash)]
440pub struct ContractId {
441 pub version: Version,
442 pub build_id: String,
443 pub source_id: usize,
444 pub contract_name: Arc<str>,
445}
446
447impl fmt::Display for ContractId {
448 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449 write!(
450 f,
451 "Contract \"{}\" (solc {}, source ID {})",
452 self.contract_name, self.version, self.source_id
453 )
454 }
455}
456
457#[derive(Clone, Debug)]
459pub struct ItemAnchor {
460 pub instruction: u32,
462 pub item_id: u32,
464}
465
466const _: () = assert!(std::mem::size_of::<ItemAnchor>() == 8);
467
468#[derive(Clone, Debug, Default)]
470pub struct ItemAnchors {
471 pub anchors: Vec<ItemAnchor>,
473 pub jumps: FxHashMap<usize, u32>,
476}
477
478impl ItemAnchors {
479 fn hits<'a>(
480 &'a self,
481 hit_map: &'a HitMap,
482 ) -> impl Iterator<Item = (&'a ItemAnchor, NonZeroU32)> {
483 self.anchors.iter().enumerate().filter_map(|(index, anchor)| {
484 let hits = if let Some(&jump) = self.jumps.get(&index) {
485 let hits = hit_map.get(jump)?.get();
488 NonZeroU32::new(
489 hits.saturating_sub(hit_map.get(jump + 1).map_or(0, NonZeroU32::get)),
490 )?
491 } else {
492 hit_map.get(anchor.instruction)?
493 };
494 Some((anchor, hits))
495 })
496 }
497}
498
499impl fmt::Display for ItemAnchor {
500 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501 write!(f, "IC {} -> Item {}", self.instruction, self.item_id)
502 }
503}
504
505#[derive(Clone, Copy, Debug)]
507pub struct ExecutionAnchor {
508 pub item_id: u32,
510 pub kind: ExecutionAnchorKind,
512}
513
514#[derive(Clone, Copy, Debug, PartialEq, Eq)]
516pub enum ExecutionAnchorKind {
517 Constructor,
519 Receive,
521 Fallback,
523}
524
525#[derive(Clone, Debug)]
526struct ContractExecutionAnchors {
527 anchors: Vec<ExecutionAnchor>,
528 function_selectors: FxHashSet<[u8; 4]>,
529 has_receive: bool,
530 fallback_payable: bool,
531}
532
533impl ContractExecutionAnchors {
534 fn hits(&self, hit_map: &HitMap, kind: ExecutionAnchorKind, is_deployed_code: bool) -> u32 {
535 match (kind, is_deployed_code) {
536 (ExecutionAnchorKind::Constructor, false) => hit_map.creations,
537 (ExecutionAnchorKind::Receive, true) => hit_map.empty_calls.total(true),
538 (ExecutionAnchorKind::Fallback, true) => {
539 let empty_calls = if self.has_receive {
540 0
541 } else {
542 hit_map.empty_calls.total(self.fallback_payable)
543 };
544 empty_calls
545 + hit_map.short_calls.total(self.fallback_payable)
546 + hit_map
547 .selector_calls
548 .iter()
549 .filter(|(selector, _)| !self.function_selectors.contains(*selector))
550 .map(|(_, hits)| hits.total(self.fallback_payable))
551 .sum::<u32>()
552 }
553 _ => 0,
554 }
555 }
556}
557
558#[derive(Clone, Debug)]
559pub enum CoverageItemKind {
560 Line,
562 Statement,
564 Branch {
566 branch_id: u32,
571 path_id: u32,
575 is_first_opcode: bool,
577 },
578 Function {
580 name: Box<str>,
582 },
583}
584
585impl PartialEq for CoverageItemKind {
586 fn eq(&self, other: &Self) -> bool {
587 self.ord_key() == other.ord_key()
588 }
589}
590
591impl Eq for CoverageItemKind {}
592
593impl PartialOrd for CoverageItemKind {
594 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
595 Some(self.cmp(other))
596 }
597}
598
599impl Ord for CoverageItemKind {
600 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
601 self.ord_key().cmp(&other.ord_key())
602 }
603}
604
605impl CoverageItemKind {
606 fn ord_key(&self) -> impl Ord + use<> {
607 match *self {
608 Self::Line => 0,
609 Self::Statement => 1,
610 Self::Branch { .. } => 2,
611 Self::Function { .. } => 3,
612 }
613 }
614}
615
616#[derive(Clone, Debug)]
617pub struct CoverageItem {
618 pub kind: CoverageItemKind,
620 pub loc: SourceLocation,
622 pub anchor_loc: Option<SourceLocation>,
624 pub hits: u32,
626}
627
628#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
629enum CoverageItemKindKey<'a> {
630 Line,
631 Statement,
632 Branch { branch_id: u32, path_id: u32 },
633 Function(&'a str),
634}
635
636#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
637struct CoverageItemKey<'a> {
638 line_start: u32,
639 line_end: u32,
640 kind_order: u8,
641 byte_start: u32,
642 byte_end: u32,
643 contract_name: &'a str,
644 kind: CoverageItemKindKey<'a>,
645}
646
647impl<'a> CoverageItemKey<'a> {
648 fn new(item: &'a CoverageItem) -> Self {
649 let (kind_order, kind) = match &item.kind {
650 CoverageItemKind::Line => (0, CoverageItemKindKey::Line),
651 CoverageItemKind::Statement => (1, CoverageItemKindKey::Statement),
652 CoverageItemKind::Branch { branch_id, path_id, .. } => {
653 (2, CoverageItemKindKey::Branch { branch_id: *branch_id, path_id: *path_id })
654 }
655 CoverageItemKind::Function { name } => {
656 (3, CoverageItemKindKey::Function(name.as_ref()))
657 }
658 };
659
660 Self {
661 line_start: item.loc.lines.start,
662 line_end: item.loc.lines.end,
663 kind_order,
664 byte_start: item.loc.bytes.start,
665 byte_end: item.loc.bytes.end,
666 contract_name: item.loc.contract_name.as_ref(),
667 kind,
668 }
669 }
670}
671
672impl PartialEq for CoverageItem {
673 fn eq(&self, other: &Self) -> bool {
674 self.ord_key() == other.ord_key()
675 }
676}
677
678impl Eq for CoverageItem {}
679
680impl PartialOrd for CoverageItem {
681 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
682 Some(self.cmp(other))
683 }
684}
685
686impl Ord for CoverageItem {
687 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
688 self.ord_key().cmp(&other.ord_key())
689 }
690}
691
692impl fmt::Display for CoverageItem {
693 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
694 self.fmt_with_source(None).fmt(f)
695 }
696}
697
698impl CoverageItem {
699 fn ord_key(&self) -> impl Ord + use<> {
700 (
701 self.loc.source_id,
702 self.loc.lines.start,
703 self.loc.lines.end,
704 self.kind.ord_key(),
705 self.loc.bytes.start,
706 self.loc.bytes.end,
707 )
708 }
709
710 pub fn fmt_with_source(&self, src: Option<&str>) -> impl fmt::Display {
711 solar::data_structures::fmt::from_fn(move |f| {
712 match &self.kind {
713 CoverageItemKind::Line => {
714 write!(f, "Line")?;
715 }
716 CoverageItemKind::Statement => {
717 write!(f, "Statement")?;
718 }
719 CoverageItemKind::Branch { branch_id, path_id, .. } => {
720 write!(f, "Branch (branch: {branch_id}, path: {path_id})")?;
721 }
722 CoverageItemKind::Function { name } => {
723 write!(f, r#"Function "{name}""#)?;
724 }
725 }
726 write!(f, " (location: ({}), hits: {})", self.loc, self.hits)?;
727
728 if let Some(src) = src
729 && let Some(src) = src.get(self.loc.bytes())
730 {
731 write!(f, " -> ")?;
732
733 let max_len = 64;
734 let max_half = max_len / 2;
735
736 if src.len() > max_len {
737 write!(f, "\"{}", src[..max_half].escape_debug())?;
738 write!(f, "...")?;
739 write!(f, "{}\"", src[src.len() - max_half..].escape_debug())?;
740 } else {
741 write!(f, "{src:?}")?;
742 }
743 }
744
745 Ok(())
746 })
747 }
748}
749
750#[derive(Clone, Debug)]
752pub struct SourceLocation {
753 pub source_id: usize,
755 pub contract_name: Arc<str>,
757 pub bytes: Range<u32>,
759 pub lines: Range<u32>,
761}
762
763impl fmt::Display for SourceLocation {
764 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
765 write!(f, "source ID: {}, lines: {:?}, bytes: {:?}", self.source_id, self.lines, self.bytes)
766 }
767}
768
769impl SourceLocation {
770 pub const fn bytes(&self) -> Range<usize> {
772 self.bytes.start as usize..self.bytes.end as usize
773 }
774
775 pub fn len(&self) -> u32 {
777 self.bytes.len() as u32
778 }
779
780 pub fn is_empty(&self) -> bool {
782 self.len() == 0
783 }
784}
785
786#[derive(Clone, Debug, Default)]
788pub struct CoverageSummary {
789 pub line_count: usize,
791 pub line_hits: usize,
793 pub statement_count: usize,
795 pub statement_hits: usize,
797 pub branch_count: usize,
799 pub branch_hits: usize,
801 pub function_count: usize,
803 pub function_hits: usize,
805}
806
807impl CoverageSummary {
808 pub fn new() -> Self {
810 Self::default()
811 }
812
813 pub fn from_items<'a>(items: impl IntoIterator<Item = &'a CoverageItem>) -> Self {
815 let mut summary = Self::default();
816 summary.add_items(items);
817 summary
818 }
819
820 pub const fn merge(&mut self, other: &Self) {
822 let Self {
823 line_count,
824 line_hits,
825 statement_count,
826 statement_hits,
827 branch_count,
828 branch_hits,
829 function_count,
830 function_hits,
831 } = self;
832 *line_count += other.line_count;
833 *line_hits += other.line_hits;
834 *statement_count += other.statement_count;
835 *statement_hits += other.statement_hits;
836 *branch_count += other.branch_count;
837 *branch_hits += other.branch_hits;
838 *function_count += other.function_count;
839 *function_hits += other.function_hits;
840 }
841
842 pub const fn add_item(&mut self, item: &CoverageItem) {
844 match item.kind {
845 CoverageItemKind::Line => {
846 self.line_count += 1;
847 if item.hits > 0 {
848 self.line_hits += 1;
849 }
850 }
851 CoverageItemKind::Statement => {
852 self.statement_count += 1;
853 if item.hits > 0 {
854 self.statement_hits += 1;
855 }
856 }
857 CoverageItemKind::Branch { .. } => {
858 self.branch_count += 1;
859 if item.hits > 0 {
860 self.branch_hits += 1;
861 }
862 }
863 CoverageItemKind::Function { .. } => {
864 self.function_count += 1;
865 if item.hits > 0 {
866 self.function_hits += 1;
867 }
868 }
869 }
870 }
871
872 pub fn add_items<'a>(&mut self, items: impl IntoIterator<Item = &'a CoverageItem>) {
874 for item in items {
875 self.add_item(item);
876 }
877 }
878}
879
880#[cfg(test)]
881mod tests {
882 use super::*;
883
884 #[test]
885 fn sparse_jump_hits_distinguish_shared_items_and_destinations() {
886 let anchors = ItemAnchors {
887 anchors: vec![
888 ItemAnchor { instruction: 20, item_id: 0 },
889 ItemAnchor { instruction: 20, item_id: 0 },
890 ItemAnchor { instruction: 20, item_id: 0 },
891 ],
892 jumps: [(1, 4), (2, 10)].into_iter().collect(),
893 };
894 for (jump_hits, fallthrough_hits, taken_hits) in
895 [(3, 2, 1), (3, 0, 3), (3, 3, 0), (0, 0, 0)]
896 {
897 let mut hit_map = HitMap::new(Bytes::new());
898 hit_map.hits(20, 5);
899 hit_map.hits(4, jump_hits);
900 hit_map.hits(5, fallthrough_hits);
901 hit_map.hits(10, 2);
902 hit_map.hits(11, 2);
903
904 let hits = anchors.hits(&hit_map).map(|(_, hits)| hits.get()).collect::<Vec<_>>();
905 let expected = if taken_hits == 0 { vec![5] } else { vec![5, taken_hits] };
906 assert_eq!(hits, expected);
907 }
908 }
909
910 #[test]
911 fn ordinary_anchors_do_not_allocate_jump_metadata() {
912 let anchors = ItemAnchors {
913 anchors: vec![ItemAnchor { instruction: 20, item_id: 0 }],
914 ..Default::default()
915 };
916 assert_eq!(anchors.jumps.capacity(), 0);
917 let mut hit_map = HitMap::new(Bytes::new());
918 assert_eq!(anchors.hits(&hit_map).count(), 0);
919 hit_map.hits(20, 3);
920 assert_eq!(anchors.hits(&hit_map).next().unwrap().1.get(), 3);
921 }
922}