use crate::types::{AuditResult, MutationResult, MutationType, TestId};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
#[derive(Debug)]
pub struct MutationAnalyzer {
workspace_root: PathBuf,
crate_paths: Vec<PathBuf>,
output_dir: PathBuf,
}
impl MutationAnalyzer {
pub fn new(
workspace_root: impl Into<PathBuf>, output_dir: impl Into<PathBuf>,
) -> AuditResult<Self> {
let workspace_root = workspace_root.into();
let output_dir = output_dir.into();
if !workspace_root.exists() {
return Err(crate::types::AuditError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Workspace root not found: {}", workspace_root.display()),
)));
}
std::fs::create_dir_all(&output_dir)?;
Ok(Self {
workspace_root,
crate_paths: Vec::new(),
output_dir,
})
}
pub fn add_crate(&mut self, crate_path: impl Into<PathBuf>) {
self.crate_paths.push(crate_path.into());
}
pub fn add_critical_paths(&mut self) {
let critical_crates = vec![
"crates/ggen-core",
"crates/ggen-rdf",
"crates/ggen-ontology",
"crates/ggen-config",
];
for crate_path in critical_crates {
self.add_crate(crate_path);
}
}
pub fn run_mutations(&self) -> AuditResult<Vec<MutationResult>> {
let mut all_results = Vec::new();
for crate_path in &self.crate_paths {
let full_path = self.workspace_root.join(crate_path);
if !full_path.exists() {
return Err(crate::types::AuditError::MutationFailed(format!(
"Crate path not found: {}",
full_path.display()
)));
}
let results = self.run_mutations_for_crate(&full_path)?;
all_results.extend(results);
}
Ok(all_results)
}
fn run_mutations_for_crate(&self, crate_path: &Path) -> AuditResult<Vec<MutationResult>> {
let output = Command::new("cargo")
.args(&[
"mutants",
"--json",
"--output",
self.output_dir.to_str().unwrap_or("."),
])
.current_dir(crate_path)
.output()
.map_err(|e| {
crate::types::AuditError::MutationFailed(format!(
"Failed to execute cargo-mutants: {}. Is cargo-mutants installed?",
e
))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(crate::types::AuditError::MutationFailed(format!(
"cargo-mutants failed: {}",
stderr
)));
}
self.parse_mutation_results(crate_path)
}
pub fn parse_mutation_results(&self, crate_path: &Path) -> AuditResult<Vec<MutationResult>> {
let mut results = Vec::new();
for entry in WalkDir::new(&self.output_dir)
.max_depth(2)
.into_iter()
.filter_map(Result::ok)
{
if entry.path().extension().and_then(|s| s.to_str()) == Some("json") {
let content = std::fs::read_to_string(entry.path())?;
let mutants: Vec<CargoMutantsResult> = serde_json::from_str(&content)?;
for mutant in mutants {
results.push(self.convert_mutant_to_result(mutant, crate_path)?);
}
}
}
Ok(results)
}
fn convert_mutant_to_result(
&self, mutant: CargoMutantsResult, _crate_path: &Path,
) -> AuditResult<MutationResult> {
let test_id = TestId::new(mutant.test_id.unwrap_or_else(|| "unknown".to_string()))?;
Ok(MutationResult {
mutation_id: mutant.mutation_id,
test_id,
mutant_survived: !mutant.killed,
mutation_type: self.classify_mutation_type(&mutant.mutation_op),
kill_timestamp: Utc::now(),
})
}
fn classify_mutation_type(&self, op: &str) -> MutationType {
if op.contains("binary") || op.contains("+") || op.contains("==") {
MutationType::BinaryOp
} else if op.contains("unary") || op.contains("!") {
MutationType::UnaryOp
} else if op.contains("const") || op.contains("literal") {
MutationType::ConstantReplacement
} else if op.contains("return") {
MutationType::ReturnValueChange
} else {
MutationType::StatementDeletion
}
}
#[must_use]
pub fn calculate_kill_rate(&self, results: &[MutationResult]) -> f64 {
if results.is_empty() {
return 0.0;
}
let killed = results.iter().filter(|r| !r.mutant_survived).count();
killed as f64 / results.len() as f64
}
pub fn generate_baseline_report(&self, results: &[MutationResult]) -> AuditResult<PathBuf> {
let report = BaselineMutationReport {
timestamp: Utc::now(),
total_mutants: results.len(),
killed_mutants: results.iter().filter(|r| !r.mutant_survived).count(),
survived_mutants: results.iter().filter(|r| r.mutant_survived).count(),
kill_rate: self.calculate_kill_rate(results),
results: results.to_vec(),
};
let report_path = self.output_dir.join("baseline-mutation-kill-rate.json");
let json = serde_json::to_string_pretty(&report)?;
std::fs::write(&report_path, json)?;
Ok(report_path)
}
}
#[derive(Debug, Deserialize)]
struct CargoMutantsResult {
mutation_id: String,
mutation_op: String,
killed: bool,
test_id: Option<String>,
}
#[derive(Debug, Serialize)]
struct BaselineMutationReport {
timestamp: chrono::DateTime<Utc>,
total_mutants: usize,
killed_mutants: usize,
survived_mutants: usize,
kill_rate: f64,
results: Vec<MutationResult>,
}