Skip to content

Commit

Permalink
Inline tree copying using ignore crate
Browse files Browse the repository at this point in the history
  • Loading branch information
sourcefrog committed Nov 25, 2023
1 parent ff14f55 commit 094a5e5
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 38 deletions.
18 changes: 18 additions & 0 deletions Cargo.lock

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

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ctrlc = { version = "3.2.1", features = ["termination"] }
fastrand = "2"
fs2 = "0.4"
globset = "0.4.8"
ignore = "0.4.20"
indoc = "2.0.0"
itertools = "0.11"
mutants = "0.0.3"
Expand All @@ -61,9 +62,6 @@ tracing-appender = "0.2"
tracing-subscriber = "0.3"
whoami = "1.2"

[dependencies.cp_r]
version = "0.5.1"

[dependencies.nutmeg]
version = "0.1.4"
# git = "https://github.com/sourcefrog/nutmeg.git"
Expand All @@ -83,6 +81,7 @@ features = ["full", "extra-traits", "visit"]

[dev-dependencies]
assert_cmd = "2.0"
cp_r = "0.5.1"
insta = "1.12"
lazy_static = "1.4"
predicates = "3"
Expand Down
131 changes: 97 additions & 34 deletions src/build_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
use std::convert::TryInto;
use std::fmt;
use std::fs::FileType;

use anyhow::Context;
use camino::{Utf8Path, Utf8PathBuf};
use ignore::WalkBuilder;
use tempfile::TempDir;
use tracing::{debug, error, info, trace};
use tracing::{info, warn};

use crate::manifest::fix_cargo_config;
use crate::*;
Expand Down Expand Up @@ -48,12 +50,13 @@ impl BuildDir {
///
/// [SOURCE_EXCLUDE] is excluded.
pub fn new(source: &Utf8Path, options: &Options, console: &Console) -> Result<BuildDir> {
let name_base = format!("cargo-mutants-{}-", source.file_name().unwrap_or(""));
let name_base = format!("cargo-mutants-{}-", source.file_name().unwrap_or("unnamed"));
let source_abs = source
.canonicalize_utf8()
.expect("canonicalize source path");
// TODO: Only exclude `target` in directories containing Cargo.toml?
let temp_dir = copy_tree(source, &name_base, SOURCE_EXCLUDE, console)?;
// TODO: Pass down gitignore
let temp_dir = copy_tree(source, &name_base, true, console)?;
let path: Utf8PathBuf = temp_dir.path().to_owned().try_into().unwrap();
fix_manifest(&path.join("Cargo.toml"), &source_abs)?;
fix_cargo_config(&path, &source_abs)?;
Expand All @@ -78,8 +81,7 @@ impl BuildDir {

/// Make a copy of this build dir, including its target directory.
pub fn copy(&self, console: &Console) -> Result<BuildDir> {
let temp_dir = copy_tree(&self.path, &self.name_base, &[], console)?;

let temp_dir = copy_tree(&self.path, &self.name_base, true, console)?;
Ok(BuildDir {
path: temp_dir.path().to_owned().try_into().unwrap(),
strategy: TempDirStrategy::Collect(temp_dir),
Expand All @@ -96,53 +98,114 @@ impl fmt::Debug for BuildDir {
}
}

/// 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.
fn copy_tree(
from_path: &Utf8Path,
name_base: &str,
exclude: &[&str],
gitignore: bool,
console: &Console,
) -> Result<TempDir> {
console.start_copy();
let mut total_bytes = 0;
let mut total_files = 0;
let temp_dir = tempfile::Builder::new()
.prefix(name_base)
.suffix(".tmp")
.tempdir()
.context("create temp dir")?;
console.start_copy();
let copy_options = cp_r::CopyOptions::new()
.after_entry_copied(|path, _ft, stats| {
console.copy_progress(stats.file_bytes);
check_interrupted().map_err(|_| cp_r::Error::new(cp_r::ErrorKind::Interrupted, path))
for entry in WalkBuilder::new(from_path)
.standard_filters(gitignore)
.hidden(false)
.filter_entry(|entry| {
!SOURCE_EXCLUDE.contains(&entry.file_name().to_string_lossy().as_ref())
})
.filter(|path, _dir_entry| {
let excluded = exclude.iter().any(|ex| path.ends_with(ex));
if excluded {
trace!("Skip {path:?}");
} else {
trace!("Copy {path:?}");
}
Ok(!excluded)
});
match copy_options
.copy_tree(from_path, temp_dir.path())
.context("copy tree")
.build()
{
Ok(stats) => {
debug!(files = stats.files, file_bytes = stats.file_bytes,);
}
Err(err) => {
error!(
"error copying {} to {}: {:?}",
&from_path.to_slash_path(),
&temp_dir.path().to_slash_lossy(),
err
);
return Err(err);
check_interrupted()?;
let entry = entry?;
let relative_path = entry
.path()
.strip_prefix(from_path)
.expect("entry path is in from_path");
let dest_path: Utf8PathBuf = temp_dir
.path()
.join(relative_path)
.try_into()
.context("Convert path to UTF-8")?;
let ft = entry
.file_type()
.with_context(|| format!("Expected file to have a file type: {:?}", entry.path()))?;
if ft.is_file() {
let bytes_copied = std::fs::copy(entry.path(), &dest_path).with_context(|| {
format!(
"Failed to copy {:?} to {dest_path:?}",
entry.path().to_slash_lossy(),
)
})?;
total_bytes += bytes_copied;
total_files += 1;
console.copy_progress(bytes_copied);
} else if ft.is_dir() {
std::fs::create_dir_all(&dest_path)
.with_context(|| format!("Failed to create directory {dest_path:?}"))?;
} else if ft.is_symlink() {
copy_symlink(
ft,
entry
.path()
.try_into()
.context("Convert filename to UTF-8")?,
&dest_path,
)?;
} else {
warn!("Unexpected file type: {:?}", entry.path());
}
}
console.finish_copy();
debug!(?total_bytes, ?total_files, "Copied source tree");
Ok(temp_dir)
}

#[cfg(unix)]
fn copy_symlink(_ft: FileType, src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> {
let link_target = std::fs::read_link(src_path)
.with_context(|| format!("Failed to read link {src_path:?}"))?;
std::os::unix::fs::symlink(link_target, dest_path)
.with_context(|| format!("Failed to create symlink {dest_path:?}",))?;
Ok(())
}

#[cfg(windows)]
#[mutants::skip] // Mutant tests run on Linux
fn copy_symlink(ft: FileType, src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> {
let link_target = std::fs::read_link(src_path)
.with_context(|| format!("read link {:?}", src_path.to_slash_lossy()))?;
if ft.is_symlink_dir() {
std::os::windows::fs::symlink_dir(link_target, dest_path).with_context(|| {
format!(
"create symlink {:?} to {:?}",
dest_path.to_slash_lossy(),
link_target.to_slash_lossy()
)
})?;
} else if ft.is_symlink_file() {
std::os::windows::fs::symlink_file(link_target, dest_path).with_context(|| {
format!(
"create symlink {:?} to {:?}",
dest_path.to_slash_lossy(),
link_target.to_slash_lossy()
)
})?;
} else {
bail!("Unknown symlink type: {:?}", ft);
}
}

#[cfg(test)]
mod test {
use regex::Regex;
Expand Down
1 change: 0 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ use crate::manifest::fix_manifest;
use crate::mutate::{Genre, Mutant};
use crate::options::Options;
use crate::outcome::{Phase, ScenarioOutcome};
use crate::path::Utf8PathSlashes;
use crate::scenario::Scenario;
use crate::workspace::{PackageFilter, Workspace};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ src/build_dir.rs: replace <impl Debug for BuildDir>::fmt -> fmt::Result with Ok(
src/build_dir.rs: replace <impl Debug for BuildDir>::fmt -> fmt::Result with Err(::anyhow::anyhow!("mutated!"))
src/build_dir.rs: replace copy_tree -> Result<TempDir> with Ok(Default::default())
src/build_dir.rs: replace copy_tree -> Result<TempDir> with Err(::anyhow::anyhow!("mutated!"))
src/build_dir.rs: replace copy_symlink -> Result<()> with Ok(())
src/build_dir.rs: replace copy_symlink -> Result<()> with Err(::anyhow::anyhow!("mutated!"))
src/cargo.rs: replace run_cargo -> Result<PhaseResult> with Ok(Default::default())
src/cargo.rs: replace run_cargo -> Result<PhaseResult> with Err(::anyhow::anyhow!("mutated!"))
src/cargo.rs: replace cargo_bin -> String with String::new()
Expand Down

0 comments on commit 094a5e5

Please sign in to comment.