use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use anyhow::{Context, anyhow, bail};
use super::{Mutant, Outcome, Position, Span, cargo};
fn mutated(src: &str, span: &Span, replacement: &str) -> anyhow::Result<String> {
let offset = |p: &Position| -> Option<usize> {
let start: usize = src
.split_inclusive('\n')
.take(p.line - 1)
.map(str::len)
.sum();
let line = src[start..].split_inclusive('\n').next().unwrap_or("");
let column = line
.char_indices()
.map(|(i, _)| i)
.chain([line.len()])
.nth(p.column - 1)?;
Some(start + column)
};
let (Some(start), Some(end)) = (offset(&span.start), offset(&span.end)) else {
bail!("a mutant's span is outside its file");
};
Ok(format!("{}{replacement}{}", &src[..start], &src[end..]))
}
pub(super) struct Scratch {
dir: PathBuf,
target: PathBuf,
}
impl Scratch {
pub(super) fn copy(root: &Path) -> anyhow::Result<Self> {
let base = root.join("target/impish/mutate");
let dir = base.join("project");
if dir.exists() {
fs::remove_dir_all(&dir).context("clear the scratch copy")?;
}
copy_dir(root, &dir, true).context("copy the project to mutate")?;
let target = std::env::var_os("CARGO_TARGET_DIR")
.map_or_else(|| root.join("target"), PathBuf::from)
.join("impish-mutate");
Ok(Self { dir, target })
}
pub(super) fn build(&self) -> anyhow::Result<Option<PathBuf>> {
let out = Command::new(cargo())
.args([
"test",
"--test",
"contract",
"--no-run",
"--message-format=json",
])
.current_dir(&self.dir)
.env("CARGO_TARGET_DIR", &self.target)
.stderr(Stdio::null())
.output()
.context("run `cargo test --no-run`")?;
if !out.status.success() {
return Ok(None);
}
Ok(String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.filter(|msg| msg["target"]["name"] == "contract")
.filter_map(|msg| msg["executable"].as_str().map(PathBuf::from))
.next_back())
}
pub(super) fn test(
&self,
exe: &Path,
primitive: &str,
timeout: Option<Duration>,
) -> anyhow::Result<Option<BTreeSet<String>>> {
let mut child = Command::new(exe)
.arg(format!("{primitive}::"))
.current_dir(&self.dir)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.context("run the contract tests")?;
let mut stdout = child.stdout.take().expect("piped stdout");
let reader = thread::spawn(move || {
let mut text = String::new();
stdout.read_to_string(&mut text).map(|_| text)
});
let started = Instant::now();
while child.try_wait()?.is_none() {
if timeout.is_some_and(|t| started.elapsed() > t) {
child.kill()?;
child.wait()?;
return Ok(None);
}
thread::sleep(Duration::from_millis(20));
}
let text = reader
.join()
.map_err(|_| anyhow!("read the contract tests' output"))??;
Ok(Some(
text.lines()
.filter_map(|l| l.strip_prefix("test ")?.strip_suffix(" ... FAILED"))
.map(String::from)
.collect(),
))
}
pub(super) fn try_mutant(
&self,
mutant: &Mutant,
primitive: &str,
timeout: Duration,
tags: &BTreeMap<String, Vec<String>>,
active: &[String],
) -> anyhow::Result<Outcome> {
let path = self.dir.join(&mutant.file);
let original =
fs::read_to_string(&path).with_context(|| format!("read {}", mutant.file))?;
fs::write(
&path,
mutated(&original, &mutant.span, &mutant.replacement)?,
)?;
let outcome = match self.build()? {
None => Outcome::Unviable,
Some(exe) => match self.test(&exe, primitive, Some(timeout))? {
None => Outcome::TimedOut,
Some(failed) => {
let by: BTreeSet<String> = failed
.iter()
.flat_map(|test| tags.get(test).into_iter().flatten())
.filter(|tag| active.contains(tag))
.cloned()
.collect();
if by.is_empty() {
Outcome::Survived
} else {
Outcome::Killed(by)
}
}
},
};
fs::write(&path, original)?;
Ok(outcome)
}
}
fn copy_dir(from: &Path, to: &Path, top: bool) -> std::io::Result<()> {
fs::create_dir_all(to)?;
for entry in fs::read_dir(from)? {
let entry = entry?;
let name = entry.file_name();
if name == ".git" || (top && name == "target") {
continue;
}
let path = entry.path();
if path.is_dir() {
copy_dir(&path, &to.join(&name), false)?;
} else {
fs::write(to.join(&name), fs::read(&path)?)?;
fs::set_permissions(to.join(&name), entry.metadata()?.permissions())?;
}
}
Ok(())
}