Skip to content

Commit 30cc56e

Browse files
authored
fix(forge): persist corpus per test suite (#11469)
* fix(forge): persist corpus per test suite * No unwrap test
1 parent 2b56727 commit 30cc56e

File tree

6 files changed

+113
-33
lines changed

6 files changed

+113
-33
lines changed

crates/config/src/fuzz.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Configuration for fuzz testing.
22
33
use alloy_primitives::U256;
4+
use foundry_compilers::utils::canonicalized;
45
use serde::{Deserialize, Serialize};
56
use std::path::PathBuf;
67

@@ -112,9 +113,9 @@ pub struct FuzzCorpusConfig {
112113
}
113114

114115
impl FuzzCorpusConfig {
115-
pub fn with_test_name(&mut self, test_name: &String) {
116+
pub fn with_test(&mut self, contract: &str, test: &str) {
116117
if let Some(corpus_dir) = &self.corpus_dir {
117-
self.corpus_dir = Some(corpus_dir.join(test_name));
118+
self.corpus_dir = Some(canonicalized(corpus_dir.join(contract).join(test)));
118119
}
119120
}
120121

crates/evm/evm/src/executors/corpus.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,16 +151,12 @@ pub(crate) struct CorpusManager {
151151

152152
impl CorpusManager {
153153
pub fn new(
154-
config: &FuzzCorpusConfig,
155-
func_name: &String,
154+
config: FuzzCorpusConfig,
156155
tx_generator: BoxedStrategy<BasicTxDetails>,
157156
executor: &Executor,
158157
fuzzed_function: Option<&Function>,
159158
fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
160159
) -> eyre::Result<Self> {
161-
let mut config = config.clone();
162-
config.with_test_name(func_name);
163-
164160
let mutation_generator = prop_oneof![
165161
Just(MutationType::Splice),
166162
Just(MutationType::Repeat),

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,7 @@ impl FuzzedExecutor {
119119
let max_traces_to_collect = std::cmp::max(1, self.config.gas_report_samples) as usize;
120120

121121
let mut corpus_manager = CorpusManager::new(
122-
&self.config.corpus,
123-
&func.name,
122+
self.config.corpus.clone(),
124123
strategy.boxed(),
125124
&self.executor,
126125
Some(func),

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -622,8 +622,7 @@ impl<'a> InvariantExecutor<'a> {
622622
}
623623

624624
let corpus_manager = CorpusManager::new(
625-
&self.config.corpus,
626-
&invariant_contract.invariant_function.name,
625+
self.config.corpus.clone(),
627626
strategy.boxed(),
628627
&self.executor,
629628
None,

crates/forge/src/runner.rs

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use alloy_primitives::{Address, Bytes, U256, address, map::HashMap};
1313
use eyre::Result;
1414
use foundry_common::{TestFunctionExt, TestFunctionKind, contracts::ContractsByAddress};
1515
use foundry_compilers::utils::canonicalized;
16-
use foundry_config::Config;
16+
use foundry_config::{Config, FuzzCorpusConfig};
1717
use foundry_evm::{
1818
constants::CALLER,
1919
decode::RevertDecoder,
@@ -725,11 +725,18 @@ impl<'a> FunctionRunner<'a> {
725725
executor
726726
.inspector_mut()
727727
.collect_edge_coverage(invariant_config.corpus.collect_edge_coverage());
728+
let mut config = invariant_config.clone();
729+
let (failure_dir, failure_file) = test_paths(
730+
&mut config.corpus,
731+
invariant_config.failure_persist_dir.clone().unwrap(),
732+
self.cr.name,
733+
&func.name,
734+
);
728735

729736
let mut evm = InvariantExecutor::new(
730737
executor,
731738
runner,
732-
invariant_config.clone(),
739+
config,
733740
identified_contracts,
734741
&self.cr.mcr.known_contracts,
735742
);
@@ -739,12 +746,6 @@ impl<'a> FunctionRunner<'a> {
739746
call_after_invariant,
740747
abi: &self.cr.contract.abi,
741748
};
742-
743-
let (failure_dir, failure_file) = test_failure_paths(
744-
invariant_config.failure_persist_dir.clone().unwrap(),
745-
self.cr.name,
746-
&invariant_contract.invariant_function.name,
747-
);
748749
let show_solidity = invariant_config.show_solidity;
749750

750751
// Try to replay recorded failure if any.
@@ -934,7 +935,13 @@ impl<'a> FunctionRunner<'a> {
934935
}
935936

936937
let runner = self.fuzz_runner();
937-
let fuzz_config = self.config.fuzz.clone();
938+
let mut fuzz_config = self.config.fuzz.clone();
939+
let (failure_dir, failure_file) = test_paths(
940+
&mut fuzz_config.corpus,
941+
fuzz_config.failure_persist_dir.clone().unwrap(),
942+
self.cr.name,
943+
&func.name,
944+
);
938945

939946
let progress = start_fuzz_progress(
940947
self.cr.progress,
@@ -944,12 +951,6 @@ impl<'a> FunctionRunner<'a> {
944951
fuzz_config.runs,
945952
);
946953

947-
let (failure_dir, failure_file) = test_failure_paths(
948-
fuzz_config.failure_persist_dir.clone().unwrap(),
949-
self.cr.name,
950-
&func.name,
951-
);
952-
953954
let mut executor = self.executor.into_owned();
954955
// Enable edge coverage if running with coverage guided fuzzing or with edge coverage
955956
// metrics (useful for benchmarking the fuzzer).
@@ -1105,14 +1106,18 @@ fn persisted_call_sequence(path: &Path, bytecode: &Bytes) -> Option<Vec<BaseCoun
11051106
)
11061107
}
11071108

1108-
/// Helper functions to return canonicalized test failure paths.
1109-
fn test_failure_paths(
1109+
/// Helper function to set test corpus dir and to compose persisted failure paths.
1110+
fn test_paths(
1111+
corpus_config: &mut FuzzCorpusConfig,
11101112
persist_dir: PathBuf,
11111113
contract_name: &str,
1112-
invariant_name: &str,
1114+
test_name: &str,
11131115
) -> (PathBuf, PathBuf) {
1114-
let dir = persist_dir.join("failures").join(contract_name.split(':').next_back().unwrap());
1115-
let dir = canonicalized(dir);
1116-
let file = canonicalized(dir.join(invariant_name));
1117-
(dir, file)
1116+
let contract = contract_name.split(':').next_back().unwrap();
1117+
// Update config with corpus dir for current test.
1118+
corpus_config.with_test(contract, test_name);
1119+
1120+
let failures_dir = canonicalized(persist_dir.join("failures").join(contract));
1121+
let failure_file = canonicalized(failures_dir.join(test_name));
1122+
(failures_dir, failure_file)
11181123
}

crates/forge/tests/it/invariant.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,3 +1547,83 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)
15471547
"#]
15481548
]);
15491549
});
1550+
1551+
// <https://github.com/foundry-rs/foundry/issues/11453>
1552+
forgetest_init!(corpus_dir, |prj, cmd| {
1553+
prj.update_config(|config| {
1554+
config.invariant.runs = 10;
1555+
config.invariant.depth = 10;
1556+
config.invariant.corpus.corpus_dir = Some("invariant_corpus".into());
1557+
1558+
config.fuzz.runs = 10;
1559+
config.fuzz.corpus.corpus_dir = Some("fuzz_corpus".into());
1560+
});
1561+
prj.add_test(
1562+
"CounterTests.t.sol",
1563+
r#"
1564+
import {Test} from "forge-std/Test.sol";
1565+
import {Counter} from "../src/Counter.sol";
1566+
1567+
contract Counter1Test is Test {
1568+
Counter public counter;
1569+
1570+
function setUp() public {
1571+
counter = new Counter();
1572+
counter.setNumber(0);
1573+
}
1574+
1575+
function testFuzz_SetNumber(uint256 x) public {
1576+
counter.setNumber(x);
1577+
assertEq(counter.number(), x);
1578+
}
1579+
1580+
function invariant_counter_called() public view {
1581+
}
1582+
}
1583+
1584+
contract Counter2Test is Test {
1585+
Counter public counter;
1586+
1587+
function setUp() public {
1588+
counter = new Counter();
1589+
counter.setNumber(0);
1590+
}
1591+
1592+
function testFuzz_SetNumber(uint256 x) public {
1593+
counter.setNumber(x);
1594+
assertEq(counter.number(), x);
1595+
}
1596+
1597+
function invariant_counter_called() public view {
1598+
}
1599+
}
1600+
"#,
1601+
);
1602+
1603+
cmd.args(["test"]).assert_success().stdout_eq(str![[r#"
1604+
...
1605+
Ran 3 test suites [ELAPSED]: 6 tests passed, 0 failed, 0 skipped (6 total tests)
1606+
1607+
"#]]);
1608+
1609+
assert!(
1610+
prj.root()
1611+
.join("invariant_corpus")
1612+
.join("Counter1Test")
1613+
.join("invariant_counter_called")
1614+
.exists()
1615+
);
1616+
assert!(
1617+
prj.root()
1618+
.join("invariant_corpus")
1619+
.join("Counter2Test")
1620+
.join("invariant_counter_called")
1621+
.exists()
1622+
);
1623+
assert!(
1624+
prj.root().join("fuzz_corpus").join("Counter1Test").join("testFuzz_SetNumber").exists()
1625+
);
1626+
assert!(
1627+
prj.root().join("fuzz_corpus").join("Counter2Test").join("testFuzz_SetNumber").exists()
1628+
);
1629+
});

0 commit comments

Comments
 (0)