Repository: sourcefrog/cargo-mutants (rust)

Issue:
Cargo mutants command fails when using a local dependency
# Description

The `cargo mutants` command fails when there is a local dependency for a binary project.

This happens in a bigger project of us, but I created a test repo for the sake of easy reproduction.

## Reproduction

Steps:
1. Clone the following [repo](https://github.com/dmoka/test-repo-for-cargo-mutants)
2. Go to the `./main` directory
3. Execute `cargo mutants` command

Expected behavior:

- Some mutations are generated (if possible)

Actual behavior:

- The command fails with the following log:
```
Freshen source tree ... ok in 0.015s
Copy source and build products to scratch directory ... 14 MB in 0.006s
Unmutated baseline ... FAILED in 0.007s

*** baseline
*** run /home/dmoka/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo check --tests
*** cargo result: Exited(101) in 0.007sency of package `main v0.1.0 (/tmp/cargo-mutants--eL1pHT.tmp)`

Caused by:
  failed to load source for dependency `utils`

Caused by:
  Unable to update /tmp/utils

Caused by:
  failed to read `/tmp/utils/Cargo.toml`

Caused by:
  No such file or directory (os error 2)
```

Note: when `cargo run` is executed in the `./main` directory, the binary is executed properly with the local `utils` dependency included. So I assume that it should have something to do with the cargo-mutants crate itself.

System: Ubunt 22.04

If you need any more information, let me know, thank you! 

Pull request description:
Rewrite path dependencies in Cargo.toml
Partly fixes #42

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index b263678..94d5051 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -151,6 +151,7 @@ dependencies = [
  "subprocess",
  "syn",
  "tempfile",
+ "toml",
  "walkdir",
  "whoami",
 ]
@@ -166,6 +167,10 @@ dependencies = [
  "mutants 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "cargo-mutants-testdata-dependency"
+version = "0.0.0"
+
 [[package]]
 name = "cargo-mutants-testdata-factorial"
 version = "0.0.0"
@@ -191,6 +196,13 @@ dependencies = [
  "mutants 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "cargo-mutants-testdata-relative-dependency"
+version = "0.0.0"
+dependencies = [
+ "cargo-mutants-testdata-dependency",
+]
+
 [[package]]
 name = "cargo-mutants-testdata-well-tested"
 version = "0.1.0"
@@ -903,6 +915,15 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "toml"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index 820006c..a13979f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ serde_json = "1"
 similar = "2.0"
 subprocess = "0.2.8"
 tempfile = "3.2"
+toml = "0.5"
 walkdir = "2.3"
 whoami = "1.2"
 
@@ -65,13 +66,21 @@ members = [
     "mutants_attrs",
     "testdata/tree/cfg_attr_mutants_skip",
     "testdata/tree/cfg_attr_test_skip",
+    "testdata/tree/dependency",
     "testdata/tree/factorial",
     "testdata/tree/hang_avoided_by_attr/",
     "testdata/tree/hang_when_mutated",
     "testdata/tree/no_opportunities",
+    "testdata/tree/relative_dependency",
     "testdata/tree/well_tested",
 ]
 
-# TODO: Re-enable these when the tests are checked by the real integration tests, but
-# skipped by `cargo test --workspace`. (It's complicated!)
-exclude = ["testdata/tree/already_hangs", "testdata/tree/already_failing_tests"]
+# `replace_dependency` can't be in the workspace, because 'replace' sections
+# must be in the workspace root.
+exclude = [
+    "testdata/tree/already_hangs",
+    "testdata/tree/already_failing_tests",
+    "testdata/tree/override_dependency",
+    "testdata/tree/patch_dependency",
+    "testdata/tree/replace_dependency",
+]
diff --git a/DESIGN.md b/DESIGN.md
index 8ad79af..e54f643 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -41,6 +41,13 @@ the content based on those addresses.
 `visit.rs` -- Walk a source file's AST. The interface to the `syn` parser is
 localized here.
 
+## Relative dependencies
+
+After copying the tree, cargo-mutants scans the top-level `Cargo.toml` and any
+`.cargo/config.toml` for relative dependencies. If there are any, the paths are
+rewritten to be absolute, so that they still work when cargo is run in the
+scratch directory.
+
 ## Handling timeouts
 
 Mutations can cause a program to go into an infinite (or just very long) loop:
diff --git a/NEWS.md b/NEWS.md
index e6777c4..b2d9ce2 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -4,6 +4,8 @@
 
 - New: Summarize the overall number of mutants generated, caught, missed, etc, at the end.
 
+- Fixed: Works properly with crates that have relative `path` dependencies in `Cargo.toml` or `.cargo/config.toml`, by rewriting them to absolute paths in the scratch directory.
+
 ## 0.2.7
 
 Released 2022-07-11
diff --git a/README.md b/README.md
index 9c43056..44bf621 100644
--- a/README.md
+++ b/README.md
@@ -385,6 +385,11 @@ The basic approach is:
   directory. The same directory is reused across all the mutations to benefit
   from incremental builds.
 
+  - After copying the tree, cargo-mutants scans the top-level `Cargo.toml` and any
+    `.cargo/config.toml` for relative dependencies. If there are any, the paths are
+    rewritten to be absolute, so that they still work when cargo is run in the
+    scratch directory.
+
   - Before applying any mutations, check that `cargo test` succeeds in the
     scratch directory: perhaps a test is already broken, or perhaps the tree
     doesn't build when copied because it relies on relative paths to find
@@ -482,11 +487,6 @@ cargo-mutants is not yet meeting its goals.
   rust-analyzer?), which would help it generate more interesting viable mutants,
   and fewer unviable mutants.
 
-- Copying the tree to build it doesn't work well if the `Cargo.toml` points to
-  dependencies by a relative `path` (other than in subdirectories). You can
-  work around this by editing `Cargo.toml` to make the paths absolute, before
-  running `cargo mutants`. (See <https://github.com/sourcefrog/cargo-mutants/issues/42>.)
-
 - To make this faster on large trees, we could keep several scratch trees and
   test them in parallel, which is likely to exploit CPU resources more
   thoroughly than Cargo's own parallelism: in particular Cargo tends to fall
diff --git a/src/build_dir.rs b/src/build_dir.rs
index 56bb060..3b6b7c8 100644
--- a/src/build_dir.rs
+++ b/src/build_dir.rs
@@ -1,3 +1,5 @@
+// Copyright 2021, 2022 Martin Pool
+
 //! A temporary directory containing mutated source to run cargo builds and tests.
 
 use std::convert::TryInto;
@@ -8,6 +10,7 @@ use camino::{Utf8Path, Utf8PathBuf};
 use tempfile::TempDir;
 
 use crate::console::CopyActivity;
+use crate::manifest::fix_cargo_config;
 use crate::*;
 
 /// Filenames excluded from being copied with the source.
@@ -25,7 +28,10 @@ const SOURCE_EXCLUDE: &[&str] = &[
 /// A temporary directory initialized with a copy of the source, where mutations can be tested.
 #[derive(Debug)]
 pub struct BuildDir {
+    /// The path of the root of the temporary directory.
     path: Utf8PathBuf,
+    /// Holds a reference to the temporary directory, so that it will be deleted when this
+    /// object is dropped.
     _temp_dir: TempDir,
 }
 
@@ -40,7 +46,7 @@ impl BuildDir {
             .suffix(".tmp")
             .tempdir()
             .context("create temp dir")?;
-        let temp_dir_path = temp_dir.path().to_owned().try_into().unwrap();
+        let build_path: Utf8PathBuf = temp_dir.path().to_owned().try_into().unwrap();
         let copy_target = options.copy_target;
         let name = if copy_target {
             "Copy source and build products to scratch directory"
@@ -75,10 +81,17 @@ impl BuildDir {
                 return Err(err);
             }
         }
-        Ok(BuildDir {
+        let source_abs = source
+            .path()
+            .canonicalize_utf8()
+            .expect("canonicalize source path");
+        fix_manifest(&build_path.join("Cargo.toml"), &source_abs)?;
+        fix_cargo_config(&build_path, &source_abs)?;
+        let build_dir = BuildDir {
             _temp_dir: temp_dir,
-            path: temp_dir_path,
-        })
+            path: build_path,
+        };
+        Ok(build_dir)
     }
 
     pub fn path(&self) -> &Utf8Path {
diff --git a/src/main.rs b/src/main.rs
index b1d8c5a..7233079 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,6 +9,7 @@ mod exit_code;
 mod interrupt;
 mod lab;
 mod log_file;
+mod manifest;
 mod mutate;
 mod options;
 mod outcome;
@@ -36,6 +37,7 @@ use crate::cargo::CargoResult;
 use crate::interrupt::check_interrupted;
 use crate::lab::Scenario;
 use crate::log_file::{last_line, LogFile};
+use crate::manifest::fix_manifest;
 use crate::mutate::{Mutant, MutationOp};
 use crate::options::Options;
 use crate::outcome::{Outcome, Phase};
diff --git a/src/manifest.rs b/src/manifest.rs
new file mode 100644
index 0000000..b4e3b10
--- /dev/null
+++ b/src/manifest.rs
@@ -0,0 +1,294 @@
+// Copyright 2022 Martin Pool.
+
+//! Manipulate Cargo manifest and config files.
+//!
+//! In particular, when the tree is copied we have to fix up relative paths, so
+//! that they still work from the new location of the scratch directory.
+
+use std::fs;
+
+use anyhow::Context;
+use camino::Utf8Path;
+
+use crate::Result;
+
+/// Rewrite the scratch copy of a manifest to have absolute paths.
+///
+/// `manifest_source_dir` is the directory originally containing the manifest, from
+/// which the absolute paths are calculated.
+pub fn fix_manifest(manifest_scratch_path: &Utf8Path, source_dir: &Utf8Path) -> Result<()> {
+    let toml_str = fs::read_to_string(manifest_scratch_path).context("read manifest")?;
+    if let Some(changed_toml) = fix_manifest_toml(&toml_str, source_dir)? {
+        fs::write(manifest_scratch_path, changed_toml.as_bytes()).context("write manifest")?;
+    }
+    Ok(())
+}
+
+/// Fix any relative paths within a Cargo.toml manifest.
+///
+/// Returns the new manifest, or None if no changes were made.
+fn fix_manifest_toml(
+    manifest_toml: &str,
+    manifest_source_dir: &Utf8Path,
+) -> Result<Option<String>> {
+    let mut value: toml::Value = manifest_toml.parse().context("parse manifest")?;
+    let orig_value = value.clone();
+    if let Some(top_table) = value.as_table_mut() {
+        if let Some(dependencies) = top_table.get_mut("dependencies") {
+            fix_dependency_table(dependencies, manifest_source_dir);
+        }
+        if let Some(replace) = top_table.get_mut("replace") {
+            // The replace section is a table from package name/version to a
+            // table which might include a `path` key. (The keys are not exactly
+            // package names but it doesn't matter.)
+            // <https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-replace-section>
+            fix_dependency_table(replace, manifest_source_dir);
+        }
+        if let Some(patch_table) = top_table.get_mut("patch").and_then(|p| p.as_table_mut()) {
+            // The keys of the patch table are registry names or source URLs;
+            // the values are like dependency tables.
+            // <https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section>
+            for (_name, dependencies) in patch_table {
+                fix_dependency_table(dependencies, manifest_source_dir);
+            }
+        }
+    }
+    if value == orig_value {
+        Ok(None)
+    } else {
+        Ok(Some(toml::to_string_pretty(&value)?))
+    }
+}
+
+/// Fix up paths in a manifest "dependency table".
+///
+/// This is a pattern that can occur at various places in the manifest. It's a
+/// map from package name to a table which may contain a "path" field.
+///
+/// The table is mutated if necessary.
+///
+/// `dependencies_table` is a TOML Value that should normally be a table;
+/// other values are ignored.
+fn fix_dependency_table(dependencies: &mut toml::Value, manifest_source_dir: &Utf8Path) {
+    if let Some(dependencies_table) = dependencies.as_table_mut() {
+        for (_, value) in dependencies_table.iter_mut() {
+            if let Some(dependency_table) = value.as_table_mut() {
+                if let Some(path_value) = dependency_table.get_mut("path") {
+                    if let Some(path_str) = path_value.as_str() {
+                        if let Some(new_path) = fix_path(path_str, manifest_source_dir) {
+                            *path_value = toml::Value::String(new_path);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+/// Rewrite relative paths within `.cargo/config.toml` to be absolute paths.
+pub fn fix_cargo_config(build_path: &Utf8Path, source_path: &Utf8Path) -> Result<()> {
+    let config_path = build_path.join(".cargo/config.toml");
+    if config_path.exists() {
+        let toml_str = fs::read_to_string(&config_path).context("read .cargo/config.toml")?;
+        if let Some(changed_toml) = fix_cargo_config_toml(&toml_str, source_path)? {
+            fs::write(build_path.join(&config_path), changed_toml.as_bytes())
+                .context("write .cargo/config.toml")?;
+        }
+    }
+    Ok(())
+}
+
+/// Replace any relative paths in a config file with absolute paths.
+///
+/// Returns None if no changes are needed.
+///
+/// See <https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html?search=#paths-overrides>.
+fn fix_cargo_config_toml(config_toml: &str, source_dir: &Utf8Path) -> Result<Option<String>> {
+    let mut value: toml::Value = config_toml.parse().context("parse config.toml")?;
+    let mut changed = false;
+    if let Some(paths) = value.get_mut("paths").and_then(|p| p.as_array_mut()) {
+        for path_value in paths {
+            if let Some(path_str) = path_value.as_str() {
+                if let Some(new_path) = fix_path(path_str, source_dir) {
+                    *path_value = toml::Value::String(new_path);
+                    changed = true;
+                }
+            }
+        }
+    }
+    if changed {
+        Ok(Some(toml::to_string_pretty(&value)?))
+    } else {
+        Ok(None)
+    }
+}
+
+/// Fix one path, from inside a scratch tree, to be absolute as interpreted
+/// relative to the source tree.
+fn fix_path(path_str: &str, source_dir: &Utf8Path) -> Option<String> {
+    let path = Utf8Path::new(path_str);
+    if path.is_absolute() {
+        None
+    } else {
+        let mut new_path = source_dir.to_owned();
+        new_path.push(path);
+        Some(new_path.to_string())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use camino::{Utf8Path, Utf8PathBuf};
+    use pretty_assertions::assert_eq;
+
+    use super::fix_manifest_toml;
+
+    #[test]
+    fn fix_path_absolute_unchanged() {
+        let dependency_abspath = Utf8Path::new("testdata/tree/dependency")
+            .canonicalize_utf8()
+            .unwrap();
+        assert_eq!(
+            super::fix_path(
+                dependency_abspath.as_str(),
+                &Utf8Path::new("/home/user/src/foo")
+            ),
+            None
+        );
+    }
+
+    #[test]
+    fn fix_path_relative() {
+        let fixed_path: Utf8PathBuf = super::fix_path(
+            "../dependency",
+            &Utf8Path::new("testdata/tree/relative_dependency"),
+        )
+        .expect("path was adjusted")
+        .into();
+        assert_eq!(
+            &fixed_path,
+            Utf8Path::new("testdata/tree/relative_dependency/../dependency"),
+        );
+    }
+
+    #[test]
+    fn fix_relative_path_in_manifest() {
+        let manifest_toml = r#"
+# A comment, which will be dropped.
+author = "A Smithee"
+[dependencies]
+wibble = { path = "../wibble" } # Use the relative path to the dependency.
+"#;
+        let orig_path = Utf8Path::new("/home/user/src/foo");
+        let fixed_toml = fix_manifest_toml(&manifest_toml, orig_path)
+            .unwrap()
+            .expect("toml was modified");
+        // Round-tripping toml produces some insignificant stylistic changes.
+        #[cfg(unix)]
+        let expected = "author = 'A Smithee'
+[dependencies.wibble]
+path = '/home/user/src/foo/../wibble'
+";
+        #[cfg(windows)]
+        let expected = "author = 'A Smithee'
+[dependencies.wibble]
+path = '/home/user/src/foo\\../wibble'
+";
+        assert_eq!(fixed_toml, expected);
+    }
+
+    #[test]
+    fn fix_replace_section() {
+        let manifest_toml = r#"
+[dependencies]
+wibble = "1.2.3"
+[replace]
+"wibble:1.2.3" = { path = "../wibble" } # Use the relative path to the dependency.
+"#;
+        let orig_path = Utf8Path::new("/home/user/src/foo");
+        let fixed_toml = fix_manifest_toml(&manifest_toml, orig_path)
+            .unwrap()
+            .expect("toml was modified");
+        // A crude adaption for Windows.
+        let fixed_toml = fixed_toml.replace('\\', "/");
+        // Round-tripping toml produces some insignificant stylistic changes.
+        let expected = r#"[dependencies]
+wibble = '1.2.3'
+[replace."wibble:1.2.3"]
+path = '/home/user/src/foo/../wibble'
+"#;
+        assert_eq!(fixed_toml, expected);
+    }
+
+    #[test]
+    fn absolute_path_in_manifest_is_unchanged() {
+        #[cfg(unix)]
+        let manifest_toml = r#"
+[dependencies]
+wibble = { path = "/home/asmithee/src/wibble" }
+"#;
+        #[cfg(windows)]
+        let manifest_toml = r#"
+[dependencies]
+wibble = { path = "c:/home/asmithee/src/wibble" }
+"#;
+
+        let orig_path = Utf8Path::new("/home/user/src/foo");
+        let fixed_toml = fix_manifest_toml(&manifest_toml, orig_path).unwrap();
+        assert_eq!(
+            fixed_toml, None,
+            "manifest containing only an absolute path should not be modified"
+        );
+    }
+
+    #[test]
+    fn fix_patch_section() {
+        let manifest_toml = r#"
+[dependencies]
+wibble = "1.2.3"
+[patch.crates-io]
+wibble = { path = "../wibble" } # Use the relative path to the dependency.
+"#;
+        let orig_path = Utf8Path::new("/home/user/src/foo");
+        let fixed_toml = fix_manifest_toml(&manifest_toml, orig_path)
+            .unwrap()
+            .expect("toml was modified");
+        // A crude adaption for Windows.
+        let fixed_toml = fixed_toml.replace('\\', "/");
+        // Round-tripping toml produces some insignificant stylistic changes.
+        let expected = r#"[dependencies]
+wibble = '1.2.3'
+[patch.crates-io.wibble]
+path = '/home/user/src/foo/../wibble'
+"#;
+        assert_eq!(fixed_toml, expected);
+    }
+
+    #[test]
+    fn fix_cargo_config_toml() {
+        let cargo_config_toml = r#"
+paths = [
+    "sub_dependency",
+    "../sibling_dependency",
+    "../../parent_dependency",
+    "/Users/jane/src/absolute_dependency",
+    "/src/other",
+    ]"#;
+        let source_dir = Utf8Path::new("/Users/jane/src/foo");
+        let fixed_toml = super::fix_cargo_config_toml(&cargo_config_toml, source_dir)
+            .unwrap()
+            .expect("toml was modified");
+        // a crude adaption for windows.
+        let fixed_toml = fixed_toml.replace('\\', "/");
+        // Round-tripping toml produces some insignificant stylistic changes.
+        let expected = r#"paths = [
+    '/Users/jane/src/foo/sub_dependency',
+    '/Users/jane/src/foo/../sibling_dependency',
+    '/Users/jane/src/foo/../../parent_dependency',
+    '/Users/jane/src/absolute_dependency',
+    '/src/other',
+]
+"#;
+        assert_eq!(fixed_toml, expected);
+    }
+}


Test patch:
diff --git a/testdata/tree/dependency/.gitignore b/testdata/tree/dependency/.gitignore
new file mode 100644
index 0000000..22ca548
--- /dev/null
+++ b/testdata/tree/dependency/.gitignore
@@ -0,0 +1,2 @@
+target/
+mutants.out*
diff --git a/testdata/tree/dependency/Cargo.toml b/testdata/tree/dependency/Cargo.toml
new file mode 100644
index 0000000..0ebce1c
--- /dev/null
+++ b/testdata/tree/dependency/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+authors = ["Martin Pool"]
+description = "A crate that is depended upon"
+edition = "2018"
+name = "cargo-mutants-testdata-dependency"
+publish = false
+version = "0.0.0"
diff --git a/testdata/tree/dependency/src/lib.rs b/testdata/tree/dependency/src/lib.rs
new file mode 100644
index 0000000..a71f8e0
--- /dev/null
+++ b/testdata/tree/dependency/src/lib.rs
@@ -0,0 +1,13 @@
+pub fn factorial(n: u32) -> u32 {
+    let mut a = 1;
+    for i in 2..=n {
+        a *= i;
+    }
+    a
+}
+
+#[test]
+fn test_factorial() {
+    println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture
+    assert_eq!(factorial(6), 720);
+}
diff --git a/testdata/tree/override_dependency/.cargo/config.toml b/testdata/tree/override_dependency/.cargo/config.toml
new file mode 100644
index 0000000..e9521b2
--- /dev/null
+++ b/testdata/tree/override_dependency/.cargo/config.toml
@@ -0,0 +1,1 @@
+paths = ["../../../mutants_attrs"]
diff --git a/testdata/tree/override_dependency/.gitignore b/testdata/tree/override_dependency/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/testdata/tree/override_dependency/.gitignore
@@ -0,0 +1,1 @@
+target/
diff --git a/testdata/tree/override_dependency/Cargo.lock b/testdata/tree/override_dependency/Cargo.lock
new file mode 100644
index 0000000..6ec2d15
--- /dev/null
+++ b/testdata/tree/override_dependency/Cargo.lock
@@ -0,0 +1,16 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "cargo-mutants-testdata-override-dependency"
+version = "0.0.0"
+dependencies = [
+ "mutants",
+]
+
+[[package]]
+name = "mutants"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126"
diff --git a/testdata/tree/override_dependency/Cargo.toml b/testdata/tree/override_dependency/Cargo.toml
new file mode 100644
index 0000000..4503d32
--- /dev/null
+++ b/testdata/tree/override_dependency/Cargo.toml
@@ -0,0 +1,18 @@
+# Tests that paths in .cargo/config.toml are fixed up to be relative to the
+# source tree.
+#
+# This crate depends on the 'mutants' crate, but pulls it in from the
+# tree of the cargo-mutants repo, rather than crates.io. If the path
+# below wasn't fixed when copied to a scratch tree, the build would
+# fail.
+
+[package]
+name = "cargo-mutants-testdata-override-dependency"
+version = "0.0.0"
+edition = "2018"
+authors = ["Martin Pool"]
+publish = false
+
+# This has to be a dependency that's published to crates.io.
+[dependencies]
+mutants = { version = "0" }
diff --git a/testdata/tree/override_dependency/src/lib.rs b/testdata/tree/override_dependency/src/lib.rs
new file mode 100644
index 0000000..8c1c16a
--- /dev/null
+++ b/testdata/tree/override_dependency/src/lib.rs
@@ -0,0 +1,8 @@
+#[mutants::skip]
+pub fn hang() -> ! {
+    loop {}
+}
+
+pub fn is_even(n: i32) -> bool {
+    n % 2 == 0
+}
diff --git a/testdata/tree/override_dependency/tests/api.rs b/testdata/tree/override_dependency/tests/api.rs
new file mode 100644
index 0000000..2a91094
--- /dev/null
+++ b/testdata/tree/override_dependency/tests/api.rs
@@ -0,0 +1,11 @@
+use cargo_mutants_testdata_override_dependency::*;
+
+#[test]
+fn zero_is_even() {
+    assert_eq!(is_even(0), true);
+}
+
+#[test]
+fn three_is_not_even() {
+    assert_eq!(is_even(3), false);
+}
diff --git a/testdata/tree/patch_dependency/.gitignore b/testdata/tree/patch_dependency/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/testdata/tree/patch_dependency/.gitignore
@@ -0,0 +1,1 @@
+target/
diff --git a/testdata/tree/patch_dependency/Cargo.lock b/testdata/tree/patch_dependency/Cargo.lock
new file mode 100644
index 0000000..cedc613
--- /dev/null
+++ b/testdata/tree/patch_dependency/Cargo.lock
@@ -0,0 +1,14 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "cargo-mutants-testdata-patch-dependency"
+version = "0.0.0"
+dependencies = [
+ "mutants",
+]
+
+[[package]]
+name = "mutants"
+version = "0.0.3"
diff --git a/testdata/tree/patch_dependency/Cargo.toml b/testdata/tree/patch_dependency/Cargo.toml
new file mode 100644
index 0000000..ac8fe47
--- /dev/null
+++ b/testdata/tree/patch_dependency/Cargo.toml
@@ -0,0 +1,20 @@
+# Tests that paths in the 'patch' section are rewritten.
+#
+# This crate depends on the 'mutants' crate, but pulls it in from the
+# tree of the cargo-mutants repo, rather than crates.io. If the path
+# below wasn't fixed when copied to a scratch tree, the build would
+# fail.
+
+[package]
+name = "cargo-mutants-testdata-patch-dependency"
+version = "0.0.0"
+edition = "2018"
+authors = ["Martin Pool"]
+publish = false
+
+# This has to be a dependency that's published to crates.io.
+[dependencies]
+mutants = { version = "0" }
+
+[patch.crates-io]
+"mutants" = { path = "../../../mutants_attrs" }
diff --git a/testdata/tree/patch_dependency/src/lib.rs b/testdata/tree/patch_dependency/src/lib.rs
new file mode 100644
index 0000000..8c1c16a
--- /dev/null
+++ b/testdata/tree/patch_dependency/src/lib.rs
@@ -0,0 +1,8 @@
+#[mutants::skip]
+pub fn hang() -> ! {
+    loop {}
+}
+
+pub fn is_even(n: i32) -> bool {
+    n % 2 == 0
+}
diff --git a/testdata/tree/patch_dependency/tests/api.rs b/testdata/tree/patch_dependency/tests/api.rs
new file mode 100644
index 0000000..62ccc00
--- /dev/null
+++ b/testdata/tree/patch_dependency/tests/api.rs
@@ -0,0 +1,11 @@
+use cargo_mutants_testdata_patch_dependency::*;
+
+#[test]
+fn zero_is_even() {
+    assert_eq!(is_even(0), true);
+}
+
+#[test]
+fn three_is_not_even() {
+    assert_eq!(is_even(3), false);
+}
diff --git a/testdata/tree/relative_dependency/.gitignore b/testdata/tree/relative_dependency/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/testdata/tree/relative_dependency/.gitignore
@@ -0,0 +1,1 @@
+target/
diff --git a/testdata/tree/relative_dependency/Cargo.lock b/testdata/tree/relative_dependency/Cargo.lock
new file mode 100644
index 0000000..d36d5b1
--- /dev/null
+++ b/testdata/tree/relative_dependency/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "cargo-mutants-testdata-factorial"
+version = "0.0.0"
diff --git a/testdata/tree/relative_dependency/Cargo.toml b/testdata/tree/relative_dependency/Cargo.toml
new file mode 100644
index 0000000..c036c00
--- /dev/null
+++ b/testdata/tree/relative_dependency/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "cargo-mutants-testdata-relative-dependency"
+version = "0.0.0"
+edition = "2018"
+authors = ["Martin Pool"]
+publish = false
+
+[dependencies]
+cargo-mutants-testdata-dependency = { version = "0", path = "../dependency" }
diff --git a/testdata/tree/relative_dependency/src/lib.rs b/testdata/tree/relative_dependency/src/lib.rs
new file mode 100644
index 0000000..28ccfce
--- /dev/null
+++ b/testdata/tree/relative_dependency/src/lib.rs
@@ -0,0 +1,10 @@
+use std::convert::TryInto;
+
+use cargo_mutants_testdata_dependency::factorial;
+
+pub fn double_factorial(n: i32) -> u32 {
+    if n < 0 {
+        return 0;
+    }
+    2 * factorial(n.try_into().unwrap())
+}
diff --git a/testdata/tree/relative_dependency/tests/api.rs b/testdata/tree/relative_dependency/tests/api.rs
new file mode 100644
index 0000000..06309a3
--- /dev/null
+++ b/testdata/tree/relative_dependency/tests/api.rs
@@ -0,0 +1,16 @@
+use cargo_mutants_testdata_relative_dependency::double_factorial;
+
+#[test]
+fn double_factorial_zero_is_2() {
+    assert_eq!(double_factorial(0), 2);
+}
+
+#[test]
+fn double_factorial_one_is_2() {
+    assert_eq!(double_factorial(1), 2);
+}
+
+#[test]
+fn double_factorial_two_is_4() {
+    assert_eq!(double_factorial(2), 4);
+}
diff --git a/testdata/tree/replace_dependency/.gitignore b/testdata/tree/replace_dependency/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/testdata/tree/replace_dependency/.gitignore
@@ -0,0 +1,1 @@
+target/
diff --git a/testdata/tree/replace_dependency/Cargo.lock b/testdata/tree/replace_dependency/Cargo.lock
new file mode 100644
index 0000000..a2d7d81
--- /dev/null
+++ b/testdata/tree/replace_dependency/Cargo.lock
@@ -0,0 +1,21 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "cargo-mutants-testdata-replace-dependency"
+version = "0.0.0"
+dependencies = [
+ "mutants 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "mutants"
+version = "0.0.3"
+
+[[package]]
+name = "mutants"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126"
+replace = "mutants 0.0.3"
diff --git a/testdata/tree/replace_dependency/Cargo.toml b/testdata/tree/replace_dependency/Cargo.toml
new file mode 100644
index 0000000..a5e6d18
--- /dev/null
+++ b/testdata/tree/replace_dependency/Cargo.toml
@@ -0,0 +1,20 @@
+# Tests that paths in the 'replace' section are rewritten.
+#
+# This crate depends on the 'mutants' crate, but pulls it in from the
+# tree of the cargo-mutants repo, rather than crates.io. If the path
+# below wasn't fixed when copied to a scratch tree, the build would
+# fail.
+
+[package]
+name = "cargo-mutants-testdata-replace-dependency"
+version = "0.0.0"
+edition = "2018"
+authors = ["Martin Pool"]
+publish = false
+
+# This has to be a dependency that's published to crates.io.
+[dependencies]
+mutants = { version = "0" }
+
+[replace]
+"mutants:0.0.3" = { path = "../../../mutants_attrs" }
diff --git a/testdata/tree/replace_dependency/src/lib.rs b/testdata/tree/replace_dependency/src/lib.rs
new file mode 100644
index 0000000..8c1c16a
--- /dev/null
+++ b/testdata/tree/replace_dependency/src/lib.rs
@@ -0,0 +1,8 @@
+#[mutants::skip]
+pub fn hang() -> ! {
+    loop {}
+}
+
+pub fn is_even(n: i32) -> bool {
+    n % 2 == 0
+}
diff --git a/testdata/tree/replace_dependency/tests/api.rs b/testdata/tree/replace_dependency/tests/api.rs
new file mode 100644
index 0000000..663e4b7
--- /dev/null
+++ b/testdata/tree/replace_dependency/tests/api.rs
@@ -0,0 +1,11 @@
+use cargo_mutants_testdata_replace_dependency::*;
+
+#[test]
+fn zero_is_even() {
+    assert_eq!(is_even(0), true);
+}
+
+#[test]
+fn three_is_not_even() {
+    assert_eq!(is_even(3), false);
+}
diff --git a/tests/cli.rs b/tests/cli.rs
index f19c0bb..11f076e 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -715,3 +715,87 @@ fn log_file_names_are_short_and_dont_collide() {
         "log file names are not disambiguated"
     );
 }
+
+#[test]
+fn cargo_mutants_in_override_dependency_tree_passes() {
+    // Run against the testdata directory directly, without copying it, so that the
+    // relative dependency `../dependency` is still used. We also pass `--no-copy-target`
+    // so that we don't do a freshening build first, so nothing should write into the
+    // source directory.
+    run_assert_cmd()
+        .arg("mutants")
+        .arg("--no-times")
+        .arg("--no-copy-target")
+        .arg("--no-shuffle")
+        .arg("-d")
+        .arg("testdata/tree/override_dependency")
+        .assert()
+        .success()
+        .stdout(predicate::function(|stdout| {
+            insta::assert_snapshot!(stdout);
+            true
+        }));
+}
+
+#[test]
+fn cargo_mutants_in_relative_dependency_tree_passes() {
+    // Run against the testdata directory directly, without copying it, so that the
+    // relative dependency `../dependency` is still used. We also pass `--no-copy-target`
+    // so that we don't do a freshening build first, so nothing should write into the
+    // source directory.
+    run_assert_cmd()
+        .arg("mutants")
+        .arg("--no-times")
+        .arg("--no-copy-target")
+        .arg("--no-shuffle")
+        .arg("-d")
+        .arg("testdata/tree/relative_dependency")
+        .assert()
+        .success()
+        .stdout(predicate::function(|stdout| {
+            insta::assert_snapshot!(stdout);
+            true
+        }));
+}
+
+#[test]
+fn cargo_mutants_in_replace_dependency_tree_passes() {
+    // Run against the testdata directory directly, without copying it, so that the
+    // relative dependency `../dependency` is still used. We also pass `--no-copy-target`
+    // so that we don't do a freshening build first, so nothing should write into the
+    // source directory.
+    run_assert_cmd()
+        .arg("mutants")
+        .arg("--no-times")
+        .arg("--no-copy-target")
+        .arg("--no-shuffle")
+        .arg("-d")
+        .arg("testdata/tree/replace_dependency")
+        .assert()
+        .success()
+        .stdout(predicate::function(|stdout| {
+            insta::assert_snapshot!(stdout);
+            true
+        }));
+}
+
+#[test]
+fn cargo_mutants_in_patch_dependency_tree_passes() {
+    // Run against the testdata directory directly, without copying it, so that the
+    // relative dependency `../dependency` is still used. We also pass `--no-copy-target`
+    // so that we don't do a freshening build first, so nothing should write into the
+    // source directory.
+    run_assert_cmd()
+        .arg("mutants")
+        .arg("--no-times")
+        .arg("--no-copy-target")
+        .arg("--no-shuffle")
+        .arg("-d")
+        .arg("testdata/tree/patch_dependency")
+        .assert()
+        .success()
+        .stdout(predicate::function(|stdout| {
+            insta::assert_snapshot!(stdout);
+            true
+        }));
+}
diff --git a/tests/snapshots/cli__cargo_mutants_in_override_dependency_tree_passes.snap b/tests/snapshots/cli__cargo_mutants_in_override_dependency_tree_passes.snap
new file mode 100644
index 0000000..683425f
--- /dev/null
+++ b/tests/snapshots/cli__cargo_mutants_in_override_dependency_tree_passes.snap
@@ -0,0 +1,9 @@
+---
+source: tests/cli.rs
+expression: stdout
+---
+Copy source to scratch directory ... done
+Unmutated baseline ... ok
+Found 2 mutants to test
+2 mutants tested: 2 caught
+
diff --git a/tests/snapshots/cli__cargo_mutants_in_patch_dependency_tree_passes.snap b/tests/snapshots/cli__cargo_mutants_in_patch_dependency_tree_passes.snap
new file mode 100644
index 0000000..683425f
--- /dev/null
+++ b/tests/snapshots/cli__cargo_mutants_in_patch_dependency_tree_passes.snap
@@ -0,0 +1,9 @@
+---
+source: tests/cli.rs
+expression: stdout
+---
+Copy source to scratch directory ... done
+Unmutated baseline ... ok
+Found 2 mutants to test
+2 mutants tested: 2 caught
+
diff --git a/tests/snapshots/cli__cargo_mutants_in_relative_dependency_tree_passes.snap b/tests/snapshots/cli__cargo_mutants_in_relative_dependency_tree_passes.snap
new file mode 100644
index 0000000..162c795
--- /dev/null
+++ b/tests/snapshots/cli__cargo_mutants_in_relative_dependency_tree_passes.snap
@@ -0,0 +1,9 @@
+---
+source: tests/cli.rs
+expression: stdout
+---
+Copy source to scratch directory ... done
+Unmutated baseline ... ok
+Found 1 mutant to test
+1 mutants tested: 1 caught
+
diff --git a/tests/snapshots/cli__cargo_mutants_in_replace_dependency_tree_passes.snap b/tests/snapshots/cli__cargo_mutants_in_replace_dependency_tree_passes.snap
new file mode 100644
index 0000000..683425f
--- /dev/null
+++ b/tests/snapshots/cli__cargo_mutants_in_replace_dependency_tree_passes.snap
@@ -0,0 +1,9 @@
+---
+source: tests/cli.rs
+expression: stdout
+---
+Copy source to scratch directory ... done
+Unmutated baseline ... ok
+Found 2 mutants to test
+2 mutants tested: 2 caught
+
diff --git a/tests/snapshots/cli__list_mutants_in_all_trees__json__dependency.snap b/tests/snapshots/cli__list_mutants_in_all_trees__json__dependency.snap
new file mode 100644
index 0000000..9d33fb1
--- /dev/null
+++ b/tests/snapshots/cli__list_mutants_in_all_trees__json__dependency.snap
@@ -0,0 +1,14 @@
+---
+source: tests/cli.rs
+assertion_line: 41
+expression: "String::from_utf8_lossy(&output.stdout)"
+---
+[
+  {
+    "file": "src/lib.rs",
+    "line": 1,
+    "function": "factorial",
+    "return_type": "-> u32",
+    "replacement": "Default::default()"
+  }
+]
diff --git a/tests/snapshots/cli__list_mutants_in_all_trees__json__override_dependency.snap b/tests/snapshots/cli__list_mutants_in_all_trees__json__override_dependency.snap
new file mode 100644
index 0000000..f2c50b0
--- /dev/null
+++ b/tests/snapshots/cli__list_mutants_in_all_trees__json__override_dependency.snap
@@ -0,0 +1,20 @@
+---
+source: tests/cli.rs
+expression: "String::from_utf8_lossy(&output.stdout)"
+---
+[
+  {
+    "file": "src/lib.rs",
+    "line": 6,
+    "function": "is_even",
+    "return_type": "-> bool",
+    "replacement": "true"
+  },
+  {
+    "file": "src/lib.rs",
+    "line": 6,
+    "function": "is_even",
+    "return_type": "-> bool",
+    "replacement": "false"
+  }
+]
diff --git a/tests/snapshots/cli__list_mutants_in_all_trees__json__patch_dependency.snap b/tests/snapshots/cli__list_mutants_in_all_trees__json__patch_dependency.snap
new file mode 100644
index 0000000..f2c50b0
--- /dev/null
+++ b/tests/snapshots/cli__list_mutants_in_all_trees__json__patch_dependency.snap
@@ -0,0 +1,20 @@
+---
+source: tests/cli.rs
+expression: "String::from_utf8_lossy(&output.stdout)"
+---
+[
+  {
+    "file": "src/lib.rs",
+    "line": 6,
+    "function": "is_even",
+    "return_type": "-> bool",
+    "replacement": "true"
+  },
+  {
+    "file": "src/lib.rs",
+    "line": 6,
+    "function": "is_even",
+    "return_type": "-> bool",
+    "replacement": "false"
+  }
+]
diff --git a/tests/snapshots/cli__list_mutants_in_all_trees__json__relative_dependency.snap b/tests/snapshots/cli__list_mutants_in_all_trees__json__relative_dependency.snap
new file mode 100644
index 0000000..01635ce
--- /dev/null
+++ b/tests/snapshots/cli__list_mutants_in_all_trees__json__relative_dependency.snap
@@ -0,0 +1,14 @@
+---
+source: tests/cli.rs
+assertion_line: 41
+expression: "String::from_utf8_lossy(&output.stdout)"
+---
+[
+  {
+    "file": "src/lib.rs",
+    "line": 5,
+    "function": "double_factorial",
+    "return_type": "-> u32",
+    "replacement": "Default::default()"
+  }
+]
diff --git a/tests/snapshots/cli__list_mutants_in_all_trees__json__replace_dependency.snap b/tests/snapshots/cli__list_mutants_in_all_trees__json__replace_dependency.snap
new file mode 100644
index 0000000..f2c50b0
--- /dev/null
+++ b/tests/snapshots/cli__list_mutants_in_all_trees__json__replace_dependency.snap
@@ -0,0 +1,20 @@
+---
+source: tests/cli.rs
+expression: "String::from_utf8_lossy(&output.stdout)"
+---
+[
+  {
+    "file": "src/lib.rs",
+    "line": 6,
+    "function": "is_even",
+    "return_type": "-> bool",
+    "replacement": "true"
+  },
+  {
+    "file": "src/lib.rs",
+    "line": 6,
+    "function": "is_even",
+    "return_type": "-> bool",
+    "replacement": "false"
+  }
+]
diff --git a/tests/snapshots/cli__list_mutants_in_all_trees__text__dependency.snap b/tests/snapshots/cli__list_mutants_in_all_trees__text__dependency.snap
new file mode 100644
index 0000000..1a2fb4e
--- /dev/null
+++ b/tests/snapshots/cli__list_mutants_in_all_trees__text__dependency.snap
@@ -0,0 +1,7 @@
+---
+source: tests/cli.rs
+assertion_line: 41
+expression: "String::from_utf8_lossy(&output.stdout)"
+---
+src/lib.rs:1: replace factorial -> u32 with Default::default()
+
diff --git a/tests/snapshots/cli__list_mutants_in_all_trees__text__override_dependency.snap b/tests/snapshots/cli__list_mutants_in_all_trees__text__override_dependency.snap
new file mode 100644
index 0000000..deccf10
--- /dev/null
+++ b/tests/snapshots/cli__list_mutants_in_all_trees__text__override_dependency.snap
@@ -0,0 +1,7 @@
+---
+source: tests/cli.rs
+expression: "String::from_utf8_lossy(&output.stdout)"
+---
+src/lib.rs:6: replace is_even -> bool with true
+src/lib.rs:6: replace is_even -> bool with false
+
diff --git a/tests/snapshots/cli__list_mutants_in_all_trees__text__relative_dependency.snap b/tests/snapshots/cli__list_mutants_in_all_trees__text__relative_dependency.snap
new file mode 100644
index 0000000..82c112f
--- /dev/null
+++ b/tests/snapshots/cli__list_mutants_in_all_trees__text__relative_dependency.snap
@@ -0,0 +1,7 @@
+---
+source: tests/cli.rs
+assertion_line: 41
+expression: "String::from_utf8_lossy(&output.stdout)"
+---
+src/lib.rs:5: replace double_factorial -> u32 with Default::default()
+
diff --git a/tests/snapshots/cli__list_mutants_in_all_trees__text__replace_dependency.snap b/tests/snapshots/cli__list_mutants_in_all_trees__text__replace_dependency.snap
new file mode 100644
index 0000000..deccf10
--- /dev/null
+++ b/tests/snapshots/cli__list_mutants_in_all_trees__text__replace_dependency.snap
@@ -0,0 +1,7 @@
+---
+source: tests/cli.rs
+expression: "String::from_utf8_lossy(&output.stdout)"
+---
+src/lib.rs:6: replace is_even -> bool with true
+src/lib.rs:6: replace is_even -> bool with false
+