Repository: mitsuhiko/insta
Pull request #609 (2024-09-22 19:11:59)

Replace complicated name clash test with simple integration test


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index be882cb..70897cf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -486,9 +486,12 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "once_cell"
-version = "1.19.0"
+version = "1.20.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
+dependencies = [
+ "portable-atomic",
+]
 
 [[package]]
 name = "open"
@@ -562,6 +565,12 @@ dependencies = [
  "sha2",
 ]
 
+[[package]]
+name = "portable-atomic"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.86"
diff --git a/cargo-insta/Cargo.toml b/cargo-insta/Cargo.toml
index f0202d9..044eddf 100644
--- a/cargo-insta/Cargo.toml
+++ b/cargo-insta/Cargo.toml
@@ -38,4 +38,4 @@ walkdir = "2.3.1"
 similar= "2.2.1"
 itertools = "0.10.0"
 termcolor = "1.1.2"
-os_pipe = "0.9.0"
\ No newline at end of file
+os_pipe = "0.9.0"
diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs
index d9bbe44..f2e5a34 100644
--- a/cargo-insta/src/cli.rs
+++ b/cargo-insta/src/cli.rs
@@ -6,10 +6,11 @@ use std::{env, fs};
 use std::{io, process};
 
 use console::{set_colors_enabled, style, Key, Term};
-use insta::Snapshot;
 use insta::_cargo_insta_support::{
     is_ci, SnapshotPrinter, SnapshotUpdate, TestRunner, ToolConfig, UnreferencedSnapshots,
 };
+use insta::{internals::SnapshotContents, Snapshot};
+use itertools::Itertools;
 use semver::Version;
 use serde::Serialize;
 use uuid::Uuid;
@@ -468,7 +469,7 @@ fn handle_target_args<'a>(
         .iter()
         .map(|x| {
             if let Some(no_period) = x.strip_prefix(".") {
-                eprintln!("`{}` supplied as an extenstion. This will use `foo.{}` as file names; likely you want `{}` instead.", x, x, no_period)
+                eprintln!("`{}` supplied as an extension. This will use `foo.{}` as file names; likely you want `{}` instead.", x, x, no_period)
             };
             x.as_str()
         })
@@ -516,7 +517,15 @@ fn process_snapshots(
         if !quiet {
             println!("{}: no snapshots to review", style("done").bold());
             if loc.tool_config.review_warn_undiscovered() {
-                show_undiscovered_hint(loc.find_flags, &snapshot_containers, &roots, &loc.exts);
+                show_undiscovered_hint(
+                    loc.find_flags,
+                    &snapshot_containers
+                        .iter()
+                        .map(|x| x.0.clone())
+                        .collect_vec(),
+                    &roots,
+                    &loc.exts,
+                );
             }
         }
         return Ok(());
@@ -732,7 +741,8 @@ fn test_run(mut cmd: TestCommand, color: ColorWhen) -> Result<(), Box<dyn Error>
         )?
     } else {
         let (snapshot_containers, roots) = load_snapshot_containers(&loc)?;
-        let snapshot_count = snapshot_containers.iter().map(|x| x.0.len()).sum::<usize>();
+        let snapshot_containers = snapshot_containers.into_iter().map(|x| x.0).collect_vec();
+        let snapshot_count = snapshot_containers.iter().map(|x| x.len()).sum::<usize>();
         if snapshot_count > 0 {
             eprintln!(
                 "{}: {} snapshot{} to review",
@@ -1130,11 +1140,15 @@ fn pending_snapshots_cmd(cmd: PendingSnapshotsCommand) -> Result<(), Box<dyn Err
         let is_inline = snapshot_container.snapshot_file().is_none();
         for snapshot_ref in snapshot_container.iter_snapshots() {
             if cmd.as_json {
-                let old_snapshot = snapshot_ref
-                    .old
-                    .as_ref()
-                    .map(|x| x.contents_string().unwrap());
-                let new_snapshot = snapshot_ref.new.contents_string().unwrap();
+                let old_snapshot = snapshot_ref.old.as_ref().map(|x| match x.contents() {
+                    SnapshotContents::Text(x) => x.to_string(),
+                    _ => unreachable!(),
+                });
+                let new_snapshot = match snapshot_ref.new.contents() {
+                    SnapshotContents::Text(x) => x.to_string(),
+                    _ => unreachable!(),
+                };
+
                 let info = if is_inline {
                     SnapshotKey::InlineSnapshot {
                         path: &target_file,
@@ -1160,7 +1174,7 @@ fn pending_snapshots_cmd(cmd: PendingSnapshotsCommand) -> Result<(), Box<dyn Err
 
 fn show_undiscovered_hint(
     find_flags: FindFlags,
-    snapshot_containers: &[(SnapshotContainer, &Package)],
+    snapshot_containers: &[SnapshotContainer],
     roots: &HashSet<PathBuf>,
     extensions: &[&str],
 ) {
@@ -1169,42 +1183,45 @@ fn show_undiscovered_hint(
         return;
     }
 
-    let mut found_extra = false;
     let found_snapshots = snapshot_containers
         .iter()
-        .filter_map(|x| x.0.snapshot_file())
+        .filter_map(|x| x.snapshot_file())
+        .map(|x| x.to_path_buf())
         .collect::<HashSet<_>>();
 
-    for root in roots {
-        for snapshot in make_snapshot_walker(
-            root,
-            extensions,
-            FindFlags {
-                include_ignored: true,
-                include_hidden: true,
-            },
-        )
-        .filter_map(|e| e.ok())
-        .filter(|x| {
-            let fname = x.file_name().to_string_lossy();
-            // TODO: use `extensions` here
-            fname.ends_with(".snap.new") || fname.ends_with(".pending-snap")
-        }) {
-            if !found_snapshots.contains(snapshot.path()) {
-                found_extra = true;
-                break;
-            }
-        }
-    }
+    let all_snapshots: HashSet<_> = roots
+        .iter()
+        .flat_map(|root| {
+            make_snapshot_walker(
+                root,
+                extensions,
+                FindFlags {
+                    include_ignored: true,
+                    include_hidden: true,
+                },
+            )
+            .filter_map(|e| e.ok())
+            .filter(|x| {
+                let fname = x.file_name().to_string_lossy();
+                extensions
+                    .iter()
+                    .any(|ext| fname.ends_with(&format!(".{}.new", ext)))
+                    || fname.ends_with(".pending-snap")
+            })
+            .map(|x| x.path().to_path_buf())
+        })
+        .collect();
+
+    let missed_snapshots = all_snapshots.difference(&found_snapshots).collect_vec();
 
     // we did not find any extra snapshots
-    if !found_extra {
+    if missed_snapshots.is_empty() {
         return;
     }
 
     let (args, paths) = match (find_flags.include_ignored, find_flags.include_hidden) {
-        (true, false) => ("--include-ignored", "ignored"),
-        (false, true) => ("--include-hidden", "hidden"),
+        (false, true) => ("--include-ignored", "ignored"),
+        (true, false) => ("--include-hidden", "hidden"),
         (false, false) => (
             "--include-ignored and --include-hidden",
             "ignored or hidden",
@@ -1212,13 +1229,18 @@ fn show_undiscovered_hint(
         (true, true) => unreachable!(),
     };
 
-    println!(
+    eprintln!(
         "{}: {}",
         style("warning").yellow().bold(),
         format_args!(
-            "found undiscovered snapshots in some paths which are not picked up by cargo \
-            insta. Use {} if you have snapshots in {} paths.",
-            args, paths,
+            "found undiscovered pending snapshots in some paths which are not picked up by cargo \
+            insta. Use {} if you have snapshots in {} paths. Files:\n{}",
+            args,
+            paths,
+            missed_snapshots
+                .iter()
+                .map(|x| x.display().to_string())
+                .join("\n")
         )
     );
 }
diff --git a/cargo-insta/src/container.rs b/cargo-insta/src/container.rs
index 1764cfa..c52a492 100644
--- a/cargo-insta/src/container.rs
+++ b/cargo-insta/src/container.rs
@@ -2,9 +2,9 @@ use std::error::Error;
 use std::fs;
 use std::path::{Path, PathBuf};
 
-use insta::Snapshot;
 pub(crate) use insta::TextSnapshotKind;
 use insta::_cargo_insta_support::{ContentError, PendingInlineSnapshot};
+use insta::{internals::SnapshotContents, Snapshot};
 
 use crate::inline::FilePatcher;
 
@@ -15,7 +15,7 @@ pub(crate) enum Operation {
     Skip,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) struct PendingSnapshot {
     #[allow(dead_code)]
     id: usize,
@@ -45,9 +45,9 @@ impl PendingSnapshot {
 /// A snapshot and its immediate context, which loads & saves the snapshot. It
 /// holds either a single file snapshot, or all the inline snapshots from a
 /// single rust file.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) struct SnapshotContainer {
-    // Path of the pending snapshot file (generally a `.new` or `.pending-snap` file)
+    // Path of the pending snapshot file (generally a `.snap.new` or `.pending-snap` file)
     pending_path: PathBuf,
     // Path of the target snapshot file (generally a `.snap` file)
     target_path: PathBuf,
@@ -186,7 +186,10 @@ impl SnapshotContainer {
                     Operation::Accept => {
                         patcher.set_new_content(
                             idx,
-                            snapshot.new.contents().as_string_contents().unwrap(),
+                            match snapshot.new.contents() {
+                                SnapshotContents::Text(c) => c,
+                                _ => unreachable!(),
+                            },
                         );
                         did_accept = true;
                     }
diff --git a/cargo-insta/src/inline.rs b/cargo-insta/src/inline.rs
index a08972d..5fdc809 100644
--- a/cargo-insta/src/inline.rs
+++ b/cargo-insta/src/inline.rs
@@ -10,13 +10,14 @@ use proc_macro2::TokenTree;
 use syn::__private::ToTokens;
 use syn::spanned::Spanned;
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 struct InlineSnapshot {
     start: (usize, usize),
     end: (usize, usize),
     indentation: usize,
 }
 
+#[derive(Clone)]
 pub(crate) struct FilePatcher {
     filename: PathBuf,
     lines: Vec<String>,
diff --git a/insta/src/runtime.rs b/insta/src/runtime.rs
index 5bc0d74..ecbfb9b 100644
--- a/insta/src/runtime.rs
+++ b/insta/src/runtime.rs
@@ -476,7 +476,7 @@ impl<'a> SnapshotAssertionContext<'a> {
     }
 
     /// Removes any old .snap.new.* files that belonged to previous pending snapshots. This should
-    /// only ever remove maxium one file because we do this every time before we create a new
+    /// only ever remove maximum one file because we do this every time before we create a new
     /// pending snapshot.
     pub fn cleanup_previous_pending_binary_snapshots(&self) -> Result<(), Box<dyn Error>> {
         if let Some(ref path) = self.snapshot_file {
diff --git a/insta/src/snapshot.rs b/insta/src/snapshot.rs
index 08037b8..8086faa 100644
--- a/insta/src/snapshot.rs
+++ b/insta/src/snapshot.rs
@@ -528,7 +528,20 @@ impl Snapshot {
     pub fn matches_fully(&self, other: &Self) -> bool {
         match (self.contents(), other.contents()) {
             (SnapshotContents::Text(self_contents), SnapshotContents::Text(other_contents)) => {
-                let contents_match_exact = self_contents == other_contents;
+                // Note that we previously would match the exact values of the
+                // unnormalized text. But that's too strict — it means we can
+                // never match a snapshot that has leading/trailing whitespace.
+                // So instead we check it matches on the latest format.
+                // Generally those should be the same — latest should be doing
+                // the minimum normalization; if they diverge we could update
+                // this to be stricter.
+                //
+                // (I think to do this perfectly, we'd want to match the
+                // _reference_ value unnormalized, but the _generated_ value
+                // normalized. That way, we can get the But at the moment we
+                // don't distinguish between which is which in our data
+                // structures.)
+                let contents_match_exact = self_contents.matches_latest(other_contents);
                 match self_contents.kind {
                     TextSnapshotKind::File => {
                         self.metadata.trim_for_persistence()
@@ -542,20 +555,12 @@ impl Snapshot {
         }
     }
 
-    /// The normalized snapshot contents as a String
-    pub fn contents_string(&self) -> Option<String> {
-        match self.contents() {
-            SnapshotContents::Text(contents) => Some(contents.normalize()),
-            SnapshotContents::Binary(_) => None,
-        }
-    }
-
     fn serialize_snapshot(&self, md: &MetaData) -> String {
         let mut buf = yaml::to_string(&md.as_content());
         buf.push_str("---\n");
 
-        if let Some(ref contents_str) = self.contents_string() {
-            buf.push_str(contents_str);
+        if let SnapshotContents::Text(ref contents) = self.snapshot {
+            buf.push_str(&contents.to_string());
             buf.push('\n');
         }
 
@@ -636,13 +641,6 @@ impl SnapshotContents {
     pub fn is_binary(&self) -> bool {
         matches!(self, SnapshotContents::Binary(_))
     }
-
-    pub fn as_string_contents(&self) -> Option<&TextSnapshotContents> {
-        match self {
-            SnapshotContents::Text(contents) => Some(contents),
-            SnapshotContents::Binary(_) => None,
-        }
-    }
 }
 
 impl TextSnapshotContents {
@@ -822,8 +820,7 @@ fn min_indentation(snapshot: &str) -> usize {
         .unwrap_or(0)
 }
 
-// Removes excess indentation, removes excess whitespace at start & end
-// and changes newlines to \n.
+/// Removes excess indentation, and changes newlines to \n.
 fn normalize_inline_snapshot(snapshot: &str) -> String {
     let indentation = min_indentation(snapshot);
     snapshot


Test patch:
diff --git a/cargo-insta/tests/main.rs b/cargo-insta/tests/main.rs
index 1ea21df..717d9bd 100644
--- a/cargo-insta/tests/main.rs
+++ b/cargo-insta/tests/main.rs
@@ -11,11 +11,15 @@
 ///     .args(["test"])
 ///     .stderr(Stdio::piped())
 ///
-/// assert!(String::from_utf8_lossy(&output.stderr).contains("info: 2 snapshots to review")
+/// assert!(
+///     String::from_utf8_lossy(&output.stderr).contains("info: 2 snapshots to review"),
+///    "{}",
+///     String::from_utf8_lossy(&output.stderr)
+/// );
 /// ```
 ///
 /// Often we want to see output from the test commands we run here; for example
-/// a `dbg!` statement we add while debugging. Cargo by default hides the output
+/// a `dbg` statement we add while debugging. Cargo by default hides the output
 /// of passing tests.
 /// - Like any test, to forward the output of a passing outer test (i.e. one of
 ///   the `#[test]`s in this file) to the terminal, pass `--nocapture` to the
@@ -1020,20 +1024,27 @@ fn test_linebreaks() {
 
     assert!(&output.status.success());
 
-    assert_snapshot!(test_project.diff("src/lib.rs"), @r#####"
-    --- Original: src/lib.rs
-    +++ Updated: src/lib.rs
-    @@ -1,8 +1,5 @@
-     
-     #[test]
-     fn test_linebreaks() {
-    -    insta::assert_snapshot!("foo", @r####"
-    -    foo
-    -    
-    -    "####);
-    +    insta::assert_snapshot!("foo", @"foo");
-     }
-    "#####);
+    // When #563 merges, or #630 is resolved, this will change the snapshot. I
+    // also think it's possible to have it work sooner, but have iterated quite
+    // a few times trying to get this to work, and then finding something else
+    // without test coverage didn't work; so not sure it's a great investment of
+    // time.
+    assert_snapshot!(test_project.diff("src/lib.rs"), @"");
+
+    // assert_snapshot!(test_project.diff("src/lib.rs"), @r#####"
+    // --- Original: src/lib.rs
+    // +++ Updated: src/lib.rs
+    // @@ -1,8 +1,5 @@
+
+    //  #[test]
+    //  fn test_linebreaks() {
+    // -    insta::assert_snapshot!("foo", @r####"
+    // -    foo
+    // -
+    // -    "####);
+    // +    insta::assert_snapshot!("foo", @"foo");
+    //  }
+    // "#####);
 }
 
 #[test]
@@ -1119,48 +1130,83 @@ fn test_wrong_indent_force() {
         )
         .create_project();
 
-    // Confirm the test passes despite the indent
+    // ...and that it passes with `--require-full-match`. Note that ideally this
+    // would fail, but we can't read the desired indent without serde, which is
+    // in `cargo-insta` only. So this tests the current state rather than the
+    // ideal state (and I don't think there's a reasonable way to get the ideal state)
+    // Now confirm that `--require-full-match` passes
     let output = test_project
         .insta_cmd()
-        .args(["test", "--check", "--", "--nocapture"])
+        .args([
+            "test",
+            "--check",
+            "--require-full-match",
+            "--",
+            "--nocapture",
+        ])
         .output()
         .unwrap();
     assert!(&output.status.success());
+}
+
+#[test]
+fn test_matches_fully_linebreaks() {
+    // Until #563 merges, we should be OK with different leading newlines, even
+    // in exact / full match mode.
+    let test_project = TestFiles::new()
+        .add_file(
+            "Cargo.toml",
+            r#"
+[package]
+name = "exact-match-inline"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+doctest = false
+
+[dependencies]
+insta = { path = '$PROJECT_PATH' }
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/lib.rs",
+            r#####"
+#[test]
+fn test_additional_linebreak() {
+    // Additional newline here
+    insta::assert_snapshot!(r#"
+
+    (
+        "name_foo",
+        "insta_tests__tests",
+    )
+    "#, @r#"
+    (
+        "name_foo",
+        "insta_tests__tests",
+    )
+    "#);
+}
+"#####
+                .to_string(),
+        )
+        .create_project();
 
-    // Then run the test with --force-update-snapshots and --accept to confirm
-    // the new snapshot is written
+    // Confirm the test passes despite the indent
     let output = test_project
         .insta_cmd()
         .args([
             "test",
-            "--force-update-snapshots",
-            "--accept",
+            "--check",
+            "--require-full-match",
             "--",
             "--nocapture",
         ])
         .output()
         .unwrap();
     assert!(&output.status.success());
-
-    // https://github.com/mitsuhiko/insta/pull/563 will fix the starting &
-    // ending newlines
-    assert_snapshot!(test_project.diff("src/lib.rs"), @r##"
-    --- Original: src/lib.rs
-    +++ Updated: src/lib.rs
-    @@ -4,8 +4,8 @@
-         insta::assert_snapshot!(r#"
-         foo
-         foo
-    -    "#, @r#"
-    -                foo
-    -                foo
-    -    "#);
-    +    "#, @r"
-    +    foo
-    +    foo
-    +    ");
-     }
-    "##);
 }
 
 #[test]
@@ -1216,6 +1262,63 @@ fn test_hashtag_escape() {
     "####);
 }
 
+#[test]
+fn test_snapshot_name_clash() {
+    let test_project = TestFiles::new()
+        .add_file(
+            "Cargo.toml",
+            r#"
+[package]
+name = "snapshot_name_clash_test"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+doctest = false
+
+[dependencies]
+insta = { path = '$PROJECT_PATH' }
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/lib.rs",
+            r#"
+#[cfg(test)]
+mod tests {
+    use insta::assert_debug_snapshot;
+
+    #[test]
+    fn test_foo_always_missing() {
+        assert_debug_snapshot!(42);
+    }
+
+    #[test]
+    fn foo_always_missing() {
+        assert_debug_snapshot!(42);
+    }
+}
+"#
+            .to_string(),
+        )
+        .create_project();
+
+    let output = test_project
+        .insta_cmd()
+        .args(["test", "--accept", "--", "--nocapture"])
+        .stderr(Stdio::piped())
+        .output()
+        .unwrap();
+
+    // The test should fail due to the name clash
+    assert!(!output.status.success());
+
+    let error_output = String::from_utf8_lossy(&output.stderr);
+
+    // Check for the name clash error message
+    assert!(error_output.contains("Insta snapshot name clash detected between 'foo_always_missing' and 'test_foo_always_missing' in 'snapshot_name_clash_test::tests'. Rename one function."));
+}
+
 /// A pending binary snapshot should have a binary file with the passed extension alongside it.
 #[test]
 fn test_binary_pending() {
@@ -1977,6 +2080,163 @@ Unused snapshot
     ");
 }
 
+#[test]
+fn test_hidden_snapshots() {
+    let test_project = TestFiles::new()
+        .add_file(
+            "Cargo.toml",
+            r#"
+[package]
+name = "test_hidden_snapshots"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+insta = { path = '$PROJECT_PATH' }
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/lib.rs",
+            r#"
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test_snapshot() {
+        insta::assert_snapshot!("Hello, world!");
+    }
+}
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/snapshots/test_hidden_snapshots__tests__snapshot.snap",
+            r#"---
+source: src/lib.rs
+expression: "\"Hello, world!\""
+---
+Hello, world!
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/snapshots/.hidden/hidden_snapshot.snap.new",
+            r#"---
+source: src/lib.rs
+expression: "Hidden snapshot"
+---
+Hidden snapshot
+"#
+            .to_string(),
+        )
+        .create_project();
+
+    // Run test without --include-hidden flag
+    let output = test_project
+        .insta_cmd()
+        .args(["test"])
+        .stderr(std::process::Stdio::piped())
+        .output()
+        .unwrap();
+
+    let stderr = String::from_utf8_lossy(&output.stderr);
+    assert!(
+        stderr.contains("found undiscovered pending snapshots")
+            && stderr.contains("--include-hidden"),
+        "{}",
+        stderr
+    );
+
+    // Run test with --include-hidden flag
+    let output = test_project
+        .insta_cmd()
+        .args(["test", "--include-hidden"])
+        .stderr(std::process::Stdio::piped())
+        .output()
+        .unwrap();
+
+    let stderr = String::from_utf8_lossy(&output.stderr);
+    assert!(
+        !stderr.contains("found undiscovered pending snapshots"),
+        "{}",
+        stderr
+    );
+}
+
+#[test]
+fn test_ignored_snapshots() {
+    let test_project = TestFiles::new()
+        .add_file(
+            "Cargo.toml",
+            r#"
+[package]
+name = "test_ignored_snapshots"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+insta = { path = '$PROJECT_PATH' }
+"#
+            .to_string(),
+        )
+        .add_file(
+            "src/lib.rs",
+            r#"
+#[test]
+fn test_snapshot() {
+    insta::assert_snapshot!("Hello, world!", @"");
+}
+"#
+            .to_string(),
+        )
+        .add_file(
+            ".gitignore",
+            r#"
+src/
+"#
+            .to_string(),
+        )
+        .create_project();
+
+    // We need to init a git repository in the project directory so it will be ignored
+    let mut git_cmd = Command::new("git");
+    git_cmd.current_dir(&test_project.workspace_dir);
+    git_cmd.args(["init"]);
+    git_cmd.output().unwrap();
+
+    // Run test without --include-ignored flag
+    let output = test_project
+        .insta_cmd()
+        // add the `--hidden` to check it's printing the correct warning
+        .args(["test", "--include-hidden"])
+        .stderr(std::process::Stdio::piped())
+        .output()
+        .unwrap();
+
+    let stderr = String::from_utf8_lossy(&output.stderr);
+    assert!(
+        stderr.contains("found undiscovered pending snapshots")
+            && stderr.contains("--include-ignored"),
+        "{}",
+        stderr
+    );
+
+    // Run test with --include-ignored flag
+    let output = test_project
+        .insta_cmd()
+        .args(["test", "--include-ignored"])
+        .stderr(std::process::Stdio::piped())
+        .output()
+        .unwrap();
+
+    let stderr = String::from_utf8_lossy(&output.stderr);
+    assert!(
+        !stderr.contains("found undiscovered pending snapshots"),
+        "{}",
+        stderr
+    );
+}
+
 #[test]
 fn test_binary_unreferenced_delete() {
     let test_project = TestFiles::new()
diff --git a/insta/tests/test_clash_detection.rs b/insta/tests/test_clash_detection.rs
deleted file mode 100644
index f65ce18..0000000
--- a/insta/tests/test_clash_detection.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-use std::env;
-use std::thread;
-
-fn test_foo_always_missing() {
-    insta::assert_debug_snapshot!(42);
-}
-
-fn foo_always_missing() {
-    insta::assert_debug_snapshot!(42);
-}
-
-#[test]
-fn test_clash_detection() {
-    let old_update_value = env::var("INSTA_UPDATE");
-    let old_force_pass_value = env::var("INSTA_FORCE_PASS");
-    env::set_var("INSTA_UPDATE", "no");
-    env::set_var("INSTA_FORCE_PASS", "0");
-
-    let err1 = thread::Builder::new()
-        .spawn(|| {
-            test_foo_always_missing();
-        })
-        .unwrap()
-        .join()
-        .unwrap_err();
-    let err2 = thread::Builder::new()
-        .spawn(|| {
-            foo_always_missing();
-        })
-        .unwrap()
-        .join()
-        .unwrap_err();
-
-    if let Ok(value) = old_update_value {
-        env::set_var("INSTA_UPDATE", value);
-    } else {
-        env::remove_var("INSTA_UPDATE");
-    }
-    if let Ok(value) = old_force_pass_value {
-        env::set_var("INSTA_FORCE_PASS", value);
-    } else {
-        env::remove_var("INSTA_FORCE_PASS");
-    }
-
-    let s1 = err1.downcast_ref::<String>().unwrap();
-    let s2 = err2.downcast_ref::<String>().unwrap();
-    let mut values = [s1.as_str(), s2.as_str()];
-    values.sort();
-    assert_eq!(&values[..], &vec![
-        "Insta snapshot name clash detected between \'foo_always_missing\' and \'test_foo_always_missing\' in \'test_clash_detection\'. Rename one function.",
-        "snapshot assertion for \'foo_always_missing\' failed in line 5",
-    ][..]);
-}