Skip to main content

foundry_evm/inspectors/
edge_cov.rs

1use alloy_primitives::{
2    Address, U256,
3    map::{DefaultHashBuilder, Entry, HashMap},
4};
5use core::{
6    fmt,
7    hash::{BuildHasher, Hash, Hasher},
8};
9use revm::{
10    Inspector,
11    bytecode::opcode,
12    context::{ContextTr, JournalTr},
13    interpreter::{
14        Interpreter,
15        interpreter_types::{InputsTr, Jumps},
16    },
17};
18
19// Default capacity for the hitcount buffer.
20pub(crate) const MAX_EDGE_COUNT: usize = 65536;
21
22// Maximum number of unique comparison sites to track for CmpLog-style feedback.
23const MAX_CMP_LOG_SITES: usize = 1024;
24
25// Maximum number of comparison operand pairs to track per site. This matches the downstream loop
26// detection threshold so a hot loop can be classified without filling the whole log.
27const MAX_CMP_OBSERVATIONS_PER_SITE: u8 = 8;
28
29/// Edge coverage collection kind.
30#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
31pub enum EdgeCovKind {
32    /// Assign dense monotonically-increasing indices to unique edges.
33    #[default]
34    CollisionFree,
35    /// Preserve the legacy fixed-size hash ID calculation.
36    Hash,
37}
38
39/// Configuration for [`EdgeCovInspector`].
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct EdgeCovConfig {
42    /// Which edge coverage representation should be collected.
43    pub kind: EdgeCovKind,
44    /// Whether call-frame depth should be included in the edge identity.
45    pub include_call_depth: bool,
46}
47
48impl EdgeCovConfig {
49    /// Creates a new edge coverage configuration.
50    pub const fn new(kind: EdgeCovKind, include_call_depth: bool) -> Self {
51        Self { kind, include_call_depth }
52    }
53
54    /// Legacy fixed-size hash ID configuration.
55    pub const fn legacy_hash_ids() -> Self {
56        Self::new(EdgeCovKind::Hash, false)
57    }
58}
59
60impl Default for EdgeCovConfig {
61    fn default() -> Self {
62        Self::new(EdgeCovKind::CollisionFree, false)
63    }
64}
65
66impl From<&foundry_config::FuzzCorpusConfig> for EdgeCovConfig {
67    fn from(corpus: &foundry_config::FuzzCorpusConfig) -> Self {
68        let kind = if corpus.evm_edge_coverage_collision_free() {
69            EdgeCovKind::CollisionFree
70        } else {
71            EdgeCovKind::Hash
72        };
73        Self::new(kind, corpus.evm_edge_coverage_include_call_depth())
74    }
75}
76
77/// A comparison operand pair captured during execution.
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79pub struct CmpOperands {
80    /// First operand of the comparison.
81    pub op1: U256,
82    /// Second operand of the comparison.
83    pub op2: U256,
84    /// Program counter where the comparison occurred.
85    pub pc: usize,
86    /// Contract address where the comparison occurred.
87    pub address: Address,
88    /// EVM opcode that performed the comparison.
89    pub opcode: u8,
90}
91
92#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
93pub struct EdgeKey {
94    pub address: Address,
95    pub depth: Option<usize>,
96    pub pc: usize,
97    pub jump_dest: U256,
98}
99
100impl EdgeKey {
101    fn new(
102        address: Address,
103        depth: usize,
104        pc: usize,
105        jump_dest: U256,
106        include_depth: bool,
107    ) -> Self {
108        Self { address, depth: include_depth.then_some(depth), pc, jump_dest }
109    }
110}
111
112#[derive(Clone, Debug, Default)]
113pub struct EdgeIndexMap {
114    edge_indices: HashMap<EdgeKey, usize>,
115    next_index: usize,
116}
117
118impl EdgeIndexMap {
119    #[inline]
120    pub fn edge_index(&mut self, edge: EdgeKey) -> usize {
121        match self.edge_indices.entry(edge) {
122            Entry::Occupied(entry) => *entry.get(),
123            Entry::Vacant(entry) => {
124                let index = self.next_index;
125                self.next_index += 1;
126                entry.insert(index);
127                index
128            }
129        }
130    }
131
132    pub const fn edge_count(&self) -> usize {
133        self.next_index
134    }
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub struct EdgeCovHit {
139    pub edge: EdgeKey,
140    pub count: u8,
141}
142
143#[derive(Clone, Debug, Eq, PartialEq)]
144pub enum EdgeCoverage {
145    Hash(Vec<u8>),
146    CollisionFree(Vec<EdgeCovHit>),
147}
148
149impl EdgeCoverage {
150    pub fn is_empty(&self) -> bool {
151        match self {
152            Self::Hash(hitcount) => hitcount.iter().all(|&count| count == 0),
153            Self::CollisionFree(hits) => hits.is_empty(),
154        }
155    }
156}
157
158/// An `Inspector` that tracks [edge coverage](https://clang.llvm.org/docs/SanitizerCoverage.html#edge-coverage).
159/// Covered edges will not wrap to zero e.g. a loop edge hit more than 255 will still be retained.
160///
161/// Also tracks comparison operands for CmpLog-style guided fuzzing.
162// see https://github.com/AFLplusplus/AFLplusplus/blob/5777ceaf23f48ae4ceae60e4f3a79263802633c6/instrumentation/afl-llvm-pass.so.cc#L810-L829
163#[derive(Clone)]
164pub struct EdgeCovInspector {
165    /// Map of hitcounts that can be diffed against to determine if new coverage was reached.
166    hitcount: Vec<u8>,
167    /// Configuration for edge ID generation.
168    config: EdgeCovConfig,
169    /// Whether to collect edge hits. Comparison-only consumers keep this off to avoid affecting
170    /// the active coverage guidance source.
171    collect_edges: bool,
172    /// Per-execution dense edge hitcounts. Stable IDs are assigned by the corpus history owner.
173    dense_hitcount: HashMap<EdgeKey, u8>,
174    hash_builder: DefaultHashBuilder,
175    /// Comparison operand log for CmpLog-style guided fuzzing.
176    cmp_log: Option<Vec<CmpOperands>>,
177    cmp_site_counts: HashMap<CmpSiteKey, u8>,
178}
179
180impl fmt::Debug for EdgeCovInspector {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        f.debug_struct("EdgeCovInspector")
183            .field("capacity", &self.hitcount.len())
184            .field("edges", &self.edge_count())
185            .field("config", &self.config)
186            .field("collect_edges", &self.collect_edges)
187            .finish()
188    }
189}
190
191impl EdgeCovInspector {
192    /// Create a new `EdgeCovInspector` with default configuration and capacity.
193    pub fn new() -> Self {
194        Self::with_config(EdgeCovConfig::default())
195    }
196
197    /// Create a new `EdgeCovInspector` with comparison operand logging enabled.
198    pub fn with_cmp_log() -> Self {
199        let mut inspector = Self::new();
200        inspector.enable_cmp_log(true);
201        inspector
202    }
203
204    /// Create a comparison-operand inspector without collecting EVM edge hits.
205    pub fn with_cmp_log_only() -> Self {
206        let mut inspector = Self::new();
207        inspector.collect_edges = false;
208        inspector.enable_cmp_log(true);
209        inspector
210    }
211
212    /// Create a new `EdgeCovInspector` with the given configuration.
213    ///
214    /// [`EdgeCovKind::Hash`] preallocates a fixed-size bitmap;
215    /// [`EdgeCovKind::CollisionFree`] grows its dense map on demand.
216    pub fn with_config(config: EdgeCovConfig) -> Self {
217        let hitcount = match config.kind {
218            EdgeCovKind::Hash => vec![0; MAX_EDGE_COUNT],
219            EdgeCovKind::CollisionFree => Vec::new(),
220        };
221        Self {
222            hitcount,
223            config,
224            collect_edges: true,
225            dense_hitcount: HashMap::default(),
226            hash_builder: DefaultHashBuilder::default(),
227            cmp_log: None,
228            cmp_site_counts: HashMap::default(),
229        }
230    }
231
232    /// Set whether to collect comparison operand logs.
233    pub fn enable_cmp_log(&mut self, yes: bool) {
234        if yes {
235            self.cmp_log.get_or_insert_with(|| Vec::with_capacity(MAX_CMP_LOG_SITES));
236        } else {
237            self.cmp_log = None;
238            self.cmp_site_counts.clear();
239        }
240    }
241
242    /// Reset the hitcount to zero and clear the comparison log.
243    pub fn reset(&mut self) {
244        match self.config.kind {
245            EdgeCovKind::CollisionFree => self.dense_hitcount.clear(),
246            EdgeCovKind::Hash => self.hitcount.fill(0),
247        }
248        if let Some(cmp_log) = &mut self.cmp_log {
249            cmp_log.clear();
250        }
251        self.cmp_site_counts.clear();
252    }
253
254    /// Get an immutable reference to the comparison operand log.
255    pub const fn get_cmp_log(&self) -> &[CmpOperands] {
256        match &self.cmp_log {
257            Some(cmp_log) => cmp_log.as_slice(),
258            None => &[],
259        }
260    }
261
262    /// Consume the inspector and take ownership of both the hitcount and comparison log.
263    pub fn into_parts(mut self) -> (EdgeCoverage, Vec<CmpOperands>) {
264        let cmp_log = self.cmp_log.take().unwrap_or_default();
265        (self.into(), cmp_log)
266    }
267
268    /// Number of unique collision-free edges discovered so far.
269    pub fn edge_count(&self) -> usize {
270        self.dense_hitcount.len()
271    }
272
273    /// Mark the edge `(address, depth, pc, jump_dest)` as hit.
274    fn store_hit(&mut self, address: Address, depth: usize, pc: usize, jump_dest: U256) {
275        if !self.collect_edges {
276            return;
277        }
278
279        let edge_id = match self.config.kind {
280            EdgeCovKind::CollisionFree => {
281                self.store_dense_hit(address, depth, pc, jump_dest);
282                return;
283            }
284            EdgeCovKind::Hash => self.hash_edge_id(address, depth, pc, jump_dest),
285        };
286        self.hitcount[edge_id] = self.hitcount[edge_id].wrapping_add(1).max(1);
287    }
288
289    fn store_dense_hit(&mut self, address: Address, depth: usize, pc: usize, jump_dest: U256) {
290        let key = EdgeKey::new(address, depth, pc, jump_dest, self.config.include_call_depth);
291        let count = self.dense_hitcount.entry(key).or_default();
292        *count = count.wrapping_add(1).max(1);
293    }
294
295    fn hash_edge_id(
296        &mut self,
297        address: Address,
298        depth: usize,
299        pc: usize,
300        jump_dest: U256,
301    ) -> usize {
302        let mut hasher = self.hash_builder.build_hasher();
303        address.hash(&mut hasher);
304        if self.config.include_call_depth {
305            depth.hash(&mut hasher);
306        }
307        pc.hash(&mut hasher);
308        jump_dest.hash(&mut hasher);
309        // The hash is used to index into the hitcount array,
310        // so it must be modulo the map size.
311        (hasher.finish() % self.hitcount.len() as u64) as usize
312    }
313
314    #[cfg(test)]
315    fn dense_hits(&self) -> Vec<EdgeCovHit> {
316        let mut hits = self
317            .dense_hitcount
318            .iter()
319            .map(|(&edge, &count)| EdgeCovHit { edge, count })
320            .collect::<Vec<_>>();
321        hits.sort_by_key(|hit| hit.edge);
322        hits
323    }
324
325    /// Store comparison operands for CmpLog-style guided fuzzing.
326    fn store_cmp(&mut self, cmp: CmpOperands) {
327        let Some(cmp_log) = &mut self.cmp_log else {
328            return;
329        };
330
331        let site = CmpSiteKey::new(&cmp);
332        if let Some(count) = self.cmp_site_counts.get_mut(&site) {
333            if *count >= MAX_CMP_OBSERVATIONS_PER_SITE {
334                return;
335            }
336            *count += 1;
337            cmp_log.push(cmp);
338        } else if self.cmp_site_counts.len() < MAX_CMP_LOG_SITES {
339            self.cmp_site_counts.insert(site, 1);
340            cmp_log.push(cmp);
341        }
342    }
343
344    #[cold]
345    fn do_step<CTX>(&mut self, interp: &mut Interpreter, context: &mut CTX)
346    where
347        CTX: ContextTr,
348    {
349        let address = interp.input.target_address();
350        let depth = context.journal_ref().depth();
351        let current_pc = interp.bytecode.pc();
352
353        match interp.bytecode.opcode() {
354            opcode::JUMP => {
355                // unconditional jump
356                if let Ok(jump_dest) = interp.stack.peek(0) {
357                    self.store_hit(address, depth, current_pc, jump_dest);
358                }
359            }
360            opcode::JUMPI => {
361                if let Ok(stack_value) = interp.stack.peek(1) {
362                    let jump_dest = if stack_value.is_zero() {
363                        // fall through
364                        Ok(U256::from(current_pc + 1))
365                    } else {
366                        // branch taken
367                        interp.stack.peek(0)
368                    };
369
370                    if let Ok(jump_dest) = jump_dest {
371                        self.store_hit(address, depth, current_pc, jump_dest);
372                    }
373                }
374            }
375            _ => {
376                // no-op
377            }
378        }
379    }
380
381    #[cold]
382    fn do_cmp_step(&mut self, interp: &mut Interpreter) {
383        if self.cmp_log.is_none() {
384            return;
385        }
386
387        let address = interp.input.target_address();
388        let current_pc = interp.bytecode.pc();
389
390        match interp.bytecode.opcode() {
391            op @ (opcode::EQ | opcode::LT | opcode::SLT | opcode::GT | opcode::SGT) => {
392                if let (Ok(op1), Ok(op2)) = (interp.stack.peek(0), interp.stack.peek(1)) {
393                    self.store_cmp(CmpOperands { op1, op2, pc: current_pc, address, opcode: op });
394                }
395            }
396            op @ opcode::ISZERO => {
397                if let Ok(op1) = interp.stack.peek(0) {
398                    self.store_cmp(CmpOperands {
399                        op1,
400                        op2: U256::ZERO,
401                        pc: current_pc,
402                        address,
403                        opcode: op,
404                    });
405                }
406            }
407            _ => {}
408        }
409    }
410}
411
412impl Default for EdgeCovInspector {
413    fn default() -> Self {
414        Self::new()
415    }
416}
417
418impl From<EdgeCovInspector> for EdgeCoverage {
419    fn from(inspector: EdgeCovInspector) -> Self {
420        let EdgeCovInspector { hitcount, config, dense_hitcount, .. } = inspector;
421        match config.kind {
422            // Hits are deliberately not sorted here — this is the per-call drain
423            // path and `merge_edge_coverage` doesn't care about order. Consumers
424            // that need a deterministic order (e.g. `snapshot_edge_fingerprint`)
425            // sort locally.
426            EdgeCovKind::CollisionFree => Self::CollisionFree(
427                dense_hitcount
428                    .into_iter()
429                    .map(|(edge, count)| EdgeCovHit { edge, count })
430                    .collect(),
431            ),
432            EdgeCovKind::Hash => Self::Hash(hitcount),
433        }
434    }
435}
436
437impl<CTX> Inspector<CTX> for EdgeCovInspector
438where
439    CTX: ContextTr,
440{
441    #[inline]
442    fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) {
443        let op = interp.bytecode.opcode();
444        if self.collect_edges && matches!(op, opcode::JUMP | opcode::JUMPI) {
445            self.do_step(interp, context);
446        }
447        if self.cmp_log.is_some()
448            && matches!(
449                op,
450                opcode::EQ | opcode::LT | opcode::GT | opcode::SLT | opcode::SGT | opcode::ISZERO
451            )
452        {
453            self.do_cmp_step(interp);
454        }
455    }
456}
457
458#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
459struct CmpSiteKey {
460    address: Address,
461    pc: usize,
462    opcode: u8,
463}
464
465impl CmpSiteKey {
466    const fn new(cmp: &CmpOperands) -> Self {
467        Self { address: cmp.address, pc: cmp.pc, opcode: cmp.opcode }
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    fn dense_counts(inspector: &EdgeCovInspector) -> Vec<u8> {
476        inspector.dense_hits().into_iter().map(|hit| hit.count).collect()
477    }
478
479    #[test]
480    fn cmp_operands_defaults_and_clones() {
481        let cmp = CmpOperands {
482            op1: U256::from(123),
483            op2: U256::from(456),
484            pc: 42,
485            address: Address::repeat_byte(0xaa),
486            opcode: opcode::EQ,
487        };
488
489        assert_eq!(cmp.op1, U256::from(123));
490        assert_eq!(cmp.op2, U256::from(456));
491        assert_eq!(cmp.pc, 42);
492
493        assert_eq!(CmpOperands::default(), CmpOperands::default());
494        let cloned = cmp;
495        assert_eq!(cloned, cmp);
496    }
497
498    #[test]
499    fn cmp_log_starts_empty_and_is_returned_by_into_parts() {
500        let inspector = EdgeCovInspector::new();
501
502        assert!(inspector.get_cmp_log().is_empty());
503
504        let (coverage, cmp_log) = inspector.into_parts();
505        assert_eq!(coverage, EdgeCoverage::CollisionFree(Vec::new()));
506        assert!(cmp_log.is_empty());
507    }
508
509    #[test]
510    fn cmp_log_only_does_not_collect_edges() {
511        let mut inspector = EdgeCovInspector::with_cmp_log_only();
512        let addr = Address::ZERO;
513
514        inspector.store_hit(addr, 0, 0, U256::from(1));
515        inspector.store_cmp(CmpOperands {
516            op1: U256::from(123),
517            op2: U256::from(456),
518            pc: 42,
519            address: addr,
520            opcode: opcode::EQ,
521        });
522
523        let (coverage, cmp_log) = inspector.into_parts();
524        assert_eq!(coverage, EdgeCoverage::CollisionFree(Vec::new()));
525        assert_eq!(cmp_log.len(), 1);
526    }
527
528    #[test]
529    fn collision_free_ids() {
530        let mut inspector = EdgeCovInspector::new();
531        let addr = Address::ZERO;
532
533        inspector.store_hit(addr, 0, 0, U256::from(10));
534        inspector.store_hit(addr, 0, 0, U256::from(20));
535        inspector.store_hit(addr, 0, 1, U256::from(10));
536
537        assert_eq!(inspector.edge_count(), 3);
538        assert_eq!(dense_counts(&inspector), [1, 1, 1]);
539    }
540
541    #[test]
542    fn same_edge_increments_same_dense_slot() {
543        let mut inspector = EdgeCovInspector::new();
544        let addr = Address::ZERO;
545
546        for _ in 0..5 {
547            inspector.store_hit(addr, 0, 42, U256::from(100));
548        }
549
550        assert_eq!(inspector.edge_count(), 1);
551        assert_eq!(dense_counts(&inspector), [5]);
552    }
553
554    #[test]
555    fn edge_index_map_keeps_indices_stable() {
556        let addr = Address::ZERO;
557        let mut indices = EdgeIndexMap::default();
558        let first = EdgeKey::new(addr, 0, 0, U256::from(10), false);
559        let second = EdgeKey::new(addr, 0, 0, U256::from(20), false);
560
561        assert_eq!(indices.edge_index(first), 0);
562        assert_eq!(indices.edge_index(second), 1);
563        assert_eq!(indices.edge_index(first), 0);
564        assert_eq!(indices.edge_count(), 2);
565    }
566
567    #[test]
568    fn hitcount_neverzero_on_wrap() {
569        let mut inspector = EdgeCovInspector::new();
570        let addr = Address::ZERO;
571
572        for _ in 0..256 {
573            inspector.store_hit(addr, 0, 0, U256::from(1));
574        }
575
576        assert_eq!(inspector.edge_count(), 1);
577        assert_eq!(dense_counts(&inspector), [1]);
578    }
579
580    #[test]
581    fn reset_clears_dense_hitcounts() {
582        let mut inspector = EdgeCovInspector::new();
583        let addr = Address::ZERO;
584
585        inspector.store_hit(addr, 0, 0, U256::from(1));
586        inspector.store_hit(addr, 0, 0, U256::from(2));
587        assert_eq!(inspector.edge_count(), 2);
588        assert_eq!(dense_counts(&inspector), [1, 1]);
589
590        inspector.reset();
591        assert_eq!(inspector.edge_count(), 0);
592        assert!(inspector.dense_hits().is_empty());
593
594        inspector.store_hit(addr, 0, 0, U256::from(1));
595        assert_eq!(inspector.edge_count(), 1);
596        assert_eq!(dense_counts(&inspector), [1]);
597    }
598
599    #[test]
600    fn legacy_hash_ids_match_old_calculation() {
601        let mut inspector = EdgeCovInspector::with_config(EdgeCovConfig::legacy_hash_ids());
602        let addr = Address::ZERO;
603        let pc = 42;
604        let jump_dest = U256::from(100);
605
606        let mut hasher = inspector.hash_builder.build_hasher();
607        addr.hash(&mut hasher);
608        pc.hash(&mut hasher);
609        jump_dest.hash(&mut hasher);
610        let expected_id = (hasher.finish() % MAX_EDGE_COUNT as u64) as usize;
611
612        inspector.store_hit(addr, 0, pc, jump_dest);
613
614        assert_eq!(inspector.hitcount[expected_id], 1);
615        assert_eq!(inspector.hitcount.iter().filter(|&&count| count != 0).count(), 1);
616    }
617
618    #[test]
619    fn call_depth_option_delineates_same_edge() {
620        let addr = Address::ZERO;
621
622        let mut without_depth = EdgeCovInspector::new();
623        without_depth.store_hit(addr, 0, 0, U256::from(1));
624        without_depth.store_hit(addr, 1, 0, U256::from(1));
625        assert_eq!(without_depth.edge_count(), 1);
626        assert_eq!(dense_counts(&without_depth), [2]);
627
628        let mut with_depth =
629            EdgeCovInspector::with_config(EdgeCovConfig::new(EdgeCovKind::CollisionFree, true));
630        with_depth.store_hit(addr, 0, 0, U256::from(1));
631        with_depth.store_hit(addr, 1, 0, U256::from(1));
632        assert_eq!(with_depth.edge_count(), 2);
633        assert_eq!(dense_counts(&with_depth), [1, 1]);
634    }
635
636    #[test]
637    fn reset_clears_hitcount_and_cmp_log() {
638        let mut inspector = EdgeCovInspector::with_cmp_log();
639
640        inspector.store_hit(Address::ZERO, 0, 0, U256::from(1));
641        inspector.store_cmp(CmpOperands {
642            op1: U256::from(123),
643            op2: U256::from(456),
644            pc: 42,
645            address: Address::ZERO,
646            opcode: opcode::EQ,
647        });
648
649        inspector.reset();
650
651        assert!(inspector.dense_hits().is_empty());
652        assert!(inspector.get_cmp_log().is_empty());
653    }
654
655    #[test]
656    fn cmp_log_is_capped_per_site() {
657        let mut inspector = EdgeCovInspector::with_cmp_log();
658
659        for i in 0..usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) + 1 {
660            inspector.store_cmp(CmpOperands {
661                op1: U256::from(i),
662                op2: U256::from(i + 1),
663                pc: 42,
664                address: Address::ZERO,
665                opcode: opcode::EQ,
666            });
667        }
668
669        assert_eq!(inspector.get_cmp_log().len(), usize::from(MAX_CMP_OBSERVATIONS_PER_SITE));
670    }
671
672    #[test]
673    fn cmp_log_caps_sites_without_starving_later_observations() {
674        let mut inspector = EdgeCovInspector::with_cmp_log();
675
676        for i in 0..usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) * 2 {
677            inspector.store_cmp(CmpOperands {
678                op1: U256::from(i),
679                op2: U256::from(i + 1),
680                pc: 1,
681                address: Address::ZERO,
682                opcode: opcode::EQ,
683            });
684        }
685        inspector.store_cmp(CmpOperands {
686            op1: U256::from(123),
687            op2: U256::from(456),
688            pc: 2,
689            address: Address::ZERO,
690            opcode: opcode::EQ,
691        });
692
693        assert_eq!(inspector.get_cmp_log().len(), usize::from(MAX_CMP_OBSERVATIONS_PER_SITE) + 1);
694        assert_eq!(inspector.get_cmp_log().last().unwrap().pc, 2);
695    }
696}