forge/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#[macro_use]
extern crate foundry_common;
#[macro_use]
extern crate tracing;
use foundry_compilers::ProjectCompileOutput;
use foundry_config::{
validate_profiles, Config, FuzzConfig, InlineConfig, InlineConfigError, InlineConfigParser,
InvariantConfig, NatSpec,
};
use proptest::test_runner::{
FailurePersistence, FileFailurePersistence, RngAlgorithm, TestRng, TestRunner,
};
use std::path::Path;
pub mod coverage;
pub mod gas_report;
pub mod multi_runner;
pub use multi_runner::{MultiContractRunner, MultiContractRunnerBuilder};
mod runner;
pub use runner::ContractRunner;
mod progress;
pub mod result;
// TODO: remove
pub use foundry_common::traits::TestFilter;
pub use foundry_evm::*;
/// Metadata on how to run fuzz/invariant tests
#[derive(Clone, Debug, Default)]
pub struct TestOptions {
/// The base "fuzz" test configuration. To be used as a fallback in case
/// no more specific configs are found for a given run.
pub fuzz: FuzzConfig,
/// The base "invariant" test configuration. To be used as a fallback in case
/// no more specific configs are found for a given run.
pub invariant: InvariantConfig,
/// Contains per-test specific "fuzz" configurations.
pub inline_fuzz: InlineConfig<FuzzConfig>,
/// Contains per-test specific "invariant" configurations.
pub inline_invariant: InlineConfig<InvariantConfig>,
}
impl TestOptions {
/// Tries to create a new instance by detecting inline configurations from the project compile
/// output.
pub fn new(
output: &ProjectCompileOutput,
root: &Path,
profiles: Vec<String>,
base_fuzz: FuzzConfig,
base_invariant: InvariantConfig,
) -> Result<Self, InlineConfigError> {
let natspecs: Vec<NatSpec> = NatSpec::parse(output, root);
let mut inline_invariant = InlineConfig::<InvariantConfig>::default();
let mut inline_fuzz = InlineConfig::<FuzzConfig>::default();
// Validate all natspecs
for natspec in &natspecs {
validate_profiles(natspec, &profiles)?;
}
// Firstly, apply contract-level configurations
for natspec in natspecs.iter().filter(|n| n.function.is_none()) {
if let Some(fuzz) = base_fuzz.merge(natspec)? {
inline_fuzz.insert_contract(&natspec.contract, fuzz);
}
if let Some(invariant) = base_invariant.merge(natspec)? {
inline_invariant.insert_contract(&natspec.contract, invariant);
}
}
for (natspec, f) in natspecs.iter().filter_map(|n| n.function.as_ref().map(|f| (n, f))) {
// Apply in-line configurations for the current profile
let c = &natspec.contract;
// We might already have inserted contract-level configs above, so respect data already
// present in inline configs.
let base_fuzz = inline_fuzz.get(c, f).unwrap_or(&base_fuzz);
let base_invariant = inline_invariant.get(c, f).unwrap_or(&base_invariant);
if let Some(fuzz) = base_fuzz.merge(natspec)? {
inline_fuzz.insert_fn(c, f, fuzz);
}
if let Some(invariant) = base_invariant.merge(natspec)? {
inline_invariant.insert_fn(c, f, invariant);
}
}
Ok(Self { fuzz: base_fuzz, invariant: base_invariant, inline_fuzz, inline_invariant })
}
/// Returns a "fuzz" test runner instance. Parameters are used to select tight scoped fuzz
/// configs that apply for a contract-function pair. A fallback configuration is applied
/// if no specific setup is found for a given input.
///
/// - `contract_id` is the id of the test contract, expressed as a relative path from the
/// project root.
/// - `test_fn` is the name of the test function declared inside the test contract.
pub fn fuzz_runner(&self, contract_id: &str, test_fn: &str) -> TestRunner {
let fuzz_config = self.fuzz_config(contract_id, test_fn).clone();
let failure_persist_path = fuzz_config
.failure_persist_dir
.unwrap()
.join(fuzz_config.failure_persist_file.unwrap())
.into_os_string()
.into_string()
.unwrap();
self.fuzzer_with_cases(
fuzz_config.runs,
fuzz_config.max_test_rejects,
Some(Box::new(FileFailurePersistence::Direct(failure_persist_path.leak()))),
)
}
/// Returns an "invariant" test runner instance. Parameters are used to select tight scoped fuzz
/// configs that apply for a contract-function pair. A fallback configuration is applied
/// if no specific setup is found for a given input.
///
/// - `contract_id` is the id of the test contract, expressed as a relative path from the
/// project root.
/// - `test_fn` is the name of the test function declared inside the test contract.
pub fn invariant_runner(&self, contract_id: &str, test_fn: &str) -> TestRunner {
let invariant = self.invariant_config(contract_id, test_fn);
self.fuzzer_with_cases(invariant.runs, invariant.max_assume_rejects, None)
}
/// Returns a "fuzz" configuration setup. Parameters are used to select tight scoped fuzz
/// configs that apply for a contract-function pair. A fallback configuration is applied
/// if no specific setup is found for a given input.
///
/// - `contract_id` is the id of the test contract, expressed as a relative path from the
/// project root.
/// - `test_fn` is the name of the test function declared inside the test contract.
pub fn fuzz_config(&self, contract_id: &str, test_fn: &str) -> &FuzzConfig {
self.inline_fuzz.get(contract_id, test_fn).unwrap_or(&self.fuzz)
}
/// Returns an "invariant" configuration setup. Parameters are used to select tight scoped
/// invariant configs that apply for a contract-function pair. A fallback configuration is
/// applied if no specific setup is found for a given input.
///
/// - `contract_id` is the id of the test contract, expressed as a relative path from the
/// project root.
/// - `test_fn` is the name of the test function declared inside the test contract.
pub fn invariant_config(&self, contract_id: &str, test_fn: &str) -> &InvariantConfig {
self.inline_invariant.get(contract_id, test_fn).unwrap_or(&self.invariant)
}
pub fn fuzzer_with_cases(
&self,
cases: u32,
max_global_rejects: u32,
file_failure_persistence: Option<Box<dyn FailurePersistence>>,
) -> TestRunner {
let config = proptest::test_runner::Config {
failure_persistence: file_failure_persistence,
cases,
max_global_rejects,
// Disable proptest shrink: for fuzz tests we provide single counterexample,
// for invariant tests we shrink outside proptest.
max_shrink_iters: 0,
..Default::default()
};
if let Some(seed) = &self.fuzz.seed {
trace!(target: "forge::test", %seed, "building deterministic fuzzer");
let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &seed.to_be_bytes::<32>());
TestRunner::new_with_rng(config, rng)
} else {
trace!(target: "forge::test", "building stochastic fuzzer");
TestRunner::new(config)
}
}
}
/// Builder utility to create a [`TestOptions`] instance.
#[derive(Default)]
#[must_use = "builders do nothing unless you call `build` on them"]
pub struct TestOptionsBuilder {
fuzz: Option<FuzzConfig>,
invariant: Option<InvariantConfig>,
profiles: Option<Vec<String>>,
}
impl TestOptionsBuilder {
/// Sets a [`FuzzConfig`] to be used as base "fuzz" configuration.
pub fn fuzz(mut self, conf: FuzzConfig) -> Self {
self.fuzz = Some(conf);
self
}
/// Sets a [`InvariantConfig`] to be used as base "invariant" configuration.
pub fn invariant(mut self, conf: InvariantConfig) -> Self {
self.invariant = Some(conf);
self
}
/// Sets available configuration profiles. Profiles are useful to validate existing in-line
/// configurations. This argument is necessary in case a `compile_output`is provided.
pub fn profiles(mut self, p: Vec<String>) -> Self {
self.profiles = Some(p);
self
}
/// Creates an instance of [`TestOptions`]. This takes care of creating "fuzz" and
/// "invariant" fallbacks, and extracting all inline test configs, if available.
///
/// `root` is a reference to the user's project root dir. This is essential
/// to determine the base path of generated contract identifiers. This is to provide correct
/// matchers for inline test configs.
pub fn build(
self,
output: &ProjectCompileOutput,
root: &Path,
) -> Result<TestOptions, InlineConfigError> {
let profiles: Vec<String> =
self.profiles.unwrap_or_else(|| vec![Config::selected_profile().into()]);
let base_fuzz = self.fuzz.unwrap_or_default();
let base_invariant = self.invariant.unwrap_or_default();
TestOptions::new(output, root, profiles, base_fuzz, base_invariant)
}
}