From da7beecce95fa3611d2ff2fd7acc0aa7e2f0684b Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sat, 11 Nov 2023 07:22:37 -0800 Subject: [PATCH] WIP: Read diffs so we can filter by them --- Cargo.lock | 112 ++++++++++++++++++ Cargo.toml | 1 + src/diff_filter.rs | 108 +++++++++++++++++ src/main.rs | 1 + ..._expected_mutants_for_own_source_tree.snap | 7 ++ src/source.rs | 4 + 6 files changed, 233 insertions(+) create mode 100644 src/diff_filter.rs diff --git a/Cargo.lock b/Cargo.lock index 2a5c8784..53a8f017 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.5.0" @@ -126,6 +141,12 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + [[package]] name = "camino" version = "1.1.6" @@ -158,6 +179,7 @@ dependencies = [ "mutants 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "nix", "nutmeg", + "patch", "path-slash", "predicates", "pretty_assertions", @@ -217,6 +239,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.48.5", +] + [[package]] name = "clap" version = "4.4.3" @@ -286,6 +322,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cp_r" version = "0.5.1" @@ -470,6 +512,29 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "2.0.0" @@ -595,6 +660,12 @@ version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mutants" version = "0.0.3" @@ -616,6 +687,27 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom_locate" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" +dependencies = [ + "bytecount", + "memchr", + "nom", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -688,6 +780,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "patch" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c07fdcdd8b05bdcf2a25bc195b6c34cbd52762ada9dba88bf81e7686d14e7a" +dependencies = [ + "chrono", + "nom", + "nom_locate", +] + [[package]] name = "path-slash" version = "0.2.1" @@ -1280,6 +1383,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index beca1f30..cbff72aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/diff_filter.rs b/src/diff_filter.rs new file mode 100644 index 00000000..8f557db2 --- /dev/null +++ b/src/diff_filter.rs @@ -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> { + // 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); + } +} diff --git a/src/main.rs b/src/main.rs index dafe78b8..d42d1a2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod build_dir; mod cargo; mod config; mod console; +mod diff_filter; mod exit_code; mod fnvalue; mod interrupt; diff --git a/src/snapshots/cargo_mutants__visit__test__expected_mutants_for_own_source_tree.snap b/src/snapshots/cargo_mutants__visit__test__expected_mutants_for_own_source_tree.snap index 73524271..741cb523 100644 --- a/src/snapshots/cargo_mutants__visit__test__expected_mutants_for_own_source_tree.snap +++ b/src/snapshots/cargo_mutants__visit__test__expected_mutants_for_own_source_tree.snap @@ -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> with Ok(vec![]) +src/diff_filter.rs: replace diff_filter -> Result> with Ok(vec![&Default::default()]) +src/diff_filter.rs: replace diff_filter -> Result> 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 with ::std::iter::empty() src/fnvalue.rs: replace return_type_replacements -> impl Iterator with ::std::iter::once(Default::default()) src/fnvalue.rs: replace type_replacements -> impl Iterator with ::std::iter::empty() @@ -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 ::from -> Self with Default::default() src/textedit.rs: replace ::from -> Self with Default::default() src/textedit.rs: replace ::from -> Self with Default::default() diff --git a/src/source.rs b/src/source.rs index bf5ee6e5..d1866442 100644 --- a/src/source.rs +++ b/src/source.rs @@ -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)]