Skip to main content

forge_guard/fuzzing/
mod.rs

1//! Fuzzing adapter — provides integration points for fuzzing campaigns.
2
3use crate::core::ForgeGuardError;
4
5/// Configuration for a fuzzing campaign.
6pub struct FuzzingConfig {
7    /// Number of fuzz runs.
8    pub runs: u32,
9    /// Fuzz seed for reproducibility.
10    pub seed: Option<u64>,
11    /// Test function filter.
12    pub test_filter: Option<String>,
13    /// Contract filter.
14    pub contract_filter: Option<String>,
15    /// Whether to fail on revert.
16    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/// Result of a fuzzing campaign.
32#[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
41/// Adapter for running fuzzing campaigns via Forge.
42pub struct FuzzingAdapter;
43
44impl FuzzingAdapter {
45    /// Run a fuzzing campaign using forge.
46    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        // Without forge, run_forge_fuzz should error gracefully, not panic
235        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}