Repository: mitsuhiko/insta
Pull request #537 (2024-07-28 22:29:54)

New integration test approach
~Very draft — but shows a reasonably scalable way to add integration tests. There are some TODOs at the top.~

~This would help with issues such as #531~

Replaced the existing integration tests with a new approach, which:
- Copies a set of files defined inline into a temp dir, executes tests, and then shows the diff
- Allows adding integration tests much more easily — including whole repo tests — so problems we've had with `cargo-insta` are very tractable to solve
- Adds the repro of @regexident as a test for #531 (which currently will fail; requires merging this first and then we'll un-skip the test)
- ...while it might be overkill for the existing tests, it already allows us to remove some complicated code which was handling tests-running-tests. And it lets us do much more.

~This is stacked on #540, that should merge first~ merged


Patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ae4c161..7c0cbba 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,7 +1,11 @@
 {
   "editor.formatOnSave": true,
   "files.insertFinalNewline": true,
-  "files.trimTrailingWhitespace": true,
+  // We have some snapshots that have trailing whitespace, so we don't want to
+  // trim whitespace by default, so we set this to `false`. `rustfmt` does this for rust files (outside
+  // snapshots). (We could add something to pre-commit for other files if we
+  // wanted...)
+  "files.trimTrailingWhitespace": false,
   "rust-analyzer.cargo.features": "all",
   "rust-analyzer.check.command": "clippy"
 }
diff --git a/Cargo.lock b/Cargo.lock
index 38f813a..77cd471 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -40,7 +40,18 @@ checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
 dependencies = [
  "lazy_static",
  "memchr",
- "regex-automata",
+ "regex-automata 0.1.10",
+ "serde",
+]
+
+[[package]]
+name = "bstr"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
+dependencies = [
+ "memchr",
+ "regex-automata 0.4.7",
  "serde",
 ]
 
@@ -65,6 +76,7 @@ dependencies = [
  "proc-macro2",
  "serde",
  "serde_json",
+ "similar",
  "syn",
  "tempfile",
  "uuid",
@@ -196,7 +208,7 @@ version = "1.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
 dependencies = [
- "bstr",
+ "bstr 0.2.17",
  "csv-core",
  "itoa 0.4.8",
  "ryu",
@@ -292,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
 dependencies = [
  "aho-corasick",
- "bstr",
+ "bstr 0.2.17",
  "fnv",
  "log",
  "regex",
@@ -428,9 +440,9 @@ dependencies = [
 
 [[package]]
 name = "memchr"
-version = "2.5.0"
+version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "once_cell"
@@ -527,6 +539,12 @@ version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
 
+[[package]]
+name = "regex-automata"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+
 [[package]]
 name = "regex-syntax"
 version = "0.6.27"
@@ -635,11 +653,11 @@ dependencies = [
 
 [[package]]
 name = "similar"
-version = "2.2.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803"
+checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
 dependencies = [
- "bstr",
+ "bstr 1.10.0",
  "unicode-segmentation",
 ]
 
diff --git a/Makefile b/Makefile
index 26590d1..64e647a 100644
--- a/Makefile
+++ b/Makefile
@@ -15,10 +15,7 @@ cargotest:
 	@cargo test -p insta --all-features
 	@cargo test -p insta --no-default-features
 	@cargo test -p insta --features redactions -- --test-threads 1
-	@echo "CARGO-INSTA TESTS"
-	# Turn off CI flag so that cargo insta test behaves as we expect
-	# under normal operation
-	@CI=0 cargo test -p cargo-insta
+	@cargo test -p cargo-insta
 
 check-minver:
 	@echo "MINVER CHECK"
diff --git a/cargo-insta/Cargo.toml b/cargo-insta/Cargo.toml
index 19e30b5..1992d84 100644
--- a/cargo-insta/Cargo.toml
+++ b/cargo-insta/Cargo.toml
@@ -32,3 +32,4 @@ clap = {version = "=4.1", features = ["derive", "env"]}
 
 [dev-dependencies]
 walkdir = "2.3.1"
+similar= "2.2.1"
diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs
index 44ac9ad..5401af2 100644
--- a/cargo-insta/src/cli.rs
+++ b/cargo-insta/src/cli.rs
@@ -87,8 +87,8 @@ struct TargetArgs {
     /// Explicit path to the workspace root
     #[arg(long, value_name = "PATH")]
     workspace_root: Option<PathBuf>,
-    /// Sets the extensions to consider. Defaults to `.snap`
-    #[arg(short = 'e', long, value_name = "EXTENSIONS", num_args = 1.., value_delimiter = ',')]
+    /// Sets the extensions to consider. Defaults to `snap`.
+    #[arg(short = 'e', long, value_name = "EXTENSIONS", num_args = 1.., value_delimiter = ',', default_value = "snap")]
     extensions: Vec<String>,
     /// Work on all packages in the workspace
     #[arg(long)]
@@ -117,10 +117,8 @@ struct ProcessCommand {
 }
 
 #[derive(Args, Debug)]
-#[command(rename_all = "kebab-case")]
-struct TestCommand {
-    #[command(flatten)]
-    target_args: TargetArgs,
+#[command(rename_all = "kebab-case", next_help_heading = "Test Runner Options")]
+struct TestRunnerOptions {
     /// Test only this package's library unit tests
     #[arg(long)]
     lib: bool,
@@ -148,12 +146,6 @@ struct TestCommand {
     /// Exclude packages from the test
     #[arg(long, value_name = "SPEC")]
     exclude: Vec<String>,
-    /// Disable force-passing of snapshot tests
-    #[arg(long)]
-    no_force_pass: bool,
-    /// Prevent running all tests regardless of failure
-    #[arg(long)]
-    fail_fast: bool,
     /// Space-separated list of features to activate
     #[arg(long, value_name = "FEATURES")]
     features: Option<String>,
@@ -178,42 +170,57 @@ struct TestCommand {
     /// Build for the target triple
     #[arg(long)]
     target: Option<String>,
+}
+
+#[derive(Args, Debug)]
+#[command(rename_all = "kebab-case")]
+struct TestCommand {
+    /// Accept all snapshots after test.
+    #[arg(long, conflicts_with_all = ["review", "check"])]
+    accept: bool,
+    /// Instructs the test command to just assert.
+    #[arg(long, conflicts_with_all = ["review"])]
+    check: bool,
     /// Follow up with review.
     #[arg(long)]
     review: bool,
-    /// Accept all snapshots after test.
-    #[arg(long, conflicts_with = "review")]
-    accept: bool,
     /// Accept all new (previously unseen).
     #[arg(long)]
     accept_unseen: bool,
-    /// Instructs the test command to just assert.
-    #[arg(long)]
-    check: bool,
     /// Do not reject pending snapshots before run.
     #[arg(long)]
     keep_pending: bool,
     /// Update all snapshots even if they are still matching.
     #[arg(long)]
     force_update_snapshots: bool,
-    /// Require metadata as well as snapshots' contents to match (experimental).
-    #[arg(long)]
-    require_full_match: bool,
     /// Handle unreferenced snapshots after a successful test run.
     #[arg(long, default_value = "ignore")]
     unreferenced: UnreferencedSnapshots,
-    /// Delete unreferenced snapshots after a successful test run.
-    #[arg(long, hide = true)]
-    delete_unreferenced_snapshots: bool,
     /// Filters to apply to the insta glob feature.
     #[arg(long)]
     glob_filter: Vec<String>,
+    /// Require metadata as well as snapshots' contents to match.
+    #[arg(long)]
+    require_full_match: bool,
+    /// Prevent running all tests regardless of failure
+    #[arg(long)]
+    fail_fast: bool,
     /// Do not pass the quiet flag (`-q`) to tests.
     #[arg(short = 'Q', long)]
     no_quiet: bool,
     /// Picks the test runner.
     #[arg(long, default_value = "auto")]
     test_runner: TestRunner,
+    /// Delete unreferenced snapshots after a successful test run.
+    #[arg(long, hide = true)]
+    delete_unreferenced_snapshots: bool,
+    /// Disable force-passing of snapshot tests
+    #[arg(long)]
+    no_force_pass: bool,
+    #[command(flatten)]
+    target_args: TargetArgs,
+    #[command(flatten)]
+    test_runner_options: TestRunnerOptions,
     /// Options passed to cargo test
     #[arg(last = true)]
     cargo_options: Vec<String>,
@@ -362,10 +369,7 @@ fn get_find_flags(tool_config: &ToolConfig, target_args: &TargetArgs) -> FindFla
 }
 
 fn handle_target_args(target_args: &TargetArgs) -> Result<LocationInfo<'_>, Box<dyn Error>> {
-    let mut exts: Vec<&str> = target_args.extensions.iter().map(|x| x.as_str()).collect();
-    if exts.is_empty() {
-        exts.push("snap");
-    }
+    let exts: Vec<&str> = target_args.extensions.iter().map(|x| x.as_str()).collect();
 
     // if a workspace root is provided we first check if it points to a `Cargo.toml`.  If it
     // does we instead treat it as manifest path.  If both are provided we fail with an error
@@ -641,7 +645,12 @@ fn test_run(mut cmd: TestCommand, color: ColorWhen) -> Result<(), Box<dyn Error>
     // tests ran successfully
     if success {
         if let Some(ref path) = snapshot_ref_file {
-            handle_unreferenced_snapshots(path.borrow(), &loc, cmd.unreferenced, &cmd.package[..])?;
+            handle_unreferenced_snapshots(
+                path.borrow(),
+                &loc,
+                cmd.unreferenced,
+                &cmd.test_runner_options.package[..],
+            )?;
         }
     }
 
@@ -823,42 +832,42 @@ fn prepare_test_runner<'snapshot_ref>(
     if cmd.target_args.all || cmd.target_args.workspace {
         proc.arg("--all");
     }
-    if cmd.lib {
+    if cmd.test_runner_options.lib {
         proc.arg("--lib");
         prevents_doc_run = true;
     }
-    if let Some(ref bin) = cmd.bin {
+    if let Some(ref bin) = cmd.test_runner_options.bin {
         proc.arg("--bin");
         proc.arg(bin);
         prevents_doc_run = true;
     }
-    if cmd.bins {
+    if cmd.test_runner_options.bins {
         proc.arg("--bins");
         prevents_doc_run = true;
     }
-    if let Some(ref example) = cmd.example {
+    if let Some(ref example) = cmd.test_runner_options.example {
         proc.arg("--example");
         proc.arg(example);
         prevents_doc_run = true;
     }
-    if cmd.examples {
+    if cmd.test_runner_options.examples {
         proc.arg("--examples");
         prevents_doc_run = true;
     }
-    for test in &cmd.test {
+    for test in &cmd.test_runner_options.test {
         proc.arg("--test");
         proc.arg(test);
         prevents_doc_run = true;
     }
-    if cmd.tests {
+    if cmd.test_runner_options.tests {
         proc.arg("--tests");
         prevents_doc_run = true;
     }
-    for pkg in &cmd.package {
+    for pkg in &cmd.test_runner_options.package {
         proc.arg("--package");
         proc.arg(pkg);
     }
-    for spec in &cmd.exclude {
+    for spec in &cmd.test_runner_options.exclude {
         proc.arg("--exclude");
         proc.arg(spec);
     }
@@ -903,32 +912,32 @@ fn prepare_test_runner<'snapshot_ref>(
     if !glob_filter.is_empty() {
         proc.env("INSTA_GLOB_FILTER", glob_filter);
     }
-    if cmd.release {
+    if cmd.test_runner_options.release {
         proc.arg("--release");
     }
-    if let Some(ref profile) = cmd.profile {
+    if let Some(ref profile) = cmd.test_runner_options.profile {
         proc.arg("--profile");
         proc.arg(profile);
     }
-    if cmd.all_targets {
+    if cmd.test_runner_options.all_targets {
         proc.arg("--all-targets");
     }
-    if let Some(n) = cmd.jobs {
+    if let Some(n) = cmd.test_runner_options.jobs {
         // use -j instead of --jobs since both nextest and cargo test use it
         proc.arg("-j");
         proc.arg(n.to_string());
     }
-    if let Some(ref features) = cmd.features {
+    if let Some(ref features) = cmd.test_runner_options.features {
         proc.arg("--features");
         proc.arg(features);
     }
-    if cmd.all_features {
+    if cmd.test_runner_options.all_features {
         proc.arg("--all-features");
     }
-    if cmd.no_default_features {
+    if cmd.test_runner_options.no_default_features {
         proc.arg("--no-default-features");
     }
-    if let Some(ref target) = cmd.target {
+    if let Some(ref target) = cmd.test_runner_options.target {
         proc.arg("--target");
         proc.arg(target);
     }


Test patch:
diff --git a/cargo-insta/tests/main.rs b/cargo-insta/tests/main.rs
index 8ba8a1f..fbdc10d 100644
--- a/cargo-insta/tests/main.rs
+++ b/cargo-insta/tests/main.rs
@@ -1,87 +1,468 @@
+use std::collections::HashMap;
 use std::env;
 use std::fs;
-use std::path::Path;
-use std::process::{Command, Output};
+use std::path::{Path, PathBuf};
+use std::process::Command;
 
-use insta::{assert_snapshot, Settings};
-use walkdir::WalkDir;
+use ignore::WalkBuilder;
+use insta::assert_snapshot;
+use similar::udiff::unified_diff;
+use tempfile::TempDir;
 
-struct OnDrop<F: FnOnce()>(Option<F>);
+struct TestProject {
+    files: HashMap<PathBuf, String>,
+    /// Temporary directory where the project is created
+    temp_dir: TempDir,
+    /// Path of this repo, so we can have it as a dependency in the test project
+    project_path: Option<PathBuf>,
+    /// File tree at start of test
+    file_tree: Option<String>,
+}
 
-impl<F: FnOnce()> Drop for OnDrop<F> {
-    fn drop(&mut self) {
-        let Self(f) = self;
-        f.take().unwrap()();
-    }
+fn workspace_path() -> PathBuf {
+    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+        .parent()
+        .unwrap()
+        .to_path_buf()
 }
 
-#[test]
-fn main() {
-    const NO_RECURSION: &str = "CARGO_INSTA_INTEGRATION_TESTS_NO_RECURSION";
+fn target_dir() -> PathBuf {
+    let target_dir = env::var("CARGO_TARGET_DIR")
+        .map(PathBuf::from)
+        .unwrap_or_else(|_| workspace_path().join("target"))
+        .join("test-projects");
+    fs::create_dir_all(&target_dir).unwrap();
+    target_dir
+}
+
+fn assert_success(output: &std::process::Output) {
+    assert!(
+        output.status.success(),
+        "Tests failed: {}\n{}",
+        String::from_utf8_lossy(&output.stdout),
+        String::from_utf8_lossy(&output.stderr)
+    );
+}
+
+impl TestProject {
+    fn new() -> Self {
+        Self {
+            files: HashMap::new(),
+            temp_dir: TempDir::new().unwrap(),
+            project_path: None,
+            file_tree: None,
+        }
+    }
 
-    if env::var_os(NO_RECURSION).is_some() {
-        return;
+    fn add_file<P: AsRef<Path>>(mut self, path: P, content: String) -> Self {
+        self.files.insert(path.as_ref().to_path_buf(), content);
+        self
     }
 
-    // copy new tests over
-
-    // late-bind files as they're copied to ensure cargo doesn't try to run
-    // these tests on the next invocation
-    let copied = std::cell::RefCell::new(Vec::new());
-    let _on_drop = OnDrop(Some(|| {
-        let copied = copied.borrow();
-        let copied = copied
-            .iter()
-            .filter_map(|copied| fs::remove_file(copied).err().map(|err| (copied, err)))
-            .collect::<Vec<_>>();
-        assert!(copied.is_empty(), "{:?}", copied);
-    }));
-
-    const SRC: &str = "tests/test-input";
-    const DST: &str = "tests";
-    for entry in WalkDir::new(SRC) {
-        let entry = entry.unwrap();
-        let source = entry.path();
-        if source.is_dir() {
-            continue;
+    fn create(mut self) -> Self {
+        let project_path = self.temp_dir.path();
+        let insta_path = workspace_path().join("insta");
+
+        // Create files and replace $PROJECT_PATH in all files
+        for (path, content) in &self.files {
+            let full_path = project_path.join(path);
+            if let Some(parent) = full_path.parent() {
+                fs::create_dir_all(parent).unwrap();
+            }
+            let replaced_content = content.replace("$PROJECT_PATH", insta_path.to_str().unwrap());
+            fs::write(full_path, replaced_content).unwrap();
         }
-        let relative_source = source.strip_prefix(SRC).unwrap();
-        let destination = Path::new(DST).join(relative_source);
-        fs::copy(source, &destination).unwrap();
-        copied.borrow_mut().push(destination);
+
+        self.project_path = Some(project_path.to_path_buf());
+        self
     }
 
-    // run tests and accept snapshots
-    let Output {
-        status,
-        stdout,
-        stderr,
-    } = Command::new(env!("CARGO_BIN_EXE_cargo-insta"))
-        .env(NO_RECURSION, "this value doesn't matter")
-        .arg("test")
-        .arg("--accept")
-        .arg("--no-ignore")
+    fn cmd(&mut self) -> Command {
+        self.file_tree = Some(self.current_file_tree());
+        let project_path = self
+            .project_path
+            .as_ref()
+            .expect("Project has not been created yet. Call create() first.");
+        let mut command = Command::new(env!("CARGO_BIN_EXE_cargo-insta"));
+        command.current_dir(project_path);
+        // Use the same target directory as other tests, consistent across test
+        // run. This makes the compilation much faster (though do some tests
+        // tread on the toes of others? We could have a different cache for each
+        // project if so...)
+        command.env("CARGO_TARGET_DIR", target_dir());
+        // Turn off CI flag so that cargo insta test behaves as we expect
+        // under normal operation
+        command.env("CI", "0");
+        command
+    }
+
+    fn diff(&self, file_path: &str) -> String {
+        let original_content = self.files.get(Path::new(file_path)).unwrap();
+        let file_path_buf = self.project_path.as_ref().unwrap().join(file_path);
+        let updated_content = fs::read_to_string(&file_path_buf).unwrap();
+
+        unified_diff(
+            similar::Algorithm::Patience,
+            original_content,
+            &updated_content,
+            3,
+            Some((
+                &format!("Original: {}", file_path),
+                &format!("Updated: {}", file_path),
+            )),
+        )
+    }
+
+    fn current_file_tree(&self) -> String {
+        WalkBuilder::new(&self.temp_dir)
+            .filter_entry(|e| e.path().file_name() != Some(std::ffi::OsStr::new("target")))
+            .build()
+            .filter_map(|e| e.ok())
+            .map(|entry| {
+                let path = entry
+                    .path()
+                    .strip_prefix(&self.temp_dir)
+                    .unwrap_or(entry.path());
+                format!("{}{}", "  ".repeat(entry.depth()), path.display())
+            })
+            .collect::<Vec<_>>()
+            .join("\n")
+    }
+
+    fn file_tree_diff(&self) -> String {
+        unified_diff(
+            similar::Algorithm::Patience,
+            &self.file_tree.clone().unwrap(),
+            self.current_file_tree().as_ref(),
+            3,
+            Some(("Original file tree", "Updated file tree")),
+        )
+    }
+}
+
+#[test]
+fn test_json_inline() {
+    let mut test_project = TestProject::new()
+        .add_file(
+            "Cargo.toml",
+            r#"
+[package]
+name = "test_json_inline"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+insta = { path = '$PROJECT_PATH', features=["json", "redactions"] }
+serde = { version = "1.0", features = ["derive"] }
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/main.rs",
+            r#"
+use serde::Serialize;
+
+#[derive(Serialize)]
+struct User {
+    id: u64,
+    email: String,
+}
+
+#[test]
+fn test_json_snapshot() {
+    let user = User {
+        id: 42,
+        email: "john.doe@example.com".into(),
+    };
+    insta::assert_json_snapshot!(&user, {
+        ".id" => "[user_id]",
+    }, @"");
+}
+"#
+            .to_string(),
+        )
+        .create();
+
+    let output = test_project
+        .cmd()
+        .args(["test", "--accept"])
         .output()
         .unwrap();
-    use std::io::Write as _;
-    std::io::stdout().write_all(&stdout).unwrap();
-    std::io::stderr().write_all(&stderr).unwrap();
-    assert!(status.success());
-    let stdout = std::str::from_utf8(stdout.as_slice()).unwrap();
-    assert!(stdout.contains("insta review finished"));
-    assert!(stdout.contains("accepted"));
-
-    // use insta itself to assert snapshots
-    for entry in WalkDir::new(SRC) {
-        let entry = entry.unwrap();
-        let filename = entry.path().strip_prefix(SRC).unwrap().to_str().unwrap();
-        if let Some(snapshot) = filename.strip_suffix(".rs") {
-            let gen_file = Path::new(DST).join(filename);
-            let mut settings = Settings::clone_current();
-            settings.set_input_file(&gen_file);
-            settings.bind(|| {
-                assert_snapshot!(snapshot, &fs::read_to_string(gen_file).unwrap());
-            });
-        }
-    }
+
+    assert_success(&output);
+
+    assert_snapshot!(test_project.diff("src/main.rs"), @r##"
+    --- Original: src/main.rs
+    +++ Updated: src/main.rs
+    @@ -15,5 +15,10 @@
+         };
+         insta::assert_json_snapshot!(&user, {
+             ".id" => "[user_id]",
+    -    }, @"");
+    +    }, @r#"
+    +    {
+    +      "id": "[user_id]",
+    +      "email": "john.doe@example.com"
+    +    }
+    +    "#);
+     }
+    "##);
+}
+
+#[test]
+fn test_yaml_inline() {
+    let mut test_project = TestProject::new()
+        .add_file(
+            "Cargo.toml",
+            r#"
+[package]
+name = "test_yaml_inline"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+insta = { path = '$PROJECT_PATH', features=["yaml", "redactions"] }
+serde = { version = "1.0", features = ["derive"] }
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/main.rs",
+            r#"
+use serde::Serialize;
+
+#[derive(Serialize)]
+struct User {
+    id: u64,
+    email: String,
+}
+
+#[test]
+fn test_yaml_snapshot() {
+    let user = User {
+        id: 42,
+        email: "john.doe@example.com".into(),
+    };
+    insta::assert_yaml_snapshot!(&user, {
+        ".id" => "[user_id]",
+    }, @"");
+}
+"#
+            .to_string(),
+        )
+        .create();
+
+    let output = test_project
+        .cmd()
+        .args(["test", "--accept"])
+        .output()
+        .unwrap();
+
+    assert_success(&output);
+
+    assert_snapshot!(test_project.diff("src/main.rs"), @r##"
+    --- Original: src/main.rs
+    +++ Updated: src/main.rs
+    @@ -15,5 +15,9 @@
+         };
+         insta::assert_yaml_snapshot!(&user, {
+             ".id" => "[user_id]",
+    -    }, @"");
+    +    }, @r#"
+    +    ---
+    +    id: "[user_id]"
+    +    email: john.doe@example.com
+    +    "#);
+     }
+    "##);
+}
+
+#[test]
+fn test_utf8_inline() {
+    let mut test_project = TestProject::new()
+        .add_file(
+            "Cargo.toml",
+            r#"
+[package]
+name = "test_utf8_inline"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+insta = { path = '$PROJECT_PATH' }
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/main.rs",
+            r#"
+#[test]
+fn test_non_basic_plane() {
+    /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @"");
+}
+
+#[test]
+fn test_remove_existing_value() {
+    insta::assert_snapshot!("this is the new value", @"this is the old value");
+}
+
+#[test]
+fn test_remove_existing_value_multiline() {
+    insta::assert_snapshot!(
+        "this is the new value",
+        @"this is\
+        this is the old value\
+        it really is"
+    );
+}
+
+#[test]
+fn test_trailing_comma_in_inline_snapshot() {
+    insta::assert_snapshot!(
+        "new value",
+        @"old value",  // comma here
+    );
+}
+"#
+            .to_string(),
+        )
+        .create();
+
+    let output = test_project
+        .cmd()
+        .args(["test", "--accept"])
+        .output()
+        .unwrap();
+
+    assert_success(&output);
+
+    assert_snapshot!(test_project.diff("src/main.rs"), @r##"
+    --- Original: src/main.rs
+    +++ Updated: src/main.rs
+    @@ -1,21 +1,19 @@
+     
+     #[test]
+     fn test_non_basic_plane() {
+    -    /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @"");
+    +    /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @"a 😀oeu");
+     }
+     
+     #[test]
+     fn test_remove_existing_value() {
+    -    insta::assert_snapshot!("this is the new value", @"this is the old value");
+    +    insta::assert_snapshot!("this is the new value", @"this is the new value");
+     }
+     
+     #[test]
+     fn test_remove_existing_value_multiline() {
+         insta::assert_snapshot!(
+             "this is the new value",
+    -        @"this is\
+    -        this is the old value\
+    -        it really is"
+    +        @"this is the new value"
+         );
+     }
+     
+    @@ -23,6 +21,6 @@
+     fn test_trailing_comma_in_inline_snapshot() {
+         insta::assert_snapshot!(
+             "new value",
+    -        @"old value",  // comma here
+    +        @"new value",  // comma here
+         );
+     }
+    "##);
+}
+
+// TODO: This panics and will be fixed by #531 (and the snapshot requires
+// updating; the result is not what we want)
+#[ignore]
+#[test]
+fn test_nested_crate() {
+    let mut test_project = TestProject::new()
+        .add_file(
+            "Cargo.toml",
+            r#"
+[workspace]
+members = [
+    "crates/member-crate",
+]
+
+[workspace.dependencies]
+insta = {path = '$PROJECT_PATH'}
+
+
+[package]
+name = "nested"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+insta = { workspace = true }
+
+"#
+            .to_string(),
+        )
+        .add_file(
+            "crates/member-crate/Cargo.toml",
+            r#"
+[package]
+name = "member-crate"
+version = "0.0.0"
+edition = "2021"
+
+[dependencies]
+insta = { workspace = true }
+"#
+            .to_string(),
+        )
+        .add_file(
+            "crates/member-crate/src/lib.rs",
+            r#"
+#[test]
+fn test_member() {
+    insta::assert_debug_snapshot!(vec![1, 2, 3]);
+}
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/main.rs",
+            r#"
+fn main() {
+    println!("Hello, world!");
+}
+
+#[test]
+fn test_root() {
+    insta::assert_debug_snapshot!(vec![1, 2, 3]);
+}
+"#
+            .to_string(),
+        )
+        .create();
+
+    let output = test_project
+        .cmd()
+        .args(["test", "--accept", "--workspace"])
+        .output()
+        .unwrap();
+
+    assert_success(&output);
+
+    assert_snapshot!(test_project.file_tree_diff(), @r#"
+    --- Original file tree
+    +++ Updated file tree
+    @@ -5,5 +5,8 @@
+           crates/member-crate/Cargo.toml
+           crates/member-crate/src
+             crates/member-crate/src/lib.rs
+    +  Cargo.lock
+       src
+    +    src/snapshots
+    +      src/snapshots/nested__root.snap
+         src/main.rs
+    \ No newline at end of file
+    "#     );
 }
diff --git a/cargo-insta/tests/snapshots/main__test_basic_utf8_inline.snap b/cargo-insta/tests/snapshots/main__test_basic_utf8_inline.snap
deleted file mode 100644
index 5f842a8..0000000
--- a/cargo-insta/tests/snapshots/main__test_basic_utf8_inline.snap
+++ /dev/null
@@ -1,30 +0,0 @@
----
-source: cargo-insta/tests/main.rs
-expression: "&fs::read_to_string(gen_file).unwrap()"
----
-#[test]
-#[rustfmt::skip]
-fn test_non_basic_plane() {
-    /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @"a 😀oeu");
-}
-
-#[test]
-fn test_remove_existing_value() {
-    insta::assert_snapshot!("this is the new value", @"this is the new value");
-}
-
-#[test]
-fn test_remove_existing_value_multiline() {
-    insta::assert_snapshot!(
-        "this is the new value",
-        @"this is the new value"
-    );
-}
-
-#[test]
-fn test_trailing_comma_in_inline_snapshot() {
-    insta::assert_snapshot!(
-        "new value",
-        @"new value",  // comma here
-    );
-}
diff --git a/cargo-insta/tests/snapshots/main__test_json_inline.snap b/cargo-insta/tests/snapshots/main__test_json_inline.snap
deleted file mode 100644
index ca186b3..0000000
--- a/cargo-insta/tests/snapshots/main__test_json_inline.snap
+++ /dev/null
@@ -1,59 +0,0 @@
----
-source: cargo-insta/tests/main.rs
-expression: "&fs::read_to_string(gen_file).unwrap()"
----
-use serde::Serialize;
-
-#[derive(Serialize)]
-struct User {
-    id: u64,
-    email: String,
-}
-
-#[test]
-fn test_json_snapshot() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_json_snapshot!(&user, {
-        ".id" => "[user_id]",
-    }, @r#"
-    {
-      "id": "[user_id]",
-      "email": "john.doe@example.com"
-    }
-    "#);
-}
-
-#[test]
-fn test_json_snapshot_trailing_comma() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_compact_json_snapshot!(
-        &user,
-        @r#"{"id": 42, "email": "john.doe@example.com"}"#,
-    );
-}
-
-#[test]
-fn test_json_snapshot_trailing_comma_redaction() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_json_snapshot!(
-        &user,
-        {
-            ".id" => "[user_id]",
-        },
-        @r#"
-    {
-      "id": "[user_id]",
-      "email": "john.doe@example.com"
-    }
-    "#,
-    );
-}
diff --git a/cargo-insta/tests/snapshots/main__test_yaml_inline.snap b/cargo-insta/tests/snapshots/main__test_yaml_inline.snap
deleted file mode 100644
index 3d739b7..0000000
--- a/cargo-insta/tests/snapshots/main__test_yaml_inline.snap
+++ /dev/null
@@ -1,45 +0,0 @@
----
-source: cargo-insta/tests/main.rs
-expression: "&fs::read_to_string(gen_file).unwrap()"
----
-use serde::Serialize;
-
-#[derive(Serialize)]
-struct User {
-    id: u64,
-    email: String,
-}
-
-#[test]
-fn test_yaml_snapshot() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_yaml_snapshot!(&user, {
-        ".id" => "[user_id]",
-    }, @r#"
-    ---
-    id: "[user_id]"
-    email: john.doe@example.com
-    "#);
-}
-
-#[test]
-fn test_yaml_snapshot_trailing_comma() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_yaml_snapshot!(
-        &user,
-        {
-            ".id" => "[user_id]",
-        },
-        @r#"
-    ---
-    id: "[user_id]"
-    email: john.doe@example.com
-    "#,
-    );
-}
diff --git a/cargo-insta/tests/test-input/test_basic_utf8_inline.rs b/cargo-insta/tests/test-input/test_basic_utf8_inline.rs
deleted file mode 100644
index 6ad28d3..0000000
--- a/cargo-insta/tests/test-input/test_basic_utf8_inline.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-#[test]
-#[rustfmt::skip]
-fn test_non_basic_plane() {
-    /* an offset here ❄️ */ insta::assert_snapshot!("a 😀oeu", @"");
-}
-
-#[test]
-fn test_remove_existing_value() {
-    insta::assert_snapshot!("this is the new value", @"this is the old value");
-}
-
-#[test]
-fn test_remove_existing_value_multiline() {
-    insta::assert_snapshot!(
-        "this is the new value",
-        @"this is\
-        this is the old value\
-        it really is"
-    );
-}
-
-#[test]
-fn test_trailing_comma_in_inline_snapshot() {
-    insta::assert_snapshot!(
-        "new value",
-        @"old value",  // comma here
-    );
-}
diff --git a/cargo-insta/tests/test-input/test_json_inline.rs b/cargo-insta/tests/test-input/test_json_inline.rs
deleted file mode 100644
index ba118dd..0000000
--- a/cargo-insta/tests/test-input/test_json_inline.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-use serde::Serialize;
-
-#[derive(Serialize)]
-struct User {
-    id: u64,
-    email: String,
-}
-
-#[test]
-fn test_json_snapshot() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_json_snapshot!(&user, {
-        ".id" => "[user_id]",
-    }, @"");
-}
-
-#[test]
-fn test_json_snapshot_trailing_comma() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_compact_json_snapshot!(
-        &user,
-        @"",
-    );
-}
-
-#[test]
-fn test_json_snapshot_trailing_comma_redaction() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_json_snapshot!(
-        &user,
-        {
-            ".id" => "[user_id]",
-        },
-        @"",
-    );
-}
diff --git a/cargo-insta/tests/test-input/test_yaml_inline.rs b/cargo-insta/tests/test-input/test_yaml_inline.rs
deleted file mode 100644
index 0530c32..0000000
--- a/cargo-insta/tests/test-input/test_yaml_inline.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-use serde::Serialize;
-
-#[derive(Serialize)]
-struct User {
-    id: u64,
-    email: String,
-}
-
-#[test]
-fn test_yaml_snapshot() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_yaml_snapshot!(&user, {
-        ".id" => "[user_id]",
-    }, @"");
-}
-
-#[test]
-fn test_yaml_snapshot_trailing_comma() {
-    let user = User {
-        id: 42,
-        email: "john.doe@example.com".into(),
-    };
-    insta::assert_yaml_snapshot!(
-        &user,
-        {
-            ".id" => "[user_id]",
-        },
-        @"",
-    );
-}