-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(fuzz): ability to declare fuzz test fixtures #7428
Conversation
2398972
to
e1f8a95
Compare
12c4e0c
to
5b92ee1
Compare
5b92ee1
to
254017c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
smol nits,
ptal @DaniPopes @klkvr
crates/evm/fuzz/src/lib.rs
Outdated
Self { inner: Arc::new(fixtures) } | ||
} | ||
|
||
pub fn param_fixtures(&self, param_name: &String) -> Option<&[DynSolValue]> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs oneline doc
/// `function fixture_owner() public returns (address[] memory)`. | ||
/// Use `owner` named parameter in fuzzed test in order to create a custom strategy | ||
/// `function testFuzz_ownerAddress(address owner, uint amount)`. | ||
#[derive(Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[derive(Debug)] | |
#[derive(Debug, Default)] | |
#[non_exhaustive] |
crates/forge/bin/cmd/test/mod.rs
Outdated
@@ -221,7 +221,7 @@ impl TestArgs { | |||
*test_pattern = Some(debug_test_pattern.clone()); | |||
} | |||
|
|||
let outcome = self.run_tests(runner, config, verbosity, &filter).await?; | |||
let outcome = self.run_tests(runner, config.clone(), verbosity, &filter).await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These clones are not necessary
crates/forge/bin/cmd/test/mod.rs
Outdated
@@ -181,7 +181,7 @@ impl TestArgs { | |||
|
|||
let test_options: TestOptions = TestOptionsBuilder::default() | |||
.fuzz(config.clone().fuzz) | |||
.invariant(config.invariant) | |||
.invariant(config.clone().invariant) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move clone after .invariant to clone only that object instead of the entire config
same above for fuzz
crates/forge/src/runner.rs
Outdated
fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures { | ||
// collect test fixtures param:array of values | ||
let mut fixtures = HashMap::new(); | ||
self.contract.functions().for_each(|func| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use a for loop with .filter(|f| f.name.is_fixture())
crates/forge/src/runner.rs
Outdated
self.executor.call(CALLER, address, func, &[], U256::ZERO, None) | ||
{ | ||
fixtures.insert( | ||
func.name.strip_prefix("fixture_").unwrap().to_string(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should check if there's a function with the stripped name and issue a warning
crates/forge/src/runner.rs
Outdated
{ | ||
fixtures.insert( | ||
func.name.strip_prefix("fixture_").unwrap().to_string(), | ||
decoded_result, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should check for validity of the decoded result here instead of later when running the tests since all necessary info to do this is available here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that would be nice to have but needs special handling of handler based testing (where fuzzed functions are not in same contract that defines setUp/invariant_/fixture_
functions). It can be done but it adds code complexity to look up such info in all contracts created when setUp
, wdyt?
I addressed comments (but #7428 (comment)) and additionally moved |
69e8c51
to
9276893
Compare
hey @mattsse @DaniPopes I added fixtures for string and bytes too and a Solidity test to cover all combinations, pls have a look when you get time. thank you |
crates/forge/bin/cmd/test/mod.rs
Outdated
@@ -202,7 +202,7 @@ impl TestArgs { | |||
let runner = MultiContractRunnerBuilder::default() | |||
.set_debug(should_debug) | |||
.initial_balance(evm_opts.initial_balance) | |||
.evm_spec(config.evm_spec_id()) | |||
.evm_spec(config.clone().evm_spec_id()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this clone
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, missed this comment, removed with a5e8da0
@klkvr I added support for reading fixtures from storage and dropped the inline fixtures config with fb86084 Didn't find a nicer way to populate fixtures from storage array but to call function with incremented indexes, pls lmk if you see a nicer way for doing this. see |
…rent type. Keep level of randomness in fixture strategy, at par with uint / int strategies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@grandizzy we shouldn't just panic on user errors, I meant that we should also have validation somewhere which will throw a error, thus making panic more safe
we can probably either do another pass through ABIs before running tests to check if there are any mismatches between fixture types and parameter types or validate this when generating strategies.
/// `function fixture_owner() public returns (address[] memory){}` | ||
/// returns an array of addresses to be used for fuzzing `owner` named parameter in scope of the | ||
/// current test. | ||
fn fuzz_fixtures(&mut self, address: Address) -> FuzzFixtures { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can just have this check here
@klkvr if I am not missing something a fully validation when collecting fixtures from invariant contract isn't possible as there could be targets created during runs that we don't have at the moment test is set up. Even if we manage to do the validation then the option would be to panic if params not of same types (otherwise it will still panic later in the run). If my rationale is correct what do you think if we just use some default values in case fixture of different type (like 0 for uint, adrress(0), empty string, etc.) and raise errors instead panic, they are quite noisy in the console and hard to be missed, so dev will know fixtures needs to be amended. thanks! |
hmm, I see. what if we do |
… for fixture strategy and raise error
oh, awesome stuff, this should handle all cases, added in 997c5d4 |
proptest::prop_oneof![ | ||
50 => { | ||
let custom_fixtures: Vec<DynSolValue> = | ||
fixtures.iter().enumerate().map(|(_, value)| value.to_owned()).collect(); | ||
let custom_fixtures_len = custom_fixtures.len(); | ||
any::<prop::sample::Index>() | ||
.prop_filter_map("invalid fixture", move |index| { | ||
let index = index.index(custom_fixtures_len); | ||
$strategy_value(custom_fixtures.get(index)) | ||
}) | ||
}, | ||
50 => $default_strategy | ||
].boxed() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't we apply prop_filter_map
to the entire prop_oneof!
output? I believe right now if we only have invalid fixtures it will fail with max rejects while it'd be better if it used default strategy instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was thinking that could be better to fail (faster) with max rejects rather than run tests with a config that is not really desired, but can change to apply to both strategies, wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, let's keep it like this for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! nice work
@mattsse @DaniPopes mind taking a look?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!
Motivation
Closes #3521
Proposed implementation for #735 (review) with fixtures declared in Solidity tests
As mentioned in #3521 the number of fuzzed edge cases is too high (only 29% unique), this is due to:
foundry/crates/evm/fuzz/src/strategies/uint.rs
Lines 114 to 118 in 0026488
foundry/crates/evm/fuzz/src/strategies/int.rs
Lines 130 to 134 in 0026488
vec![]
) so this gives a much higher weight for generating edge cases, seefoundry/crates/evm/fuzz/src/strategies/param.rs
Lines 36 to 38 in 0026488
foundry/crates/evm/fuzz/src/strategies/param.rs
Lines 39 to 41 in 0026488
Solution
Proposed solution:
Fixtures can be defined as array of values to be used in fuzzed tests (support for
address
,address
,uint
,int
andbytes
types) byfixture
and followed by param name to be fuzzed. Function should return an (fixed size or dynamic) array of values to be used for fuzzing. For example, fixtures to be used when fuzzing parameter namedowner
of typeaddress
can be defined in Solidity test asfixture
and followed by param name to be fuzzed. For example, fixtures to be used when fuzzing parameter namedamount
of typeuint32
can be defined asFixtures with
setUp
sample:owner
fuzzed parameter of typeaddress
owner
param name so all defined fixtures are used when fuzzingthen fuzzer will panic
Fixtures without
setUp
sample:Fixtures from storage arrays:
Fixtures as fixed sized arrays:
Some numbers collected from running without and with fixtures for a uint fuzzed param: