use std::fs;
use camino::Utf8PathBuf;
use super::super::workspace::Workspace;
use super::{Converger, Guards};
use crate::discover::{Plan, TargetFile};
use crate::error::error;
use crate::parse::{BOM, strip_bom};
use crate::schema::{self, Guard};
use crate::{HashMap, HashSet, Result};
#[derive(Debug, Default)]
pub(super) struct Splices {
pub(super) root: Utf8PathBuf,
pub(super) sources: HashMap<Utf8PathBuf, Original>,
pub(super) placed: HashMap<Utf8PathBuf, (Vec<u32>, HashMap<u32, Guard>)>,
file_index: HashMap<Utf8PathBuf, usize>,
mutants_by_file: HashMap<Utf8PathBuf, Vec<usize>>,
file_by_ordinal: HashMap<u32, Utf8PathBuf>,
indexed_files: usize,
indexed_mutants: usize,
plan_identity: Option<usize>,
withdrawn: HashSet<u32>,
}
#[derive(Debug)]
pub(super) struct Original {
parsed: String,
serialized: String,
}
impl Original {
fn instrumented(&self, parsed: String) -> String {
if self.serialized.starts_with(BOM) {
let mut serialized = String::with_capacity(BOM.len_utf8() + parsed.len());
serialized.push(BOM);
serialized.push_str(&parsed);
serialized
} else {
parsed
}
}
}
impl Splices {
pub(super) fn plan_reordered(&mut self) {
self.file_index.clear();
self.mutants_by_file.clear();
self.file_by_ordinal.clear();
self.indexed_files = 0;
self.indexed_mutants = 0;
self.plan_identity = None;
self.withdrawn.clear();
}
pub(super) fn instrument(&mut self, work: &Workspace, plan: &Plan, withdrawn: &HashSet<u32>) -> Result<Guards> {
if self.root != work.root {
self.root = work.root.clone();
self.sources.clear();
self.placed.clear();
self.file_index.clear();
self.mutants_by_file.clear();
self.file_by_ordinal.clear();
self.indexed_files = 0;
self.indexed_mutants = 0;
self.withdrawn.clear();
}
self.restore_removed_files(work, plan)?;
let dirty = self.refresh_index(plan, withdrawn);
let mut guards = Guards::default();
for (path, (_ordinals, found)) in &self.placed {
if !dirty.contains(path) {
for (ordinal, guard) in found {
let _ = guards.insert(*ordinal, (path.clone(), guard.clone()));
}
}
}
let mut dirty: Vec<usize> = dirty.iter().filter_map(|path| self.file_index.get(path).copied()).collect();
dirty.sort_unstable();
for position in dirty {
let Some(file) = plan.files.get(position) else {
continue;
};
let live: Vec<_> = self
.mutants_by_file
.get(&file.path)
.into_iter()
.flatten()
.filter_map(|position| plan.mutants.get(*position))
.filter(|mutant| mutant.ordinal > 0 && !withdrawn.contains(&mutant.ordinal))
.collect();
let ordinals: Vec<u32> = live.iter().map(|mutant| mutant.ordinal).collect();
if let Some((placed, found)) = self.placed.get(&file.path)
&& *placed == ordinals
{
for (ordinal, guard) in found {
let _ = guards.insert(*ordinal, (file.path.clone(), guard.clone()));
}
continue;
}
let original = self.original(file)?;
let (instrumented, found) = if live.is_empty() {
(original.serialized.clone(), HashMap::default())
} else {
let (parsed, found) = schema::instrument_with_guards(&original.parsed, &live)?;
(original.instrumented(parsed), found)
};
for (ordinal, guard) in &found {
let _ = guards.insert(*ordinal, (file.path.clone(), guard.clone()));
}
if let Some(missing) = live.iter().find(|mutant| !guards.contains_key(&mutant.ordinal)) {
return Err(Converger::missing_guard_error(missing));
}
let destination = work.root.join(&file.path);
let _written = Workspace::overwrite(&work.root, &destination, &instrumented)?;
let _replaced = self.placed.insert(file.path.clone(), (ordinals, found));
if live.is_empty() {
let _dropped = self.sources.remove(&file.path);
}
}
Ok(guards)
}
fn restore_removed_files(&mut self, work: &Workspace, plan: &Plan) -> Result<()> {
let identity = core::ptr::from_ref(plan) as usize;
if self.plan_identity.is_none_or(|previous| previous == identity) {
return Ok(());
}
let current: HashSet<&camino::Utf8Path> = plan.files.iter().map(|file| file.path.as_path()).collect();
let removed: Vec<Utf8PathBuf> = self
.placed
.keys()
.filter(|path| !current.contains(path.as_path()))
.cloned()
.collect();
for path in removed {
if let Some(original) = self.sources.get(&path) {
let destination = work.root.join(&path);
let _written = Workspace::overwrite(&work.root, &destination, &original.serialized)?;
}
let _placed = self.placed.remove(&path);
let _source = self.sources.remove(&path);
}
Ok(())
}
fn refresh_index(&mut self, plan: &Plan, withdrawn: &HashSet<u32>) -> HashSet<Utf8PathBuf> {
let mut dirty = HashSet::default();
let plan_identity = core::ptr::from_ref(plan) as usize;
if self.plan_identity != Some(plan_identity) || self.indexed_files > plan.files.len() || self.indexed_mutants > plan.mutants.len() {
dirty.extend(self.file_index.keys().cloned());
self.file_index.clear();
self.mutants_by_file.clear();
self.file_by_ordinal.clear();
self.indexed_files = 0;
self.indexed_mutants = 0;
dirty.extend(plan.files.iter().map(|file| file.path.clone()));
}
self.plan_identity = Some(plan_identity);
for (position, file) in plan.files.iter().enumerate().skip(self.indexed_files) {
let _previous = self.file_index.insert(file.path.clone(), position);
let _new = dirty.insert(file.path.clone());
}
self.indexed_files = plan.files.len();
for (position, mutant) in plan.mutants.iter().enumerate().skip(self.indexed_mutants) {
if mutant.ordinal > 0 {
self.mutants_by_file.entry(mutant.file.to_path_buf()).or_default().push(position);
let _previous = self.file_by_ordinal.insert(mutant.ordinal, mutant.file.to_path_buf());
let _new = dirty.insert(mutant.file.to_path_buf());
}
}
self.indexed_mutants = plan.mutants.len();
if self.withdrawn.is_subset(withdrawn) {
for ordinal in withdrawn.difference(&self.withdrawn) {
if let Some(path) = self.file_by_ordinal.get(ordinal) {
let _new = dirty.insert(path.clone());
}
}
} else {
dirty.extend(self.file_index.keys().cloned());
}
self.withdrawn.clone_from(withdrawn);
dirty
}
pub(super) fn original(&mut self, file: &TargetFile) -> Result<&Original> {
if !self.sources.contains_key(&file.path) {
let serialized = fs::read_to_string(file.absolute.as_std_path())
.map_err(|cause| error!("could not read `{}`", file.absolute).caused_by(cause))?;
let parsed = strip_bom(&serialized).to_owned();
let _stored = self.sources.insert(file.path.clone(), Original { parsed, serialized });
}
Ok(self.sources.get(&file.path).unwrap_or_else(|| unreachable!("just inserted")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instrumented_text_retains_the_original_byte_order_mark() {
let parsed = "fn f() {}\n";
let original = Original {
parsed: parsed.to_owned(),
serialized: format!("{BOM}{parsed}"),
};
assert_eq!(
original.instrumented("fn f() { gamma(); }\n".to_owned()),
format!("{BOM}fn f() {{ gamma(); }}\n")
);
}
}