insta/
runtime.rs

1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3use std::error::Error;
4use std::fs;
5use std::io::{ErrorKind, Write};
6use std::path::{Path, PathBuf};
7use std::rc::Rc;
8use std::str;
9use std::sync::{Arc, Mutex};
10use std::{borrow::Cow, env};
11
12use crate::settings::Settings;
13use crate::snapshot::{
14    MetaData, PendingInlineSnapshot, Snapshot, SnapshotContents, SnapshotKind, TextSnapshotContents,
15};
16use crate::utils::{path_to_storage, style};
17use crate::{env::get_tool_config, output::SnapshotPrinter};
18use crate::{
19    env::{
20        memoize_snapshot_file, snapshot_update_behavior, OutputBehavior, SnapshotUpdateBehavior,
21        ToolConfig,
22    },
23    snapshot::TextSnapshotKind,
24};
25
26use once_cell::sync::Lazy;
27
28static TEST_NAME_COUNTERS: Lazy<Mutex<BTreeMap<String, usize>>> =
29    Lazy::new(|| Mutex::new(BTreeMap::new()));
30static TEST_NAME_CLASH_DETECTION: Lazy<Mutex<BTreeMap<String, bool>>> =
31    Lazy::new(|| Mutex::new(BTreeMap::new()));
32static INLINE_DUPLICATES: Lazy<Mutex<BTreeSet<String>>> = Lazy::new(|| Mutex::new(BTreeSet::new()));
33
34thread_local! {
35    static RECORDED_DUPLICATES: RefCell<Vec<BTreeMap<String, Snapshot>>> = RefCell::default()
36}
37
38// This macro is basically eprintln but without being captured and
39// hidden by the test runner.
40#[macro_export]
41macro_rules! elog {
42    () => (write!(std::io::stderr()).ok());
43    ($($arg:tt)*) => ({
44        writeln!(std::io::stderr(), $($arg)*).ok();
45    })
46}
47#[cfg(feature = "glob")]
48macro_rules! print_or_panic {
49    ($fail_fast:expr, $($tokens:tt)*) => {{
50        if (!$fail_fast) {
51            eprintln!($($tokens)*);
52            eprintln!();
53        } else {
54            panic!($($tokens)*);
55        }
56    }}
57}
58
59/// Special marker to use an automatic name.
60///
61/// This can be passed as a snapshot name in a macro to explicitly tell
62/// insta to use the automatic name.  This is useful in ambiguous syntax
63/// situations.
64#[derive(Debug)]
65pub struct AutoName;
66
67pub struct InlineValue<'a>(pub &'a str);
68
69/// The name of a snapshot, from which the path is derived.
70type SnapshotName<'a> = Option<Cow<'a, str>>;
71
72pub struct BinarySnapshotValue<'a> {
73    pub name_and_extension: &'a str,
74    pub content: Vec<u8>,
75}
76
77pub enum SnapshotValue<'a> {
78    /// A text snapshot that gets stored along with the metadata in the same file.
79    FileText {
80        name: SnapshotName<'a>,
81
82        /// The new generated value to compare against any previously approved content.
83        content: &'a str,
84    },
85
86    /// An inline snapshot.
87    InlineText {
88        /// The reference content from the macro invocation that will be compared against.
89        reference_content: &'a str,
90
91        /// The new generated value to compare against any previously approved content.
92        content: &'a str,
93    },
94
95    /// A binary snapshot that gets stored as a separate file next to the metadata file.
96    Binary {
97        name: SnapshotName<'a>,
98
99        /// The new generated value to compare against any previously approved content.
100        content: Vec<u8>,
101
102        /// The extension of the separate file.
103        extension: &'a str,
104    },
105}
106
107impl<'a> From<(AutoName, &'a str)> for SnapshotValue<'a> {
108    fn from((_, content): (AutoName, &'a str)) -> Self {
109        SnapshotValue::FileText {
110            name: None,
111            content,
112        }
113    }
114}
115
116impl<'a> From<(Option<String>, &'a str)> for SnapshotValue<'a> {
117    fn from((name, content): (Option<String>, &'a str)) -> Self {
118        SnapshotValue::FileText {
119            name: name.map(Cow::Owned),
120            content,
121        }
122    }
123}
124
125impl<'a> From<(String, &'a str)> for SnapshotValue<'a> {
126    fn from((name, content): (String, &'a str)) -> Self {
127        SnapshotValue::FileText {
128            name: Some(Cow::Owned(name)),
129            content,
130        }
131    }
132}
133
134impl<'a> From<(Option<&'a str>, &'a str)> for SnapshotValue<'a> {
135    fn from((name, content): (Option<&'a str>, &'a str)) -> Self {
136        SnapshotValue::FileText {
137            name: name.map(Cow::Borrowed),
138            content,
139        }
140    }
141}
142
143impl<'a> From<(&'a str, &'a str)> for SnapshotValue<'a> {
144    fn from((name, content): (&'a str, &'a str)) -> Self {
145        SnapshotValue::FileText {
146            name: Some(Cow::Borrowed(name)),
147            content,
148        }
149    }
150}
151
152impl<'a> From<(InlineValue<'a>, &'a str)> for SnapshotValue<'a> {
153    fn from((InlineValue(reference_content), content): (InlineValue<'a>, &'a str)) -> Self {
154        SnapshotValue::InlineText {
155            reference_content,
156            content,
157        }
158    }
159}
160
161impl<'a> From<BinarySnapshotValue<'a>> for SnapshotValue<'a> {
162    fn from(
163        BinarySnapshotValue {
164            name_and_extension,
165            content,
166        }: BinarySnapshotValue<'a>,
167    ) -> Self {
168        let (name, extension) = name_and_extension.split_once('.').unwrap_or_else(|| {
169            panic!(
170                "\"{}\" does not match the format \"name.extension\"",
171                name_and_extension,
172            )
173        });
174
175        let name = if name.is_empty() {
176            None
177        } else {
178            Some(Cow::Borrowed(name))
179        };
180
181        SnapshotValue::Binary {
182            name,
183            extension,
184            content,
185        }
186    }
187}
188
189fn is_doctest(function_name: &str) -> bool {
190    function_name.starts_with("rust_out::main::_doctest")
191}
192
193fn detect_snapshot_name(function_name: &str, module_path: &str) -> Result<String, &'static str> {
194    // clean test name first
195    let name = function_name.rsplit("::").next().unwrap();
196
197    let (name, test_prefixed) = if let Some(stripped) = name.strip_prefix("test_") {
198        (stripped, true)
199    } else {
200        (name, false)
201    };
202
203    // next check if we need to add a suffix
204    let name = add_suffix_to_snapshot_name(Cow::Borrowed(name));
205    let key = format!("{}::{}", module_path.replace("::", "__"), name);
206
207    // because fn foo and fn test_foo end up with the same snapshot name we
208    // make sure we detect this here and raise an error.
209    let mut name_clash_detection = TEST_NAME_CLASH_DETECTION
210        .lock()
211        .unwrap_or_else(|x| x.into_inner());
212    match name_clash_detection.get(&key) {
213        None => {
214            name_clash_detection.insert(key.clone(), test_prefixed);
215        }
216        Some(&was_test_prefixed) => {
217            if was_test_prefixed != test_prefixed {
218                panic!(
219                    "Insta snapshot name clash detected between '{}' \
220                     and 'test_{}' in '{}'. Rename one function.",
221                    name, name, module_path
222                );
223            }
224        }
225    }
226
227    // The rest of the code just deals with duplicates, which we in some
228    // cases do not want to guard against.
229    if allow_duplicates() {
230        return Ok(name.to_string());
231    }
232
233    // if the snapshot name clashes we need to increment a counter.
234    // we really do not care about poisoning here.
235    let mut counters = TEST_NAME_COUNTERS.lock().unwrap_or_else(|x| x.into_inner());
236    let test_idx = counters.get(&key).cloned().unwrap_or(0) + 1;
237    let rv = if test_idx == 1 {
238        name.to_string()
239    } else {
240        format!("{}-{}", name, test_idx)
241    };
242    counters.insert(key, test_idx);
243
244    Ok(rv)
245}
246
247/// If there is a suffix on the settings, append it to the snapshot name.
248fn add_suffix_to_snapshot_name(name: Cow<'_, str>) -> Cow<'_, str> {
249    Settings::with(|settings| {
250        settings
251            .snapshot_suffix()
252            .map(|suffix| Cow::Owned(format!("{}@{}", name, suffix)))
253            .unwrap_or_else(|| name)
254    })
255}
256
257fn get_snapshot_filename(
258    module_path: &str,
259    assertion_file: &str,
260    snapshot_name: &str,
261    cargo_workspace: &Path,
262    is_doctest: bool,
263) -> PathBuf {
264    let root = Path::new(cargo_workspace);
265    let base = Path::new(assertion_file);
266    Settings::with(|settings| {
267        root.join(base.parent().unwrap())
268            .join(settings.snapshot_path())
269            .join({
270                use std::fmt::Write;
271                let mut f = String::new();
272                if settings.prepend_module_to_snapshot() {
273                    if is_doctest {
274                        write!(
275                            &mut f,
276                            "doctest_{}__",
277                            base.file_name()
278                                .unwrap()
279                                .to_string_lossy()
280                                .replace('.', "_")
281                        )
282                        .unwrap();
283                    } else {
284                        write!(&mut f, "{}__", module_path.replace("::", "__")).unwrap();
285                    }
286                }
287                write!(
288                    &mut f,
289                    "{}.snap",
290                    snapshot_name.replace(&['/', '\\'][..], "__")
291                )
292                .unwrap();
293                f
294            })
295    })
296}
297
298/// The context around a snapshot, such as the reference value, location, etc.
299/// (but not including the generated value). Responsible for saving the
300/// snapshot.
301#[derive(Debug)]
302struct SnapshotAssertionContext<'a> {
303    tool_config: Arc<ToolConfig>,
304    workspace: &'a Path,
305    module_path: &'a str,
306    snapshot_name: Option<Cow<'a, str>>,
307    snapshot_file: Option<PathBuf>,
308    duplication_key: Option<String>,
309    old_snapshot: Option<Snapshot>,
310    pending_snapshots_path: Option<PathBuf>,
311    assertion_file: &'a str,
312    assertion_line: u32,
313    is_doctest: bool,
314    snapshot_kind: SnapshotKind,
315}
316
317impl<'a> SnapshotAssertionContext<'a> {
318    fn prepare(
319        new_snapshot_value: &SnapshotValue<'a>,
320        workspace: &'a Path,
321        function_name: &'a str,
322        module_path: &'a str,
323        assertion_file: &'a str,
324        assertion_line: u32,
325    ) -> Result<SnapshotAssertionContext<'a>, Box<dyn Error>> {
326        let tool_config = get_tool_config(workspace);
327        let snapshot_name;
328        let mut duplication_key = None;
329        let mut snapshot_file = None;
330        let mut old_snapshot = None;
331        let mut pending_snapshots_path = None;
332        let is_doctest = is_doctest(function_name);
333
334        match new_snapshot_value {
335            SnapshotValue::FileText { name, .. } | SnapshotValue::Binary { name, .. } => {
336                let name = match &name {
337                    Some(name) => add_suffix_to_snapshot_name(name.clone()),
338                    None => {
339                        if is_doctest {
340                            panic!("Cannot determine reliable names for snapshot in doctests.  Please use explicit names instead.");
341                        }
342                        detect_snapshot_name(function_name, module_path)
343                            .unwrap()
344                            .into()
345                    }
346                };
347                if allow_duplicates() {
348                    duplication_key = Some(format!("named:{}|{}", module_path, name));
349                }
350                let file = get_snapshot_filename(
351                    module_path,
352                    assertion_file,
353                    &name,
354                    workspace,
355                    is_doctest,
356                );
357                if fs::metadata(&file).is_ok() {
358                    old_snapshot = Some(Snapshot::from_file(&file)?);
359                }
360                snapshot_name = Some(name);
361                snapshot_file = Some(file);
362            }
363            SnapshotValue::InlineText {
364                reference_content: contents,
365                ..
366            } => {
367                if allow_duplicates() {
368                    duplication_key = Some(format!(
369                        "inline:{}|{}|{}",
370                        function_name, assertion_file, assertion_line
371                    ));
372                } else {
373                    prevent_inline_duplicate(function_name, assertion_file, assertion_line);
374                }
375                snapshot_name = detect_snapshot_name(function_name, module_path)
376                    .ok()
377                    .map(Cow::Owned);
378                let mut pending_file = workspace.join(assertion_file);
379                pending_file.set_file_name(format!(
380                    ".{}.pending-snap",
381                    pending_file
382                        .file_name()
383                        .expect("no filename")
384                        .to_str()
385                        .expect("non unicode filename")
386                ));
387                pending_snapshots_path = Some(pending_file);
388                old_snapshot = Some(Snapshot::from_components(
389                    module_path.replace("::", "__"),
390                    None,
391                    MetaData::default(),
392                    TextSnapshotContents::new(contents.to_string(), TextSnapshotKind::Inline)
393                        .into(),
394                ));
395            }
396        };
397
398        let snapshot_type = match new_snapshot_value {
399            SnapshotValue::FileText { .. } | SnapshotValue::InlineText { .. } => SnapshotKind::Text,
400            &SnapshotValue::Binary { extension, .. } => SnapshotKind::Binary {
401                extension: extension.to_string(),
402            },
403        };
404
405        Ok(SnapshotAssertionContext {
406            tool_config,
407            workspace,
408            module_path,
409            snapshot_name,
410            snapshot_file,
411            old_snapshot,
412            pending_snapshots_path,
413            assertion_file,
414            assertion_line,
415            duplication_key,
416            is_doctest,
417            snapshot_kind: snapshot_type,
418        })
419    }
420
421    /// Given a path returns the local path within the workspace.
422    pub fn localize_path(&self, p: &Path) -> Option<PathBuf> {
423        let workspace = self.workspace.canonicalize().ok()?;
424        let p = self.workspace.join(p).canonicalize().ok()?;
425        p.strip_prefix(&workspace).ok().map(|x| x.to_path_buf())
426    }
427
428    /// Creates the new snapshot from input values.
429    pub fn new_snapshot(&self, contents: SnapshotContents, expr: &str) -> Snapshot {
430        assert_eq!(
431            contents.is_binary(),
432            matches!(self.snapshot_kind, SnapshotKind::Binary { .. })
433        );
434
435        Snapshot::from_components(
436            self.module_path.replace("::", "__"),
437            self.snapshot_name.as_ref().map(|x| x.to_string()),
438            Settings::with(|settings| MetaData {
439                source: Some(path_to_storage(Path::new(self.assertion_file))),
440                assertion_line: Some(self.assertion_line),
441                description: settings.description().map(Into::into),
442                expression: if settings.omit_expression() {
443                    None
444                } else {
445                    Some(expr.to_string())
446                },
447                info: settings.info().map(ToOwned::to_owned),
448                input_file: settings
449                    .input_file()
450                    .and_then(|x| self.localize_path(x))
451                    .map(|x| path_to_storage(&x)),
452                snapshot_kind: self.snapshot_kind.clone(),
453            }),
454            contents,
455        )
456    }
457
458    /// Cleanup logic for passing snapshots.
459    pub fn cleanup_passing(&self) -> Result<(), Box<dyn Error>> {
460        // let's just make sure there are no more pending files lingering
461        // around.
462        if let Some(ref snapshot_file) = self.snapshot_file {
463            let snapshot_file = snapshot_file.clone().with_extension("snap.new");
464            fs::remove_file(snapshot_file).ok();
465        }
466
467        // and add a null pending snapshot to a pending snapshot file if needed
468        if let Some(ref pending_snapshots) = self.pending_snapshots_path {
469            if fs::metadata(pending_snapshots).is_ok() {
470                PendingInlineSnapshot::new(None, None, self.assertion_line)
471                    .save(pending_snapshots)?;
472            }
473        }
474        Ok(())
475    }
476
477    /// Removes any old .snap.new.* files that belonged to previous pending snapshots. This should
478    /// only ever remove maximum one file because we do this every time before we create a new
479    /// pending snapshot.
480    pub fn cleanup_previous_pending_binary_snapshots(&self) -> Result<(), Box<dyn Error>> {
481        if let Some(ref path) = self.snapshot_file {
482            // The file name to compare against has to be valid utf-8 as it is generated by this crate
483            // out of utf-8 strings.
484            let file_name_prefix = format!("{}.new.", path.file_name().unwrap().to_str().unwrap());
485
486            let read_dir = path.parent().unwrap().read_dir();
487
488            match read_dir {
489                Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
490                _ => (),
491            }
492
493            // We have to loop over where whole directory here because there is no filesystem API
494            // for getting files by prefix.
495            for entry in read_dir? {
496                let entry = entry?;
497                let entry_file_name = entry.file_name();
498
499                // We'll just skip over files with non-utf-8 names. The assumption being that those
500                // would not have been generated by this crate.
501                if entry_file_name
502                    .to_str()
503                    .map(|f| f.starts_with(&file_name_prefix))
504                    .unwrap_or(false)
505                {
506                    std::fs::remove_file(entry.path())?;
507                }
508            }
509        }
510
511        Ok(())
512    }
513
514    /// Writes the changes of the snapshot back.
515    pub fn update_snapshot(
516        &self,
517        new_snapshot: Snapshot,
518    ) -> Result<SnapshotUpdateBehavior, Box<dyn Error>> {
519        // TODO: this seems to be making `unseen` be true when there is an
520        // existing snapshot file; which seems wrong??
521        let unseen = self
522            .snapshot_file
523            .as_ref()
524            .map_or(false, |x| fs::metadata(x).is_ok());
525        let should_print = self.tool_config.output_behavior() != OutputBehavior::Nothing;
526        let snapshot_update = snapshot_update_behavior(&self.tool_config, unseen);
527
528        // If snapshot_update is `InPlace` and we have an inline snapshot, then
529        // use `NewFile`, since we can't use `InPlace` for inline. `cargo-insta`
530        // then accepts all snapshots at the end of the test.
531        let snapshot_update =
532            // TODO: could match on the snapshot kind instead of whether snapshot_file is None
533            if snapshot_update == SnapshotUpdateBehavior::InPlace && self.snapshot_file.is_none() {
534                SnapshotUpdateBehavior::NewFile
535            } else {
536                snapshot_update
537            };
538
539        match snapshot_update {
540            SnapshotUpdateBehavior::InPlace => {
541                if let Some(ref snapshot_file) = self.snapshot_file {
542                    new_snapshot.save(snapshot_file)?;
543                    if should_print {
544                        elog!(
545                            "{} {}",
546                            if unseen {
547                                style("created previously unseen snapshot").green()
548                            } else {
549                                style("updated snapshot").green()
550                            },
551                            style(snapshot_file.display()).cyan().underlined(),
552                        );
553                    }
554                } else {
555                    // Checked self.snapshot_file.is_none() above
556                    unreachable!()
557                }
558            }
559            SnapshotUpdateBehavior::NewFile => {
560                if let Some(ref snapshot_file) = self.snapshot_file {
561                    // File snapshot
562                    let new_path = new_snapshot.save_new(snapshot_file)?;
563                    if should_print {
564                        elog!(
565                            "{} {}",
566                            style("stored new snapshot").green(),
567                            style(new_path.display()).cyan().underlined(),
568                        );
569                    }
570                } else if self.is_doctest {
571                    if should_print {
572                        elog!(
573                            "{}",
574                            style("warning: cannot update inline snapshots in doctests")
575                                .red()
576                                .bold(),
577                        );
578                    }
579                } else {
580                    PendingInlineSnapshot::new(
581                        Some(new_snapshot),
582                        self.old_snapshot.clone(),
583                        self.assertion_line,
584                    )
585                    .save(self.pending_snapshots_path.as_ref().unwrap())?;
586                }
587            }
588            SnapshotUpdateBehavior::NoUpdate => {}
589        }
590
591        Ok(snapshot_update)
592    }
593
594    /// This prints the information about the snapshot
595    fn print_snapshot_info(&self, new_snapshot: &Snapshot) {
596        let mut printer =
597            SnapshotPrinter::new(self.workspace, self.old_snapshot.as_ref(), new_snapshot);
598        printer.set_line(Some(self.assertion_line));
599        printer.set_snapshot_file(self.snapshot_file.as_deref());
600        printer.set_title(Some("Snapshot Summary"));
601        printer.set_show_info(true);
602        match self.tool_config.output_behavior() {
603            OutputBehavior::Summary => {
604                printer.print();
605            }
606            OutputBehavior::Diff => {
607                printer.set_show_diff(true);
608                printer.print();
609            }
610            _ => {}
611        }
612    }
613
614    /// Finalizes the assertion when the snapshot comparison fails, potentially
615    /// panicking to fail the test
616    fn finalize(&self, update_result: SnapshotUpdateBehavior) {
617        // if we are in glob mode, we want to adjust the finalization
618        // so that we do not show the hints immediately.
619        let fail_fast = {
620            #[cfg(feature = "glob")]
621            {
622                if let Some(top) = crate::glob::GLOB_STACK.lock().unwrap().last() {
623                    top.fail_fast
624                } else {
625                    true
626                }
627            }
628            #[cfg(not(feature = "glob"))]
629            {
630                true
631            }
632        };
633
634        if fail_fast
635            && update_result == SnapshotUpdateBehavior::NewFile
636            && self.tool_config.output_behavior() != OutputBehavior::Nothing
637            && !self.is_doctest
638        {
639            println!(
640                "{hint}",
641                hint = style("To update snapshots run `cargo insta review`").dim(),
642            );
643        }
644
645        if update_result != SnapshotUpdateBehavior::InPlace && !self.tool_config.force_pass() {
646            if fail_fast && self.tool_config.output_behavior() != OutputBehavior::Nothing {
647                let msg = if env::var("INSTA_CARGO_INSTA") == Ok("1".to_string()) {
648                    "Stopped on the first failure."
649                } else {
650                    "Stopped on the first failure. Run `cargo insta test` to run all snapshots."
651                };
652                println!("{hint}", hint = style(msg).dim(),);
653            }
654
655            // if we are in glob mode, count the failures and print the
656            // errors instead of panicking.  The glob will then panic at
657            // the end.
658            #[cfg(feature = "glob")]
659            {
660                let mut stack = crate::glob::GLOB_STACK.lock().unwrap();
661                if let Some(glob_collector) = stack.last_mut() {
662                    glob_collector.failed += 1;
663                    if update_result == SnapshotUpdateBehavior::NewFile
664                        && self.tool_config.output_behavior() != OutputBehavior::Nothing
665                    {
666                        glob_collector.show_insta_hint = true;
667                    }
668
669                    print_or_panic!(
670                        fail_fast,
671                        "snapshot assertion from glob for '{}' failed in line {}",
672                        self.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
673                        self.assertion_line
674                    );
675                    return;
676                }
677            }
678
679            panic!(
680                "snapshot assertion for '{}' failed in line {}",
681                self.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
682                self.assertion_line
683            );
684        }
685    }
686}
687
688fn prevent_inline_duplicate(function_name: &str, assertion_file: &str, assertion_line: u32) {
689    let key = format!("{}|{}|{}", function_name, assertion_file, assertion_line);
690    let mut set = INLINE_DUPLICATES.lock().unwrap();
691    if set.contains(&key) {
692        // drop the lock so we don't poison it
693        drop(set);
694        panic!(
695            "Insta does not allow inline snapshot assertions in loops. \
696            Wrap your assertions in allow_duplicates! to change this."
697        );
698    }
699    set.insert(key);
700}
701
702fn record_snapshot_duplicate(
703    results: &mut BTreeMap<String, Snapshot>,
704    snapshot: &Snapshot,
705    ctx: &SnapshotAssertionContext,
706) {
707    let key = ctx.duplication_key.as_deref().unwrap();
708    if let Some(prev_snapshot) = results.get(key) {
709        if prev_snapshot.contents() != snapshot.contents() {
710            println!("Snapshots in allow-duplicates block do not match.");
711            let mut printer = SnapshotPrinter::new(ctx.workspace, Some(prev_snapshot), snapshot);
712            printer.set_line(Some(ctx.assertion_line));
713            printer.set_snapshot_file(ctx.snapshot_file.as_deref());
714            printer.set_title(Some("Differences in Block"));
715            printer.set_snapshot_hints("previous assertion", "current assertion");
716            if ctx.tool_config.output_behavior() == OutputBehavior::Diff {
717                printer.set_show_diff(true);
718            }
719            printer.print();
720            panic!(
721                "snapshot assertion for '{}' failed in line {}. Result \
722                    does not match previous snapshot in allow-duplicates block.",
723                ctx.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
724                ctx.assertion_line
725            );
726        }
727    } else {
728        results.insert(key.to_string(), snapshot.clone());
729    }
730}
731
732/// Do we allow recording of duplicates?
733fn allow_duplicates() -> bool {
734    RECORDED_DUPLICATES.with(|x| !x.borrow().is_empty())
735}
736
737/// Helper function to support perfect duplicate detection.
738pub fn with_allow_duplicates<R, F>(f: F) -> R
739where
740    F: FnOnce() -> R,
741{
742    RECORDED_DUPLICATES.with(|x| x.borrow_mut().push(BTreeMap::new()));
743    let rv = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
744    RECORDED_DUPLICATES.with(|x| x.borrow_mut().pop().unwrap());
745    match rv {
746        Ok(rv) => rv,
747        Err(payload) => std::panic::resume_unwind(payload),
748    }
749}
750
751/// This function is invoked from the macros to run the main assertion logic.
752///
753/// This will create the assertion context, run the main logic to assert
754/// on snapshots and write changes to the pending snapshot files.  It will
755/// also print the necessary bits of information to the output and fail the
756/// assertion with a panic if needed.
757#[allow(clippy::too_many_arguments)]
758pub fn assert_snapshot(
759    snapshot_value: SnapshotValue<'_>,
760    workspace: &Path,
761    function_name: &str,
762    module_path: &str,
763    assertion_file: &str,
764    assertion_line: u32,
765    expr: &str,
766) -> Result<(), Box<dyn Error>> {
767    let ctx = SnapshotAssertionContext::prepare(
768        &snapshot_value,
769        workspace,
770        function_name,
771        module_path,
772        assertion_file,
773        assertion_line,
774    )?;
775
776    ctx.cleanup_previous_pending_binary_snapshots()?;
777
778    let content = match snapshot_value {
779        SnapshotValue::FileText { content, .. } | SnapshotValue::InlineText { content, .. } => {
780            // apply filters if they are available
781            #[cfg(feature = "filters")]
782            let content = Settings::with(|settings| settings.filters().apply_to(content));
783
784            let kind = match ctx.snapshot_file {
785                Some(_) => TextSnapshotKind::File,
786                None => TextSnapshotKind::Inline,
787            };
788
789            TextSnapshotContents::new(content.into(), kind).into()
790        }
791        SnapshotValue::Binary {
792            content, extension, ..
793        } => {
794            assert!(
795                extension != "new",
796                "'.new' is not allowed as a file extension"
797            );
798            assert!(
799                !extension.starts_with("new."),
800                "file extensions starting with 'new.' are not allowed",
801            );
802
803            SnapshotContents::Binary(Rc::new(content))
804        }
805    };
806
807    let new_snapshot = ctx.new_snapshot(content, expr);
808
809    // memoize the snapshot file if requested, as part of potentially removing unreferenced snapshots
810    if let Some(ref snapshot_file) = ctx.snapshot_file {
811        memoize_snapshot_file(snapshot_file);
812    }
813
814    // If we allow assertion with duplicates, we record the duplicate now.  This will
815    // in itself fail the assertion if the previous visit of the same assertion macro
816    // did not yield the same result.
817    RECORDED_DUPLICATES.with(|x| {
818        if let Some(results) = x.borrow_mut().last_mut() {
819            record_snapshot_duplicate(results, &new_snapshot, &ctx);
820        }
821    });
822
823    let pass = ctx
824        .old_snapshot
825        .as_ref()
826        .map(|x| {
827            if ctx.tool_config.require_full_match() {
828                x.matches_fully(&new_snapshot)
829            } else {
830                x.matches(&new_snapshot)
831            }
832        })
833        .unwrap_or(false);
834
835    if pass {
836        ctx.cleanup_passing()?;
837
838        if matches!(
839            ctx.tool_config.snapshot_update(),
840            crate::env::SnapshotUpdate::Force
841        ) {
842            ctx.update_snapshot(new_snapshot)?;
843        }
844    // otherwise print information and update snapshots.
845    } else {
846        ctx.print_snapshot_info(&new_snapshot);
847        let update_result = ctx.update_snapshot(new_snapshot)?;
848        ctx.finalize(update_result);
849    }
850
851    Ok(())
852}
853
854#[allow(rustdoc::private_doc_tests)]
855/// Test snapshots in doctests.
856///
857/// ```
858/// // this is only working on newer rust versions
859/// extern crate rustc_version;
860/// use rustc_version::{Version, version};
861/// if version().unwrap() > Version::parse("1.72.0").unwrap() {
862///     insta::assert_debug_snapshot!("named", vec![1, 2, 3, 4, 5]);
863/// }
864/// ```
865///
866/// ```should_panic
867/// insta::assert_debug_snapshot!(vec![1, 2, 3, 4, 5]);
868/// ```
869///
870/// ```
871/// let some_string = "Coucou je suis un joli bug";
872/// insta::assert_snapshot!(some_string, @"Coucou je suis un joli bug");
873/// ```
874///
875/// ```
876/// let some_string = "Coucou je suis un joli bug";
877/// insta::assert_snapshot!(some_string, @"Coucou je suis un joli bug");
878/// ```
879const _DOCTEST1: bool = false;