From 92f96617f53511c9cb8bbfeadc7a17fa5b3be4ba Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 24 Nov 2024 12:46:13 -0800 Subject: [PATCH 1/9] tests: More tests for copy_tree gitignore --- src/build_dir.rs | 2 +- src/copy_tree.rs | 88 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/build_dir.rs b/src/build_dir.rs index 7cff8b71..2df19e95 100644 --- a/src/build_dir.rs +++ b/src/build_dir.rs @@ -57,7 +57,7 @@ impl BuildDir { let source_abs = source .canonicalize_utf8() .context("canonicalize source path")?; - let temp_dir = copy_tree(source, &name_base, options.gitignore, console)?; + let temp_dir = copy_tree(source, &name_base, options, console)?; let path: Utf8PathBuf = temp_dir .path() .to_owned() diff --git a/src/copy_tree.rs b/src/copy_tree.rs index a02d2ff2..2727aa67 100644 --- a/src/copy_tree.rs +++ b/src/copy_tree.rs @@ -9,6 +9,7 @@ use path_slash::PathExt; use tempfile::TempDir; use tracing::{debug, warn}; +use crate::options::Options; use crate::{check_interrupted, Console, Result}; #[cfg(unix)] @@ -35,14 +36,11 @@ static SOURCE_EXCLUDE: &[&str] = &[ /// Copy a source tree, with some exclusions, to a new temporary directory. /// -/// If `git` is true, ignore files that are excluded by all the various `.gitignore` -/// files. -/// /// Regardless, anything matching [SOURCE_EXCLUDE] is excluded. pub fn copy_tree( from_path: &Utf8Path, name_base: &str, - gitignore: bool, + options: &Options, console: &Console, ) -> Result { let mut total_bytes = 0; @@ -59,7 +57,9 @@ pub fn copy_tree( console.start_copy(dest); let mut walk_builder = WalkBuilder::new(from_path); walk_builder - .standard_filters(gitignore) + .git_ignore(options.gitignore) + .git_exclude(options.gitignore) + .git_global(options.gitignore) .hidden(false) // copy hidden files .ignore(false) // don't use .ignore .require_git(true) // stop at git root; only read gitignore files inside git trees @@ -115,12 +115,15 @@ pub fn copy_tree( #[cfg(test)] mod test { + // TODO: Maybe run these with $HOME set to a temp dir so that global git config has no effect? + use std::fs::{create_dir, write}; use camino::Utf8PathBuf; use tempfile::TempDir; use crate::console::Console; + use crate::options::Options; use crate::Result; use super::copy_tree; @@ -139,7 +142,8 @@ mod test { create_dir(&src)?; write(src.join("main.rs"), "fn main() {}")?; - let dest_tmpdir = copy_tree(&a, "a", true, &Console::new())?; + let options = Options::from_arg_strs(&["--gitignore=true"]); + let dest_tmpdir = copy_tree(&a, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); assert!(dest.join("Cargo.toml").is_file()); assert!(dest.join("src").is_dir()); @@ -147,4 +151,76 @@ mod test { Ok(()) } + + /// With `gitignore` set to `true`, but no `.git`, don't exclude anything. + #[test] + fn copy_with_gitignore_but_without_git_dir() -> Result<()> { + let tmp_dir = TempDir::new().unwrap(); + let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); + write(tmp.join(".gitignore"), "foo\n")?; + + write(tmp.join("Cargo.toml"), "[package]\nname = a")?; + let src = tmp.join("src"); + create_dir(&src)?; + write(src.join("main.rs"), "fn main() {}")?; + write(tmp.join("foo"), "bar")?; + + let options = Options::from_arg_strs(["--gitignore=true"]); + let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; + let dest = dest_tmpdir.path(); + assert!( + dest.join("foo").is_file(), + "foo should be copied because gitignore is not used without .git" + ); + + Ok(()) + } + + /// With `gitignore` set to `true`, in a tree with `.git`, `.gitignore` is respected. + #[test] + fn copy_with_gitignore_and_git_dir() -> Result<()> { + let tmp_dir = TempDir::new().unwrap(); + let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); + write(tmp.join(".gitignore"), "foo\n")?; + create_dir(tmp.join(".git"))?; + + write(tmp.join("Cargo.toml"), "[package]\nname = a")?; + let src = tmp.join("src"); + create_dir(&src)?; + write(src.join("main.rs"), "fn main() {}")?; + write(tmp.join("foo"), "bar")?; + + let options = Options::from_arg_strs(["--gitignore=true"]); + let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; + let dest = dest_tmpdir.path(); + assert!( + !dest.join("foo").is_file(), + "foo should have been excluded by gitignore" + ); + + Ok(()) + } + + /// With `gitignore` set to `false`, patterns in that file have no effect. + #[test] + fn copy_without_gitignore() -> Result<()> { + let tmp_dir = TempDir::new().unwrap(); + let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); + write(tmp.join(".gitignore"), "foo\n")?; + create_dir(tmp.join(".git"))?; + + write(tmp.join("Cargo.toml"), "[package]\nname = a")?; + let src = tmp.join("src"); + create_dir(&src)?; + write(src.join("main.rs"), "fn main() {}")?; + write(tmp.join("foo"), "bar")?; + + let options = Options::from_arg_strs(["--gitignore=false"]); + let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; + let dest = dest_tmpdir.path(); + // gitignore didn't exclude `foo` + assert!(dest.join("foo").is_file()); + + Ok(()) + } } From 399b405b1a48c6ebb78ec7ab92ac1a44bf208111 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 24 Nov 2024 12:51:45 -0800 Subject: [PATCH 2/9] test: .git isn't copied by default --- src/copy_tree.rs | 50 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/copy_tree.rs b/src/copy_tree.rs index 2727aa67..67629c0e 100644 --- a/src/copy_tree.rs +++ b/src/copy_tree.rs @@ -22,17 +22,7 @@ mod windows; #[cfg(windows)] use windows::copy_symlink; -/// Filenames excluded from being copied with the source. -static SOURCE_EXCLUDE: &[&str] = &[ - ".git", - ".hg", - ".bzr", - ".svn", - "_darcs", - ".pijul", - "mutants.out", - "mutants.out.old", -]; +static VCS_DIRS: &[&str] = &[".git", ".hg", ".bzr", ".svn", "_darcs", ".pijul"]; /// Copy a source tree, with some exclusions, to a new temporary directory. /// @@ -64,7 +54,11 @@ pub fn copy_tree( .ignore(false) // don't use .ignore .require_git(true) // stop at git root; only read gitignore files inside git trees .filter_entry(|entry| { - !SOURCE_EXCLUDE.contains(&entry.file_name().to_string_lossy().as_ref()) + let name = entry.file_name().to_string_lossy(); + if name == "mutants.out" || name == "mutants.out.old" { + return false; + } + !VCS_DIRS.contains(&name.as_ref()) }); debug!(?walk_builder); for entry in walk_builder.build() { @@ -142,7 +136,7 @@ mod test { create_dir(&src)?; write(src.join("main.rs"), "fn main() {}")?; - let options = Options::from_arg_strs(&["--gitignore=true"]); + let options = Options::from_arg_strs(["--gitignore=true"]); let dest_tmpdir = copy_tree(&a, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); assert!(dest.join("Cargo.toml").is_file()); @@ -223,4 +217,34 @@ mod test { Ok(()) } + + #[test] + fn dont_copy_git_dir_by_default() -> Result<()> { + let tmp_dir = TempDir::new().unwrap(); + let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); + create_dir(tmp.join(".git"))?; + write(tmp.join(".git/foo"), "bar")?; + + write(tmp.join("Cargo.toml"), "[package]\nname = a")?; + let src = tmp.join("src"); + create_dir(&src)?; + write(src.join("main.rs"), "fn main() {}")?; + + let options = Options::from_arg_strs([""; 0]); + let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; + let dest = dest_tmpdir.path(); + assert!(!dest.join(".git").is_dir(), ".git should not be copied"); + assert!( + !dest.join(".git/foo").is_file(), + ".git/foo should not be copied" + ); + assert!( + dest.join("Cargo.toml").is_file(), + "Cargo.toml should be copied" + ); + + Ok(()) + } + + // fn copy_git_dir_when_requested() -> Result<()> {} } From e10cd8d69a85df6b9f12d729c37684dbb3725148 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 24 Nov 2024 12:53:01 -0800 Subject: [PATCH 3/9] test: mutants.out is not copied --- src/copy_tree.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/copy_tree.rs b/src/copy_tree.rs index 67629c0e..09b079fd 100644 --- a/src/copy_tree.rs +++ b/src/copy_tree.rs @@ -219,11 +219,13 @@ mod test { } #[test] - fn dont_copy_git_dir_by_default() -> Result<()> { + fn dont_copy_git_dir_or_mutants_out_by_default() -> Result<()> { let tmp_dir = TempDir::new().unwrap(); let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); create_dir(tmp.join(".git"))?; write(tmp.join(".git/foo"), "bar")?; + create_dir(tmp.join("mutants.out"))?; + write(tmp.join("mutants.out/foo"), "bar")?; write(tmp.join("Cargo.toml"), "[package]\nname = a")?; let src = tmp.join("src"); @@ -238,6 +240,10 @@ mod test { !dest.join(".git/foo").is_file(), ".git/foo should not be copied" ); + assert!( + !dest.join("mutants.out").exists(), + "mutants.out should not be copied" + ); assert!( dest.join("Cargo.toml").is_file(), "Cargo.toml should be copied" From 1ba958263663f8e21bfe10a7d53f3641de5b9e20 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 24 Nov 2024 13:04:04 -0800 Subject: [PATCH 4/9] feat: Add copy_vcs Fixes #348 --- src/config.rs | 2 ++ src/copy_tree.rs | 47 ++++++++++++++++++++++++++++++++++++++--------- src/main.rs | 6 ++++++ src/options.rs | 27 +++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/config.rs b/src/config.rs index 65df1599..c50ded02 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,6 +29,8 @@ use crate::Result; pub struct Config { /// Pass `--cap-lints` to rustc. pub cap_lints: bool, + /// Copy `.git` and other VCS directories to the build directory. + pub copy_vcs: Option, /// Generate these error values from functions returning Result. pub error_values: Vec, /// Generate mutants from source files matching these globs. diff --git a/src/copy_tree.rs b/src/copy_tree.rs index 09b079fd..92cb7813 100644 --- a/src/copy_tree.rs +++ b/src/copy_tree.rs @@ -46,6 +46,7 @@ pub fn copy_tree( .context("Convert path to UTF-8")?; console.start_copy(dest); let mut walk_builder = WalkBuilder::new(from_path); + let copy_vcs = options.copy_vcs; // for lifetime walk_builder .git_ignore(options.gitignore) .git_exclude(options.gitignore) @@ -53,12 +54,11 @@ pub fn copy_tree( .hidden(false) // copy hidden files .ignore(false) // don't use .ignore .require_git(true) // stop at git root; only read gitignore files inside git trees - .filter_entry(|entry| { + .filter_entry(move |entry| { let name = entry.file_name().to_string_lossy(); - if name == "mutants.out" || name == "mutants.out.old" { - return false; - } - !VCS_DIRS.contains(&name.as_ref()) + name != "mutants.out" + && name != "mutants.out.old" + && (copy_vcs || !VCS_DIRS.contains(&name.as_ref())) }); debug!(?walk_builder); for entry in walk_builder.build() { @@ -184,7 +184,7 @@ mod test { write(src.join("main.rs"), "fn main() {}")?; write(tmp.join("foo"), "bar")?; - let options = Options::from_arg_strs(["--gitignore=true"]); + let options = Options::from_arg_strs(["mutants", "--gitignore=true"]); let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); assert!( @@ -209,7 +209,7 @@ mod test { write(src.join("main.rs"), "fn main() {}")?; write(tmp.join("foo"), "bar")?; - let options = Options::from_arg_strs(["--gitignore=false"]); + let options = Options::from_arg_strs(["mutants", "--gitignore=false"]); let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); // gitignore didn't exclude `foo` @@ -232,7 +232,7 @@ mod test { create_dir(&src)?; write(src.join("main.rs"), "fn main() {}")?; - let options = Options::from_arg_strs([""; 0]); + let options = Options::from_arg_strs(["mutants"]); let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); assert!(!dest.join(".git").is_dir(), ".git should not be copied"); @@ -252,5 +252,34 @@ mod test { Ok(()) } - // fn copy_git_dir_when_requested() -> Result<()> {} + #[test] + fn copy_git_dir_when_requested() -> Result<()> { + let tmp_dir = TempDir::new().unwrap(); + let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); + create_dir(tmp.join(".git"))?; + write(tmp.join(".git/foo"), "bar")?; + create_dir(tmp.join("mutants.out"))?; + write(tmp.join("mutants.out/foo"), "bar")?; + + write(tmp.join("Cargo.toml"), "[package]\nname = a")?; + let src = tmp.join("src"); + create_dir(&src)?; + write(src.join("main.rs"), "fn main() {}")?; + + let options = Options::from_arg_strs(["mutants", "--copy-vcs=true"]); + let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; + let dest = dest_tmpdir.path(); + assert!(dest.join(".git").is_dir(), ".git should be copied"); + assert!(dest.join(".git/foo").is_file(), ".git/foo should be copied"); + assert!( + !dest.join("mutants.out").exists(), + "mutants.out should not be copied" + ); + assert!( + dest.join("Cargo.toml").is_file(), + "Cargo.toml should be copied" + ); + + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index 4c193d02..5eaac5a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -139,6 +139,12 @@ pub struct Args { )] colors: Colors, + /// Copy `.git` and other VCS directories to the build directory. + /// + /// This is useful if you have tests that depend on the presence of these directories. + #[arg(long, help_heading = "Copying")] + copy_vcs: Option, + /// Show the mutation diffs. #[arg(long, help_heading = "Filters")] diff: bool, diff --git a/src/options.rs b/src/options.rs index 0f596736..86620022 100644 --- a/src/options.rs +++ b/src/options.rs @@ -36,6 +36,9 @@ pub struct Options { /// Don't run the tests, just see if each mutant builds. pub check_only: bool, + /// Copy `.git` and other VCS directories to build directories. + pub copy_vcs: bool, + /// Don't copy files matching gitignore patterns to build directories. pub gitignore: bool, @@ -280,6 +283,7 @@ impl Options { cap_lints: args.cap_lints.unwrap_or(config.cap_lints), check_only: args.check, colors: args.colors, + copy_vcs: args.copy_vcs.or(config.copy_vcs).unwrap_or(false), emit_json: args.json, emit_diffs: args.diff, error_values: join_slices(&args.error, &config.error_values), @@ -828,4 +832,27 @@ mod test { // In this case the default is not used assert_eq!(options.skip_calls, ["x", "y", "with_capacity"]); } + + #[test] + fn copy_vcs() { + let args = Args::parse_from(["mutants", "--copy-vcs=true"]); + let config = Config::default(); + let options = Options::new(&args, &config).unwrap(); + assert!(options.copy_vcs); + + let args = Args::parse_from(["mutants", "--copy-vcs=false"]); + let config = Config::default(); + let options = Options::new(&args, &config).unwrap(); + assert!(!options.copy_vcs); + + let args = Args::parse_from(["mutants"]); + let config = Config::from_str("copy_vcs = true").unwrap(); + let options = Options::new(&args, &config).unwrap(); + assert!(options.copy_vcs); + + let args = Args::parse_from(["mutants"]); + let config = Config::from_str("").unwrap(); + let options = Options::new(&args, &config).unwrap(); + assert!(!options.copy_vcs); + } } From 8268a0b6b5e75f29cdcc6e02151329b48bc740ba Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 8 Dec 2024 08:38:14 -0800 Subject: [PATCH 5/9] Add alias --copy-git and more help --- src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 5eaac5a8..73f23000 100644 --- a/src/main.rs +++ b/src/main.rs @@ -142,7 +142,10 @@ pub struct Args { /// Copy `.git` and other VCS directories to the build directory. /// /// This is useful if you have tests that depend on the presence of these directories. - #[arg(long, help_heading = "Copying")] + /// + /// Known VCS directories are + /// `.git`, `.hg`, `.bzr`, `.svn`, `_darcs`, `.pijul`. + #[arg(long, help_heading = "Copying", visible_alias = "copy_git")] copy_vcs: Option, /// Show the mutation diffs. From 3f22f0e0ed814321780a7c4d31cd408bd5c55014 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 8 Dec 2024 08:38:20 -0800 Subject: [PATCH 6/9] News for --copy-git --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 6a3449f1..a57586d1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,8 @@ - Better estimation of time remaining, based on the time taken to test mutants so far, excluding the time for the baseline. +- New: `--copy-vcs` option and config option will copy `.git` and other VCS directories, to accommodate trees whose tests depend on the contents or presence of the VCS directory. + ## 24.11.2 - Changed: `.gitignore` (and other git ignore files) are only consulted when copying the tree if it is contained within a directory with a `.git` directory. From 50203a1105c008564ec7ab93e0b2d9795436b2b3 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 8 Dec 2024 08:47:57 -0800 Subject: [PATCH 7/9] Doc: copying the tree and --copy-vcs --- book/src/SUMMARY.md | 2 +- book/src/build-dirs.md | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index e43e0e25..657cd221 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -17,7 +17,7 @@ - [Listing and previewing mutations](list.md) - [Workspaces and packages](workspaces.md) - [Passing options to Cargo](cargo-args.md) - - [Build directories](build-dirs.md) + - [Copying the tree](build-dirs.md) - [Using nextest](nextest.md) - [Baseline tests](baseline.md) - [Testing in-place](in-place.md) diff --git a/book/src/build-dirs.md b/book/src/build-dirs.md index 09e562eb..a69b052b 100644 --- a/book/src/build-dirs.md +++ b/book/src/build-dirs.md @@ -1,10 +1,18 @@ -# Build directories +# Copying the tree -cargo-mutants builds mutated code in a temporary directory, containing a copy of your source tree with each mutant successively applied. With `--jobs`, multiple build directories are used in parallel. +By default, cargo-mutants copies your tree to a temporary directory before mutating and building it. This behavior is turned of by the [`--in-place`](in_place.md) option, which builds mutated code in the original source directory. -## Build-in ignores +When the [`--jobs`](parallelism.md) option is used, one build directory is created per job. -Files or directories matching these patterns are not copied: +Some filters are applied while copying the tree, which can be configured by options. + +## Troubleshooting tree copies + +If the baseline tests fail in the copied directory it is a good first debugging step to try building with `--in-place`. + +## `.git` and other version control directories + +By default, files or directories matching these patterns are not copied, because they can be large and typically are not needed to build the source: .git .hg @@ -13,7 +21,9 @@ Files or directories matching these patterns are not copied: _darcs .pijul -## gitignore +If your tree's build or tests require the VCS directory then it can be copied with `--copy-vcs=true` or by setting `copy_vcs = true` in `.cargo/mutants.toml`. + +## `.gitignore` From 23.11.2, by default, cargo-mutants will not copy files that are excluded by gitignore patterns, to make copying faster in large trees. @@ -22,3 +32,5 @@ gitignore filtering is only used within trees containing a `.git` directory. The filter, based on the [`ignore` crate](https://docs.rs/ignore/), also respects global git ignore configuration in the home directory, as well as `.gitignore` files within the tree. This behavior can be turned off with `--gitignore=false`, causing ignored files to be copied. + +Rust projects typically configure gitignore to exclude the `target/` directory. From c6bdb3ad74076c568c0acaef36fcdddf4a406d16 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Sun, 8 Dec 2024 08:56:36 -0800 Subject: [PATCH 8/9] Fix book link --- book/src/build-dirs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/build-dirs.md b/book/src/build-dirs.md index a69b052b..7ad63d35 100644 --- a/book/src/build-dirs.md +++ b/book/src/build-dirs.md @@ -1,6 +1,6 @@ # Copying the tree -By default, cargo-mutants copies your tree to a temporary directory before mutating and building it. This behavior is turned of by the [`--in-place`](in_place.md) option, which builds mutated code in the original source directory. +By default, cargo-mutants copies your tree to a temporary directory before mutating and building it. This behavior is turned of by the [`--in-place`](in-place.md) option, which builds mutated code in the original source directory. When the [`--jobs`](parallelism.md) option is used, one build directory is created per job. From 589639034914643411f9ed38a9b299dae3024019 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Wed, 11 Dec 2024 13:31:51 -0800 Subject: [PATCH 9/9] Mention mutants.out is not copied --- book/src/build-dirs.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/book/src/build-dirs.md b/book/src/build-dirs.md index 7ad63d35..4585da15 100644 --- a/book/src/build-dirs.md +++ b/book/src/build-dirs.md @@ -34,3 +34,7 @@ The filter, based on the [`ignore` crate](https://docs.rs/ignore/), also respect This behavior can be turned off with `--gitignore=false`, causing ignored files to be copied. Rust projects typically configure gitignore to exclude the `target/` directory. + +## `mutants.out` + +`mutants.out` and `mutants.out.old` are never copied, even if they're not covered by `.gitignore`.