Skip to main content

foundry_evm/executors/invariant/
shrink.rs

1use crate::executors::{
2    EarlyExit, EvmError, Executor, RawCallResult,
3    campaign::{apply_block_delay, execute_invariant_replay_tx},
4    invariant::{
5        IInvariantTest, call_after_invariant_function, call_invariant_function,
6        error::{handler_edge_fingerprint, snapshot_edge_fingerprint},
7        result::did_fail_on_assert,
8    },
9};
10use alloy_json_abi::Function;
11use alloy_primitives::{Address, B256, Bytes, I256, Selector, U256, map::HashSet};
12use alloy_sol_types::SolCall;
13use foundry_common::ContractsByAddress;
14use foundry_config::InvariantConfig;
15use foundry_evm_core::{constants::MAGIC_ASSUME, decode::RevertDecoder, evm::FoundryEvmNetwork};
16use foundry_evm_fuzz::{BaseCounterExample, BasicTxDetails, invariant::InvariantContract};
17use indicatif::ProgressBar;
18use proptest::bits::{BitSetLike, VarBitSet};
19use std::{cell::Cell, fmt::Write, hash::Hash};
20
21const LIVE_SHRINK_SEQUENCE_EDGE_CALLS: usize = 16;
22
23/// Shrinker for a call sequence failure.
24/// Iterates sequence call sequence top down and removes calls one by one.
25/// If the failure is still reproducible with removed call then moves to the next one.
26/// If the failure is not reproducible then restore removed call and moves to next one.
27#[derive(Debug)]
28pub struct SequenceShrink {
29    /// Length of call sequence to be shrunk.
30    call_sequence_len: usize,
31    /// Call ids contained in current shrunk sequence.
32    included_calls: VarBitSet,
33}
34
35impl SequenceShrink {
36    pub fn new(call_sequence_len: usize) -> Self {
37        Self { call_sequence_len, included_calls: VarBitSet::saturated(call_sequence_len) }
38    }
39
40    /// Return candidate shrink sequence to be tested, by removing ids from original sequence.
41    pub fn current(&self) -> impl Iterator<Item = usize> + '_ {
42        (0..self.call_sequence_len).filter(|&call_id| self.included_calls.test(call_id))
43    }
44
45    pub fn contains(&self, call_idx: usize) -> bool {
46        self.included_calls.test(call_idx)
47    }
48
49    pub fn included_count(&self) -> usize {
50        self.included_calls.count()
51    }
52
53    pub fn apply<T: Clone>(&self, calls: &[T]) -> Vec<T> {
54        self.current().map(|idx| calls[idx].clone()).collect()
55    }
56
57    pub fn apply_with_accumulated_delay<T, D, A>(
58        &self,
59        calls: &[T],
60        mut delay: D,
61        mut apply_delay: A,
62    ) -> Vec<T>
63    where
64        T: Clone,
65        D: FnMut(&T) -> (Option<U256>, Option<U256>),
66        A: FnMut(T, U256, U256) -> T,
67    {
68        let mut result = Vec::new();
69        let mut accumulated_warp = U256::ZERO;
70        let mut accumulated_roll = U256::ZERO;
71
72        for (idx, call) in calls.iter().enumerate() {
73            let (warp, roll) = delay(call);
74            accumulated_warp += warp.unwrap_or(U256::ZERO);
75            accumulated_roll += roll.unwrap_or(U256::ZERO);
76
77            if self.contains(idx) {
78                result.push(apply_delay(call.clone(), accumulated_warp, accumulated_roll));
79                accumulated_warp = U256::ZERO;
80                accumulated_roll = U256::ZERO;
81            }
82        }
83
84        result
85    }
86
87    fn remove(&mut self, call_idx: usize) {
88        self.included_calls.clear(call_idx);
89    }
90
91    fn restore(&mut self, call_idx: usize) {
92        self.included_calls.set(call_idx);
93    }
94
95    /// Advance to the next call index, wrapping around to 0 at the end.
96    const fn next_index(&self, call_idx: usize) -> usize {
97        if call_idx + 1 == self.call_sequence_len { 0 } else { call_idx + 1 }
98    }
99}
100
101/// How `run_shrink_loop` handles a predicate error.
102#[derive(Clone, Copy)]
103enum ShrinkErrorPolicy {
104    /// "Bug still present" — keep the call removed (legacy `shrink_sequence` behavior).
105    KeepRemoved,
106    /// "Bug gone" — restore the call. Used by handler shrink so a replay error never
107    /// produces a sequence that no longer reproduces the anchor.
108    RestoreRemoved,
109}
110
111/// Attempt counters collected while trying shrink candidates.
112#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
113pub struct ShrinkRunStats {
114    pub attempts: usize,
115    pub accepted: usize,
116}
117
118/// Shared shrink attempt driver.
119///
120/// Candidate generation stays with each shrinker; this type only centralizes limit enforcement
121/// and the "accept when the candidate still reproduces the bug" accounting.
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub struct ShrinkRun {
124    max_attempts: usize,
125    stats: ShrinkRunStats,
126}
127
128impl ShrinkRun {
129    pub const fn new(max_attempts: usize) -> Self {
130        Self { max_attempts, stats: ShrinkRunStats { attempts: 0, accepted: 0 } }
131    }
132
133    pub const fn can_try(&self) -> bool {
134        self.stats.attempts < self.max_attempts
135    }
136
137    pub const fn remaining_attempts(&self) -> usize {
138        self.max_attempts - self.stats.attempts
139    }
140
141    pub const fn finish(self) -> ShrinkRunStats {
142        self.stats
143    }
144
145    pub fn try_candidate(&mut self, still_fails: impl FnOnce() -> bool) -> bool {
146        self.try_candidate_decision(|| Some(still_fails())).unwrap_or(false)
147    }
148
149    fn try_candidate_decision(&mut self, decide: impl FnOnce() -> Option<bool>) -> Option<bool> {
150        if !self.can_try() {
151            return None;
152        }
153
154        let accepted = decide()?;
155        self.stats.attempts += 1;
156        if accepted {
157            self.stats.accepted += 1;
158        }
159        Some(accepted)
160    }
161}
162
163/// Shared key set for shrinkers that need to skip duplicate concrete replays.
164#[derive(Clone, Debug)]
165pub struct ShrinkCandidateKeys<K> {
166    seen: HashSet<K>,
167}
168
169impl<K: Eq + Hash> ShrinkCandidateKeys<K> {
170    pub fn new(initial: K) -> Self {
171        Self { seen: HashSet::<_>::from_iter([initial]) }
172    }
173
174    pub fn insert(&mut self, key: K) -> bool {
175        self.seen.insert(key)
176    }
177}
178
179/// Per-call decision returned by callbacks driving `replay_sequence`.
180enum ReplayDecision<T> {
181    Stop(T),
182    Continue,
183}
184
185/// Options controlling how `check_sequence` evaluates a candidate call sequence.
186pub struct CheckSequenceOptions<'a> {
187    pub accumulate_warp_roll: bool,
188    pub fail_on_revert: bool,
189    pub expect_assertion_failure: bool,
190    pub call_after_invariant: bool,
191    pub rd: Option<&'a RevertDecoder>,
192}
193
194/// Concrete failure site observed while replaying a sequence through [`check_sequence`].
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub enum CheckSequenceFailureSite {
197    SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
198    Invariant { target: Address, selector: Selector, fingerprint: B256 },
199    AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
200}
201
202/// Outcome from replaying an invariant call sequence through [`check_sequence`].
203#[derive(Clone, Debug)]
204pub struct CheckSequenceOutcome {
205    pub success: bool,
206    pub replayed_entirely: bool,
207    pub reason: Option<String>,
208    pub calls_count: usize,
209    pub reverts: usize,
210    pub failure_site: Option<CheckSequenceFailureSite>,
211    /// Whether replay stopped on an assertion in a sequence call rather than a plain revert or
212    /// terminal invariant check.
213    pub sequence_assertion_failure: bool,
214    /// Innermost reverter for a sequence assertion, used to recognize legacy handler identities.
215    pub sequence_reverter: Option<Address>,
216}
217
218pub struct ShrunkSequence {
219    pub calls: Vec<BasicTxDetails>,
220    pub result: Option<CheckSequenceOutcome>,
221}
222
223/// Result of a strict handler-bug replay: anchor asserts, no earlier call asserts, and the
224/// recomputed edge fingerprint identifies which path the assertion took.
225#[derive(Debug)]
226pub struct HandlerReplayOutcome {
227    pub anchor_asserted: bool,
228    /// Top-level fuzzed handler target used as the canonical failure identity.
229    pub handler_target: Address,
230    /// Innermost reverting frame, retained to recognize legacy persisted identities.
231    pub reverter: Address,
232    pub selector: Selector,
233    pub revert_reason: Option<String>,
234    /// Normalized via `handler_edge_fingerprint` so callers can compare directly.
235    pub anchor_fingerprint: B256,
236}
237
238/// Resets the progress bar before each shrink. `position = Some((i, N))` renders
239/// `[i/N] Shrink: <label>` for multi-invariant campaigns.
240pub(crate) fn reset_shrink_progress(
241    config: &InvariantConfig,
242    progress: Option<&ProgressBar>,
243    label: &str,
244    position: Option<(usize, usize)>,
245) -> String {
246    let message = match position {
247        Some((current, total)) if total > 1 => {
248            format!(" [{current}/{total}] Shrink: {label}")
249        }
250        _ => format!(" Shrink: {label}"),
251    };
252    if let Some(progress) = progress {
253        progress.set_length(config.shrink_run_limit as u64);
254        progress.reset();
255        progress.set_message(message.clone());
256    }
257    message
258}
259
260/// Live shrink progress display. The progress bar itself is owned by forge's test runner; this
261/// type only formats the transient message shown while invariant shrinking is active.
262pub(crate) struct ShrinkProgress<'a> {
263    progress: Option<&'a ProgressBar>,
264    message: String,
265    identified_contracts: Option<&'a ContractsByAddress>,
266    show_solidity: bool,
267}
268
269impl<'a> ShrinkProgress<'a> {
270    pub(crate) fn new(
271        config: &InvariantConfig,
272        progress: Option<&'a ProgressBar>,
273        label: &str,
274        position: Option<(usize, usize)>,
275        identified_contracts: Option<&'a ContractsByAddress>,
276        show_solidity: bool,
277    ) -> Self {
278        let message = reset_shrink_progress(config, progress, label, position);
279        Self { progress, message, identified_contracts, show_solidity }
280    }
281
282    fn inc(&self) {
283        if let Some(progress) = self.progress {
284            progress.inc(1);
285        }
286    }
287
288    fn update(
289        &self,
290        calls: &[BasicTxDetails],
291        shrinker: &SequenceShrink,
292        accumulate_warp_roll: bool,
293    ) {
294        let Some(progress) = self.progress else {
295            return;
296        };
297        if progress.is_hidden() {
298            return;
299        }
300
301        let sequence = build_shrunk_sequence(calls, shrinker, accumulate_warp_roll);
302        let message = format_shrink_progress_message(
303            &self.message,
304            &sequence,
305            self.identified_contracts,
306            self.show_solidity,
307        );
308        progress.set_message(message);
309    }
310}
311
312fn format_shrink_progress_message(
313    phase: &str,
314    sequence: &[BasicTxDetails],
315    identified_contracts: Option<&ContractsByAddress>,
316    show_solidity: bool,
317) -> String {
318    let mut message = String::with_capacity(phase.len() + sequence.len().min(32) * 96);
319    message.push_str(phase);
320    write!(message, "\n\t[Sequence] (shrunk: {})", sequence.len()).unwrap();
321
322    if sequence.len() <= LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2 {
323        for tx in sequence {
324            push_shrink_progress_call(&mut message, tx, identified_contracts, show_solidity);
325        }
326        return message;
327    }
328
329    for tx in &sequence[..LIVE_SHRINK_SEQUENCE_EDGE_CALLS] {
330        push_shrink_progress_call(&mut message, tx, identified_contracts, show_solidity);
331    }
332    writeln!(
333        message,
334        "\n\t\t... {} call(s) omitted ...",
335        sequence.len() - LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2
336    )
337    .unwrap();
338    for tx in &sequence[sequence.len() - LIVE_SHRINK_SEQUENCE_EDGE_CALLS..] {
339        push_shrink_progress_call(&mut message, tx, identified_contracts, show_solidity);
340    }
341    message
342}
343
344fn push_shrink_progress_call(
345    message: &mut String,
346    tx: &BasicTxDetails,
347    identified_contracts: Option<&ContractsByAddress>,
348    show_solidity: bool,
349) {
350    let empty_contracts;
351    let identified_contracts = if let Some(identified_contracts) = identified_contracts {
352        identified_contracts
353    } else {
354        empty_contracts = ContractsByAddress::default();
355        &empty_contracts
356    };
357
358    let call =
359        BaseCounterExample::from_invariant_call(tx, identified_contracts, None, show_solidity)
360            .to_string();
361    for line in call.lines() {
362        message.push('\n');
363        message.push_str(line);
364    }
365}
366
367/// Applies accumulated warp/roll to a call, returning a modified copy.
368fn apply_warp_roll(mut result: BasicTxDetails, warp: U256, roll: U256) -> BasicTxDetails {
369    if warp > U256::ZERO {
370        result.warp = Some(warp);
371    }
372    if roll > U256::ZERO {
373        result.roll = Some(roll);
374    }
375    result
376}
377
378/// Builds the final shrunk sequence from the shrinker state.
379///
380/// When `accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the next
381/// kept call so the final sequence remains reproducible.
382fn build_shrunk_sequence(
383    calls: &[BasicTxDetails],
384    shrinker: &SequenceShrink,
385    accumulate_warp_roll: bool,
386) -> Vec<BasicTxDetails> {
387    if !accumulate_warp_roll {
388        return shrinker.apply(calls);
389    }
390
391    shrinker.apply_with_accumulated_delay(calls, |call| (call.warp, call.roll), apply_warp_roll)
392}
393
394/// Shared sequence shrinker. Tries to drop each call; `predicate` decides whether the candidate
395/// should be accepted, rejected, or skipped without spending a replay attempt.
396pub fn shrink_sequence_by_removing<P, S, A>(
397    calls_len: usize,
398    run: &mut ShrinkRun,
399    mut should_stop: S,
400    mut on_attempt: A,
401    mut predicate: P,
402) -> SequenceShrink
403where
404    P: FnMut(&SequenceShrink) -> Option<bool>,
405    S: FnMut() -> bool,
406    A: FnMut(),
407{
408    let mut shrinker = SequenceShrink::new(calls_len);
409    let mut call_idx = 0;
410    let mut skipped_candidates = 0usize;
411
412    while run.can_try() {
413        if should_stop() {
414            break;
415        }
416        let included_count = shrinker.included_count();
417        if included_count == 0 || skipped_candidates >= included_count {
418            break;
419        }
420
421        // Already-removed indices have nothing to drop.
422        if !shrinker.contains(call_idx) {
423            call_idx = shrinker.next_index(call_idx);
424            continue;
425        }
426
427        shrinker.remove(call_idx);
428
429        let Some(accepted) = run.try_candidate_decision(|| predicate(&shrinker)) else {
430            shrinker.restore(call_idx);
431            skipped_candidates += 1;
432            call_idx = shrinker.next_index(call_idx);
433            continue;
434        };
435
436        on_attempt();
437        skipped_candidates = 0;
438        if accepted {
439            if shrinker.included_count() == 1 {
440                break;
441            }
442        } else {
443            shrinker.restore(call_idx);
444        }
445
446        call_idx = shrinker.next_index(call_idx);
447    }
448
449    shrinker
450}
451
452/// Shared shrink loop driver. Tries to drop each call; `predicate` returns whether the
453/// candidate still triggers the bug.
454fn run_shrink_loop<P>(
455    config: &InvariantConfig,
456    calls: &[BasicTxDetails],
457    progress: &ShrinkProgress<'_>,
458    accumulate_warp_roll: bool,
459    early_exit: &EarlyExit,
460    error_policy: ShrinkErrorPolicy,
461    mut predicate: P,
462) -> SequenceShrink
463where
464    P: FnMut(&SequenceShrink) -> eyre::Result<bool>,
465{
466    let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
467    let initial = SequenceShrink::new(calls.len());
468    progress.update(calls, &initial, accumulate_warp_roll);
469
470    let shrinker = shrink_sequence_by_removing(
471        calls.len(),
472        &mut run,
473        || early_exit.should_stop(),
474        || progress.inc(),
475        |shrinker| {
476            progress.update(calls, shrinker, accumulate_warp_roll);
477            match predicate(shrinker) {
478                Ok(bug_still_present) => Some(bug_still_present),
479                Err(_) => Some(matches!(error_policy, ShrinkErrorPolicy::KeepRemoved)),
480            }
481        },
482    );
483    progress.update(calls, &shrinker, accumulate_warp_roll);
484    shrinker
485}
486
487#[expect(clippy::too_many_arguments)]
488pub(crate) fn shrink_sequence<FEN: FoundryEvmNetwork>(
489    config: &InvariantConfig,
490    invariant_contract: &InvariantContract<'_>,
491    target_invariant: &Function,
492    calls: &[BasicTxDetails],
493    expect_assertion_failure: bool,
494    executor: &Executor<FEN>,
495    rd: Option<&RevertDecoder>,
496    progress: &ShrinkProgress<'_>,
497    early_exit: &EarlyExit,
498) -> eyre::Result<ShrunkSequence> {
499    trace!(target: "forge::test", "Shrinking sequence of {} calls.", calls.len());
500
501    let target_address = invariant_contract.address;
502    let calldata: Bytes = target_invariant.selector().into();
503    // Special case test: the invariant is *unsatisfiable* - it took 0 calls to
504    // break the invariant -- consider emitting a warning.
505    let (_, success) = call_invariant_function(executor, target_address, calldata.clone())?;
506    if !success {
507        return Ok(ShrunkSequence { calls: vec![], result: None });
508    }
509
510    let accumulate_warp_roll = config.has_delay();
511    let mut sequence = Vec::with_capacity(calls.len());
512    let mut last_result = None;
513    let mut last_result_matches_shrinker = true;
514    let shrinker = run_shrink_loop(
515        config,
516        calls,
517        progress,
518        accumulate_warp_roll,
519        early_exit,
520        // Preserve legacy invariant-shrink behavior: errors during candidate evaluation
521        // do not roll back the removal.
522        ShrinkErrorPolicy::KeepRemoved,
523        |shrinker| {
524            sequence.clear();
525            sequence.extend(shrinker.current());
526            let result = match check_sequence(
527                executor.clone(),
528                calls,
529                &sequence,
530                target_address,
531                calldata.clone(),
532                CheckSequenceOptions {
533                    accumulate_warp_roll,
534                    fail_on_revert: config.fail_on_revert,
535                    expect_assertion_failure,
536                    call_after_invariant: invariant_contract.call_after_invariant,
537                    rd,
538                },
539            ) {
540                Ok(result) => result,
541                Err(err) => {
542                    last_result_matches_shrinker = false;
543                    return Err(err);
544                }
545            };
546            // Bug still present iff the invariant predicate did not pass.
547            let bug_still_present = !result.success;
548            if bug_still_present {
549                last_result = Some(result);
550                last_result_matches_shrinker = true;
551            }
552            Ok(bug_still_present)
553        },
554    );
555
556    let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
557    let result = if last_result_matches_shrinker {
558        last_result
559    } else {
560        match check_sequence(
561            executor.clone(),
562            &shrunk,
563            &(0..shrunk.len()).collect::<Vec<_>>(),
564            target_address,
565            calldata,
566            CheckSequenceOptions {
567                accumulate_warp_roll: false,
568                fail_on_revert: config.fail_on_revert,
569                expect_assertion_failure,
570                call_after_invariant: invariant_contract.call_after_invariant,
571                rd,
572            },
573        ) {
574            Ok(result) => Some(result),
575            Err(err) => {
576                trace!(target: "forge::test", %err, "failed to recompute shrunk replay metrics");
577                None
578            }
579        }
580    };
581
582    Ok(ShrunkSequence { calls: shrunk, result })
583}
584
585/// Replays `sequence` (indices into `calls`) against `executor`. When
586/// `accumulate_warp_roll` is set, warp/roll from skipped calls is folded into the next
587/// included call. `on_call` may stop after each campaign-faithful replay transition.
588fn replay_sequence<FEN, T, F>(
589    executor: &mut Executor<FEN>,
590    calls: &[BasicTxDetails],
591    sequence: &[usize],
592    accumulate_warp_roll: bool,
593    mut on_call: F,
594) -> eyre::Result<Option<T>>
595where
596    FEN: FoundryEvmNetwork,
597    F: FnMut(usize, RawCallResult<FEN>) -> eyre::Result<ReplayDecision<T>>,
598{
599    // Fast path: no warp/roll accumulation → iterate only kept indices (O(k)) and pass
600    // `&calls[idx]` directly to skip the per-call `BasicTxDetails` clone.
601    if !accumulate_warp_roll {
602        for &idx in sequence {
603            let (_, call_result) = execute_invariant_replay_tx(executor, &calls[idx])?;
604            match on_call(idx, call_result)? {
605                ReplayDecision::Stop(val) => return Ok(Some(val)),
606                ReplayDecision::Continue => {}
607            }
608        }
609        return Ok(None);
610    }
611
612    // Accumulating path: must scan the full `calls` so warp/roll from skipped txs lands on
613    // the next kept tx as a concrete delta.
614    let mut accumulated_warp = U256::ZERO;
615    let mut accumulated_roll = U256::ZERO;
616    let mut seq_iter = sequence.iter().peekable();
617
618    for (idx, tx) in calls.iter().enumerate() {
619        accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
620        accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
621        if seq_iter.peek() != Some(&&idx) {
622            continue;
623        }
624        seq_iter.next();
625
626        let executed = apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
627        let (_, call_result) = execute_invariant_replay_tx(executor, &executed)?;
628
629        match on_call(idx, call_result)? {
630            ReplayDecision::Stop(val) => return Ok(Some(val)),
631            ReplayDecision::Continue => {}
632        }
633
634        accumulated_warp = U256::ZERO;
635        accumulated_roll = U256::ZERO;
636    }
637
638    Ok(None)
639}
640
641/// Checks if the given call sequence breaks the invariant.
642///
643/// Used in shrinking phase for checking candidate sequences and in replay failures phase to test
644/// persisted failures.
645/// Returns the result of invariant check (and afterInvariant call if needed) and if sequence was
646/// entirely applied, plus the concrete failure site when replay fails.
647///
648/// When `options.accumulate_warp_roll` is enabled, warp/roll from removed calls is folded into the
649/// next kept call so the candidate sequence stays representable as a concrete counterexample.
650pub fn check_sequence<FEN: FoundryEvmNetwork>(
651    mut executor: Executor<FEN>,
652    calls: &[BasicTxDetails],
653    sequence: &[usize],
654    test_address: Address,
655    calldata: Bytes,
656    options: CheckSequenceOptions<'_>,
657) -> eyre::Result<CheckSequenceOutcome> {
658    let mut calls_executed = 0;
659    let mut reverts = 0;
660    let early = replay_sequence(
661        &mut executor,
662        calls,
663        sequence,
664        options.accumulate_warp_roll,
665        |idx, call_result| {
666            calls_executed += 1;
667            // Ignore calls reverted with `MAGIC_ASSUME`. This is needed to handle failed
668            // scenarios that are replayed with a modified version of test driver (that use
669            // new `vm.assume` cheatcodes).
670            if call_result.result.as_ref() == MAGIC_ASSUME {
671                return Ok(ReplayDecision::Continue);
672            }
673            if call_result.reverted {
674                reverts += 1;
675            }
676            if did_fail_on_assert(&call_result, &call_result.state_changeset) {
677                let call = &calls[idx];
678                let target = call.call_details.target;
679                let selector = selector_from_calldata(&call.call_details.calldata);
680                let fingerprint = handler_edge_fingerprint(
681                    snapshot_edge_fingerprint(&call_result),
682                    target,
683                    selector,
684                );
685                let sequence_reverter = call_result.reverter.or(Some(target));
686                let site = CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint };
687                return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
688                    success: false,
689                    replayed_entirely: false,
690                    reason: assertion_failure_reason(call_result, options.rd),
691                    calls_count: calls_executed,
692                    reverts,
693                    failure_site: Some(site),
694                    sequence_assertion_failure: true,
695                    sequence_reverter,
696                }));
697            }
698            if call_result.reverted && options.fail_on_revert {
699                if options.expect_assertion_failure {
700                    return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
701                        success: true,
702                        replayed_entirely: false,
703                        reason: None,
704                        calls_count: calls_executed,
705                        reverts,
706                        failure_site: None,
707                        sequence_assertion_failure: false,
708                        sequence_reverter: None,
709                    }));
710                }
711                let site = sequence_call_failure_site(&calls[idx], &call_result);
712                return Ok(ReplayDecision::Stop(CheckSequenceOutcome {
713                    success: false,
714                    replayed_entirely: false,
715                    reason: call_failure_reason(call_result, options.rd),
716                    calls_count: calls_executed,
717                    reverts,
718                    failure_site: Some(site),
719                    sequence_assertion_failure: false,
720                    sequence_reverter: None,
721                }));
722            }
723            Ok(ReplayDecision::Continue)
724        },
725    )?;
726    if let Some(result) = early {
727        return Ok(result);
728    }
729
730    // Unlike optimization mode we intentionally do not apply trailing warp/roll before the
731    // invariant call: those delays would not be representable in the final shrunk sequence.
732    let (success, replayed_entirely, reason, failure_site) =
733        finish_sequence_check(&executor, test_address, calldata, &options)?;
734    Ok(CheckSequenceOutcome {
735        success,
736        replayed_entirely,
737        reason,
738        calls_count: calls_executed,
739        reverts,
740        failure_site,
741        sequence_assertion_failure: false,
742        sequence_reverter: None,
743    })
744}
745
746fn finish_sequence_check<FEN: FoundryEvmNetwork>(
747    executor: &Executor<FEN>,
748    test_address: Address,
749    calldata: Bytes,
750    options: &CheckSequenceOptions<'_>,
751) -> eyre::Result<(bool, bool, Option<String>, Option<CheckSequenceFailureSite>)> {
752    let handle_terminal_failure =
753        |call_result: RawCallResult<FEN>, site_kind: TerminalFailureSite| {
754            let should_ignore_failure = options.expect_assertion_failure
755                && !executor.has_global_failure(&call_result.state_changeset)
756                && !did_fail_on_assert(&call_result, &call_result.state_changeset);
757
758            if should_ignore_failure {
759                return (true, true, None, None);
760            }
761
762            let site = terminal_failure_site(site_kind, test_address, &calldata, &call_result);
763            let reason = if options.expect_assertion_failure {
764                assertion_failure_reason(call_result, options.rd)
765            } else {
766                call_failure_reason(call_result, options.rd)
767            };
768
769            (false, true, reason, Some(site))
770        };
771
772    let (invariant_result, mut success) =
773        call_invariant_function(executor, test_address, calldata.clone())?;
774    if !success {
775        return Ok(handle_terminal_failure(invariant_result, TerminalFailureSite::Invariant));
776    }
777
778    // Check after invariant result if invariant is success and `afterInvariant` function is
779    // declared.
780    if success && options.call_after_invariant {
781        let (after_invariant_result, after_invariant_success) =
782            call_after_invariant_function(executor, test_address)?;
783        success = after_invariant_success;
784        if !success {
785            return Ok(handle_terminal_failure(
786                after_invariant_result,
787                TerminalFailureSite::AfterInvariant,
788            ));
789        }
790    }
791
792    Ok((success, true, None, None))
793}
794
795#[derive(Clone, Copy)]
796enum TerminalFailureSite {
797    Invariant,
798    AfterInvariant,
799}
800
801fn sequence_call_failure_site<FEN: FoundryEvmNetwork>(
802    call: &BasicTxDetails,
803    call_result: &RawCallResult<FEN>,
804) -> CheckSequenceFailureSite {
805    let target = call_result.reverter.unwrap_or(call.call_details.target);
806    let selector = selector_from_calldata(&call.call_details.calldata);
807    let fingerprint =
808        handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
809    CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint }
810}
811
812fn terminal_failure_site<FEN: FoundryEvmNetwork>(
813    kind: TerminalFailureSite,
814    target: Address,
815    calldata: &Bytes,
816    call_result: &RawCallResult<FEN>,
817) -> CheckSequenceFailureSite {
818    let target = call_result.reverter.unwrap_or(target);
819    let selector = match kind {
820        TerminalFailureSite::Invariant => selector_from_calldata(calldata),
821        TerminalFailureSite::AfterInvariant => {
822            Selector::from(IInvariantTest::afterInvariantCall::SELECTOR)
823        }
824    };
825    let fingerprint =
826        handler_edge_fingerprint(snapshot_edge_fingerprint(call_result), target, selector);
827    match kind {
828        TerminalFailureSite::Invariant => {
829            CheckSequenceFailureSite::Invariant { target, selector, fingerprint }
830        }
831        TerminalFailureSite::AfterInvariant => {
832            CheckSequenceFailureSite::AfterInvariant { target, selector, fingerprint }
833        }
834    }
835}
836
837fn selector_from_calldata(calldata: &Bytes) -> Selector {
838    let selector: [u8; 4] = calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
839    Selector::from(selector)
840}
841
842fn call_failure_reason<FEN: FoundryEvmNetwork>(
843    call_result: RawCallResult<FEN>,
844    rd: Option<&RevertDecoder>,
845) -> Option<String> {
846    match call_result.into_evm_error(rd) {
847        EvmError::Execution(err) => Some(err.reason),
848        _ => None,
849    }
850}
851
852fn assertion_failure_reason<FEN: FoundryEvmNetwork>(
853    call_result: RawCallResult<FEN>,
854    rd: Option<&RevertDecoder>,
855) -> Option<String> {
856    call_failure_reason(call_result, rd).or_else(|| Some("assertion failed".to_string()))
857}
858
859/// Shrinks a call sequence to the shortest sequence that still produces the target optimization
860/// value. This is specifically for optimization mode where we want to find the minimal sequence
861/// that achieves the maximum value.
862///
863/// Unlike `shrink_sequence` (for check mode), this function:
864/// - Accumulates warp/roll values from removed calls into the next kept call
865/// - Checks for target value equality rather than invariant failure
866#[expect(clippy::too_many_arguments)]
867pub(crate) fn shrink_sequence_value<FEN: FoundryEvmNetwork>(
868    config: &InvariantConfig,
869    invariant_contract: &InvariantContract<'_>,
870    target_invariant: &Function,
871    calls: &[BasicTxDetails],
872    executor: &Executor<FEN>,
873    target_value: I256,
874    progress: &ShrinkProgress<'_>,
875    early_exit: &EarlyExit,
876) -> eyre::Result<Vec<BasicTxDetails>> {
877    trace!(target: "forge::test", "Shrinking optimization sequence of {} calls for target value {}.", calls.len(), target_value);
878
879    let target_address = invariant_contract.address;
880    let calldata: Bytes = target_invariant.selector().into();
881
882    // Special case: check if target value is achieved with 0 calls.
883    if check_sequence_value(executor.clone(), calls, &[], target_address, calldata.clone())?
884        == Some(target_value)
885    {
886        return Ok(vec![]);
887    }
888
889    let replay_failed = Cell::new(false);
890    let mut replay_error = None;
891    let mut run = ShrinkRun::new(config.shrink_run_limit as usize);
892    let initial = SequenceShrink::new(calls.len());
893    progress.update(calls, &initial, true);
894    let mut sequence = Vec::with_capacity(calls.len());
895
896    let shrinker = shrink_sequence_by_removing(
897        calls.len(),
898        &mut run,
899        || early_exit.should_stop() || replay_failed.get(),
900        || progress.inc(),
901        |shrinker| {
902            progress.update(calls, shrinker, true);
903            sequence.clear();
904            sequence.extend(shrinker.current());
905            match check_sequence_value(
906                executor.clone(),
907                calls,
908                &sequence,
909                target_address,
910                calldata.clone(),
911            ) {
912                Ok(Some(value)) => Some(value == target_value),
913                Ok(None) => Some(false),
914                Err(err) => {
915                    replay_error = Some(err);
916                    replay_failed.set(true);
917                    None
918                }
919            }
920        },
921    );
922    progress.update(calls, &shrinker, true);
923    if let Some(err) = replay_error {
924        return Err(err);
925    }
926
927    Ok(build_shrunk_sequence(calls, &shrinker, true))
928}
929
930/// Replays a handler-bug sequence and returns whether the anchor still asserts on the same
931/// path. Rejects sequences with a pre-anchor assertion (would be a different bug).
932pub fn replay_handler_failure_sequence<FEN: FoundryEvmNetwork>(
933    mut executor: Executor<FEN>,
934    calls: &[BasicTxDetails],
935    sequence: &[usize],
936    accumulate_warp_roll: bool,
937    rd: Option<&RevertDecoder>,
938) -> eyre::Result<HandlerReplayOutcome> {
939    let Some(&anchor_idx) = sequence.last() else {
940        return Ok(HandlerReplayOutcome {
941            anchor_asserted: false,
942            handler_target: Address::ZERO,
943            reverter: Address::ZERO,
944            selector: Selector::ZERO,
945            revert_reason: None,
946            anchor_fingerprint: B256::ZERO,
947        });
948    };
949
950    let outcome = replay_sequence(
951        &mut executor,
952        calls,
953        sequence,
954        accumulate_warp_roll,
955        |idx, call_result| {
956            let asserted = did_fail_on_assert(&call_result, &call_result.state_changeset);
957            if idx == anchor_idx {
958                let snapshot = snapshot_edge_fingerprint(&call_result);
959                let anchor = &calls[anchor_idx];
960                // Handler failures are identified by the top-level fuzzed call. The innermost
961                // reverter can instead be a nested callee or the cheatcode address.
962                let handler_target = anchor.call_details.target;
963                let reverter = call_result.reverter.unwrap_or(handler_target);
964                let selector_bytes: [u8; 4] = anchor
965                    .call_details
966                    .calldata
967                    .get(..4)
968                    .and_then(|s| s.try_into().ok())
969                    .unwrap_or_default();
970                let selector = Selector::from(selector_bytes);
971                let fingerprint = handler_edge_fingerprint(snapshot, handler_target, selector);
972                let reason =
973                    if asserted { assertion_failure_reason(call_result, rd) } else { None };
974                return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
975                    anchor_asserted: asserted,
976                    handler_target,
977                    reverter,
978                    selector,
979                    revert_reason: reason,
980                    anchor_fingerprint: fingerprint,
981                }));
982            }
983            if asserted {
984                // Pre-anchor assertion = different bug; reject.
985                return Ok(ReplayDecision::Stop(HandlerReplayOutcome {
986                    anchor_asserted: false,
987                    handler_target: Address::ZERO,
988                    reverter: Address::ZERO,
989                    selector: Selector::ZERO,
990                    revert_reason: None,
991                    anchor_fingerprint: B256::ZERO,
992                }));
993            }
994            Ok(ReplayDecision::Continue)
995        },
996    )?;
997
998    Ok(outcome.unwrap_or(HandlerReplayOutcome {
999        anchor_asserted: false,
1000        handler_target: Address::ZERO,
1001        reverter: Address::ZERO,
1002        selector: Selector::ZERO,
1003        revert_reason: None,
1004        anchor_fingerprint: B256::ZERO,
1005    }))
1006}
1007
1008/// Shrinks a handler-bug sequence to the shortest prefix that still asserts on the anchor
1009/// AND keeps the same edge fingerprint (so we don't change bug identity).
1010pub(crate) fn shrink_handler_sequence<FEN: FoundryEvmNetwork>(
1011    config: &InvariantConfig,
1012    calls: &[BasicTxDetails],
1013    expected_fingerprint: B256,
1014    executor: &Executor<FEN>,
1015    progress: &ShrinkProgress<'_>,
1016    early_exit: &EarlyExit,
1017) -> eyre::Result<Vec<BasicTxDetails>> {
1018    if calls.is_empty() {
1019        return Ok(vec![]);
1020    }
1021    let accumulate_warp_roll = config.has_delay();
1022    let mut sequence = Vec::with_capacity(calls.len());
1023    let shrinker = run_shrink_loop(
1024        config,
1025        calls,
1026        progress,
1027        accumulate_warp_roll,
1028        early_exit,
1029        ShrinkErrorPolicy::RestoreRemoved,
1030        |shrinker| {
1031            sequence.clear();
1032            sequence.extend(shrinker.current());
1033            handler_sequence_still_triggers_bug(
1034                executor.clone(),
1035                calls,
1036                &sequence,
1037                accumulate_warp_roll,
1038                expected_fingerprint,
1039            )
1040        },
1041    );
1042
1043    let shrunk = build_shrunk_sequence(calls, &shrinker, accumulate_warp_roll);
1044
1045    // Verify shrunk repro; fall back to original on any failure.
1046    sequence.clear();
1047    sequence.extend(shrinker.current());
1048    let verified = handler_sequence_still_triggers_bug(
1049        executor.clone(),
1050        calls,
1051        &sequence,
1052        accumulate_warp_roll,
1053        expected_fingerprint,
1054    )
1055    .unwrap_or(false);
1056    if verified { Ok(shrunk) } else { Ok(calls.to_vec()) }
1057}
1058
1059/// Shrink predicate: anchor asserts on the same path as the originally recorded bug.
1060fn handler_sequence_still_triggers_bug<FEN: FoundryEvmNetwork>(
1061    executor: Executor<FEN>,
1062    calls: &[BasicTxDetails],
1063    sequence: &[usize],
1064    accumulate_warp_roll: bool,
1065    expected_fingerprint: B256,
1066) -> eyre::Result<bool> {
1067    let outcome =
1068        replay_handler_failure_sequence(executor, calls, sequence, accumulate_warp_roll, None)?;
1069    Ok(outcome.anchor_asserted && outcome.anchor_fingerprint == expected_fingerprint)
1070}
1071
1072/// Executes a call sequence and returns the optimization value (int256) from the invariant
1073/// function. Used during shrinking for optimization mode.
1074///
1075/// Returns `None` if the invariant call fails or doesn't return a valid int256.
1076/// Unlike `check_sequence`, this applies warp/roll from ALL calls (including removed ones).
1077pub fn check_sequence_value<FEN: FoundryEvmNetwork>(
1078    mut executor: Executor<FEN>,
1079    calls: &[BasicTxDetails],
1080    sequence: &[usize],
1081    test_address: Address,
1082    calldata: Bytes,
1083) -> eyre::Result<Option<I256>> {
1084    let mut accumulated_warp = U256::ZERO;
1085    let mut accumulated_roll = U256::ZERO;
1086    let mut seq_iter = sequence.iter().peekable();
1087
1088    for (idx, tx) in calls.iter().enumerate() {
1089        accumulated_warp += tx.warp.unwrap_or(U256::ZERO);
1090        accumulated_roll += tx.roll.unwrap_or(U256::ZERO);
1091
1092        if seq_iter.peek() == Some(&&idx) {
1093            seq_iter.next();
1094
1095            let tx_with_accumulated =
1096                apply_warp_roll(tx.clone(), accumulated_warp, accumulated_roll);
1097            execute_invariant_replay_tx(&mut executor, &tx_with_accumulated)?;
1098
1099            accumulated_warp = U256::ZERO;
1100            accumulated_roll = U256::ZERO;
1101        }
1102    }
1103
1104    // Apply any remaining accumulated warp/roll before calling invariant.
1105    apply_block_delay(&mut executor, accumulated_warp, accumulated_roll);
1106
1107    let (inv_result, success) = call_invariant_function(&executor, test_address, calldata)?;
1108
1109    if success
1110        && inv_result.result.len() >= 32
1111        && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
1112    {
1113        return Ok(Some(value));
1114    }
1115
1116    Ok(None)
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121    use super::*;
1122    use crate::executors::ExecutorBuilder;
1123    use alloy_evm::EvmEnv;
1124    use alloy_primitives::hex;
1125    use foundry_cheatcodes::CheatsConfig;
1126    use foundry_evm_core::{FoundryBlock, backend::Backend, evm::EthEvmNetwork};
1127    use foundry_evm_fuzz::CallDetails;
1128    use revm::{
1129        bytecode::Bytecode,
1130        context::{Block, TxEnv},
1131    };
1132    use std::sync::Arc;
1133
1134    fn tx(warp: Option<u64>, roll: Option<u64>) -> BasicTxDetails {
1135        BasicTxDetails {
1136            warp: warp.map(U256::from),
1137            roll: roll.map(U256::from),
1138            sender: Address::ZERO,
1139            call_details: CallDetails {
1140                target: Address::ZERO,
1141                calldata: Bytes::new(),
1142                value: None,
1143            },
1144        }
1145    }
1146
1147    fn tx_with_calldata(byte: u8) -> BasicTxDetails {
1148        BasicTxDetails {
1149            warp: None,
1150            roll: None,
1151            sender: Address::ZERO,
1152            call_details: CallDetails {
1153                target: Address::ZERO,
1154                calldata: Bytes::from(vec![byte]),
1155                value: None,
1156            },
1157        }
1158    }
1159
1160    #[test]
1161    fn build_shrunk_sequence_accumulates_removed_delay_into_next_kept_call() {
1162        let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11)), tx(Some(13), Some(17))];
1163        let mut shrinker = SequenceShrink::new(calls.len());
1164        shrinker.remove(0);
1165
1166        let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1167
1168        assert_eq!(shrunk.len(), 2);
1169        assert_eq!(shrunk[0].warp, Some(U256::from(10)));
1170        assert_eq!(shrunk[0].roll, Some(U256::from(16)));
1171        assert_eq!(shrunk[1].warp, Some(U256::from(13)));
1172        assert_eq!(shrunk[1].roll, Some(U256::from(17)));
1173    }
1174
1175    #[test]
1176    fn build_shrunk_sequence_does_not_move_trailing_delay_backward() {
1177        let calls = vec![tx(Some(3), Some(5)), tx(Some(7), Some(11))];
1178        let mut shrinker = SequenceShrink::new(calls.len());
1179        shrinker.remove(1);
1180
1181        let shrunk = build_shrunk_sequence(&calls, &shrinker, true);
1182
1183        assert_eq!(shrunk.len(), 1);
1184        assert_eq!(shrunk[0].warp, Some(U256::from(3)));
1185        assert_eq!(shrunk[0].roll, Some(U256::from(5)));
1186    }
1187
1188    #[test]
1189    fn check_sequence_value_applies_trailing_delay_before_invariant() {
1190        let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
1191        let mut executor = ExecutorBuilder::default()
1192            .inspectors(|stack| stack.cheatcodes(Arc::new(CheatsConfig::default())))
1193            .gas_limit(1 << 24)
1194            .build(EvmEnv::default(), TxEnv::default(), backend, Default::default());
1195        // Returns `(block.number << 128) + block.timestamp`.
1196        let test_address = Address::repeat_byte(0x11);
1197        let code = hex!("4360801b420160005260206000f3");
1198        executor.set_code(test_address, Bytecode::new_raw(Bytes::from(code))).unwrap();
1199        let block_value =
1200            |timestamp: U256, number: U256| I256::from_raw((number << 128) + timestamp);
1201        let calls = [tx(Some(3), Some(5)), tx(Some(7), Some(11))];
1202        let block = executor.evm_env().block_env.clone();
1203
1204        // Removed calls still contribute their delays to the invariant check, whether the
1205        // cheatcode block mirrors the environment, is unset, or overrides it.
1206        let delayed =
1207            Some(block_value(block.timestamp() + U256::from(10), block.number() + U256::from(16)));
1208        let mut cheatcode_block = block;
1209        cheatcode_block.set_timestamp(U256::from(1000));
1210        cheatcode_block.set_number(U256::from(2000));
1211        let cases = [
1212            (&[0][..], None, delayed),
1213            (&[], None, delayed),
1214            (&[], Some(None), delayed),
1215            (
1216                &[],
1217                Some(Some(cheatcode_block)),
1218                Some(block_value(U256::from(1010), U256::from(2016))),
1219            ),
1220        ];
1221        for (sequence, cheatcode_block, expected) in cases {
1222            let mut executor = executor.clone();
1223            if let Some(block) = cheatcode_block {
1224                executor.inspector_mut().cheatcodes.as_mut().unwrap().block = block;
1225            }
1226            let value =
1227                check_sequence_value(executor, &calls, sequence, test_address, Bytes::new())
1228                    .unwrap();
1229            assert_eq!(value, expected);
1230        }
1231    }
1232
1233    #[test]
1234    fn shrink_run_counts_attempts_and_accepts() {
1235        let mut run = ShrinkRun::new(2);
1236
1237        assert_eq!(run.remaining_attempts(), 2);
1238        assert!(!run.try_candidate(|| false));
1239        assert!(run.try_candidate(|| true));
1240
1241        let mut called_after_limit = false;
1242        assert!(!run.try_candidate(|| {
1243            called_after_limit = true;
1244            true
1245        }));
1246
1247        assert!(!called_after_limit);
1248        let stats = run.finish();
1249        assert_eq!(stats.attempts, 2);
1250        assert_eq!(stats.accepted, 1);
1251    }
1252
1253    #[test]
1254    fn shrink_candidate_keys_skip_duplicates() {
1255        let mut candidates = ShrinkCandidateKeys::new("initial");
1256
1257        assert!(!candidates.insert("initial"));
1258        assert!(candidates.insert("first"));
1259        assert!(!candidates.insert("first"));
1260        assert!(candidates.insert("second"));
1261    }
1262
1263    #[test]
1264    fn shrink_loop_keep_removed_treats_candidate_error_as_still_failing() {
1265        let config = InvariantConfig { shrink_run_limit: 1, ..Default::default() };
1266        let early_exit = EarlyExit::new(false);
1267        let calls = vec![tx(None, None), tx(None, None)];
1268        let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1269
1270        let shrinker = run_shrink_loop(
1271            &config,
1272            &calls,
1273            &progress,
1274            false,
1275            &early_exit,
1276            ShrinkErrorPolicy::KeepRemoved,
1277            |_| Err(eyre::eyre!("candidate replay failed")),
1278        );
1279
1280        assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![1]);
1281    }
1282
1283    #[test]
1284    fn shrink_loop_limit_counts_candidate_replays_not_skipped_indices() {
1285        let config = InvariantConfig { shrink_run_limit: 4, ..Default::default() };
1286        let early_exit = EarlyExit::new(false);
1287        let calls = vec![tx(None, None), tx(None, None), tx(None, None)];
1288        let progress = ShrinkProgress::new(&config, None, "test", None, None, false);
1289        let mut replay_attempts = 0;
1290
1291        let shrinker = run_shrink_loop(
1292            &config,
1293            &calls,
1294            &progress,
1295            false,
1296            &early_exit,
1297            ShrinkErrorPolicy::RestoreRemoved,
1298            |_| {
1299                replay_attempts += 1;
1300                Ok(matches!(replay_attempts, 1 | 4))
1301            },
1302        );
1303
1304        assert_eq!(replay_attempts, 4);
1305        assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![2]);
1306    }
1307
1308    #[test]
1309    fn sequence_shrinker_skips_duplicate_candidates_without_spending_attempts() {
1310        let mut run = ShrinkRun::new(2);
1311        let mut seen = Vec::new();
1312
1313        let shrinker = shrink_sequence_by_removing(
1314            2,
1315            &mut run,
1316            || false,
1317            || {},
1318            |shrinker| {
1319                let candidate = shrinker.current().collect::<Vec<_>>();
1320                if seen.contains(&candidate) {
1321                    None
1322                } else {
1323                    seen.push(candidate);
1324                    Some(false)
1325                }
1326            },
1327        );
1328
1329        assert_eq!(shrinker.current().collect::<Vec<_>>(), vec![0, 1]);
1330        let stats = run.finish();
1331        assert_eq!(stats.attempts, 2);
1332        assert_eq!(stats.accepted, 0);
1333    }
1334
1335    #[test]
1336    fn shrink_progress_message_renders_current_sequence() {
1337        let calls = vec![tx_with_calldata(1), tx_with_calldata(2)];
1338
1339        let message =
1340            format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1341
1342        assert!(message.contains(" Shrink: invariant_live"));
1343        assert!(message.contains("[Sequence] (shrunk: 2)"));
1344        assert!(message.contains("calldata=0x01 args=[]"));
1345        assert!(message.contains("calldata=0x02 args=[]"));
1346    }
1347
1348    #[test]
1349    fn shrink_progress_message_omits_middle_of_large_sequence() {
1350        let calls = (0..(LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2 + 3))
1351            .map(|idx| tx_with_calldata(idx as u8))
1352            .collect::<Vec<_>>();
1353
1354        let message =
1355            format_shrink_progress_message(" Shrink: invariant_live", &calls, None, false);
1356
1357        assert!(message.contains("[Sequence] (shrunk: 35)"));
1358        assert!(message.contains("... 3 call(s) omitted ..."));
1359        assert_eq!(message.matches("sender=").count(), LIVE_SHRINK_SEQUENCE_EDGE_CALLS * 2);
1360        assert!(message.contains("calldata=0x00 args=[]"));
1361        assert!(message.contains("calldata=0x22 args=[]"));
1362    }
1363}