Skip to content

Commit 7b84375

Browse files
authored
fix(forge): replay fuzz failure only if same test selector (#11947)
1 parent a841db1 commit 7b84375

File tree

2 files changed

+110
-4
lines changed

2 files changed

+110
-4
lines changed

crates/evm/evm/src/executors/fuzz/mod.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,10 @@ impl FuzzedExecutor {
141141

142142
'stop: while continue_campaign(test_data.runs) {
143143
// If counterexample recorded, replay it first, without incrementing runs.
144-
let input = if let Some(failure) = self.persisted_failure.take() {
145-
failure.calldata
144+
let input = if let Some(failure) = self.persisted_failure.take()
145+
&& func.selector() == failure.calldata[..4]
146+
{
147+
failure.calldata.clone()
146148
} else {
147149
// If running with progress, then increment current run.
148150
if let Some(progress) = progress {
@@ -222,8 +224,9 @@ impl FuzzedExecutor {
222224
break 'stop;
223225
}
224226
TestCaseError::Reject(_) => {
225-
// Discard run and apply max rejects if configured.
226-
test_data.runs -= 1;
227+
// Discard run and apply max rejects if configured. Saturate to handle
228+
// the case of replayed failure, which doesn't count as a run.
229+
test_data.runs = test_data.runs.saturating_sub(1);
227230
if self.config.max_test_rejects > 0 {
228231
test_data.rejects += 1;
229232
if test_data.rejects >= self.config.max_test_rejects {

crates/forge/tests/it/fuzz.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,106 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)
391391
392392
"#]]);
393393
});
394+
395+
// Test that counterexample is not replayed if test changes.
396+
// <https://github.com/foundry-rs/foundry/issues/11927>
397+
forgetest_init!(test_fuzz_replay_with_changed_test, |prj, cmd| {
398+
prj.update_config(|config| config.fuzz.seed = Some(U256::from(100u32)));
399+
prj.add_test(
400+
"Counter.t.sol",
401+
r#"
402+
import {Test} from "forge-std/Test.sol";
403+
404+
contract CounterTest is Test {
405+
function testFuzz_SetNumber(uint256 x) public pure {
406+
require(x > 200);
407+
}
408+
}
409+
"#,
410+
);
411+
// Tests should fail and record counterexample with value 2.
412+
cmd.args(["test"]).assert_failure().stdout_eq(str![[r#"
413+
...
414+
Failing tests:
415+
Encountered 1 failing test in test/Counter.t.sol:CounterTest
416+
[FAIL: EvmError: Revert; counterexample: calldata=0x5c7f60d70000000000000000000000000000000000000000000000000000000000000002 args=[2]] testFuzz_SetNumber(uint256) (runs: 19, [AVG_GAS])
417+
...
418+
419+
"#]]);
420+
421+
// Change test to assume counterexample 2 is discarded.
422+
prj.add_test(
423+
"Counter.t.sol",
424+
r#"
425+
import {Test} from "forge-std/Test.sol";
426+
427+
contract CounterTest is Test {
428+
function testFuzz_SetNumber(uint256 x) public pure {
429+
vm.assume(x != 2);
430+
}
431+
}
432+
"#,
433+
);
434+
// Test should pass when replay failure with changed assume logic.
435+
cmd.forge_fuse().args(["test"]).assert_success().stdout_eq(str![[r#"
436+
[COMPILING_FILES] with [SOLC_VERSION]
437+
[SOLC_VERSION] [ELAPSED]
438+
Compiler run successful!
439+
440+
Ran 1 test for test/Counter.t.sol:CounterTest
441+
[PASS] testFuzz_SetNumber(uint256) (runs: 256, [AVG_GAS])
442+
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
443+
444+
Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)
445+
446+
"#]]);
447+
448+
// Change test signature.
449+
prj.add_test(
450+
"Counter.t.sol",
451+
r#"
452+
import {Test} from "forge-std/Test.sol";
453+
454+
contract CounterTest is Test {
455+
function testFuzz_SetNumber(uint8 x) public pure {
456+
}
457+
}
458+
"#,
459+
);
460+
// Test should pass when replay failure with changed function signature.
461+
cmd.forge_fuse().args(["test"]).assert_success().stdout_eq(str![[r#"
462+
[COMPILING_FILES] with [SOLC_VERSION]
463+
[SOLC_VERSION] [ELAPSED]
464+
Compiler run successful!
465+
466+
Ran 1 test for test/Counter.t.sol:CounterTest
467+
[PASS] testFuzz_SetNumber(uint8) (runs: 256, [AVG_GAS])
468+
Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]
469+
470+
Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)
471+
472+
"#]]);
473+
474+
// Change test back to the original one that produced the counterexample.
475+
prj.add_test(
476+
"Counter.t.sol",
477+
r#"
478+
import {Test} from "forge-std/Test.sol";
479+
480+
contract CounterTest is Test {
481+
function testFuzz_SetNumber(uint256 x) public pure {
482+
require(x > 200);
483+
}
484+
}
485+
"#,
486+
);
487+
// Test should fail with replayed counterexample 2 (0 runs).
488+
cmd.forge_fuse().args(["test"]).assert_failure().stdout_eq(str![[r#"
489+
...
490+
Failing tests:
491+
Encountered 1 failing test in test/Counter.t.sol:CounterTest
492+
[FAIL: EvmError: Revert; counterexample: calldata=0x5c7f60d70000000000000000000000000000000000000000000000000000000000000002 args=[2]] testFuzz_SetNumber(uint256) (runs: 0, [AVG_GAS])
493+
...
494+
495+
"#]]);
496+
});

0 commit comments

Comments
 (0)