Skip to content

Commit

Permalink
foundry-rs#991 Deterministic Fuzzer with RNG Seeding (foundry-rs#1658)
Browse files Browse the repository at this point in the history
* feat: basic rng seeding

* chore: bump u32 to U256

* feat(config): add additional helper macro

* feat: finish fuzz seed impl

* bump ethers

Co-authored-by: Matthias Seitz <[email protected]>
  • Loading branch information
2 people authored and iFrostizz committed Nov 9, 2022
1 parent 0a0bba9 commit 0b971c7
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 21 deletions.
22 changes: 11 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 41 additions & 10 deletions cli/src/cmd/forge/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
suggestions, utils,
};
use clap::{AppSettings, Parser};
use ethers::solc::utils::RuntimeOrHandle;
use ethers::{solc::utils::RuntimeOrHandle, types::U256};
use forge::{
decode::decode_console_logs,
executor::{inspector::CheatsConfig, opts::EvmOpts},
Expand All @@ -22,17 +22,22 @@ use forge::{
MultiContractRunner, MultiContractRunnerBuilder,
};
use foundry_common::evm::EvmArgs;
use foundry_config::{figment::Figment, Config};
use foundry_config::{figment, figment::Figment, Config};
use proptest::test_runner::{RngAlgorithm, TestRng};
use regex::Regex;
use std::{collections::BTreeMap, path::PathBuf, sync::mpsc::channel, thread, time::Duration};
use tracing::trace;
use watchexec::config::{InitConfig, RuntimeConfig};
use yansi::Paint;
mod filter;
pub use filter::Filter;
use foundry_config::figment::{
value::{Dict, Map},
Metadata, Profile, Provider,
};

// Loads project's figment and merges the build cli arguments into it
foundry_config::impl_figment_convert!(TestArgs, opts, evm_opts);
foundry_config::merge_impl_figment_convert!(TestArgs, opts, evm_opts);

#[derive(Debug, Clone, Parser)]
#[clap(global_setting = AppSettings::DeriveDisplayOrder)]
Expand Down Expand Up @@ -90,6 +95,9 @@ pub struct TestArgs {
/// List tests instead of running them
#[clap(long, short, help_heading = "DISPLAY OPTIONS")]
list: bool,

#[clap(long, help = "Set seed used to generate randomness during your fuzz runs", parse(try_from_str = utils::parse_u256))]
pub fuzz_seed: Option<U256>,
}

impl TestArgs {
Expand All @@ -108,12 +116,7 @@ impl TestArgs {
// merge all configs
let figment: Figment = self.into();
let evm_opts = figment.extract()?;
let mut config = Config::from_provider(figment).sanitized();

// merging etherscan api key into Config
if let Some(etherscan_api_key) = &self.etherscan_api_key {
config.etherscan_api_key = Some(etherscan_api_key.to_string());
}
let config = Config::from_provider(figment).sanitized();
Ok((config, evm_opts))
}

Expand All @@ -132,6 +135,25 @@ impl TestArgs {
}
}

impl Provider for TestArgs {
fn metadata(&self) -> Metadata {
Metadata::named("Core Build Args Provider")
}

fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
let mut dict = Dict::default();
if let Some(fuzz_seed) = self.fuzz_seed {
dict.insert("fuzz_seed".to_string(), fuzz_seed.to_string().into());
}

if let Some(ref etherscan_api_key) = self.etherscan_api_key {
dict.insert("etherscan_api_key".to_string(), etherscan_api_key.to_string().into());
}

Ok(Map::from([(Config::selected_profile(), dict)]))
}
}

impl Cmd for TestArgs {
type Output = TestOutcome;

Expand Down Expand Up @@ -286,7 +308,16 @@ pub fn custom_run(args: TestArgs, include_fuzz_tests: bool) -> eyre::Result<Test
max_global_rejects: config.fuzz_max_global_rejects,
..Default::default()
};
let fuzzer = proptest::test_runner::TestRunner::new(cfg);

let fuzzer = if let Some(ref fuzz_seed) = config.fuzz_seed {
let mut bytes: [u8; 32] = [0; 32];
fuzz_seed.to_big_endian(&mut bytes);
let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &bytes);
proptest::test_runner::TestRunner::new_with_rng(cfg, rng)
} else {
proptest::test_runner::TestRunner::new(cfg)
};

let mut filter = args.filter(&config);

trace!(target: "forge::test", ?filter, "using filter");
Expand Down
1 change: 1 addition & 0 deletions cli/tests/it/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ forgetest!(can_extract_config_values, |prj: TestProject, mut cmd: TestCommand| {
fuzz_runs: 1000,
fuzz_max_local_rejects: 2000,
fuzz_max_global_rejects: 100203,
fuzz_seed: Some(1000.into()),
ffi: true,
sender: "00a329c0648769A73afAc7F9381D08FB43dBEA72".parse().unwrap(),
tx_origin: "00a329c0648769A73afAc7F9F81E08FB43dBEA72".parse().unwrap(),
Expand Down
9 changes: 9 additions & 0 deletions config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ pub struct Config {
/// by proptest, to be encountered during usage of `vm.assume`
/// cheatcode.
pub fuzz_max_global_rejects: u32,
/// Optional seed for the fuzzing RNG algorithm
#[serde(
deserialize_with = "ethers_core::types::serde_helpers::deserialize_stringified_numeric_opt"
)]
pub fuzz_seed: Option<U256>,
/// Print the names of the compiled contracts
pub names: bool,
/// Print the sizes of the compiled contracts
Expand Down Expand Up @@ -1460,6 +1465,7 @@ impl Default for Config {
fuzz_runs: 256,
fuzz_max_local_rejects: 1024,
fuzz_max_global_rejects: 65536,
fuzz_seed: None,
ffi: false,
sender: Config::DEFAULT_SENDER,
tx_origin: Config::DEFAULT_SENDER,
Expand Down Expand Up @@ -2850,6 +2856,7 @@ mod tests {
fuzz_max_global_rejects = 65536
fuzz_max_local_rejects = 1024
fuzz_runs = 256
fuzz_seed = '0x3e8'
gas_limit = 9223372036854775807
gas_price = 0
gas_reports = ['*']
Expand Down Expand Up @@ -2887,6 +2894,8 @@ mod tests {
)?;

let config = Config::load_with_root(jail.directory());

assert_eq!(config.fuzz_seed, Some(1000.into()));
assert_eq!(
config.remappings,
vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
Expand Down

0 comments on commit 0b971c7

Please sign in to comment.