1use 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, Default, PartialEq, Eq)]
12pub enum InvariantWorkers {
13 #[default]
15 Auto,
16 Fixed(NonZeroUsize),
18}
19
20impl Serialize for InvariantWorkers {
21 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
22 where
23 S: Serializer,
24 {
25 match self {
26 Self::Auto => serializer.serialize_str("auto"),
27 Self::Fixed(workers) => workers.get().serialize(serializer),
28 }
29 }
30}
31
32impl<'de> Deserialize<'de> for InvariantWorkers {
33 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
34 where
35 D: Deserializer<'de>,
36 {
37 deserializer.deserialize_any(InvariantWorkersVisitor)
38 }
39}
40
41impl FromStr for InvariantWorkers {
42 type Err = String;
43
44 fn from_str(value: &str) -> Result<Self, Self::Err> {
45 let value = value.trim();
46 if value.eq_ignore_ascii_case("auto") {
47 return Ok(Self::Auto);
48 }
49
50 let workers = value.parse::<usize>().map_err(|err| err.to_string())?;
51 fixed_workers(workers)
52 }
53}
54
55struct InvariantWorkersVisitor;
56
57impl Visitor<'_> for InvariantWorkersVisitor {
58 type Value = InvariantWorkers;
59
60 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61 formatter.write_str("`auto` or a positive integer worker count")
62 }
63
64 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
65 where
66 E: Error,
67 {
68 value.parse().map_err(E::custom)
69 }
70
71 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
72 where
73 E: Error,
74 {
75 let workers = usize::try_from(value).map_err(E::custom)?;
76 fixed_workers(workers).map_err(E::custom)
77 }
78
79 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
80 where
81 E: Error,
82 {
83 let workers =
84 usize::try_from(value).map_err(|_| E::custom("invariant workers must be positive"))?;
85 fixed_workers(workers).map_err(E::custom)
86 }
87}
88
89fn fixed_workers(workers: usize) -> Result<InvariantWorkers, String> {
90 NonZeroUsize::new(workers)
91 .map(InvariantWorkers::Fixed)
92 .ok_or_else(|| "invariant workers must be greater than 0".to_string())
93}
94
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum InvariantDepthMode {
99 #[default]
101 Fixed,
102 #[serde(alias = "uniform")]
104 Random,
105}
106
107impl FromStr for InvariantDepthMode {
108 type Err = String;
109
110 fn from_str(value: &str) -> Result<Self, Self::Err> {
111 match value.trim().to_ascii_lowercase().as_str() {
112 "fixed" => Ok(Self::Fixed),
113 "random" | "uniform" => Ok(Self::Random),
114 value => {
115 Err(format!("unknown invariant depth mode `{value}`, expected `fixed` or `random`"))
116 }
117 }
118 }
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
123pub struct InvariantConfig {
124 pub runs: u32,
126 pub depth: u32,
128 pub min_depth: u32,
130 pub depth_mode: InvariantDepthMode,
132 pub workers: InvariantWorkers,
137 pub fail_on_revert: bool,
139 pub call_override: bool,
142 #[serde(flatten)]
144 pub dictionary: FuzzDictionaryConfig,
145 pub shrink_run_limit: u32,
147 pub max_assume_rejects: u32,
150 pub gas_report_samples: u32,
152 #[serde(flatten)]
154 pub corpus: FuzzCorpusConfig,
155 #[serde(default, skip_serializing)]
157 #[doc(hidden)]
158 pub corpus_random_sequence_weight_configured: bool,
159 #[serde(default, skip_serializing)]
161 #[doc(hidden)]
162 pub workers_configured: bool,
163 pub failure_persist_dir: Option<PathBuf>,
165 pub show_metrics: bool,
167 pub timeout: Option<u32>,
169 pub show_solidity: bool,
171 pub max_time_delay: Option<u32>,
173 pub max_block_delay: Option<u32>,
175 pub check_interval: u32,
183}
184
185impl Default for InvariantConfig {
186 fn default() -> Self {
187 Self {
188 runs: 256,
189 depth: 500,
190 min_depth: 1,
191 depth_mode: InvariantDepthMode::default(),
192 workers: InvariantWorkers::default(),
193 fail_on_revert: false,
194 call_override: false,
195 dictionary: FuzzDictionaryConfig { dictionary_weight: 80, ..Default::default() },
196 shrink_run_limit: 5000,
197 max_assume_rejects: 65536,
198 gas_report_samples: 256,
199 corpus: FuzzCorpusConfig::default(),
200 corpus_random_sequence_weight_configured: false,
201 workers_configured: false,
202 failure_persist_dir: None,
203 show_metrics: true,
204 timeout: None,
205 show_solidity: false,
206 max_time_delay: None,
207 max_block_delay: None,
208 check_interval: 1,
209 }
210 }
211}
212
213impl InvariantConfig {
214 pub fn new(cache_dir: PathBuf) -> Self {
216 Self { failure_persist_dir: Some(cache_dir), ..Default::default() }
217 }
218
219 pub const fn has_delay(&self) -> bool {
221 self.max_block_delay.is_some() || self.max_time_delay.is_some()
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn invariant_workers_accept_auto_and_fixed_counts() {
231 assert_eq!("AUTO".parse::<InvariantWorkers>().unwrap(), InvariantWorkers::Auto);
232 assert_eq!(
233 serde_json::from_str::<InvariantWorkers>(r#""auto""#).unwrap(),
234 InvariantWorkers::Auto
235 );
236 assert_eq!(
237 serde_json::from_str::<InvariantWorkers>(r#"4"#).unwrap(),
238 InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap())
239 );
240 assert_eq!(
241 serde_json::from_str::<InvariantWorkers>(r#""4""#).unwrap(),
242 InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap())
243 );
244 }
245
246 #[test]
247 fn invariant_workers_default_to_auto() {
248 assert_eq!(InvariantWorkers::default(), InvariantWorkers::Auto);
249 assert_eq!(InvariantConfig::default().workers, InvariantWorkers::Auto);
250 }
251
252 #[test]
253 fn invariant_workers_reject_zero() {
254 let err = serde_json::from_str::<InvariantWorkers>(r#"0"#).unwrap_err();
255 assert!(err.to_string().contains("greater than 0"));
256 }
257
258 #[test]
259 fn invariant_config_equality_includes_corpus_random_sequence_provenance() {
260 let explicit = InvariantConfig {
261 corpus_random_sequence_weight_configured: true,
262 ..InvariantConfig::default()
263 };
264
265 assert_ne!(InvariantConfig::default(), explicit);
266 }
267
268 #[test]
269 fn invariant_depth_mode_accepts_fixed_and_random() {
270 assert_eq!("fixed".parse::<InvariantDepthMode>().unwrap(), InvariantDepthMode::Fixed);
271 assert_eq!("uniform".parse::<InvariantDepthMode>().unwrap(), InvariantDepthMode::Random);
272 assert_eq!(
273 serde_json::from_str::<InvariantDepthMode>(r#""random""#).unwrap(),
274 InvariantDepthMode::Random
275 );
276 assert_eq!(
277 serde_json::from_str::<InvariantDepthMode>(r#""uniform""#).unwrap(),
278 InvariantDepthMode::Random
279 );
280 }
281}