Skip to content

Commit

Permalink
WIP: Read diffs so we can filter by them
Browse files Browse the repository at this point in the history
  • Loading branch information
sourcefrog committed Nov 11, 2023
1 parent 7831e91 commit da7beec
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 0 deletions.
112 changes: 112 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ indoc = "2.0.0"
itertools = "0.11"
mutants = "0.0.3"
nix = "0.27"
patch = "0.7"
path-slash = "0.2"
quote = "1.0"
serde_json = "1"
Expand Down
108 changes: 108 additions & 0 deletions src/diff_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2023 Martin Pool

//! Filter mutants to those intersecting a diff on the file tree,
//! for example from uncommitted or unmerged changes.
#![allow(unused_imports)]

use std::collections::HashMap;

use anyhow::{anyhow, bail, Context};
use camino::Utf8Path;
use patch::{Patch, Range};
use tracing::warn;

use crate::mutate::Mutant;
use crate::Result;

/// Return only mutants to functions whose source was touched by this diff.
pub fn diff_filter<'a>(mutants: &[&'a Mutant], diff_text: &str) -> Result<Vec<&'a Mutant>> {
// Flatten the error to a string because otherwise it references the diff, and can't be returned.
let patches =
Patch::from_multiple(diff_text).map_err(|err| anyhow!("Failed to parse diff: {err}"))?;
let mut patch_by_path: HashMap<&Utf8Path, &Patch> = HashMap::new();
for patch in &patches {
let path = strip_patch_path(&patch.new.path);
if patch_by_path.insert(path, patch).is_some() {
bail!("Patch input contains repeated filename: {path:?}");
}
}

/* TODO: Find the intersection of the patches and mutants:
For each patch, changing one file:
Only mutants matching that file could be relevant: we might need some heuristics to
strip a `b/` prefix off the filesname.
The naive way is quadratic but we could first group the mutants by filename. And,
there are probably not so many mutants to make it too expensive for a first version.
Allow for diffs that might have multiple changes to the same file.
We shouldn't duplicate mutants even if the diffs have duplicates.
*/
let mut matched: Vec<&Mutant> = Vec::with_capacity(mutants.len());
'mutant: for mutant in mutants {
if let Some(patch) = patch_by_path.get(mutant.source_file.path()) {
for hunk in &patch.hunks {
if range_overlaps(&hunk.new_range, mutant) {
matched.push(mutant);
continue 'mutant;
}
}
}
}
Ok(matched)
}

/// Remove the `b/` prefix commonly found in paths within diffs.
fn strip_patch_path(path: &str) -> &Utf8Path {
let path = Utf8Path::new(path);
path.strip_prefix("b").unwrap_or(path)
}

fn range_overlaps(diff_range: &Range, mutant: &Mutant) -> bool {
let diff_end = diff_range.start + diff_range.count;
diff_end >= mutant.span.start.line.try_into().unwrap()
&& diff_range.start <= mutant.span.end.line.try_into().unwrap()
}

#[cfg(test)]
mod test_super {
use pretty_assertions::assert_eq;

use super::*;

#[test]
fn patch_parse_error() {
let diff = "not really a diff\n";
let err = diff_filter(&[], diff).unwrap_err();
assert_eq!(
err.to_string(),
"Failed to parse diff: Line 1: Error while parsing: not really a diff\n"
);
}

#[test]
fn read_diff_with_empty_mutants() {
let diff = "\
diff --git a/src/mutate.rs b/src/mutate.rs
index eb42779..a0091b7 100644
--- a/src/mutate.rs
+++ b/src/mutate.rs
@@ -6,9 +6,7 @@ use std::fmt;
use std::fs;
use std::sync::Arc;
use std::foo;
-use anyhow::ensure;
-use anyhow::Context;
-use anyhow::Result;
+use anyhow::{ensure, Context, Result};
use serde::ser::{SerializeStruct, Serializer};
use serde::Serialize;
use similar::TextDiff;
";
let filtered: Vec<&Mutant> = diff_filter(&[], diff).expect("diff filtered");
assert_eq!(filtered.len(), 0);
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod build_dir;
mod cargo;
mod config;
mod console;
mod diff_filter;
mod exit_code;
mod fnvalue;
mod interrupt;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ src/console.rs: replace style_scenario -> Cow<'static, str> with Cow::Borrowed("
src/console.rs: replace style_scenario -> Cow<'static, str> with Cow::Owned("xyzzy".to_owned())
src/console.rs: replace plural -> String with String::new()
src/console.rs: replace plural -> String with "xyzzy".into()
src/diff_filter.rs: replace diff_filter -> Result<Vec<&'a Mutant>> with Ok(vec![])
src/diff_filter.rs: replace diff_filter -> Result<Vec<&'a Mutant>> with Ok(vec![&Default::default()])
src/diff_filter.rs: replace diff_filter -> Result<Vec<&'a Mutant>> with Err(::anyhow::anyhow!("mutated!"))
src/diff_filter.rs: replace strip_patch_path -> &Utf8Path with &Default::default()
src/diff_filter.rs: replace range_overlaps -> bool with true
src/diff_filter.rs: replace range_overlaps -> bool with false
src/fnvalue.rs: replace return_type_replacements -> impl Iterator<Item = TokenStream> with ::std::iter::empty()
src/fnvalue.rs: replace return_type_replacements -> impl Iterator<Item = TokenStream> with ::std::iter::once(Default::default())
src/fnvalue.rs: replace type_replacements -> impl Iterator<Item = TokenStream> with ::std::iter::empty()
Expand Down Expand Up @@ -288,6 +294,7 @@ src/scenario.rs: replace Scenario::log_file_name_base -> String with String::new
src/scenario.rs: replace Scenario::log_file_name_base -> String with "xyzzy".into()
src/source.rs: replace SourceFile::tree_relative_slashes -> String with String::new()
src/source.rs: replace SourceFile::tree_relative_slashes -> String with "xyzzy".into()
src/source.rs: replace SourceFile::path -> &Utf8Path with &Default::default()
src/textedit.rs: replace <impl From for LineColumn>::from -> Self with Default::default()
src/textedit.rs: replace <impl From for Span>::from -> Self with Default::default()
src/textedit.rs: replace <impl From for Span>::from -> Self with Default::default()
Expand Down
4 changes: 4 additions & 0 deletions src/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ impl SourceFile {
pub fn tree_relative_slashes(&self) -> String {
self.tree_relative_path.to_slash_path()
}

pub fn path(&self) -> &Utf8Path {
self.tree_relative_path.as_path()
}
}

#[cfg(test)]
Expand Down

0 comments on commit da7beec

Please sign in to comment.