1use crate::core::ForgeGuardError;
4
5pub struct FuzzingConfig {
7 pub runs: u32,
9 pub seed: Option<u64>,
11 pub test_filter: Option<String>,
13 pub contract_filter: Option<String>,
15 pub fail_on_revert: bool,
17}
18
19impl Default for FuzzingConfig {
20 fn default() -> Self {
21 Self {
22 runs: 10_000,
23 seed: None,
24 test_filter: None,
25 contract_filter: None,
26 fail_on_revert: false,
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
33pub struct FuzzingReport {
34 pub passed: bool,
35 pub runs: u32,
36 pub duration_secs: f64,
37 pub output: String,
38 pub errors: String,
39}
40
41pub struct FuzzingAdapter;
43
44impl FuzzingAdapter {
45 pub fn run_forge_fuzz(config: &FuzzingConfig) -> Result<FuzzingReport, ForgeGuardError> {
47 let mut cmd = std::process::Command::new("forge");
48 cmd.arg("test");
49 cmd.arg("--fuzz-runs").arg(config.runs.to_string());
50
51 if let Some(seed) = config.seed {
52 cmd.arg("--fuzz-seed").arg(seed.to_string());
53 }
54
55 if let Some(test) = &config.test_filter {
56 cmd.arg("--match-test").arg(test);
57 }
58
59 if let Some(contract) = &config.contract_filter {
60 cmd.arg("--match-contract").arg(contract);
61 }
62
63 if config.fail_on_revert {
64 cmd.arg("--fail-on-revert");
65 }
66
67 let start = std::time::Instant::now();
68 let output = cmd
69 .output()
70 .map_err(|e| ForgeGuardError::Command(format!("Failed to run fuzz: {}", e)))?;
71
72 let duration = start.elapsed();
73
74 Ok(FuzzingReport {
75 passed: output.status.success(),
76 runs: config.runs,
77 duration_secs: duration.as_secs_f64(),
78 output: String::from_utf8_lossy(&output.stdout).to_string(),
79 errors: if output.status.success() {
80 String::new()
81 } else {
82 String::from_utf8_lossy(&output.stderr).to_string()
83 },
84 })
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_fuzzing_config_default_values() {
94 let config = FuzzingConfig::default();
95 assert_eq!(config.runs, 10_000);
96 assert_eq!(config.seed, None);
97 assert_eq!(config.test_filter, None);
98 assert_eq!(config.contract_filter, None);
99 assert!(!config.fail_on_revert);
100 }
101
102 #[test]
103 fn test_fuzzing_config_zero_runs() {
104 let config = FuzzingConfig {
105 runs: 0,
106 ..Default::default()
107 };
108 assert_eq!(config.runs, 0);
109 }
110
111 #[test]
112 fn test_fuzzing_config_seed_zero() {
113 let config = FuzzingConfig {
114 seed: Some(0),
115 ..Default::default()
116 };
117 assert_eq!(config.seed, Some(0));
118 }
119
120 #[test]
121 fn test_fuzzing_config_with_empty_filters() {
122 let config = FuzzingConfig {
123 test_filter: Some(String::new()),
124 contract_filter: Some(String::new()),
125 ..Default::default()
126 };
127 assert_eq!(config.test_filter, Some(String::new()));
128 assert_eq!(config.contract_filter, Some(String::new()));
129 }
130
131 #[test]
132 fn test_fuzzing_config_all_fields_custom() {
133 let config = FuzzingConfig {
134 runs: 100_000,
135 seed: Some(99999),
136 test_filter: Some("testFuzz".into()),
137 contract_filter: Some("Vault".into()),
138 fail_on_revert: true,
139 };
140 assert_eq!(config.runs, 100_000);
141 assert_eq!(config.seed, Some(99999));
142 assert_eq!(config.test_filter, Some("testFuzz".into()));
143 assert_eq!(config.contract_filter, Some("Vault".into()));
144 assert!(config.fail_on_revert);
145 }
146
147 #[test]
148 fn test_fuzzing_report_pass() {
149 let report = FuzzingReport {
150 passed: true,
151 runs: 10_000,
152 duration_secs: 1.5,
153 output: "[PASS] all tests passed".into(),
154 errors: String::new(),
155 };
156 assert!(report.passed);
157 assert_eq!(report.runs, 10_000);
158 assert!(report.duration_secs > 0.0);
159 assert!(report.output.contains("PASS"));
160 assert!(report.errors.is_empty());
161 }
162
163 #[test]
164 fn test_fuzzing_report_failure() {
165 let report = FuzzingReport {
166 passed: false,
167 runs: 5_000,
168 duration_secs: 30.0,
169 output: String::new(),
170 errors: "[FAIL] reentrancy detected in withdraw".into(),
171 };
172 assert!(!report.passed);
173 assert_eq!(report.runs, 5_000);
174 assert!(report.errors.contains("reentrancy"));
175 assert!(report.output.is_empty());
176 }
177
178 #[test]
179 fn test_fuzzing_report_duration_edge_cases() {
180 let instant = FuzzingReport {
181 passed: true,
182 runs: 1,
183 duration_secs: 0.000_001,
184 output: "fast".into(),
185 errors: String::new(),
186 };
187 assert!(instant.duration_secs > 0.0);
188 assert!(instant.duration_secs < 1.0);
189
190 let long = FuzzingReport {
191 passed: true,
192 runs: 1_000_000,
193 duration_secs: 999_999.99,
194 output: String::new(),
195 errors: String::new(),
196 };
197 assert!(long.duration_secs > 3600.0);
198 }
199
200 #[test]
201 fn test_fuzzing_report_debug_format() {
202 let report = FuzzingReport {
203 passed: true,
204 runs: 10_000,
205 duration_secs: 2.5,
206 output: "output".into(),
207 errors: String::new(),
208 };
209 let debug = format!("{:?}", report);
210 assert!(debug.contains("passed"));
211 assert!(debug.contains("runs"));
212 assert!(debug.contains("duration_secs"));
213 }
214
215 #[test]
216 fn test_fuzzing_report_clone_equality() {
217 let report = FuzzingReport {
218 passed: true,
219 runs: 10_000,
220 duration_secs: 2.5,
221 output: "test output".into(),
222 errors: "test errors".into(),
223 };
224 let cloned = report.clone();
225 assert_eq!(cloned.passed, report.passed);
226 assert_eq!(cloned.runs, report.runs);
227 assert_eq!(cloned.duration_secs, report.duration_secs);
228 assert_eq!(cloned.output, report.output);
229 assert_eq!(cloned.errors, report.errors);
230 }
231
232 #[test]
233 fn test_fuzzing_adapter_forge_not_installed() {
234 let config = FuzzingConfig::default();
236 let result = FuzzingAdapter::run_forge_fuzz(&config);
237 assert!(result.is_err());
238 let err = result.unwrap_err().to_string();
239 assert!(
240 err.contains("Failed to run fuzz"),
241 "Error should mention command failure: {}",
242 err
243 );
244 }
245
246 #[test]
247 fn test_fuzzing_report_stderr_on_failure() {
248 let report = FuzzingReport {
249 passed: false,
250 runs: 100,
251 duration_secs: 0.5,
252 output: String::new(),
253 errors: "Error: stack underflow\n".into(),
254 };
255 assert!(!report.errors.is_empty());
256 assert!(report.output.is_empty());
257 }
258}