forked from foundry-rs/foundry
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlib.rs
255 lines (225 loc) · 9.91 KB
/
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
use ethers::solc::ProjectCompileOutput;
use foundry_config::{
validate_profiles, Config, FuzzConfig, InlineConfig, InlineConfigError, InlineConfigParser,
InvariantConfig, NatSpec,
};
use proptest::test_runner::{RngAlgorithm, TestRng, TestRunner};
use std::path::Path;
#[macro_use]
extern crate tracing;
/// Gas reports
pub mod gas_report;
/// Coverage reports
pub mod coverage;
/// The Forge test runner
mod runner;
pub use runner::ContractRunner;
/// Forge test runners for multiple contracts
mod multi_runner;
pub use multi_runner::{MultiContractRunner, MultiContractRunnerBuilder};
/// reexport
pub use foundry_common::traits::TestFilter;
pub mod result;
/// The Forge EVM backend
pub use foundry_evm::*;
/// Metadata on how to run fuzz/invariant tests
#[derive(Debug, Clone, 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 {
/// 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<S>(&self, contract_id: S, test_fn: S) -> TestRunner
where
S: Into<String>,
{
let fuzz = self.fuzz_config(contract_id, test_fn);
self.fuzzer_with_cases(fuzz.runs)
}
/// 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<S>(&self, contract_id: S, test_fn: S) -> TestRunner
where
S: Into<String>,
{
let invariant = self.invariant_config(contract_id, test_fn);
self.fuzzer_with_cases(invariant.runs)
}
/// 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<S>(&self, contract_id: S, test_fn: S) -> &FuzzConfig
where
S: Into<String>,
{
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<S>(&self, contract_id: S, test_fn: S) -> &InvariantConfig
where
S: Into<String>,
{
self.inline_invariant.get(contract_id, test_fn).unwrap_or(&self.invariant)
}
pub fn fuzzer_with_cases(&self, cases: u32) -> TestRunner {
// TODO: Add Options to modify the persistence
let cfg = proptest::test_runner::Config {
failure_persistence: None,
cases,
max_global_rejects: self.fuzz.max_test_rejects,
..Default::default()
};
if let Some(ref fuzz_seed) = self.fuzz.seed {
trace!(target: "forge::test", "building deterministic fuzzer with seed {}", fuzz_seed);
let mut bytes: [u8; 32] = [0; 32];
fuzz_seed.to_big_endian(&mut bytes);
let rng = TestRng::from_seed(RngAlgorithm::ChaCha, &bytes);
TestRunner::new_with_rng(cfg, rng)
} else {
trace!(target: "forge::test", "building stochastic fuzzer");
TestRunner::new(cfg)
}
}
}
impl<'a, P> TryFrom<(&'a ProjectCompileOutput, &'a P, Vec<String>, FuzzConfig, InvariantConfig)>
for TestOptions
where
P: AsRef<Path>,
{
type Error = InlineConfigError;
/// Tries to create an instance of `Self`, detecting inline configurations from the project
/// compile output.
///
/// Param is a tuple, whose elements are:
/// 1. Solidity compiler output, essential to extract natspec test configs.
/// 2. Root path to express contract base dirs. This is essential to match inline configs at
/// runtime. 3. List of available configuration profiles
/// 4. Reference to a fuzz base configuration.
/// 5. Reference to an invariant base configuration.
fn try_from(
value: (&'a ProjectCompileOutput, &'a P, Vec<String>, FuzzConfig, InvariantConfig),
) -> Result<Self, Self::Error> {
let output = value.0;
let root = value.1;
let profiles = &value.2;
let base_fuzz: FuzzConfig = value.3;
let base_invariant: InvariantConfig = value.4;
let natspecs: Vec<NatSpec> = NatSpec::parse(output, root);
let mut inline_invariant = InlineConfig::<InvariantConfig>::default();
let mut inline_fuzz = InlineConfig::<FuzzConfig>::default();
for natspec in natspecs {
// Perform general validation
validate_profiles(&natspec, profiles)?;
FuzzConfig::validate_configs(&natspec)?;
InvariantConfig::validate_configs(&natspec)?;
// Apply in-line configurations for the current profile
let configs: Vec<String> = natspec.current_profile_configs();
let c: &str = &natspec.contract;
let f: &str = &natspec.function;
let line: String = natspec.debug_context();
match base_fuzz.try_merge(&configs) {
Ok(Some(conf)) => inline_fuzz.insert(c, f, conf),
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
_ => { /* No inline config found, do nothing */ }
}
match base_invariant.try_merge(&configs) {
Ok(Some(conf)) => inline_invariant.insert(c, f, conf),
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
_ => { /* No inline config found, do nothing */ }
}
}
Ok(Self { fuzz: base_fuzz, invariant: base_invariant, inline_fuzz, inline_invariant })
}
}
/// Builder utility to create a [`TestOptions`] instance.
#[derive(Default)]
pub struct TestOptionsBuilder {
fuzz: Option<FuzzConfig>,
invariant: Option<InvariantConfig>,
profiles: Option<Vec<String>>,
output: Option<ProjectCompileOutput>,
}
impl TestOptionsBuilder {
/// Sets a [`FuzzConfig`] to be used as base "fuzz" configuration.
#[must_use = "A base 'fuzz' config must be provided"]
pub fn fuzz(mut self, conf: FuzzConfig) -> Self {
self.fuzz = Some(conf);
self
}
/// Sets a [`InvariantConfig`] to be used as base "invariant" configuration.
#[must_use = "A base 'invariant' config must be provided"]
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
}
/// Sets a project compiler output instance. This is used to extract
/// inline test configurations that override `self.fuzz` and `self.invariant`
/// specs when necessary.
pub fn compile_output(mut self, output: &ProjectCompileOutput) -> Self {
self.output = Some(output.clone());
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, root: impl AsRef<Path>) -> Result<TestOptions, InlineConfigError> {
let default_profiles = vec![Config::selected_profile().into()];
let profiles: Vec<String> = self.profiles.unwrap_or(default_profiles);
let base_fuzz = self.fuzz.unwrap_or_default();
let base_invariant = self.invariant.unwrap_or_default();
match self.output {
Some(compile_output) => Ok(TestOptions::try_from((
&compile_output,
&root,
profiles,
base_fuzz,
base_invariant,
))?),
None => Ok(TestOptions {
fuzz: base_fuzz,
invariant: base_invariant,
inline_fuzz: InlineConfig::default(),
inline_invariant: InlineConfig::default(),
}),
}
}
}