Skip to main content

foundry_evm/executors/fuzz/
frontier.rs

1use crate::inspectors::CmpOperands;
2use alloy_json_abi::Function;
3use alloy_primitives::{
4    Address, I256, U256,
5    map::{Entry, HashMap},
6};
7use foundry_common::fs;
8use foundry_evm_fuzz::{BasicTxDetails, FuzzRunMetadata};
9use revm::bytecode::opcode;
10use serde::{Serialize, Serializer};
11use std::{
12    path::Path,
13    sync::Arc,
14    time::{SystemTime, UNIX_EPOCH},
15};
16
17const FRONTIER_SCHEMA: &str = "foundry:fuzz.branch-frontiers@v1";
18const STATEFUL_FRONTIER_SCHEMA: &str = "foundry:fuzz.branch-frontiers@v2";
19pub(super) const FRONTIER_FILE: &str = "branch-frontiers.json";
20
21#[derive(Debug, Serialize)]
22pub(super) struct FuzzBranchFrontierArtifact {
23    /// Stable artifact schema identifier for downstream symbolic consumers.
24    schema: &'static str,
25    /// Schema version for consumers that prefer numeric dispatch.
26    version: u32,
27    /// Unix timestamp, in seconds, when the artifact was written.
28    generated_at: u64,
29    /// Fuzz test signature that produced the frontier records.
30    test: String,
31    /// Configured maximum number of records retained for this test.
32    limit: usize,
33    /// Captured comparison frontiers.
34    frontiers: Vec<FuzzBranchFrontier>,
35}
36
37impl FuzzBranchFrontierArtifact {
38    pub(super) fn new(
39        func: &Function,
40        limit: usize,
41        mut frontiers: Vec<FuzzBranchFrontier>,
42    ) -> Self {
43        for (id, frontier) in frontiers.iter_mut().enumerate() {
44            frontier.id = id as u64;
45        }
46
47        Self {
48            schema: FRONTIER_SCHEMA,
49            version: 1,
50            generated_at: SystemTime::now()
51                .duration_since(UNIX_EPOCH)
52                .expect("time went backwards")
53                .as_secs(),
54            test: func.signature(),
55            limit,
56            frontiers,
57        }
58    }
59}
60
61#[derive(Debug, Serialize)]
62pub(in crate::executors) struct StatefulFuzzBranchFrontierArtifact {
63    /// Stable artifact schema identifier for downstream symbolic consumers.
64    schema: &'static str,
65    /// Schema version for consumers that prefer numeric dispatch.
66    version: u32,
67    /// Unix timestamp, in seconds, when the artifact was written.
68    generated_at: u64,
69    /// Invariant campaign anchor that produced the frontier records.
70    test: String,
71    /// Configured maximum number of records retained for this campaign.
72    limit: usize,
73    /// Deduplicated concrete transaction sequences referenced by frontier records.
74    #[serde(serialize_with = "serialize_sequences")]
75    sequences: Vec<StatefulFuzzSequence>,
76    /// Captured comparison frontiers.
77    frontiers: Vec<StatefulFuzzBranchFrontier>,
78}
79
80impl StatefulFuzzBranchFrontierArtifact {
81    pub(in crate::executors) fn new(
82        func: &Function,
83        limit: usize,
84        frontiers: Vec<FuzzBranchFrontier>,
85    ) -> Self {
86        let mut sequence_indexes = HashMap::<*const BasicTxDetails, usize>::default();
87        let mut sequences = Vec::<StatefulFuzzSequence>::new();
88        let mut records = Vec::with_capacity(frontiers.len());
89        for (id, frontier) in frontiers.into_iter().enumerate() {
90            let sequence_key = frontier.sequence.as_ptr();
91            let required_len = frontier.call_index + 1;
92            let sequence_index = match sequence_indexes.entry(sequence_key) {
93                Entry::Occupied(entry) => {
94                    let index = *entry.get();
95                    sequences[index].len = sequences[index].len.max(required_len);
96                    index
97                }
98                Entry::Vacant(entry) => {
99                    let index = sequences.len();
100                    entry.insert(index);
101                    sequences.push(StatefulFuzzSequence {
102                        calls: Arc::clone(&frontier.sequence),
103                        len: required_len,
104                    });
105                    index
106                }
107            };
108            records.push(StatefulFuzzBranchFrontier {
109                id: id as u64,
110                call_index: frontier.call_index,
111                sequence_index,
112                site: frontier.site,
113                operands: frontier.operands,
114            });
115        }
116
117        Self {
118            schema: STATEFUL_FRONTIER_SCHEMA,
119            version: 2,
120            generated_at: SystemTime::now()
121                .duration_since(UNIX_EPOCH)
122                .expect("time went backwards")
123                .as_secs(),
124            test: func.signature(),
125            limit,
126            sequences,
127            frontiers: records,
128        }
129    }
130}
131
132#[derive(Debug)]
133struct StatefulFuzzSequence {
134    calls: Arc<[BasicTxDetails]>,
135    len: usize,
136}
137
138#[derive(Debug, Serialize)]
139pub(in crate::executors) struct FuzzBranchFrontier {
140    /// Unique record identifier.
141    id: u64,
142    /// Reproducible fuzz seed, if configured.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    seed: Option<U256>,
145    /// 1-based fuzz run number, if known.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    run: Option<u32>,
148    /// Fuzz worker that produced the record, if known.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    worker: Option<u32>,
151    /// Whether this call also expanded coverage from the worker's current map. Only present when
152    /// edge coverage is collected (corpus, edge-coverage metrics, or sancov); omitted otherwise.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    new_coverage: Option<bool>,
155    /// Index of the call in the recorded sequence. Stateless fuzzing records one call.
156    call_index: usize,
157    /// Concrete call sequence that reached the frontier.
158    #[serde(serialize_with = "serialize_sequence")]
159    sequence: Arc<[BasicTxDetails]>,
160    /// EVM comparison site to target symbolically.
161    site: FuzzBranchFrontierSite,
162    /// Concrete operands observed at the site.
163    operands: FuzzBranchFrontierOperands,
164}
165
166#[derive(Debug, Serialize)]
167struct StatefulFuzzBranchFrontier {
168    id: u64,
169    call_index: usize,
170    sequence_index: usize,
171    site: FuzzBranchFrontierSite,
172    operands: FuzzBranchFrontierOperands,
173}
174
175#[derive(Clone, Copy, Debug, Serialize)]
176struct FuzzBranchFrontierSite {
177    address: Address,
178    pc: usize,
179    opcode: u8,
180    opcode_name: &'static str,
181}
182
183#[derive(Clone, Copy, Debug, Serialize)]
184struct FuzzBranchFrontierOperands {
185    lhs: U256,
186    rhs: U256,
187    /// Result of evaluating the captured comparison with these concrete operands.
188    result: bool,
189    /// Absolute operand delta interpreted according to the comparison opcode's signedness.
190    operand_delta: U256,
191}
192
193#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
194struct FuzzBranchFrontierKey {
195    address: Address,
196    pc: usize,
197    opcode: u8,
198    result: bool,
199}
200
201impl FuzzBranchFrontierKey {
202    const fn new(cmp: &CmpOperands, result: bool) -> Self {
203        Self { address: cmp.address, pc: cmp.pc, opcode: cmp.opcode, result }
204    }
205}
206
207#[derive(Debug, Default)]
208pub(in crate::executors) struct FuzzFrontierRecorder {
209    limit: usize,
210    frontiers: Vec<FuzzBranchFrontier>,
211    indexes: HashMap<FuzzBranchFrontierKey, usize>,
212}
213
214impl FuzzFrontierRecorder {
215    pub(in crate::executors) fn new(limit: usize) -> Self {
216        Self { limit, frontiers: Vec::with_capacity(limit.min(32)), indexes: HashMap::default() }
217    }
218
219    pub(super) fn capture_call(
220        &mut self,
221        run: Option<&FuzzRunMetadata>,
222        sequence: &[BasicTxDetails],
223        call_index: usize,
224        cmp_values: &[CmpOperands],
225        new_coverage: Option<bool>,
226    ) {
227        if self.limit == 0 || cmp_values.is_empty() {
228            return;
229        }
230        debug_assert!(call_index < sequence.len());
231
232        let mut recorded_sequence = None;
233        self.capture_comparisons(
234            run,
235            sequence,
236            call_index,
237            cmp_values,
238            new_coverage,
239            &mut recorded_sequence,
240        );
241    }
242
243    pub(in crate::executors) fn capture_sequence(
244        &mut self,
245        sequence: &[BasicTxDetails],
246        cmp_sequence: &[Vec<CmpOperands>],
247    ) {
248        if self.limit == 0 || cmp_sequence.is_empty() {
249            return;
250        }
251        debug_assert!(sequence.len() >= cmp_sequence.len());
252        let sequence = &sequence[..cmp_sequence.len()];
253
254        let mut recorded_sequence = None;
255        for (call_index, cmp_values) in cmp_sequence.iter().enumerate() {
256            self.capture_comparisons(
257                None,
258                sequence,
259                call_index,
260                cmp_values,
261                None,
262                &mut recorded_sequence,
263            );
264        }
265    }
266
267    fn capture_comparisons(
268        &mut self,
269        run: Option<&FuzzRunMetadata>,
270        sequence: &[BasicTxDetails],
271        call_index: usize,
272        cmp_values: &[CmpOperands],
273        new_coverage: Option<bool>,
274        recorded_sequence: &mut Option<Arc<[BasicTxDetails]>>,
275    ) {
276        if cmp_values.is_empty() {
277            return;
278        }
279
280        let mut new_frontier = |cmp: &CmpOperands, result, operand_delta| {
281            let sequence = recorded_sequence.get_or_insert_with(|| Arc::from(sequence));
282            FuzzBranchFrontier::new(
283                run,
284                Arc::clone(sequence),
285                *cmp,
286                result,
287                operand_delta,
288                new_coverage,
289                call_index,
290            )
291        };
292
293        for cmp in cmp_values {
294            let result = comparison_result(cmp);
295            let key = FuzzBranchFrontierKey::new(cmp, result);
296            let operand_delta = operand_delta(cmp);
297
298            match self.indexes.entry(key) {
299                Entry::Occupied(entry) => {
300                    let index = *entry.get();
301                    let previous = &self.frontiers[index];
302                    if operand_delta < previous.operands.operand_delta
303                        || (operand_delta == previous.operands.operand_delta
304                            && call_index < previous.call_index)
305                    {
306                        self.frontiers[index] = new_frontier(cmp, result, operand_delta);
307                    }
308                }
309                Entry::Vacant(entry) => {
310                    if self.frontiers.len() < self.limit {
311                        entry.insert(self.frontiers.len());
312                        let frontier = new_frontier(cmp, result, operand_delta);
313                        self.frontiers.push(frontier);
314                    }
315                }
316            }
317        }
318    }
319
320    pub(in crate::executors) fn into_frontiers(self) -> Vec<FuzzBranchFrontier> {
321        self.frontiers
322    }
323}
324
325impl FuzzBranchFrontier {
326    const fn key(&self) -> FuzzBranchFrontierKey {
327        FuzzBranchFrontierKey {
328            address: self.site.address,
329            pc: self.site.pc,
330            opcode: self.site.opcode,
331            result: self.operands.result,
332        }
333    }
334
335    fn new(
336        run: Option<&FuzzRunMetadata>,
337        sequence: Arc<[BasicTxDetails]>,
338        cmp: CmpOperands,
339        result: bool,
340        operand_delta: U256,
341        new_coverage: Option<bool>,
342        call_index: usize,
343    ) -> Self {
344        Self {
345            id: 0,
346            seed: run.and_then(|run| run.seed),
347            run: run.and_then(|run| run.run),
348            worker: run.and_then(|run| run.worker),
349            new_coverage,
350            call_index,
351            sequence,
352            site: FuzzBranchFrontierSite {
353                address: cmp.address,
354                pc: cmp.pc,
355                opcode: cmp.opcode,
356                opcode_name: opcode_name(cmp.opcode),
357            },
358            operands: FuzzBranchFrontierOperands {
359                lhs: cmp.op1,
360                rhs: cmp.op2,
361                result,
362                operand_delta,
363            },
364        }
365    }
366}
367
368/// Merges per-worker frontier records into a single bounded, globally deduplicated set.
369///
370/// Each worker deduplicates its own records by comparison site key while keeping the smallest
371/// `operand_delta`, but workers run independently, so the same site can appear in several workers'
372/// records with different observed deltas. This applies the same key dedup and smallest-delta
373/// policy across all workers so the artifact keeps one globally closest record per site and does
374/// not spend `limit` on duplicates. Iteration continues after `limit` is reached because a later
375/// record may be a smaller-delta duplicate of an already retained key.
376pub(in crate::executors) fn merge_frontiers(
377    limit: usize,
378    frontiers: impl IntoIterator<Item = FuzzBranchFrontier>,
379) -> Vec<FuzzBranchFrontier> {
380    if limit == 0 {
381        return Vec::new();
382    }
383
384    let mut merged = Vec::<FuzzBranchFrontier>::with_capacity(limit.min(32));
385    let mut indexes = HashMap::<FuzzBranchFrontierKey, usize>::default();
386    for frontier in frontiers {
387        match indexes.entry(frontier.key()) {
388            Entry::Occupied(entry) => {
389                let index = *entry.get();
390                let previous = &merged[index];
391                if frontier.operands.operand_delta < previous.operands.operand_delta
392                    || (frontier.operands.operand_delta == previous.operands.operand_delta
393                        && frontier.call_index < previous.call_index)
394                {
395                    merged[index] = frontier;
396                }
397            }
398            Entry::Vacant(entry) => {
399                if merged.len() < limit {
400                    entry.insert(merged.len());
401                    merged.push(frontier);
402                }
403            }
404        }
405    }
406
407    merged
408}
409
410pub(in crate::executors) fn write_frontier_artifact(
411    dir: &Path,
412    artifact: &impl Serialize,
413) -> fs::Result<()> {
414    fs::create_dir_all(dir)?;
415    fs::write_json_file(&dir.join(FRONTIER_FILE), artifact)
416}
417
418fn serialize_sequence<S>(sequence: &Arc<[BasicTxDetails]>, serializer: S) -> Result<S::Ok, S::Error>
419where
420    S: Serializer,
421{
422    Serialize::serialize(sequence.as_ref(), serializer)
423}
424
425fn serialize_sequences<S>(
426    sequences: &[StatefulFuzzSequence],
427    serializer: S,
428) -> Result<S::Ok, S::Error>
429where
430    S: Serializer,
431{
432    serializer.collect_seq(sequences.iter().map(|sequence| &sequence.calls[..sequence.len]))
433}
434
435fn comparison_result(cmp: &CmpOperands) -> bool {
436    match cmp.opcode {
437        opcode::EQ => cmp.op1 == cmp.op2,
438        opcode::LT => cmp.op1 < cmp.op2,
439        opcode::GT => cmp.op1 > cmp.op2,
440        opcode::SLT => I256::from_raw(cmp.op1) < I256::from_raw(cmp.op2),
441        opcode::SGT => I256::from_raw(cmp.op1) > I256::from_raw(cmp.op2),
442        opcode::ISZERO => cmp.op1.is_zero(),
443        _ => false,
444    }
445}
446
447fn operand_delta(cmp: &CmpOperands) -> U256 {
448    match cmp.opcode {
449        opcode::SLT | opcode::SGT => signed_operand_delta(cmp.op1, cmp.op2),
450        _ => unsigned_operand_delta(cmp.op1, cmp.op2),
451    }
452}
453
454fn unsigned_operand_delta(left: U256, right: U256) -> U256 {
455    if left >= right { left - right } else { right - left }
456}
457
458fn signed_operand_delta(left: U256, right: U256) -> U256 {
459    let (left_negative, left_magnitude) = signed_magnitude(left);
460    let (right_negative, right_magnitude) = signed_magnitude(right);
461
462    if left_negative == right_negative {
463        unsigned_operand_delta(left_magnitude, right_magnitude)
464    } else {
465        left_magnitude + right_magnitude
466    }
467}
468
469fn signed_magnitude(value: U256) -> (bool, U256) {
470    let negative = I256::from_raw(value) < I256::ZERO;
471    let magnitude = if negative { U256::ZERO.wrapping_sub(value) } else { value };
472    (negative, magnitude)
473}
474
475const fn opcode_name(op: u8) -> &'static str {
476    match op {
477        opcode::EQ => "EQ",
478        opcode::LT => "LT",
479        opcode::GT => "GT",
480        opcode::SLT => "SLT",
481        opcode::SGT => "SGT",
482        opcode::ISZERO => "ISZERO",
483        _ => "UNKNOWN",
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use alloy_primitives::Bytes;
491    use foundry_evm_fuzz::CallDetails;
492
493    #[test]
494    fn stateless_frontier_preserves_concrete_transaction() {
495        let tx = BasicTxDetails {
496            warp: None,
497            roll: None,
498            sender: Address::with_last_byte(1),
499            call_details: CallDetails {
500                target: Address::with_last_byte(2),
501                calldata: Bytes::from_static(&[1, 2, 3, 4]),
502                value: Some(U256::from(7)),
503            },
504        };
505        let cmp = CmpOperands {
506            op1: U256::ZERO,
507            op2: U256::from(1),
508            pc: 1,
509            address: tx.call_details.target,
510            opcode: opcode::LT,
511        };
512        let mut recorder = FuzzFrontierRecorder::new(1);
513
514        recorder.capture_call(None, std::slice::from_ref(&tx), 0, &[cmp], Some(true));
515
516        let frontier = recorder.into_frontiers().pop().unwrap();
517        assert_eq!(frontier.sequence.len(), 1);
518        assert_eq!(frontier.call_index, 0);
519        let recorded = &frontier.sequence[0];
520        assert_eq!(recorded.sender, tx.sender);
521        assert_eq!(recorded.call_details.target, tx.call_details.target);
522        assert_eq!(recorded.call_details.calldata, tx.call_details.calldata);
523        assert_eq!(recorded.call_details.value, tx.call_details.value);
524    }
525
526    #[test]
527    fn operand_delta_uses_signed_distance_for_signed_comparisons() {
528        let unsigned = CmpOperands {
529            op1: U256::MAX,
530            op2: U256::from(1),
531            pc: 0,
532            address: Address::ZERO,
533            opcode: opcode::LT,
534        };
535        assert_eq!(operand_delta(&unsigned), U256::MAX - U256::from(1));
536
537        let signed = CmpOperands { opcode: opcode::SLT, ..unsigned };
538        assert_eq!(operand_delta(&signed), U256::from(2));
539    }
540
541    #[test]
542    fn signed_operand_delta_handles_full_int256_range() {
543        let min = I256::MIN.into_raw();
544        let max = I256::MAX.into_raw();
545
546        assert_eq!(signed_operand_delta(min, max), U256::MAX);
547    }
548
549    fn frontier(pc: usize, result: bool, operand_delta: u64) -> FuzzBranchFrontier {
550        frontier_at(Address::ZERO, pc, result, operand_delta)
551    }
552
553    fn frontier_at(
554        address: Address,
555        pc: usize,
556        result: bool,
557        operand_delta: u64,
558    ) -> FuzzBranchFrontier {
559        let cmp = CmpOperands { op1: U256::ZERO, op2: U256::ZERO, pc, address, opcode: opcode::LT };
560        FuzzBranchFrontier::new(
561            None,
562            Arc::from(Vec::<BasicTxDetails>::new().into_boxed_slice()),
563            cmp,
564            result,
565            U256::from(operand_delta),
566            None,
567            0,
568        )
569    }
570
571    #[test]
572    fn merge_frontiers_dedupes_across_workers_keeping_smallest_delta() {
573        let merged = merge_frontiers(
574            8,
575            [frontier(1, false, 30), frontier(1, false, 10), frontier(1, false, 20)],
576        );
577
578        assert_eq!(merged.len(), 1);
579        assert_eq!(merged[0].operands.operand_delta, U256::from(10));
580    }
581
582    #[test]
583    fn frontier_keeps_shortest_stateful_sequence_for_equal_delta() {
584        let tx = BasicTxDetails {
585            warp: None,
586            roll: None,
587            sender: Address::with_last_byte(1),
588            call_details: CallDetails {
589                target: Address::with_last_byte(2),
590                calldata: Bytes::from_static(&[1, 2, 3, 4]),
591                value: None,
592            },
593        };
594        let cmp = CmpOperands {
595            op1: U256::from(1),
596            op2: U256::from(2),
597            pc: 1,
598            address: tx.call_details.target,
599            opcode: opcode::LT,
600        };
601        let mut recorder = FuzzFrontierRecorder::new(1);
602
603        recorder.capture_call(None, &[tx.clone(), tx.clone()], 1, &[cmp], None);
604        recorder.capture_call(None, std::slice::from_ref(&tx), 0, &[cmp], None);
605
606        let frontier = recorder.into_frontiers().pop().unwrap();
607        assert_eq!(frontier.sequence.len(), 1);
608        assert_eq!(frontier.call_index, 0);
609    }
610
611    #[test]
612    fn stateful_artifact_stores_shared_sequence_once() {
613        let tx = BasicTxDetails {
614            warp: None,
615            roll: None,
616            sender: Address::with_last_byte(1),
617            call_details: CallDetails {
618                target: Address::with_last_byte(2),
619                calldata: Bytes::from_static(&[1, 2, 3, 4]),
620                value: None,
621            },
622        };
623        let first = CmpOperands {
624            op1: U256::ZERO,
625            op2: U256::from(1),
626            pc: 1,
627            address: tx.call_details.target,
628            opcode: opcode::LT,
629        };
630        let second = CmpOperands { pc: 2, ..first };
631        let mut recorder = FuzzFrontierRecorder::new(2);
632        recorder.capture_sequence(&[tx.clone(), tx], &[vec![first], vec![second]]);
633        let func: Function = serde_json::from_str(
634            r#"{"type":"function","name":"invariant_ok","inputs":[],"outputs":[],"stateMutability":"pure"}"#,
635        )
636        .unwrap();
637
638        let artifact = StatefulFuzzBranchFrontierArtifact::new(&func, 2, recorder.into_frontiers());
639
640        assert_eq!(artifact.sequences.len(), 1);
641        assert_eq!(artifact.sequences[0].len, 2);
642        assert_eq!(artifact.frontiers.len(), 2);
643        assert_eq!(artifact.frontiers[0].sequence_index, 0);
644        assert_eq!(artifact.frontiers[1].sequence_index, 0);
645        assert_eq!(artifact.frontiers[0].call_index, 0);
646        assert_eq!(artifact.frontiers[1].call_index, 1);
647    }
648
649    #[test]
650    fn merge_frontiers_keeps_records_with_distinct_result_keys() {
651        // Same site but different comparison result is a distinct key.
652        let merged = merge_frontiers(8, [frontier(1, false, 5), frontier(1, true, 5)]);
653
654        assert_eq!(merged.len(), 2);
655    }
656
657    #[test]
658    fn merge_frontiers_replaces_retained_key_after_limit_is_full() {
659        // The first two unique keys fill the limit; a later new key is dropped, but a later
660        // smaller-delta duplicate of an already retained key still replaces it.
661        let merged = merge_frontiers(
662            2,
663            [
664                frontier(1, false, 30),
665                frontier(2, false, 30),
666                frontier(3, false, 5),
667                frontier(1, false, 10),
668            ],
669        );
670
671        assert_eq!(merged.len(), 2);
672        let retained = merged.iter().find(|f| f.site.pc == 1).unwrap();
673        assert_eq!(retained.operands.operand_delta, U256::from(10));
674        assert!(merged.iter().all(|f| f.site.pc != 3));
675    }
676
677    #[test]
678    fn merge_frontiers_does_not_spend_limit_on_duplicates() {
679        // A duplicate of a retained key must not count against the limit, so a later distinct key
680        // still fits. This guards against counting processed records instead of unique keys.
681        let merged = merge_frontiers(
682            2,
683            [frontier(1, false, 30), frontier(1, false, 10), frontier(2, false, 5)],
684        );
685
686        assert_eq!(merged.len(), 2);
687        assert_eq!(
688            merged.iter().find(|f| f.site.pc == 1).unwrap().operands.operand_delta,
689            U256::from(10)
690        );
691        assert!(merged.iter().any(|f| f.site.pc == 2));
692    }
693
694    #[test]
695    fn merge_frontiers_distinguishes_by_address() {
696        // Same pc/opcode/result at different addresses are distinct sites.
697        let other = Address::with_last_byte(1);
698        let merged = merge_frontiers(
699            8,
700            [frontier_at(Address::ZERO, 1, false, 5), frontier_at(other, 1, false, 5)],
701        );
702
703        assert_eq!(merged.len(), 2);
704    }
705
706    #[test]
707    fn merge_frontiers_with_zero_limit_is_empty() {
708        assert!(merge_frontiers(0, [frontier(1, false, 1)]).is_empty());
709    }
710}