foundry_config/
invariant.rs1use crate::fuzz::{FuzzCorpusConfig, FuzzDictionaryConfig};
4use serde::{
5 Deserialize, Deserializer, Serialize, Serializer,
6 de::{Error, Visitor},
7};
8use std::{fmt, num::NonZeroUsize, path::PathBuf, str::FromStr};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum InvariantWorkers {
13 Auto,
15 Fixed(NonZeroUsize),
17}
18
19impl Default for InvariantWorkers {
20 fn default() -> Self {
21 Self::Fixed(NonZeroUsize::MIN)
22 }
23}
24
25impl Serialize for InvariantWorkers {
26 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
27 where
28 S: Serializer,
29 {
30 match self {
31 Self::Auto => serializer.serialize_str("auto"),
32 Self::Fixed(workers) => workers.get().serialize(serializer),
33 }
34 }
35}
36
37impl<'de> Deserialize<'de> for InvariantWorkers {
38 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39 where
40 D: Deserializer<'de>,
41 {
42 deserializer.deserialize_any(InvariantWorkersVisitor)
43 }
44}
45
46impl FromStr for InvariantWorkers {
47 type Err = String;
48
49 fn from_str(value: &str) -> Result<Self, Self::Err> {
50 let value = value.trim();
51 if value.eq_ignore_ascii_case("auto") {
52 return Ok(Self::Auto);
53 }
54
55 let workers = value.parse::<usize>().map_err(|err| err.to_string())?;
56 fixed_workers(workers)
57 }
58}
59
60struct InvariantWorkersVisitor;
61
62impl Visitor<'_> for InvariantWorkersVisitor {
63 type Value = InvariantWorkers;
64
65 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66 formatter.write_str("`auto` or a positive integer worker count")
67 }
68
69 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
70 where
71 E: Error,
72 {
73 value.parse().map_err(E::custom)
74 }
75
76 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
77 where
78 E: Error,
79 {
80 let workers = usize::try_from(value).map_err(E::custom)?;
81 fixed_workers(workers).map_err(E::custom)
82 }
83
84 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
85 where
86 E: Error,
87 {
88 let workers =
89 usize::try_from(value).map_err(|_| E::custom("invariant workers must be positive"))?;
90 fixed_workers(workers).map_err(E::custom)
91 }
92}
93
94fn fixed_workers(workers: usize) -> Result<InvariantWorkers, String> {
95 NonZeroUsize::new(workers)
96 .map(InvariantWorkers::Fixed)
97 .ok_or_else(|| "invariant workers must be greater than 0".to_string())
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
102pub struct InvariantConfig {
103 pub runs: u32,
105 pub depth: u32,
107 pub workers: InvariantWorkers,
112 pub fail_on_revert: bool,
114 pub call_override: bool,
117 #[serde(flatten)]
119 pub dictionary: FuzzDictionaryConfig,
120 pub shrink_run_limit: u32,
122 pub max_assume_rejects: u32,
125 pub gas_report_samples: u32,
127 #[serde(flatten)]
129 pub corpus: FuzzCorpusConfig,
130 pub failure_persist_dir: Option<PathBuf>,
132 pub show_metrics: bool,
134 pub timeout: Option<u32>,
136 pub show_solidity: bool,
138 pub max_time_delay: Option<u32>,
140 pub max_block_delay: Option<u32>,
142 pub check_interval: u32,
150}
151
152impl Default for InvariantConfig {
153 fn default() -> Self {
154 Self {
155 runs: 256,
156 depth: 500,
157 workers: InvariantWorkers::default(),
158 fail_on_revert: false,
159 call_override: false,
160 dictionary: FuzzDictionaryConfig { dictionary_weight: 80, ..Default::default() },
161 shrink_run_limit: 5000,
162 max_assume_rejects: 65536,
163 gas_report_samples: 256,
164 corpus: FuzzCorpusConfig::default(),
165 failure_persist_dir: None,
166 show_metrics: true,
167 timeout: None,
168 show_solidity: false,
169 max_time_delay: None,
170 max_block_delay: None,
171 check_interval: 1,
172 }
173 }
174}
175
176impl InvariantConfig {
177 pub fn new(cache_dir: PathBuf) -> Self {
179 Self { failure_persist_dir: Some(cache_dir), ..Default::default() }
180 }
181
182 pub const fn has_delay(&self) -> bool {
184 self.max_block_delay.is_some() || self.max_time_delay.is_some()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn invariant_workers_accept_auto_and_fixed_counts() {
194 assert_eq!("AUTO".parse::<InvariantWorkers>().unwrap(), InvariantWorkers::Auto);
195 assert_eq!(
196 serde_json::from_str::<InvariantWorkers>(r#""auto""#).unwrap(),
197 InvariantWorkers::Auto
198 );
199 assert_eq!(
200 serde_json::from_str::<InvariantWorkers>(r#"4"#).unwrap(),
201 InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap())
202 );
203 assert_eq!(
204 serde_json::from_str::<InvariantWorkers>(r#""4""#).unwrap(),
205 InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap())
206 );
207 }
208
209 #[test]
210 fn invariant_workers_default_to_one() {
211 assert_eq!(InvariantWorkers::default(), InvariantWorkers::Fixed(NonZeroUsize::MIN));
212 assert_eq!(InvariantConfig::default().workers, InvariantWorkers::Fixed(NonZeroUsize::MIN));
213 }
214
215 #[test]
216 fn invariant_workers_reject_zero() {
217 let err = serde_json::from_str::<InvariantWorkers>(r#"0"#).unwrap_err();
218 assert!(err.to_string().contains("greater than 0"));
219 }
220}