use crate::error::{MutationError, Result};
use chrono::{DateTime, Local};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::process::Command;
#[derive(Debug, Serialize, Deserialize)]
pub struct MutantInfo {
pub id: usize,
pub commit: String,
pub diff: String,
pub status: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReportData {
pub filename: String,
pub mutation_score: f64,
pub date: String,
pub diffs: HashMap<String, Vec<MutantInfo>>,
}
pub async fn generate_report(
not_killed_mutants: &[String],
folder: &str,
original_file: &str,
score: f64,
) -> Result<()> {
if not_killed_mutants.is_empty() {
return Ok(());
}
let now: DateTime<Local> = Local::now();
let mut original_file_path = original_file.to_string();
if original_file_path.contains("test/") && !original_file_path.contains(".cpp") {
if let Some(start_index) = original_file_path.find("test/") {
original_file_path = original_file_path[start_index..].to_string();
}
}
restore_original_file(&original_file_path).await?;
println!("Surviving mutants:");
let mut diffs = Vec::new();
for filename in not_killed_mutants {
let modified_file = Path::new(folder).join(filename);
let diff_output =
get_git_diff(&original_file_path, modified_file.to_str().unwrap()).await?;
println!("{}", diff_output);
println!("--------------");
diffs.push(diff_output);
}
let parsed_diffs = parse_diffs_to_json(&diffs).await?;
let report_data = ReportData {
filename: original_file_path.clone(),
mutation_score: score,
date: now.format("%d/%m/%Y %H:%M:%S").to_string(),
diffs: parsed_diffs,
};
save_report(report_data, &original_file_path).await?;
Ok(())
}
async fn restore_original_file(file_path: &str) -> Result<()> {
let output = Command::new("git")
.args(&["checkout", "--", file_path])
.output()
.map_err(|e| MutationError::Git(format!("Failed to restore file: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(MutationError::Git(format!(
"Git checkout failed: {}",
stderr
)));
}
Ok(())
}
async fn get_git_diff(original_file: &str, modified_file: &str) -> Result<String> {
let output = Command::new("git")
.args(&["diff", "--no-index", original_file, modified_file])
.output()
.map_err(|e| MutationError::Git(format!("Failed to get git diff: {}", e)))?;
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout.to_string())
}
async fn parse_diffs_to_json(diffs_list: &[String]) -> Result<HashMap<String, Vec<MutantInfo>>> {
let mut result = HashMap::new();
let line_regex = Regex::new(r"@@ -(\d+),")?;
let commit = get_git_hash().await?;
for diff in diffs_list {
if let Some(captures) = line_regex.captures(diff) {
let line_num = captures[1].parse::<usize>().map_err(|_| {
MutationError::InvalidInput("Invalid line number in diff".to_string())
})?;
let line_key = (line_num + 3).to_string();
let entry = result.entry(line_key).or_insert_with(Vec::new);
let diff_content = if let Some(pos) = diff.find("@@") {
&diff[pos..]
} else {
diff
};
entry.push(MutantInfo {
id: entry.len() + 1,
commit: commit.clone(),
diff: diff_content.to_string(),
status: "alive".to_string(),
});
}
}
Ok(result)
}
async fn get_git_hash() -> Result<String> {
let output = Command::new("git")
.args(&["log", "--pretty=format:%h", "-n", "1"])
.output()
.map_err(|e| MutationError::Git(format!("Failed to get git hash: {}", e)))?;
if output.status.success() {
let hash = String::from_utf8_lossy(&output.stdout);
Ok(hash.trim().to_string())
} else {
Ok("unknown".to_string())
}
}
async fn save_report(report_data: ReportData, _original_file_path: &str) -> Result<()> {
let json_file = "diff_not_killed.json";
let final_data = if Path::new(json_file).exists() {
let existing_content = fs::read_to_string(json_file)?;
let mut existing_data: serde_json::Value = serde_json::from_str(&existing_content)?;
match existing_data {
serde_json::Value::Array(ref mut arr) => {
arr.push(serde_json::to_value(report_data)?);
existing_data
}
_ => {
serde_json::json!([existing_data, report_data])
}
}
} else {
serde_json::json!([report_data])
};
let json_content = serde_json::to_string_pretty(&final_data)?;
fs::write(json_file, json_content)?;
println!("Report saved to {}", json_file);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_parse_diffs_to_json() {
let diffs = vec![
"@@ -10,3 +11,5 @@ some context\n-old line\n+new line".to_string(),
"@@ -20,1 +21,1 @@ other context\n-another old\n+another new".to_string(),
];
let result = parse_diffs_to_json(&diffs).await.unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains_key("13")); assert!(result.contains_key("23"));
let first_entry = &result["13"][0];
assert_eq!(first_entry.id, 1);
assert_eq!(first_entry.status, "alive");
assert!(first_entry.diff.contains("@@"));
}
#[test]
fn test_report_data_serialization() {
let mut diffs = HashMap::new();
diffs.insert(
"10".to_string(),
vec![MutantInfo {
id: 1,
commit: "abc123".to_string(),
diff: "@@ test diff".to_string(),
status: "alive".to_string(),
}],
);
let report = ReportData {
filename: "test.cpp".to_string(),
mutation_score: 0.85,
date: "01/01/2024 12:00:00".to_string(),
diffs,
};
let json = serde_json::to_string(&report).unwrap();
let deserialized: ReportData = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.filename, "test.cpp");
assert_eq!(deserialized.mutation_score, 0.85);
assert_eq!(deserialized.diffs.len(), 1);
}
}