1use crate::error::{MutationError, Result};
2use chrono::{DateTime, Local};
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fs;
7use std::path::Path;
8use std::process::Command;
9
10#[derive(Debug, Serialize, Deserialize)]
11pub struct MutantInfo {
12 pub id: usize,
13 pub commit: String,
14 pub diff: String,
15 pub status: String,
16}
17
18#[derive(Debug, Serialize, Deserialize)]
19pub struct ReportData {
20 pub filename: String,
21 pub mutation_score: f64,
22 pub date: String,
23 pub diffs: HashMap<String, Vec<MutantInfo>>,
24}
25
26pub async fn generate_report(
27 not_killed_mutants: &[String],
28 folder: &str,
29 original_file: &str,
30 score: f64,
31) -> Result<()> {
32 if not_killed_mutants.is_empty() {
34 return Ok(());
35 }
36
37 let now: DateTime<Local> = Local::now();
38 let mut original_file_path = original_file.to_string();
39
40 if original_file_path.contains("test/") && !original_file_path.contains(".cpp") {
42 if let Some(start_index) = original_file_path.find("test/") {
43 original_file_path = original_file_path[start_index..].to_string();
44 }
45 }
46
47 restore_original_file(&original_file_path).await?;
49
50 println!("Surviving mutants:");
51
52 let mut diffs = Vec::new();
53
54 for filename in not_killed_mutants {
56 let modified_file = Path::new(folder).join(filename);
57 let diff_output =
58 get_git_diff(&original_file_path, modified_file.to_str().unwrap()).await?;
59
60 println!("{}", diff_output);
61 println!("--------------");
62
63 diffs.push(diff_output);
64 }
65
66 let parsed_diffs = parse_diffs_to_json(&diffs).await?;
68
69 let report_data = ReportData {
70 filename: original_file_path.clone(),
71 mutation_score: score,
72 date: now.format("%d/%m/%Y %H:%M:%S").to_string(),
73 diffs: parsed_diffs,
74 };
75
76 save_report(report_data, &original_file_path).await?;
78
79 Ok(())
80}
81
82async fn restore_original_file(file_path: &str) -> Result<()> {
83 let output = Command::new("git")
84 .args(&["checkout", "--", file_path])
85 .output()
86 .map_err(|e| MutationError::Git(format!("Failed to restore file: {}", e)))?;
87
88 if !output.status.success() {
89 let stderr = String::from_utf8_lossy(&output.stderr);
90 return Err(MutationError::Git(format!(
91 "Git checkout failed: {}",
92 stderr
93 )));
94 }
95
96 Ok(())
97}
98
99async fn get_git_diff(original_file: &str, modified_file: &str) -> Result<String> {
100 let output = Command::new("git")
101 .args(&["diff", "--no-index", original_file, modified_file])
102 .output()
103 .map_err(|e| MutationError::Git(format!("Failed to get git diff: {}", e)))?;
104
105 let stdout = String::from_utf8_lossy(&output.stdout);
107 Ok(stdout.to_string())
108}
109
110async fn parse_diffs_to_json(diffs_list: &[String]) -> Result<HashMap<String, Vec<MutantInfo>>> {
111 let mut result = HashMap::new();
112 let line_regex = Regex::new(r"@@ -(\d+),")?;
113 let commit = get_git_hash().await?;
114
115 for diff in diffs_list {
116 if let Some(captures) = line_regex.captures(diff) {
117 let line_num = captures[1].parse::<usize>().map_err(|_| {
118 MutationError::InvalidInput("Invalid line number in diff".to_string())
119 })?;
120 let line_key = (line_num + 3).to_string();
121
122 let entry = result.entry(line_key).or_insert_with(Vec::new);
123
124 let diff_content = if let Some(pos) = diff.find("@@") {
126 &diff[pos..]
127 } else {
128 diff
129 };
130
131 entry.push(MutantInfo {
132 id: entry.len() + 1,
133 commit: commit.clone(),
134 diff: diff_content.to_string(),
135 status: "alive".to_string(),
136 });
137 }
138 }
139
140 Ok(result)
141}
142
143async fn get_git_hash() -> Result<String> {
144 let output = Command::new("git")
145 .args(&["log", "--pretty=format:%h", "-n", "1"])
146 .output()
147 .map_err(|e| MutationError::Git(format!("Failed to get git hash: {}", e)))?;
148
149 if output.status.success() {
150 let hash = String::from_utf8_lossy(&output.stdout);
151 Ok(hash.trim().to_string())
152 } else {
153 Ok("unknown".to_string())
154 }
155}
156
157async fn save_report(report_data: ReportData, _original_file_path: &str) -> Result<()> {
158 let json_file = "diff_not_killed.json";
159
160 let final_data = if Path::new(json_file).exists() {
161 let existing_content = fs::read_to_string(json_file)?;
163 let mut existing_data: serde_json::Value = serde_json::from_str(&existing_content)?;
164
165 match existing_data {
166 serde_json::Value::Array(ref mut arr) => {
167 arr.push(serde_json::to_value(report_data)?);
168 existing_data
169 }
170 _ => {
171 serde_json::json!([existing_data, report_data])
173 }
174 }
175 } else {
176 serde_json::json!([report_data])
178 };
179
180 let json_content = serde_json::to_string_pretty(&final_data)?;
181 fs::write(json_file, json_content)?;
182
183 println!("Report saved to {}", json_file);
184 Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[tokio::test]
192 async fn test_parse_diffs_to_json() {
193 let diffs = vec![
194 "@@ -10,3 +11,5 @@ some context\n-old line\n+new line".to_string(),
195 "@@ -20,1 +21,1 @@ other context\n-another old\n+another new".to_string(),
196 ];
197
198 let result = parse_diffs_to_json(&diffs).await.unwrap();
199
200 assert_eq!(result.len(), 2);
201 assert!(result.contains_key("13")); assert!(result.contains_key("23")); let first_entry = &result["13"][0];
205 assert_eq!(first_entry.id, 1);
206 assert_eq!(first_entry.status, "alive");
207 assert!(first_entry.diff.contains("@@"));
208 }
209
210 #[test]
211 fn test_report_data_serialization() {
212 let mut diffs = HashMap::new();
213 diffs.insert(
214 "10".to_string(),
215 vec![MutantInfo {
216 id: 1,
217 commit: "abc123".to_string(),
218 diff: "@@ test diff".to_string(),
219 status: "alive".to_string(),
220 }],
221 );
222
223 let report = ReportData {
224 filename: "test.cpp".to_string(),
225 mutation_score: 0.85,
226 date: "01/01/2024 12:00:00".to_string(),
227 diffs,
228 };
229
230 let json = serde_json::to_string(&report).unwrap();
231 let deserialized: ReportData = serde_json::from_str(&json).unwrap();
232
233 assert_eq!(deserialized.filename, "test.cpp");
234 assert_eq!(deserialized.mutation_score, 0.85);
235 assert_eq!(deserialized.diffs.len(), 1);
236 }
237}