use crate::core::ForgeGuardError;
pub struct FuzzingConfig {
pub runs: u32,
pub seed: Option<u64>,
pub test_filter: Option<String>,
pub contract_filter: Option<String>,
pub fail_on_revert: bool,
}
impl Default for FuzzingConfig {
fn default() -> Self {
Self {
runs: 10_000,
seed: None,
test_filter: None,
contract_filter: None,
fail_on_revert: false,
}
}
}
#[derive(Debug, Clone)]
pub struct FuzzingReport {
pub passed: bool,
pub runs: u32,
pub duration_secs: f64,
pub output: String,
pub errors: String,
}
pub struct FuzzingAdapter;
impl FuzzingAdapter {
pub fn run_forge_fuzz(config: &FuzzingConfig) -> Result<FuzzingReport, ForgeGuardError> {
let mut cmd = std::process::Command::new("forge");
cmd.arg("test");
cmd.arg("--fuzz-runs").arg(config.runs.to_string());
if let Some(seed) = config.seed {
cmd.arg("--fuzz-seed").arg(seed.to_string());
}
if let Some(test) = &config.test_filter {
cmd.arg("--match-test").arg(test);
}
if let Some(contract) = &config.contract_filter {
cmd.arg("--match-contract").arg(contract);
}
if config.fail_on_revert {
cmd.arg("--fail-on-revert");
}
let start = std::time::Instant::now();
let output = cmd
.output()
.map_err(|e| ForgeGuardError::Command(format!("Failed to run fuzz: {}", e)))?;
let duration = start.elapsed();
Ok(FuzzingReport {
passed: output.status.success(),
runs: config.runs,
duration_secs: duration.as_secs_f64(),
output: String::from_utf8_lossy(&output.stdout).to_string(),
errors: if output.status.success() {
String::new()
} else {
String::from_utf8_lossy(&output.stderr).to_string()
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fuzzing_config_default_values() {
let config = FuzzingConfig::default();
assert_eq!(config.runs, 10_000);
assert_eq!(config.seed, None);
assert_eq!(config.test_filter, None);
assert_eq!(config.contract_filter, None);
assert!(!config.fail_on_revert);
}
#[test]
fn test_fuzzing_config_zero_runs() {
let config = FuzzingConfig {
runs: 0,
..Default::default()
};
assert_eq!(config.runs, 0);
}
#[test]
fn test_fuzzing_config_seed_zero() {
let config = FuzzingConfig {
seed: Some(0),
..Default::default()
};
assert_eq!(config.seed, Some(0));
}
#[test]
fn test_fuzzing_config_with_empty_filters() {
let config = FuzzingConfig {
test_filter: Some(String::new()),
contract_filter: Some(String::new()),
..Default::default()
};
assert_eq!(config.test_filter, Some(String::new()));
assert_eq!(config.contract_filter, Some(String::new()));
}
#[test]
fn test_fuzzing_config_all_fields_custom() {
let config = FuzzingConfig {
runs: 100_000,
seed: Some(99999),
test_filter: Some("testFuzz".into()),
contract_filter: Some("Vault".into()),
fail_on_revert: true,
};
assert_eq!(config.runs, 100_000);
assert_eq!(config.seed, Some(99999));
assert_eq!(config.test_filter, Some("testFuzz".into()));
assert_eq!(config.contract_filter, Some("Vault".into()));
assert!(config.fail_on_revert);
}
#[test]
fn test_fuzzing_report_pass() {
let report = FuzzingReport {
passed: true,
runs: 10_000,
duration_secs: 1.5,
output: "[PASS] all tests passed".into(),
errors: String::new(),
};
assert!(report.passed);
assert_eq!(report.runs, 10_000);
assert!(report.duration_secs > 0.0);
assert!(report.output.contains("PASS"));
assert!(report.errors.is_empty());
}
#[test]
fn test_fuzzing_report_failure() {
let report = FuzzingReport {
passed: false,
runs: 5_000,
duration_secs: 30.0,
output: String::new(),
errors: "[FAIL] reentrancy detected in withdraw".into(),
};
assert!(!report.passed);
assert_eq!(report.runs, 5_000);
assert!(report.errors.contains("reentrancy"));
assert!(report.output.is_empty());
}
#[test]
fn test_fuzzing_report_duration_edge_cases() {
let instant = FuzzingReport {
passed: true,
runs: 1,
duration_secs: 0.000_001,
output: "fast".into(),
errors: String::new(),
};
assert!(instant.duration_secs > 0.0);
assert!(instant.duration_secs < 1.0);
let long = FuzzingReport {
passed: true,
runs: 1_000_000,
duration_secs: 999_999.99,
output: String::new(),
errors: String::new(),
};
assert!(long.duration_secs > 3600.0);
}
#[test]
fn test_fuzzing_report_debug_format() {
let report = FuzzingReport {
passed: true,
runs: 10_000,
duration_secs: 2.5,
output: "output".into(),
errors: String::new(),
};
let debug = format!("{:?}", report);
assert!(debug.contains("passed"));
assert!(debug.contains("runs"));
assert!(debug.contains("duration_secs"));
}
#[test]
fn test_fuzzing_report_clone_equality() {
let report = FuzzingReport {
passed: true,
runs: 10_000,
duration_secs: 2.5,
output: "test output".into(),
errors: "test errors".into(),
};
let cloned = report.clone();
assert_eq!(cloned.passed, report.passed);
assert_eq!(cloned.runs, report.runs);
assert_eq!(cloned.duration_secs, report.duration_secs);
assert_eq!(cloned.output, report.output);
assert_eq!(cloned.errors, report.errors);
}
#[test]
fn test_fuzzing_adapter_forge_not_installed() {
let config = FuzzingConfig::default();
let result = FuzzingAdapter::run_forge_fuzz(&config);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Failed to run fuzz"),
"Error should mention command failure: {}",
err
);
}
#[test]
fn test_fuzzing_report_stderr_on_failure() {
let report = FuzzingReport {
passed: false,
runs: 100,
duration_secs: 0.5,
output: String::new(),
errors: "Error: stack underflow\n".into(),
};
assert!(!report.errors.is_empty());
assert!(report.output.is_empty());
}
}