bcore_mutation/
analyze.rs1use 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_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 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 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 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 let mutant_content = fs::read_to_string(&file_path)?;
121 fs::write(&target_file_path, &mutant_content)?;
122
123 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 let score = num_killed as f64 / total_mutants as f64;
137 println!("\nMUTATION SCORE: {:.2}%", score * 100.0);
138
139 generate_report(
140 ¬_killed,
141 folder_path.to_str().unwrap(),
142 &target_file_path,
143 score,
144 )
145 .await?;
146
147 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 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); 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?; 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 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 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 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 let result = run_command("echo 'test'", 5).await.unwrap();
283 assert!(result);
284
285 let result = run_command("false", 5).await.unwrap();
287 assert!(!result);
288
289 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 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}