use std::path::PathBuf;
use anyhow::{Context as _, Result};
use crate::{pool, reporter, worker};
pub struct MutOpts {
pub cfg: worker::Config,
pub jobs: usize,
pub isolation: pool::IsolationMode,
pub timeout_ms: u64,
}
pub struct MutationReport {
pub total: usize,
pub killed: usize,
pub survived: Vec<String>,
}
pub async fn run(
mutate_file: &str,
items: Vec<pool::TestItem>,
opts: MutOpts,
) -> Result<MutationReport> {
let project_abs = PathBuf::from(&opts.cfg.project_path)
.canonicalize()
.context("project path")?;
let res_path = if mutate_file.starts_with("res://") {
mutate_file.to_string()
} else {
let p = PathBuf::from(mutate_file);
let abs = if p.is_absolute() {
p
} else {
project_abs.join(p)
};
gdtest_analysis::to_res_path(&abs, &project_abs)?
};
let abs_path = gdtest_analysis::res_to_abs(&res_path, &project_abs)?;
let source =
std::fs::read(&abs_path).with_context(|| format!("reading {}", abs_path.display()))?;
let mutations = gdtest_analysis::mutate::find_mutations(&source);
let total = mutations.len();
if total == 0 {
eprintln!("gdtest mutate: no mutations found in {res_path}");
return Ok(MutationReport {
total: 0,
killed: 0,
survived: Vec::new(),
});
}
eprintln!("gdtest mutate: {total} mutations in {res_path}");
let mut killed = 0usize;
let mut survived: Vec<String> = Vec::new();
for (idx, mutation) in mutations.iter().enumerate() {
let desc = mutation.description();
eprint!(
" [{:>width$}/{}] {} … ",
idx + 1,
total,
desc,
width = total.to_string().len()
);
let mutated = mutation.apply(&source);
let backup = abs_path.with_extension("gd.mut_bak");
std::fs::write(&backup, &source)
.with_context(|| format!("writing backup {}", backup.display()))?;
if let Err(e) = std::fs::write(&abs_path, &mutated) {
std::fs::remove_file(&backup).ok();
return Err(e).with_context(|| format!("writing mutant {}", abs_path.display()));
}
let pool_opts = pool::PoolOptions {
jobs: opts.jobs.min(items.len()).max(1),
cfg: opts.cfg.clone(),
reporter: reporter::Reporter::Silent,
isolation: opts.isolation,
timeout_ms: opts.timeout_ms,
fail_fast: true,
timeout_warn_ms: None,
retries: 0,
};
let result = pool::run(items.clone(), pool_opts).await;
if let Err(e) = std::fs::write(&abs_path, &source) {
eprintln!(
"\ngdtest mutate: FATAL: could not restore {}: {e}",
abs_path.display()
);
std::fs::remove_file(&backup).ok();
return Err(anyhow::anyhow!("source restore failed: {e}"));
}
std::fs::remove_file(&backup).ok();
match result {
Ok((summary, _)) if summary.fail > 0 || summary.error > 0 => {
eprintln!("KILLED");
killed += 1;
}
Ok(_) => {
eprintln!("survived");
survived.push(desc);
}
Err(_) => {
eprintln!("KILLED (crash)");
killed += 1;
}
}
}
Ok(MutationReport {
total,
killed,
survived,
})
}