proptest_plugin.rs
1 use anyhow::{anyhow, Result}; 2 use ferris_proof_core::{ 3 plugins::{ 4 PerformanceMetrics, PluginMetadata, StructuredResult, ToolInfo, VerificationInput, 5 VerificationOutput, VerificationPlugin, VersionRange, 6 }, 7 types::*, 8 verification::Target, 9 }; 10 use serde_json::json; 11 use std::path::PathBuf; 12 use std::process::Command; 13 use std::time::Duration; 14 use tracing::{debug, info}; 15 16 pub struct ProptestPlugin { 17 tool_path: PathBuf, 18 initialized: bool, 19 } 20 21 impl ProptestPlugin { 22 pub fn new() -> Self { 23 Self { 24 tool_path: PathBuf::from("proptest"), // Default to system PATH 25 initialized: false, 26 } 27 } 28 29 /// Check if the proptest crate is available in the current environment 30 fn check_proptest_crate_availability(&self) -> bool { 31 // Try to create a minimal Cargo.toml and check if proptest can be resolved 32 let temp_dir = std::env::temp_dir().join("ferris_proof_proptest_check"); 33 34 if std::fs::create_dir_all(&temp_dir).is_err() { 35 return false; 36 } 37 38 let cargo_toml_content = r#" 39 [package] 40 name = "proptest-check" 41 version = "0.1.0" 42 edition = "2021" 43 44 [dependencies] 45 proptest = "1.0" 46 "#; 47 48 let cargo_toml_path = temp_dir.join("Cargo.toml"); 49 if std::fs::write(&cargo_toml_path, cargo_toml_content).is_err() { 50 return false; 51 } 52 53 // Try to run cargo check 54 let check_result = Command::new("cargo") 55 .current_dir(&temp_dir) 56 .args(["check", "--quiet"]) 57 .output(); 58 59 // Clean up 60 let _ = std::fs::remove_dir_all(&temp_dir); 61 62 match check_result { 63 Ok(output) => output.status.success(), 64 Err(_) => false, 65 } 66 } 67 68 /// Run proptest on a Rust target 69 fn run_proptest( 70 &self, 71 target: &Target, 72 config: &VerificationInput, 73 ) -> Result<VerificationOutput> { 74 let start_time = std::time::Instant::now(); 75 76 match target { 77 Target::RustFile(path) => { 78 info!("Running proptest on Rust file: {:?}", path); 79 80 // Create a temporary directory for test execution 81 let temp_dir = config 82 .context 83 .cache_dir 84 .join(format!("proptest_{}", uuid::Uuid::new_v4())); 85 std::fs::create_dir_all(&temp_dir)?; 86 87 // Run cargo test with proptest 88 let mut cmd = Command::new("cargo"); 89 cmd.current_dir(path.parent().unwrap_or_else(|| std::path::Path::new("."))); 90 cmd.args(["test", "--test", "prop_tests", "--", "--nocapture"]); 91 92 // Set environment variables for proptest 93 cmd.env( 94 "PROPTEST_CASES", 95 config 96 .config 97 .tool_config 98 .get("cases") 99 .and_then(|v| v.as_u64()) 100 .unwrap_or(1000) 101 .to_string(), 102 ); 103 cmd.env( 104 "PROPTEST_MAX_SHRINK_ITERS", 105 config 106 .config 107 .tool_config 108 .get("max_shrink_iters") 109 .and_then(|v| v.as_u64()) 110 .unwrap_or(10000) 111 .to_string(), 112 ); 113 114 debug!("Executing command: {:?}", cmd); 115 116 let output = cmd.output()?; 117 let execution_time = start_time.elapsed(); 118 119 // Parse proptest output 120 let structured_result = self.parse_proptest_output( 121 &String::from_utf8_lossy(&output.stdout), 122 &String::from_utf8_lossy(&output.stderr), 123 )?; 124 125 // Create violations for any test failures 126 let violations = if structured_result.status == Status::Error { 127 vec![Violation { 128 id: "PROPTEST_FAILURE".to_string(), 129 severity: Severity::Error, 130 location: Location { 131 file: path.clone(), 132 line: None, 133 column: None, 134 span: None, 135 }, 136 message: "Property-based tests failed".to_string(), 137 suggestion: Some( 138 "Check the test output for specific failure details".to_string(), 139 ), 140 rule: "proptest_verification".to_string(), 141 }] 142 } else { 143 Vec::new() 144 }; 145 146 Ok(VerificationOutput { 147 status: structured_result.status, 148 violations, 149 artifacts: vec![], // TODO: Generate test reports 150 tool_output: ToolOutput { 151 tool: "proptest".to_string(), 152 stdout: String::from_utf8_lossy(&output.stdout).to_string(), 153 stderr: String::from_utf8_lossy(&output.stderr).to_string(), 154 exit_code: output.status.code().unwrap_or(-1), 155 execution_time, 156 }, 157 metrics: VerificationMetrics { 158 total_time: execution_time, 159 cache_hit_rate: 0.0, 160 memory_usage: 0, // TODO: Monitor memory usage 161 test_cases_executed: structured_result 162 .statistics 163 .get("test_cases_executed") 164 .and_then(|v| v.as_u64()) 165 .unwrap_or(0) as u32, 166 }, 167 }) 168 } 169 _ => Err(anyhow!("Proptest plugin only supports Rust files")), 170 } 171 } 172 173 /// Parse proptest output into structured results 174 fn parse_proptest_output(&self, stdout: &str, stderr: &str) -> Result<StructuredResult> { 175 let output = stdout.to_string() + stderr; 176 177 // Look for test failures 178 if output.contains("test FAILED") || output.contains("panic") { 179 return Ok(StructuredResult { 180 status: Status::Error, 181 violations: vec![], 182 statistics: json!({ 183 "test_cases_executed": self.extract_test_cases(&output), 184 "failures": self.extract_failures(&output), 185 "successes": self.extract_successes(&output) 186 }), 187 performance: PerformanceMetrics { 188 execution_time: Duration::from_millis(0), // Will be set by caller 189 memory_usage: 0, 190 cpu_usage: 0.0, 191 cache_hits: 0, 192 }, 193 }); 194 } 195 196 // Look for successful completion 197 if output.contains("test result: ok") { 198 return Ok(StructuredResult { 199 status: Status::Success, 200 violations: vec![], 201 statistics: json!({ 202 "test_cases_executed": self.extract_test_cases(&output), 203 "failures": 0, 204 "successes": self.extract_successes(&output) 205 }), 206 performance: PerformanceMetrics { 207 execution_time: Duration::from_millis(0), 208 memory_usage: 0, 209 cpu_usage: 0.0, 210 cache_hits: 0, 211 }, 212 }); 213 } 214 215 // Default to success if no failures detected 216 Ok(StructuredResult { 217 status: Status::Success, 218 violations: vec![], 219 statistics: json!({ 220 "test_cases_executed": 0, 221 "failures": 0, 222 "successes": 0 223 }), 224 performance: PerformanceMetrics { 225 execution_time: Duration::from_millis(0), 226 memory_usage: 0, 227 cpu_usage: 0.0, 228 cache_hits: 0, 229 }, 230 }) 231 } 232 233 fn extract_test_cases(&self, output: &str) -> u64 { 234 // Look for patterns like "1030 tests run" 235 let regex = regex::Regex::new(r"(\d+)\s+(?:test|tests|case|cases)").unwrap(); 236 if let Some(captures) = regex.captures(output) { 237 captures 238 .get(1) 239 .and_then(|m| m.as_str().parse().ok()) 240 .unwrap_or(0) 241 } else { 242 0 243 } 244 } 245 246 fn extract_failures(&self, output: &str) -> u64 { 247 // Count occurrences of "FAILED" or "panicked" 248 let failed_count = output.matches("FAILED").count(); 249 let panic_count = output.matches("panicked").count(); 250 (failed_count + panic_count) as u64 251 } 252 253 fn extract_successes(&self, output: &str) -> u64 { 254 // Look for "passed" or "ok" in test results 255 let passed_count = output.matches("passed").count(); 256 let ok_count = output.matches("ok").count(); 257 (passed_count + ok_count) as u64 258 } 259 } 260 261 impl VerificationPlugin for ProptestPlugin { 262 fn name(&self) -> &str { 263 "proptest" 264 } 265 266 fn version(&self) -> &str { 267 env!("CARGO_PKG_VERSION") 268 } 269 270 fn supported_techniques(&self) -> Vec<Technique> { 271 vec![Technique::PropertyTests] 272 } 273 274 fn supported_versions(&self) -> VersionRange { 275 VersionRange { 276 min: Some(semver::Version::new(0, 1, 0)), 277 max: Some(semver::Version::new(1, 0, 0)), 278 requires_exact: None, 279 } 280 } 281 282 fn check_availability(&self) -> Result<ToolInfo> { 283 // Check if Rust and Cargo are available 284 let cargo_result = Command::new("cargo").args(["--version"]).output(); 285 286 let rustc_result = Command::new("rustc").args(["--version"]).output(); 287 288 match (cargo_result, rustc_result) { 289 (Ok(cargo_output), Ok(rustc_output)) 290 if cargo_output.status.success() && rustc_output.status.success() => 291 { 292 let cargo_version = String::from_utf8_lossy(&cargo_output.stdout); 293 let rustc_version = String::from_utf8_lossy(&rustc_output.stdout); 294 295 // Extract version numbers 296 let cargo_ver = cargo_version 297 .split_whitespace() 298 .nth(1) 299 .unwrap_or("unknown") 300 .to_string(); 301 302 let rustc_ver = rustc_version 303 .split_whitespace() 304 .nth(1) 305 .unwrap_or("unknown") 306 .to_string(); 307 308 // Check if proptest crate is available by trying to compile a simple test 309 let proptest_available = self.check_proptest_crate_availability(); 310 311 let version = format!("cargo {} / rustc {}", cargo_ver, rustc_ver); 312 let mut capabilities = vec![ 313 "property_testing".to_string(), 314 "test_generation".to_string(), 315 "shrinking".to_string(), 316 "rust_integration".to_string(), 317 ]; 318 319 if proptest_available { 320 capabilities.push("proptest_crate".to_string()); 321 } 322 323 Ok(ToolInfo { 324 name: "proptest".to_string(), 325 version, 326 path: PathBuf::from("cargo"), 327 available: true, 328 capabilities, 329 }) 330 } 331 (Ok(cargo_output), Ok(_)) if !cargo_output.status.success() => { 332 Err(anyhow!("Cargo is not working properly")) 333 } 334 (Ok(_), Ok(rustc_output)) if !rustc_output.status.success() => { 335 Err(anyhow!("Rustc is not working properly")) 336 } 337 (Ok(_), Ok(_)) => { 338 // Both commands succeeded but we didn't handle this case above 339 Err(anyhow!("Unexpected cargo/rustc status")) 340 } 341 (Err(cargo_err), _) => Err(anyhow!("Cargo not found: {}", cargo_err)), 342 (_, Err(rustc_err)) => Err(anyhow!("Rustc not found: {}", rustc_err)), 343 } 344 } 345 346 fn verify(&self, input: VerificationInput) -> Result<VerificationOutput> { 347 if !self.initialized { 348 return Err(anyhow!("Proptest plugin not initialized")); 349 } 350 351 self.run_proptest(&input.target, &input) 352 } 353 354 fn parse_output(&self, raw_output: &str) -> Result<StructuredResult> { 355 self.parse_proptest_output(raw_output, "") 356 } 357 358 fn metadata(&self) -> PluginMetadata { 359 PluginMetadata { 360 name: "proptest".to_string(), 361 version: self.version().to_string(), 362 description: "Property-based testing plugin using the proptest framework".to_string(), 363 author: "FerrisProof Team".to_string(), 364 license: "MIT".to_string(), 365 homepage: Some( 366 "https://altsysrq.github.io/proptest-book/proptest/index.html".to_string(), 367 ), 368 techniques: vec![Technique::PropertyTests], 369 supported_platforms: vec![ 370 "linux".to_string(), 371 "macos".to_string(), 372 "windows".to_string(), 373 ], 374 dependencies: vec!["cargo".to_string(), "rustc".to_string()], 375 } 376 } 377 378 fn initialize(&mut self, config: &serde_json::Value) -> Result<()> { 379 // Extract proptest configuration 380 if let Some(tool_config) = config.get("proptest") { 381 if let Some(path) = tool_config.get("path").and_then(|v| v.as_str()) { 382 self.tool_path = PathBuf::from(path); 383 } 384 } 385 386 // Verify tool availability 387 let tool_info = self.check_availability()?; 388 if !tool_info.available { 389 return Err(anyhow!("Proptest is not available: {}", tool_info.version)); 390 } 391 392 self.initialized = true; 393 info!( 394 "Proptest plugin initialized with tool: {:?}", 395 self.tool_path 396 ); 397 Ok(()) 398 } 399 400 fn cleanup(&mut self) -> Result<()> { 401 self.initialized = false; 402 debug!("Proptest plugin cleaned up"); 403 Ok(()) 404 } 405 } 406 407 impl Default for ProptestPlugin { 408 fn default() -> Self { 409 Self::new() 410 } 411 } 412 413 #[cfg(test)] 414 mod tests { 415 use super::*; 416 417 #[test] 418 fn test_plugin_metadata() { 419 let plugin = ProptestPlugin::new(); 420 let metadata = plugin.metadata(); 421 422 assert_eq!(metadata.name, "proptest"); 423 assert_eq!(metadata.techniques, vec![Technique::PropertyTests]); 424 assert!(metadata.supported_platforms.contains(&"linux".to_string())); 425 } 426 427 #[test] 428 fn test_supported_techniques() { 429 let plugin = ProptestPlugin::new(); 430 let techniques = plugin.supported_techniques(); 431 432 assert_eq!(techniques.len(), 1); 433 assert!(techniques.contains(&Technique::PropertyTests)); 434 } 435 436 #[test] 437 fn test_output_parsing() { 438 let plugin = ProptestPlugin::new(); 439 440 let success_output = "test result: ok. 1000 tests run."; 441 let result = plugin.parse_proptest_output(success_output, "").unwrap(); 442 assert_eq!(result.status, Status::Success); 443 444 let failure_output = "test FAILED: property should hold\n1 tests run."; 445 let result = plugin.parse_proptest_output(failure_output, "").unwrap(); 446 assert_eq!(result.status, Status::Error); 447 } 448 449 #[test] 450 fn test_statistics_extraction() { 451 let plugin = ProptestPlugin::new(); 452 453 let output = "test result: ok. 1030 tests run, 0 failed."; 454 assert_eq!(plugin.extract_test_cases(output), 1030); 455 assert_eq!(plugin.extract_failures(output), 0); 456 assert_eq!(plugin.extract_successes(output), 1); // from "ok" 457 } 458 }