Skip to main content

bcore_mutation/
analyze.rs

1use crate::error::{MutationError, Result};
2use crate::report::generate_report;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6use tokio::process::Command as TokioCommand;
7use tokio::time::timeout;
8use walkdir::WalkDir;
9
10pub async fn run_analysis(
11    folder: Option<PathBuf>,
12    command: Option<String>,
13    jobs: u32,
14    timeout_secs: u64,
15    survival_threshold: f64,
16) -> Result<()> {
17    let folders = if let Some(folder_path) = folder {
18        vec![folder_path]
19    } else {
20        // Find all folders starting with "muts"
21        find_mutation_folders()?
22    };
23
24    for folder_path in folders {
25        analyze_folder(
26            &folder_path,
27            command.clone(),
28            jobs,
29            timeout_secs,
30            survival_threshold,
31        )
32        .await?;
33    }
34
35    Ok(())
36}
37
38fn find_mutation_folders() -> Result<Vec<PathBuf>> {
39    let mut folders = Vec::new();
40
41    for entry in WalkDir::new(".").max_depth(1) {
42        let entry = entry?;
43        if entry.file_type().is_dir() {
44            if let Some(name) = entry.file_name().to_str() {
45                if name.starts_with("muts") {
46                    folders.push(entry.path().to_path_buf());
47                }
48            }
49        }
50    }
51
52    Ok(folders)
53}
54
55pub async fn analyze_folder(
56    folder_path: &Path,
57    command: Option<String>,
58    jobs: u32,
59    timeout_secs: u64,
60    survival_threshold: f64,
61) -> Result<()> {
62    let mut num_killed: u64 = 0;
63    let mut not_killed = Vec::new();
64
65    // Read target file path
66    let original_file_path = folder_path.join("original_file.txt");
67    let target_file_path = fs::read_to_string(original_file_path)?;
68    let target_file_path = target_file_path.trim();
69
70    // Setup command if not provided
71    let test_command = if let Some(cmd) = command {
72        cmd
73    } else {
74        run_build_command().await?;
75        get_command_to_kill(&target_file_path, jobs)?
76    };
77
78    // Get list of mutant files
79    let mut mutant_files = Vec::new();
80    for entry in fs::read_dir(folder_path)? {
81        let entry = entry?;
82        let path = entry.path();
83        if path.is_file() && !path.extension().map_or(true, |ext| ext == "txt") {
84            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
85                mutant_files.push(name.to_string());
86            }
87        }
88    }
89
90    let total_mutants = mutant_files.len();
91    println!("* {} MUTANTS *", total_mutants);
92
93    if total_mutants == 0 {
94        return Err(MutationError::InvalidInput(format!(
95            "No mutants in the provided folder path ({})",
96            folder_path.display()
97        )));
98    }
99
100    for (i, file_name) in mutant_files.iter().enumerate() {
101        let current_survival_rate = not_killed.len() as f64 / total_mutants as f64;
102        if current_survival_rate > survival_threshold {
103            println!(
104                "\nTerminating early: {:.2}% mutants surviving after {} iterations",
105                current_survival_rate * 100.0,
106                i + 1
107            );
108            println!(
109                "Survival rate exceeds threshold of {:.0}%",
110                survival_threshold * 100.0
111            );
112            break;
113        }
114
115        println!("[{}/{}] Analyzing {}", i + 1, total_mutants, file_name);
116
117        let file_path = folder_path.join(file_name);
118
119        // Read and apply mutant
120        let mutant_content = fs::read_to_string(&file_path)?;
121        fs::write(&target_file_path, &mutant_content)?;
122
123        //println!("Running: {}", test_command);
124        let result = run_command(&test_command, timeout_secs).await?;
125
126        if result {
127            println!("NOT KILLED ❌");
128            not_killed.push(file_name.clone());
129        } else {
130            println!("KILLED ✅");
131            num_killed += 1
132        }
133    }
134
135    // Generate report
136    let score = num_killed as f64 / total_mutants as f64;
137    println!("\nMUTATION SCORE: {:.2}%", score * 100.0);
138
139    generate_report(
140        &not_killed,
141        folder_path.to_str().unwrap(),
142        &target_file_path,
143        score,
144    )
145    .await?;
146
147    // Restore the original file
148    restore_file(&target_file_path).await?;
149
150    Ok(())
151}
152
153async fn run_command(command: &str, timeout_secs: u64) -> Result<bool> {
154    use std::process::Stdio;
155
156    // Split command into shell and arguments for better cross-platform support
157    let (shell, shell_arg) = if cfg!(target_os = "windows") {
158        ("cmd", "/C")
159    } else {
160        ("sh", "-c")
161    };
162
163    println!("Executing command: {}", command);
164
165    let mut cmd = TokioCommand::new(shell);
166    cmd.arg(shell_arg)
167        .arg(command)
168        .stdout(Stdio::piped())
169        .stderr(Stdio::piped())
170        .kill_on_drop(true); // Ensure child process is killed if parent dies
171
172    let timeout_duration = Duration::from_secs(timeout_secs);
173
174    match timeout(timeout_duration, cmd.output()).await {
175        Ok(Ok(output)) => {
176            let stdout = String::from_utf8_lossy(&output.stdout);
177            let stderr = String::from_utf8_lossy(&output.stderr);
178
179            println!("Command exit code: {}", output.status.code().unwrap_or(-1));
180
181            if !stdout.is_empty() {
182                println!("STDOUT:\n{}", stdout);
183            }
184
185            if !stderr.is_empty() {
186                println!("STDERR:\n{}", stderr);
187            }
188
189            Ok(output.status.success())
190        }
191        Ok(Err(e)) => {
192            println!("Command execution failed: {}", e);
193            Ok(false)
194        }
195        Err(_) => {
196            println!("Command timed out after {} seconds", timeout_secs);
197            Ok(false)
198        }
199    }
200}
201
202async fn run_build_command() -> Result<()> {
203    let build_command =
204        "rm -rf build && cmake -B build -DENABLE_IPC=OFF && cmake --build build -j $(nproc)";
205
206    let success = run_command(build_command, 3600).await?; // 1 hour timeout for build
207    if !success {
208        return Err(MutationError::Command("Build command failed".to_string()));
209    }
210
211    Ok(())
212}
213
214fn get_command_to_kill(target_file_path: &str, jobs: u32) -> Result<String> {
215    let mut build_command = "cmake --build build".to_string();
216    if jobs > 0 {
217        build_command.push_str(&format!(" -j{}", jobs));
218    }
219
220    let command = if target_file_path.contains("functional") {
221        format!("./build/{}", target_file_path)
222    } else if target_file_path.contains("test") {
223        let filename_with_extension = Path::new(target_file_path)
224            .file_name()
225            .and_then(|n| n.to_str())
226            .ok_or_else(|| MutationError::InvalidInput("Invalid file path".to_string()))?;
227
228        let test_to_run = filename_with_extension
229            .rsplit('.')
230            .nth(1)
231            .ok_or_else(|| MutationError::InvalidInput("Cannot extract test name".to_string()))?;
232
233        format!(
234            "{} && ./build/bin/test_bitcoin --run_test={}",
235            build_command, test_to_run
236        )
237    } else {
238        format!(
239            "{} && ctest --output-on-failure --stop-on-failure -C Release && CI_FAILFAST_TEST_LEAVE_DANGLING=1 ./build/test/functional/test_runner.py -F",
240            build_command
241        )
242    };
243
244    Ok(command)
245}
246
247async fn restore_file(target_file_path: &str) -> Result<()> {
248    let restore_command = format!("git restore {}", target_file_path);
249    run_command(&restore_command, 30).await?;
250    Ok(())
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use std::fs;
257    use tempfile::tempdir;
258
259    #[test]
260    fn test_get_command_to_kill() {
261        // Test functional test
262        let cmd = get_command_to_kill("test/functional/test_example.py", 4).unwrap();
263        assert_eq!(cmd, "./build/test/functional/test_example.py");
264
265        // Test unit test
266        let cmd = get_command_to_kill("src/test/test_example.cpp", 0).unwrap();
267        assert_eq!(
268            cmd,
269            "cmake --build build && ./build/bin/test_bitcoin --run_test=test_example"
270        );
271
272        // Test general case
273        let cmd = get_command_to_kill("src/wallet/wallet.cpp", 2).unwrap();
274        assert!(cmd.contains("cmake --build build -j2"));
275        assert!(cmd.contains("ctest"));
276        assert!(cmd.contains("test_runner.py"));
277    }
278
279    #[tokio::test]
280    async fn test_run_command() {
281        // Test successful command
282        let result = run_command("echo 'test'", 5).await.unwrap();
283        assert!(result);
284
285        // Test failing command
286        let result = run_command("false", 5).await.unwrap();
287        assert!(!result);
288
289        // Test command that should timeout (note: this might be flaky in CI)
290        let result = run_command("sleep 10", 1).await.unwrap();
291        assert!(!result);
292    }
293
294    #[test]
295    fn test_find_mutation_folders() {
296        let temp_dir = tempdir().unwrap();
297        std::env::set_current_dir(&temp_dir).unwrap();
298
299        // Create some test directories
300        fs::create_dir("muts-test-1").unwrap();
301        fs::create_dir("muts-test-2").unwrap();
302        fs::create_dir("not-muts").unwrap();
303        fs::create_dir("another-dir").unwrap();
304
305        let folders = find_mutation_folders().unwrap();
306        assert_eq!(folders.len(), 2);
307
308        let folder_names: Vec<String> = folders
309            .iter()
310            .filter_map(|p| p.file_name().and_then(|n| n.to_str()))
311            .map(|s| s.to_string())
312            .collect();
313
314        assert!(folder_names.contains(&"muts-test-1".to_string()));
315        assert!(folder_names.contains(&"muts-test-2".to_string()));
316        assert!(!folder_names.contains(&"not-muts".to_string()));
317    }
318}