Skip to main content

bcore_mutation/
report.rs

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    // Skip creating a report file if mutation score is 100%
33    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    // Adjust path for test files
41    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
48    restore_original_file(&original_file_path).await?;
49
50    println!("Surviving mutants:");
51
52    let mut diffs = Vec::new();
53
54    // Collect diffs for all surviving mutants
55    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    // Parse diffs and create report
67    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
77    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    // git diff --no-index returns exit code 1 when files differ, which is expected
106    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            // Find the start of the actual diff content (after @@)
125            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        // Load existing data and append
162        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                // Convert single object to array and append
172                serde_json::json!([existing_data, report_data])
173            }
174        }
175    } else {
176        // Create new array with this report
177        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")); // 10 + 3
202        assert!(result.contains_key("23")); // 20 + 3
203
204        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}