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)
    }
}