Repository: mitsuhiko/insta (rust)

Issue:
Better ways of dealing with snapshots in loops or helper functions
I have a few thoughts about https://github.com/mitsuhiko/insta/commit/745b45b5b47e5c2b60ff06ab9bf4af2138dc6d5a.

Firstly, the error message saying `Insta does not allow inline snapshot assertions in loops` isn't quite right. The new panic can happen without any loops:

```rust
fn helper(i:i64) {
   assert_snapshot!(i.to_string(), @"1");
}

#[test]
fn a_test() {
    helper(1);
    helper(2);
}
```

Here's [how I encountered this situation](https://github.com/martinvonz/jj/blob/e9952bcf76d1a25eccd414f7980d369299d02a89/tests/test_resolve_command.rs#L234-L251).

Secondly, a friendlier behavior would be to not have an error merely because the snapshot at the same line was encountered twice. Rather, `insta` could error out only if , when this snapshot is repeatedly encountered, `insta` sees a different string compared to it than it saw before. Then, tests that were correct before wouldn't start failing because of upgrading `insta`. 

In other words, the example above would have `insta` abort and report an error. But if we replace `helper` with

```rust
fn helper(i:i64) {
   do_something_complicated(i);
   assert_snapshot!(mission_report(), @"Total success!");
}
```

the test should be valid. Ideally (but not if it's too much trouble to implement), if `mission_report()` is modified to report "Complete success" both times, `insta` could handle that kind of change and allow the user to use the normal `cargo insta review` functionality to fix the test.

It's hard for me to tell how much effort it would take to make this happen. Is this the kind of thing you would accept a PR for? (No promises at this point, though) Do you think it's not worth the effort?

Pull request description:
Add support for allow_duplicates!
This adds a new macro called `allow_duplicates!`. When a block of code is wrapped with it, every snapshot assertion can be visited more than once. In that case all of the snapshot assertions in a loop etc. have to match with each other and if not, an assertion error is raised.

Motivational example:

```rust
#[test]
fn test_basic_duplicates_passes() {
    allow_duplicates! {
        for x in (0..10).step_by(2) {
            let is_even = x % 2 == 0;
            assert_debug_snapshot!(is_even, @"true");
        }
    }
}
```

Fixes #313 

Patch:
diff --git a/cargo-insta/src/cli.rs b/cargo-insta/src/cli.rs
index 8b64b54..9f1e54d 100644
--- a/cargo-insta/src/cli.rs
+++ b/cargo-insta/src/cli.rs
@@ -8,7 +8,7 @@ use std::{io, process};
 use console::{set_colors_enabled, style, Key, Term};
 use insta::Snapshot;
 use insta::_cargo_insta_support::{
-    is_ci, print_snapshot, print_snapshot_diff, SnapshotUpdate, TestRunner, ToolConfig,
+    is_ci, print_snapshot, SnapshotPrinter, SnapshotUpdate, TestRunner, ToolConfig,
     UnreferencedSnapshots,
 };
 use serde::Serialize;
@@ -246,7 +246,10 @@ fn query_snapshot(
             pkg_version,
         );
 
-        print_snapshot_diff(workspace_root, new, old, snapshot_file, line, *show_info);
+        let mut printer = SnapshotPrinter::new(workspace_root, old, new);
+        printer.set_snapshot_file(snapshot_file);
+        printer.set_line(line);
+        printer.set_show_info(*show_info);
 
         println!();
         println!(
diff --git a/src/lib.rs b/src/lib.rs
index 718c1db..5f58156 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -295,7 +295,7 @@ pub mod _cargo_insta_support {
             Error as ToolConfigError, OutputBehavior, SnapshotUpdate, TestRunner, ToolConfig,
             UnreferencedSnapshots,
         },
-        output::{print_snapshot, print_snapshot_diff},
+        output::{print_snapshot, SnapshotPrinter},
         snapshot::PendingInlineSnapshot,
         snapshot::SnapshotContents,
         utils::is_ci,
@@ -311,7 +311,7 @@ pub use crate::redaction::{dynamic_redaction, sorted_redaction};
 pub mod _macro_support {
     pub use crate::content::Content;
     pub use crate::env::get_cargo_workspace;
-    pub use crate::runtime::{assert_snapshot, AutoName, ReferenceValue};
+    pub use crate::runtime::{assert_snapshot, with_allow_duplicates, AutoName, ReferenceValue};
 
     #[cfg(feature = "serde")]
     pub use crate::serialization::{serialize_value, SerializationFormat, SnapshotLocation};
diff --git a/src/macros.rs b/src/macros.rs
index 6d0c759..006a81b 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -554,3 +554,26 @@ macro_rules! glob {
         $crate::_macro_support::glob_exec(env!("CARGO_MANIFEST_DIR"), &base, $glob, $closure);
     }};
 }
+
+/// Utility macro to create a multi-snapshot run where all snapshots match.
+///
+/// Within this block, insta will allow an assertion to be run twice (even inline) without
+/// generating another snapshot.  Instead it will assert that snapshot expressions visited
+/// more than once are matching.
+///
+/// ```rust
+/// insta::allow_duplicates! {
+///     for x in (0..10).step_by(2) {
+///         let is_even = x % 2 == 0;
+///         insta::assert_debug_snapshot!(is_even, @"true");
+///     }
+/// }
+/// ```
+#[macro_export]
+macro_rules! allow_duplicates {
+    ($($x:tt)*) => {
+        $crate::_macro_support::with_allow_duplicates(|| {
+            $($x)*
+        })
+    }
+}
diff --git a/src/output.rs b/src/output.rs
index 328cf95..65ab977 100644
--- a/src/output.rs
+++ b/src/output.rs
@@ -7,6 +7,197 @@ use crate::content::yaml;
 use crate::snapshot::{MetaData, Snapshot};
 use crate::utils::{format_rust_expression, style, term_width};
 
+/// Snapshot printer utility.
+pub struct SnapshotPrinter<'a> {
+    workspace_root: &'a Path,
+    old_snapshot: Option<&'a Snapshot>,
+    new_snapshot: &'a Snapshot,
+    old_snapshot_hint: &'a str,
+    new_snapshot_hint: &'a str,
+    show_info: bool,
+    show_diff: bool,
+    title: Option<&'a str>,
+    line: Option<u32>,
+    snapshot_file: Option<&'a Path>,
+}
+
+impl<'a> SnapshotPrinter<'a> {
+    pub fn new(
+        workspace_root: &'a Path,
+        old_snapshot: Option<&'a Snapshot>,
+        new_snapshot: &'a Snapshot,
+    ) -> SnapshotPrinter<'a> {
+        SnapshotPrinter {
+            workspace_root,
+            old_snapshot,
+            new_snapshot,
+            old_snapshot_hint: "old snapshot",
+            new_snapshot_hint: "new results",
+            show_info: false,
+            show_diff: false,
+            title: None,
+            line: None,
+            snapshot_file: None,
+        }
+    }
+
+    pub fn set_snapshot_hints(&mut self, old: &'a str, new: &'a str) {
+        self.old_snapshot_hint = old;
+        self.new_snapshot_hint = new;
+    }
+
+    pub fn set_show_info(&mut self, yes: bool) {
+        self.show_info = yes;
+    }
+
+    pub fn set_show_diff(&mut self, yes: bool) {
+        self.show_diff = yes;
+    }
+
+    pub fn set_title(&mut self, title: Option<&'a str>) {
+        self.title = title;
+    }
+
+    pub fn set_line(&mut self, line: Option<u32>) {
+        self.line = line;
+    }
+
+    pub fn set_snapshot_file(&mut self, file: Option<&'a Path>) {
+        self.snapshot_file = file;
+    }
+
+    pub fn print(&self) {
+        if let Some(title) = self.title {
+            let width = term_width();
+            println!(
+                "{title:━^width$}",
+                title = style(format!(" {} ", title)).bold(),
+                width = width
+            );
+        }
+        self.print_snapshot_diff();
+    }
+
+    fn print_snapshot_diff(&self) {
+        self.print_snapshot_summary();
+        self.print_changeset();
+    }
+
+    fn print_snapshot_summary(&self) {
+        print_snapshot_summary(
+            self.workspace_root,
+            self.new_snapshot,
+            self.snapshot_file,
+            self.line,
+        );
+    }
+
+    fn print_info(&self) {
+        print_info(self.new_snapshot.metadata());
+    }
+
+    fn print_changeset(&self) {
+        let old = self.old_snapshot.as_ref().map_or("", |x| x.contents_str());
+        let new = self.new_snapshot.contents_str();
+        let newlines_matter = newlines_matter(old, new);
+
+        let width = term_width();
+        let diff = TextDiff::configure()
+            .algorithm(Algorithm::Patience)
+            .timeout(Duration::from_millis(500))
+            .diff_lines(old, new);
+        print_line(width);
+
+        if self.show_info {
+            self.print_info();
+        }
+
+        if !old.is_empty() {
+            println!(
+                "{}",
+                style(format_args!("-{}", self.old_snapshot_hint)).red()
+            );
+        }
+        println!(
+            "{}",
+            style(format_args!("+{}", self.new_snapshot_hint)).green()
+        );
+
+        println!("────────────┬{:─^1$}", "", width.saturating_sub(13));
+        let mut has_changes = false;
+        for (idx, group) in diff.grouped_ops(4).iter().enumerate() {
+            if idx > 0 {
+                println!("┈┈┈┈┈┈┈┈┈┈┈┈┼{:┈^1$}", "", width.saturating_sub(13));
+            }
+            for op in group {
+                for change in diff.iter_inline_changes(op) {
+                    match change.tag() {
+                        ChangeTag::Insert => {
+                            has_changes = true;
+                            print!(
+                                "{:>5} {:>5} │{}",
+                                "",
+                                style(change.new_index().unwrap()).cyan().dim().bold(),
+                                style("+").green(),
+                            );
+                            for &(emphasized, change) in change.values() {
+                                let change = render_invisible(change, newlines_matter);
+                                if emphasized {
+                                    print!("{}", style(change).green().underlined());
+                                } else {
+                                    print!("{}", style(change).green());
+                                }
+                            }
+                        }
+                        ChangeTag::Delete => {
+                            has_changes = true;
+                            print!(
+                                "{:>5} {:>5} │{}",
+                                style(change.old_index().unwrap()).cyan().dim(),
+                                "",
+                                style("-").red(),
+                            );
+                            for &(emphasized, change) in change.values() {
+                                let change = render_invisible(change, newlines_matter);
+                                if emphasized {
+                                    print!("{}", style(change).red().underlined());
+                                } else {
+                                    print!("{}", style(change).red());
+                                }
+                            }
+                        }
+                        ChangeTag::Equal => {
+                            print!(
+                                "{:>5} {:>5} │ ",
+                                style(change.old_index().unwrap()).cyan().dim(),
+                                style(change.new_index().unwrap()).cyan().dim().bold(),
+                            );
+                            for &(_, change) in change.values() {
+                                let change = render_invisible(change, newlines_matter);
+                                print!("{}", style(change).dim());
+                            }
+                        }
+                    }
+                    if change.missing_newline() {
+                        println!();
+                    }
+                }
+            }
+        }
+
+        if !has_changes {
+            println!(
+                "{:>5} {:>5} │{}",
+                "",
+                style("-").dim(),
+                style(" snapshots are matching").cyan(),
+            );
+        }
+
+        println!("────────────┴{:─^1$}", "", width.saturating_sub(13),);
+    }
+}
+
 /// Prints the summary of a snapshot
 pub fn print_snapshot_summary(
     workspace_root: &Path,
@@ -54,26 +245,6 @@ pub fn print_snapshot_summary(
     }
 }
 
-/// Prints a diff against an old snapshot.
-pub fn print_snapshot_diff(
-    workspace_root: &Path,
-    new: &Snapshot,
-    old_snapshot: Option<&Snapshot>,
-    snapshot_file: Option<&Path>,
-    mut line: Option<u32>,
-    show_info: bool,
-) {
-    // default to old assertion line from snapshot.
-    if line.is_none() {
-        line = new.metadata().assertion_line();
-    }
-
-    print_snapshot_summary(workspace_root, new, snapshot_file, line);
-    let old_contents = old_snapshot.as_ref().map_or("", |x| x.contents_str());
-    let new_contents = new.contents_str();
-    print_changeset(old_contents, new_contents, new.metadata(), show_info);
-}
-
 /// Prints the snapshot not as diff.
 #[cfg(feature = "_cargo_insta_internal")]
 pub fn print_snapshot(
@@ -93,7 +264,7 @@ pub fn print_snapshot(
 
     let width = term_width();
     if show_info {
-        print_info(new.metadata(), width);
+        print_info(new.metadata());
     }
     println!("Snapshot Contents:");
     println!("──────┬{:─^1$}", "", width.saturating_sub(13));
@@ -103,47 +274,6 @@ pub fn print_snapshot(
     println!("──────┴{:─^1$}", "", width.saturating_sub(13),);
 }
 
-pub fn print_snapshot_diff_with_title(
-    workspace_root: &Path,
-    new_snapshot: &Snapshot,
-    old_snapshot: Option<&Snapshot>,
-    line: u32,
-    snapshot_file: Option<&Path>,
-) {
-    let width = term_width();
-    println!(
-        "{title:━^width$}",
-        title = style(" Snapshot Differences ").bold(),
-        width = width
-    );
-    print_snapshot_diff(
-        workspace_root,
-        new_snapshot,
-        old_snapshot,
-        snapshot_file,
-        Some(line),
-        true,
-    );
-}
-
-pub fn print_snapshot_summary_with_title(
-    workspace_root: &Path,
-    new_snapshot: &Snapshot,
-    old_snapshot: Option<&Snapshot>,
-    line: u32,
-    snapshot_file: Option<&Path>,
-) {
-    let _old_snapshot = old_snapshot;
-    let width = term_width();
-    println!(
-        "{title:━^width$}",
-        title = style(" Snapshot Summary ").bold(),
-        width = width
-    );
-    print_snapshot_summary(workspace_root, new_snapshot, snapshot_file, Some(line));
-    println!("{title:━^width$}", title = "", width = width);
-}
-
 fn print_line(width: usize) {
     println!("{:─^1$}", "", width);
 }
@@ -216,100 +346,8 @@ fn render_invisible(s: &str, newlines_matter: bool) -> Cow<'_, str> {
     }
 }
 
-pub fn print_changeset(old: &str, new: &str, metadata: &MetaData, show_info: bool) {
-    let newlines_matter = newlines_matter(old, new);
-
+fn print_info(metadata: &MetaData) {
     let width = term_width();
-    let diff = TextDiff::configure()
-        .algorithm(Algorithm::Patience)
-        .timeout(Duration::from_millis(500))
-        .diff_lines(old, new);
-    print_line(width);
-
-    if show_info {
-        print_info(metadata, width);
-    }
-
-    if !old.is_empty() {
-        println!("{}", style("-old snapshot").red());
-    }
-    println!("{}", style("+new results").green());
-
-    println!("────────────┬{:─^1$}", "", width.saturating_sub(13));
-    let mut has_changes = false;
-    for (idx, group) in diff.grouped_ops(4).iter().enumerate() {
-        if idx > 0 {
-            println!("┈┈┈┈┈┈┈┈┈┈┈┈┼{:┈^1$}", "", width.saturating_sub(13));
-        }
-        for op in group {
-            for change in diff.iter_inline_changes(op) {
-                match change.tag() {
-                    ChangeTag::Insert => {
-                        has_changes = true;
-                        print!(
-                            "{:>5} {:>5} │{}",
-                            "",
-                            style(change.new_index().unwrap()).cyan().dim().bold(),
-                            style("+").green(),
-                        );
-                        for &(emphasized, change) in change.values() {
-                            let change = render_invisible(change, newlines_matter);
-                            if emphasized {
-                                print!("{}", style(change).green().underlined());
-                            } else {
-                                print!("{}", style(change).green());
-                            }
-                        }
-                    }
-                    ChangeTag::Delete => {
-                        has_changes = true;
-                        print!(
-                            "{:>5} {:>5} │{}",
-                            style(change.old_index().unwrap()).cyan().dim(),
-                            "",
-                            style("-").red(),
-                        );
-                        for &(emphasized, change) in change.values() {
-                            let change = render_invisible(change, newlines_matter);
-                            if emphasized {
-                                print!("{}", style(change).red().underlined());
-                            } else {
-                                print!("{}", style(change).red());
-                            }
-                        }
-                    }
-                    ChangeTag::Equal => {
-                        print!(
-                            "{:>5} {:>5} │ ",
-                            style(change.old_index().unwrap()).cyan().dim(),
-                            style(change.new_index().unwrap()).cyan().dim().bold(),
-                        );
-                        for &(_, change) in change.values() {
-                            let change = render_invisible(change, newlines_matter);
-                            print!("{}", style(change).dim());
-                        }
-                    }
-                }
-                if change.missing_newline() {
-                    println!();
-                }
-            }
-        }
-    }
-
-    if !has_changes {
-        println!(
-            "{:>5} {:>5} │{}",
-            "",
-            style("-").dim(),
-            style(" snapshots are matching").cyan(),
-        );
-    }
-
-    println!("────────────┴{:─^1$}", "", width.saturating_sub(13),);
-}
-
-fn print_info(metadata: &MetaData, width: usize) {
     if let Some(expr) = metadata.expression() {
         println!("Expression: {}", style(format_rust_expression(expr)));
         print_line(width);
diff --git a/src/runtime.rs b/src/runtime.rs
index 15e2cb2..8c32bf3 100644
--- a/src/runtime.rs
+++ b/src/runtime.rs
@@ -1,4 +1,5 @@
 use std::borrow::Cow;
+use std::cell::RefCell;
 use std::collections::{BTreeMap, BTreeSet};
 use std::error::Error;
 use std::fs;
@@ -11,7 +12,7 @@ use crate::env::{
     get_cargo_workspace, get_tool_config, memoize_snapshot_file, snapshot_update_behavior,
     OutputBehavior, SnapshotUpdateBehavior, ToolConfig,
 };
-use crate::output::{print_snapshot_diff_with_title, print_snapshot_summary_with_title};
+use crate::output::SnapshotPrinter;
 use crate::settings::Settings;
 use crate::snapshot::{MetaData, PendingInlineSnapshot, Snapshot, SnapshotContents};
 use crate::utils::{path_to_storage, style};
@@ -25,6 +26,10 @@ lazy_static::lazy_static! {
         Mutex::new(BTreeSet::new());
 }
 
+thread_local! {
+    static RECORDED_DUPLICATES: RefCell<Vec<BTreeMap<String, Snapshot>>> = RefCell::default()
+}
+
 // This macro is basically eprintln but without being captured and
 // hidden by the test runner.
 macro_rules! elog {
@@ -126,6 +131,12 @@ fn detect_snapshot_name(
         }
     }
 
+    // The rest of the code just deals with duplicates, which we in some
+    // cases do not want to guard against.
+    if allow_duplicates() {
+        return Ok(name.to_string());
+    }
+
     // if the snapshot name clashes we need to increment a counter.
     // we really do not care about poisoning here.
     let mut counters = TEST_NAME_COUNTERS.lock().unwrap_or_else(|x| x.into_inner());
@@ -200,6 +211,7 @@ struct SnapshotAssertionContext<'a> {
     module_path: &'a str,
     snapshot_name: Option<Cow<'a, str>>,
     snapshot_file: Option<PathBuf>,
+    duplication_key: Option<String>,
     old_snapshot: Option<Snapshot>,
     pending_snapshots_path: Option<PathBuf>,
     assertion_file: &'a str,
@@ -219,6 +231,7 @@ impl<'a> SnapshotAssertionContext<'a> {
         let tool_config = get_tool_config(manifest_dir);
         let cargo_workspace = get_cargo_workspace(manifest_dir);
         let snapshot_name;
+        let mut duplication_key = None;
         let mut snapshot_file = None;
         let mut old_snapshot = None;
         let mut pending_snapshots_path = None;
@@ -232,6 +245,9 @@ impl<'a> SnapshotAssertionContext<'a> {
                         .unwrap()
                         .into(),
                 };
+                if allow_duplicates() {
+                    duplication_key = Some(format!("named:{}|{}", module_path, name));
+                }
                 let file = get_snapshot_filename(
                     module_path,
                     assertion_file,
@@ -247,7 +263,14 @@ impl<'a> SnapshotAssertionContext<'a> {
                 snapshot_file = Some(file);
             }
             ReferenceValue::Inline(contents) => {
-                prevent_inline_duplicate(function_name, assertion_file, assertion_line);
+                if allow_duplicates() {
+                    duplication_key = Some(format!(
+                        "inline:{}|{}|{}",
+                        function_name, assertion_file, assertion_line
+                    ));
+                } else {
+                    prevent_inline_duplicate(function_name, assertion_file, assertion_line);
+                }
                 snapshot_name = detect_snapshot_name(function_name, module_path, true, is_doctest)
                     .ok()
                     .map(Cow::Owned);
@@ -280,6 +303,7 @@ impl<'a> SnapshotAssertionContext<'a> {
             pending_snapshots_path,
             assertion_file,
             assertion_line,
+            duplication_key,
             is_doctest,
         })
     }
@@ -441,24 +465,22 @@ fn prevent_inline_duplicate(function_name: &str, assertion_file: &str, assertion
 
 /// This prints the information about the snapshot
 fn print_snapshot_info(ctx: &SnapshotAssertionContext, new_snapshot: &Snapshot) {
+    let mut printer = SnapshotPrinter::new(
+        ctx.cargo_workspace.as_path(),
+        ctx.old_snapshot.as_ref(),
+        new_snapshot,
+    );
+    printer.set_line(Some(ctx.assertion_line));
+    printer.set_snapshot_file(ctx.snapshot_file.as_deref());
+    printer.set_title(Some("Snapshot Summary"));
+    printer.set_show_info(true);
     match ctx.tool_config.output_behavior() {
         OutputBehavior::Summary => {
-            print_snapshot_summary_with_title(
-                ctx.cargo_workspace.as_path(),
-                new_snapshot,
-                ctx.old_snapshot.as_ref(),
-                ctx.assertion_line,
-                ctx.snapshot_file.as_deref(),
-            );
+            printer.print();
         }
         OutputBehavior::Diff => {
-            print_snapshot_diff_with_title(
-                ctx.cargo_workspace.as_path(),
-                new_snapshot,
-                ctx.old_snapshot.as_ref(),
-                ctx.assertion_line,
-                ctx.snapshot_file.as_deref(),
-            );
+            printer.set_show_diff(true);
+            printer.print();
         }
         _ => {}
     }
@@ -549,6 +571,56 @@ fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpd
     }
 }
 
+fn record_snapshot_duplicate(
+    results: &mut BTreeMap<String, Snapshot>,
+    snapshot: &Snapshot,
+    ctx: &SnapshotAssertionContext,
+) {
+    let key = ctx.duplication_key.as_deref().unwrap();
+    if let Some(prev_snapshot) = results.get(key) {
+        if prev_snapshot.contents() != snapshot.contents() {
+            println!("Snapshots in allow-duplicates block do not match.");
+            let mut printer =
+                SnapshotPrinter::new(ctx.cargo_workspace.as_path(), Some(prev_snapshot), snapshot);
+            printer.set_line(Some(ctx.assertion_line));
+            printer.set_snapshot_file(ctx.snapshot_file.as_deref());
+            printer.set_title(Some("Differences in Block"));
+            printer.set_snapshot_hints("previous assertion", "current assertion");
+            if ctx.tool_config.output_behavior() == OutputBehavior::Diff {
+                printer.set_show_diff(true);
+            }
+            printer.print();
+            panic!(
+                "snapshot assertion for '{}' failed in line {}. Result \
+                    does not match previous snapshot in allow-duplicates block.",
+                ctx.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
+                ctx.assertion_line
+            );
+        }
+    } else {
+        results.insert(key.to_string(), snapshot.clone());
+    }
+}
+
+/// Do we allow recording of duplicates?
+fn allow_duplicates() -> bool {
+    RECORDED_DUPLICATES.with(|x| !x.borrow().is_empty())
+}
+
+/// Helper function to support perfect duplicate detection.
+pub fn with_allow_duplicates<R, F>(f: F) -> R
+where
+    F: FnOnce() -> R,
+{
+    RECORDED_DUPLICATES.with(|x| x.borrow_mut().push(BTreeMap::new()));
+    let rv = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
+    RECORDED_DUPLICATES.with(|x| x.borrow_mut().pop().unwrap());
+    match rv {
+        Ok(rv) => rv,
+        Err(payload) => std::panic::resume_unwind(payload),
+    }
+}
+
 /// This function is invoked from the macros to run the main assertion logic.
 ///
 /// This will create the assertion context, run the main logic to assert
@@ -588,6 +660,15 @@ pub fn assert_snapshot(
         memoize_snapshot_file(snapshot_file);
     }
 
+    // If we allow assertion with duplicates, we record the duplicate now.  This will
+    // in itself fail the assertion if the previous visit of the same assertion macro
+    // did not yield the same result.
+    RECORDED_DUPLICATES.with(|x| {
+        if let Some(results) = x.borrow_mut().last_mut() {
+            record_snapshot_duplicate(results, &new_snapshot, &ctx);
+        }
+    });
+
     // pass if the snapshots are missing
     if ctx.old_snapshot.as_ref().map(|x| x.contents()) == Some(new_snapshot.contents()) {
         ctx.cleanup_passing()?;


Test patch:
diff --git a/tests/test_allow_duplicates.rs b/tests/test_allow_duplicates.rs
new file mode 100644
index 0000000..3a862de
--- /dev/null
+++ b/tests/test_allow_duplicates.rs
@@ -0,0 +1,22 @@
+use insta::{allow_duplicates, assert_debug_snapshot};
+
+#[test]
+fn test_basic_duplicates_passes() {
+    allow_duplicates! {
+        for x in (0..10).step_by(2) {
+            let is_even = x % 2 == 0;
+            assert_debug_snapshot!(is_even, @"true");
+        }
+    }
+}
+
+#[test]
+#[should_panic = "snapshot assertion for 'basic_duplicates_assertion_failed' failed in line"]
+fn test_basic_duplicates_assertion_failed() {
+    allow_duplicates! {
+        for x in (0..10).step_by(3) {
+            let is_even = x % 2 == 0;
+            assert_debug_snapshot!(is_even, @"true");
+        }
+    }
+}