use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, PartialEq)]
pub enum VerificationResult {
FullyVerified,
CompilesOnly,
BehaviorChanged(Vec<String>),
CompileFailed(String),
}
impl VerificationResult {
pub fn allows_promotion(&self) -> bool {
matches!(
self,
VerificationResult::FullyVerified | VerificationResult::CompilesOnly
)
}
pub fn confidence_weight(&self) -> f32 {
match self {
VerificationResult::FullyVerified => 1.0,
VerificationResult::CompilesOnly => 0.6,
VerificationResult::BehaviorChanged(_) => 0.0,
VerificationResult::CompileFailed(_) => 0.0,
}
}
pub fn is_compile_failure(&self) -> bool {
matches!(self, VerificationResult::CompileFailed(_))
}
pub fn has_behavior_change(&self) -> bool {
matches!(self, VerificationResult::BehaviorChanged(_))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TestResult {
AllPassed,
SomeFailed(Vec<String>),
NoTests,
ExecutionError(String),
}
impl TestResult {
pub fn is_ok(&self) -> bool {
matches!(self, TestResult::AllPassed | TestResult::NoTests)
}
}
#[derive(Debug, Clone)]
pub struct VerificationConfig {
pub compile_timeout_secs: u64,
pub test_timeout_secs: u64,
pub run_tests: bool,
pub work_dir: Option<std::path::PathBuf>,
}
impl Default for VerificationConfig {
fn default() -> Self {
Self {
compile_timeout_secs: 60,
test_timeout_secs: 120,
run_tests: true,
work_dir: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VerificationStats {
pub fully_verified: usize,
pub compiles_only: usize,
pub behavior_changed: usize,
pub compile_failed: usize,
pub total_evaluated: usize,
}
impl VerificationStats {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, result: &VerificationResult) {
self.total_evaluated += 1;
match result {
VerificationResult::FullyVerified => self.fully_verified += 1,
VerificationResult::CompilesOnly => self.compiles_only += 1,
VerificationResult::BehaviorChanged(_) => self.behavior_changed += 1,
VerificationResult::CompileFailed(_) => self.compile_failed += 1,
}
}
pub fn promotion_rate(&self) -> f32 {
if self.total_evaluated == 0 {
0.0
} else {
(self.fully_verified + self.compiles_only) as f32 / self.total_evaluated as f32
}
}
pub fn full_verification_rate(&self) -> f32 {
if self.total_evaluated == 0 {
0.0
} else {
self.fully_verified as f32 / self.total_evaluated as f32
}
}
pub fn average_confidence(&self) -> f32 {
if self.total_evaluated == 0 {
0.0
} else {
let weighted_sum = (self.fully_verified as f32 * 1.0)
+ (self.compiles_only as f32 * 0.6)
+ (self.behavior_changed as f32 * 0.0)
+ (self.compile_failed as f32 * 0.0);
weighted_sum / self.total_evaluated as f32
}
}
}
pub fn check_rust_compilation(rust_code: &str, config: &VerificationConfig) -> Result<(), String> {
use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
let temp_dir = config.work_dir.clone().unwrap_or_else(std::env::temp_dir);
static COUNTER: AtomicU64 = AtomicU64::new(0);
let counter = COUNTER.fetch_add(1, Ordering::SeqCst);
let unique_id = format!("{}_{}", std::process::id(), counter);
let temp_file = temp_dir.join(format!("decy_verify_{}.rs", unique_id));
let temp_output = temp_dir.join(format!("decy_verify_{}.out", unique_id));
let mut file = std::fs::File::create(&temp_file)
.map_err(|e| format!("Failed to create temp file: {}", e))?;
file.write_all(rust_code.as_bytes())
.map_err(|e| format!("Failed to write temp file: {}", e))?;
drop(file);
let output = Command::new("rustc")
.args([
"--edition",
"2021",
"--crate-type",
"lib",
"--emit",
"metadata",
"-A",
"warnings", "-o",
])
.arg(&temp_output)
.arg(&temp_file)
.output()
.map_err(|e| format!("Failed to run rustc: {}", e))?;
let _ = std::fs::remove_file(&temp_file);
let _ = std::fs::remove_file(&temp_output);
let rmeta_file = temp_dir.join(format!("libdecy_verify_{}.rmeta", unique_id));
let _ = std::fs::remove_file(&rmeta_file);
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(stderr.to_string())
}
}
pub fn run_test_suite(test_path: &Path, _rust_code: &str) -> TestResult {
if !test_path.exists() {
return TestResult::NoTests;
}
let has_tests = test_path.is_dir()
&& std::fs::read_dir(test_path)
.map(|entries| entries.count() > 0)
.unwrap_or(false);
if !has_tests {
return TestResult::NoTests;
}
let test_script = test_path.join("run_tests.sh");
if test_script.exists() {
let output = Command::new("bash").arg(&test_script).output();
match output {
Ok(result) if result.status.success() => TestResult::AllPassed,
Ok(result) => {
let stderr = String::from_utf8_lossy(&result.stderr);
TestResult::SomeFailed(vec![stderr.to_string()])
}
Err(e) => TestResult::ExecutionError(e.to_string()),
}
} else {
TestResult::NoTests
}
}
pub fn verify_fix_semantically(
rust_code: &str,
test_suite: Option<&Path>,
config: &VerificationConfig,
) -> VerificationResult {
if let Err(e) = check_rust_compilation(rust_code, config) {
return VerificationResult::CompileFailed(e);
}
if !config.run_tests {
return VerificationResult::CompilesOnly;
}
match test_suite {
Some(tests) => {
match run_test_suite(tests, rust_code) {
TestResult::AllPassed => VerificationResult::FullyVerified,
TestResult::SomeFailed(failures) => VerificationResult::BehaviorChanged(failures),
TestResult::NoTests => VerificationResult::CompilesOnly,
TestResult::ExecutionError(e) => {
VerificationResult::BehaviorChanged(vec![e])
}
}
}
None => VerificationResult::CompilesOnly,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verification_result_allows_promotion() {
assert!(VerificationResult::FullyVerified.allows_promotion());
assert!(VerificationResult::CompilesOnly.allows_promotion());
assert!(!VerificationResult::BehaviorChanged(vec![]).allows_promotion());
assert!(!VerificationResult::CompileFailed("error".into()).allows_promotion());
}
#[test]
fn test_verification_result_confidence_weight() {
assert!((VerificationResult::FullyVerified.confidence_weight() - 1.0).abs() < f32::EPSILON);
assert!((VerificationResult::CompilesOnly.confidence_weight() - 0.6).abs() < f32::EPSILON);
assert!(
(VerificationResult::BehaviorChanged(vec![]).confidence_weight() - 0.0).abs()
< f32::EPSILON
);
assert!(
(VerificationResult::CompileFailed("".into()).confidence_weight() - 0.0).abs()
< f32::EPSILON
);
}
#[test]
fn test_verification_result_is_compile_failure() {
assert!(VerificationResult::CompileFailed("error".into()).is_compile_failure());
assert!(!VerificationResult::FullyVerified.is_compile_failure());
assert!(!VerificationResult::CompilesOnly.is_compile_failure());
assert!(!VerificationResult::BehaviorChanged(vec![]).is_compile_failure());
}
#[test]
fn test_verification_result_has_behavior_change() {
assert!(VerificationResult::BehaviorChanged(vec!["test".into()]).has_behavior_change());
assert!(!VerificationResult::FullyVerified.has_behavior_change());
assert!(!VerificationResult::CompilesOnly.has_behavior_change());
assert!(!VerificationResult::CompileFailed("".into()).has_behavior_change());
}
#[test]
fn test_test_result_is_ok() {
assert!(TestResult::AllPassed.is_ok());
assert!(TestResult::NoTests.is_ok());
assert!(!TestResult::SomeFailed(vec![]).is_ok());
assert!(!TestResult::ExecutionError("".into()).is_ok());
}
#[test]
fn test_verification_config_default() {
let config = VerificationConfig::default();
assert_eq!(config.compile_timeout_secs, 60);
assert_eq!(config.test_timeout_secs, 120);
assert!(config.run_tests);
assert!(config.work_dir.is_none());
}
#[test]
fn test_verification_stats_new() {
let stats = VerificationStats::new();
assert_eq!(stats.total_evaluated, 0);
assert_eq!(stats.fully_verified, 0);
}
#[test]
fn test_verification_stats_record() {
let mut stats = VerificationStats::new();
stats.record(&VerificationResult::FullyVerified);
stats.record(&VerificationResult::CompilesOnly);
stats.record(&VerificationResult::BehaviorChanged(vec![]));
stats.record(&VerificationResult::CompileFailed("".into()));
assert_eq!(stats.total_evaluated, 4);
assert_eq!(stats.fully_verified, 1);
assert_eq!(stats.compiles_only, 1);
assert_eq!(stats.behavior_changed, 1);
assert_eq!(stats.compile_failed, 1);
}
#[test]
fn test_verification_stats_promotion_rate() {
let mut stats = VerificationStats::new();
stats.record(&VerificationResult::FullyVerified);
stats.record(&VerificationResult::CompilesOnly);
stats.record(&VerificationResult::CompileFailed("".into()));
stats.record(&VerificationResult::CompileFailed("".into()));
assert!((stats.promotion_rate() - 0.5).abs() < 0.01);
}
#[test]
fn test_verification_stats_full_verification_rate() {
let mut stats = VerificationStats::new();
stats.record(&VerificationResult::FullyVerified);
stats.record(&VerificationResult::FullyVerified);
stats.record(&VerificationResult::CompilesOnly);
stats.record(&VerificationResult::CompileFailed("".into()));
assert!((stats.full_verification_rate() - 0.5).abs() < 0.01);
}
#[test]
fn test_verification_stats_average_confidence() {
let mut stats = VerificationStats::new();
stats.record(&VerificationResult::FullyVerified); stats.record(&VerificationResult::CompilesOnly);
assert!((stats.average_confidence() - 0.8).abs() < 0.01);
}
#[test]
fn test_verification_stats_empty_rates() {
let stats = VerificationStats::new();
assert_eq!(stats.promotion_rate(), 0.0);
assert_eq!(stats.full_verification_rate(), 0.0);
assert_eq!(stats.average_confidence(), 0.0);
}
#[test]
fn test_check_compilation_valid_code() {
let code = r#"
fn main() {
let x: i32 = 42;
println!("{}", x);
}
"#;
let config = VerificationConfig::default();
let _result = check_rust_compilation(code, &config);
}
#[test]
fn test_check_compilation_invalid_code() {
let code = r#"
fn main() {
let x: i32 = "not an integer";
}
"#;
let config = VerificationConfig::default();
let _result = check_rust_compilation(code, &config);
}
#[test]
fn test_check_compilation_syntax_error() {
let code = r#"
fn main( {
// Missing closing paren
}
"#;
let config = VerificationConfig::default();
let _result = check_rust_compilation(code, &config);
}
#[test]
fn test_verify_valid_code_no_tests() {
let code = r#"
fn main() {
let x = 42;
}
"#;
let config = VerificationConfig::default();
let result = verify_fix_semantically(code, None, &config);
assert!(
result == VerificationResult::CompilesOnly
|| matches!(result, VerificationResult::CompileFailed(_)),
);
}
#[test]
fn test_verify_invalid_code() {
let code = r#"
fn main() {
let x: i32 = "error";
}
"#;
let config = VerificationConfig::default();
let _result = verify_fix_semantically(code, None, &config);
}
#[test]
fn test_verify_compile_only_mode() {
let code = r#"
fn main() {}
"#;
let config = VerificationConfig {
run_tests: false,
..Default::default()
};
let result = verify_fix_semantically(code, None, &config);
assert!(
result == VerificationResult::CompilesOnly
|| matches!(result, VerificationResult::CompileFailed(_)),
"Expected CompilesOnly or CompileFailed, got {:?}",
result
);
}
#[test]
fn test_verify_nonexistent_test_dir() {
let code = r#"
fn main() {}
"#;
let config = VerificationConfig::default();
let test_path = Path::new("/nonexistent/test/path");
let result = verify_fix_semantically(code, Some(test_path), &config);
assert!(
result == VerificationResult::CompilesOnly
|| matches!(result, VerificationResult::CompileFailed(_)),
);
}
#[test]
fn test_run_test_suite_no_path() {
let result = run_test_suite(Path::new("/nonexistent"), "fn main() {}");
assert_eq!(result, TestResult::NoTests);
}
#[test]
fn test_spec_weight_fully_verified() {
assert_eq!(VerificationResult::FullyVerified.confidence_weight(), 1.0);
}
#[test]
fn test_spec_weight_compiles_only() {
assert!((VerificationResult::CompilesOnly.confidence_weight() - 0.6).abs() < f32::EPSILON);
}
#[test]
fn test_spec_behavior_changed_rejected() {
let result = VerificationResult::BehaviorChanged(vec!["test failed".into()]);
assert_eq!(result.confidence_weight(), 0.0);
assert!(!result.allows_promotion());
}
#[test]
fn test_spec_compile_failed_rejected() {
let result = VerificationResult::CompileFailed("error".into());
assert_eq!(result.confidence_weight(), 0.0);
assert!(!result.allows_promotion());
}
#[test]
fn test_verification_stats_default() {
let stats = VerificationStats::default();
assert_eq!(stats.total_evaluated, 0);
assert_eq!(stats.fully_verified, 0);
assert_eq!(stats.compiles_only, 0);
assert_eq!(stats.behavior_changed, 0);
assert_eq!(stats.compile_failed, 0);
assert_eq!(stats.promotion_rate(), 0.0);
}
#[test]
fn test_verification_result_debug_clone() {
let result = VerificationResult::BehaviorChanged(vec!["test1".into(), "test2".into()]);
let cloned = result.clone();
assert_eq!(result, cloned);
let debug = format!("{:?}", result);
assert!(debug.contains("BehaviorChanged"));
}
#[test]
fn test_test_result_debug_clone() {
let result = TestResult::SomeFailed(vec!["failure1".into()]);
let cloned = result.clone();
assert_eq!(result, cloned);
let debug = format!("{:?}", result);
assert!(debug.contains("SomeFailed"));
}
#[test]
fn test_verification_stats_all_behavior_changed() {
let mut stats = VerificationStats::new();
stats.record(&VerificationResult::BehaviorChanged(vec!["fail".into()]));
stats.record(&VerificationResult::BehaviorChanged(vec!["fail".into()]));
assert_eq!(stats.behavior_changed, 2);
assert_eq!(stats.promotion_rate(), 0.0);
assert_eq!(stats.full_verification_rate(), 0.0);
assert_eq!(stats.average_confidence(), 0.0);
}
#[test]
fn test_verification_stats_mixed_confidence() {
let mut stats = VerificationStats::new();
stats.record(&VerificationResult::FullyVerified); stats.record(&VerificationResult::CompileFailed("err".into())); assert!((stats.average_confidence() - 0.5).abs() < 0.01);
}
#[test]
fn test_verification_config_custom() {
let config = VerificationConfig {
compile_timeout_secs: 30,
test_timeout_secs: 60,
run_tests: false,
work_dir: Some(std::path::PathBuf::from("/tmp")),
};
assert_eq!(config.compile_timeout_secs, 30);
assert!(!config.run_tests);
assert!(config.work_dir.is_some());
}
#[test]
fn test_run_test_suite_empty_dir() {
let temp = tempfile::tempdir().unwrap();
let result = run_test_suite(temp.path(), "fn main() {}");
assert_eq!(result, TestResult::NoTests);
}
#[test]
fn test_run_test_suite_dir_with_files_no_script() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("test.rs"), "fn test() {}").unwrap();
let result = run_test_suite(temp.path(), "fn main() {}");
assert_eq!(result, TestResult::NoTests);
}
#[test]
fn test_verify_with_existing_empty_test_dir() {
let code = "fn add(a: i32, b: i32) -> i32 { a + b }";
let config = VerificationConfig::default();
let temp = tempfile::tempdir().unwrap();
let result = verify_fix_semantically(code, Some(temp.path()), &config);
assert!(
result == VerificationResult::CompilesOnly
|| matches!(result, VerificationResult::CompileFailed(_)),
"Expected CompilesOnly or CompileFailed, got {:?}",
result
);
}
#[test]
fn test_check_compilation_with_work_dir() {
let temp = tempfile::tempdir().unwrap();
let config = VerificationConfig {
work_dir: Some(temp.path().to_path_buf()),
..Default::default()
};
let _result = check_rust_compilation("fn valid() -> i32 { 42 }", &config);
}
}