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!("\"{name_and_extension}\" does not match the format \"name.extension\"",)
170        });
171
172        let name = if name.is_empty() {
173            None
174        } else {
175            Some(Cow::Borrowed(name))
176        };
177
178        SnapshotValue::Binary {
179            name,
180            extension,
181            content,
182        }
183    }
184}
185
186fn is_doctest(function_name: &str) -> bool {
187    function_name.starts_with("rust_out::main::_doctest")
188}
189
190fn detect_snapshot_name(function_name: &str, module_path: &str) -> Result<String, &'static str> {
191    // clean test name first
192    let name = function_name.rsplit("::").next().unwrap();
193
194    let (name, test_prefixed) = if let Some(stripped) = name.strip_prefix("test_") {
195        (stripped, true)
196    } else {
197        (name, false)
198    };
199
200    // next check if we need to add a suffix
201    let name = add_suffix_to_snapshot_name(Cow::Borrowed(name));
202    let key = format!("{}::{}", module_path.replace("::", "__"), name);
203
204    // because fn foo and fn test_foo end up with the same snapshot name we
205    // make sure we detect this here and raise an error.
206    let mut name_clash_detection = TEST_NAME_CLASH_DETECTION
207        .lock()
208        .unwrap_or_else(|x| x.into_inner());
209    match name_clash_detection.get(&key) {
210        None => {
211            name_clash_detection.insert(key.clone(), test_prefixed);
212        }
213        Some(&was_test_prefixed) => {
214            if was_test_prefixed != test_prefixed {
215                panic!(
216                    "Insta snapshot name clash detected between '{name}' \
217                     and 'test_{name}' in '{module_path}'. Rename one function."
218                );
219            }
220        }
221    }
222
223    // The rest of the code just deals with duplicates, which we in some
224    // cases do not want to guard against.
225    if allow_duplicates() {
226        return Ok(name.to_string());
227    }
228
229    // if the snapshot name clashes we need to increment a counter.
230    // we really do not care about poisoning here.
231    let mut counters = TEST_NAME_COUNTERS.lock().unwrap_or_else(|x| x.into_inner());
232    let test_idx = counters.get(&key).cloned().unwrap_or(0) + 1;
233    let rv = if test_idx == 1 {
234        name.to_string()
235    } else {
236        format!("{name}-{test_idx}")
237    };
238    counters.insert(key, test_idx);
239
240    Ok(rv)
241}
242
243/// If there is a suffix on the settings, append it to the snapshot name.
244fn add_suffix_to_snapshot_name(name: Cow<'_, str>) -> Cow<'_, str> {
245    Settings::with(|settings| {
246        settings
247            .snapshot_suffix()
248            .map(|suffix| Cow::Owned(format!("{name}@{suffix}")))
249            .unwrap_or_else(|| name)
250    })
251}
252
253fn get_snapshot_filename(
254    module_path: &str,
255    assertion_file: &str,
256    snapshot_name: &str,
257    cargo_workspace: &Path,
258    is_doctest: bool,
259) -> PathBuf {
260    let root = Path::new(cargo_workspace);
261    let base = Path::new(assertion_file);
262    Settings::with(|settings| {
263        root.join(base.parent().unwrap())
264            .join(settings.snapshot_path())
265            .join({
266                use std::fmt::Write;
267                let mut f = String::new();
268                if settings.prepend_module_to_snapshot() {
269                    if is_doctest {
270                        write!(
271                            &mut f,
272                            "doctest_{}__",
273                            base.file_name()
274                                .unwrap()
275                                .to_string_lossy()
276                                .replace('.', "_")
277                        )
278                        .unwrap();
279                    } else {
280                        write!(&mut f, "{}__", module_path.replace("::", "__")).unwrap();
281                    }
282                }
283                write!(
284                    &mut f,
285                    "{}.snap",
286                    snapshot_name.replace(&['/', '\\'][..], "__")
287                )
288                .unwrap();
289                f
290            })
291    })
292}
293
294/// The context around a snapshot, such as the reference value, location, etc.
295/// (but not including the generated value). Responsible for saving the
296/// snapshot.
297#[derive(Debug)]
298struct SnapshotAssertionContext<'a> {
299    tool_config: Arc<ToolConfig>,
300    workspace: &'a Path,
301    module_path: &'a str,
302    snapshot_name: Option<Cow<'a, str>>,
303    snapshot_file: Option<PathBuf>,
304    duplication_key: Option<String>,
305    old_snapshot: Option<Snapshot>,
306    pending_snapshots_path: Option<PathBuf>,
307    assertion_file: &'a str,
308    assertion_line: u32,
309    is_doctest: bool,
310    snapshot_kind: SnapshotKind,
311}
312
313impl<'a> SnapshotAssertionContext<'a> {
314    fn prepare(
315        new_snapshot_value: &SnapshotValue<'a>,
316        workspace: &'a Path,
317        function_name: &'a str,
318        module_path: &'a str,
319        assertion_file: &'a str,
320        assertion_line: u32,
321    ) -> Result<SnapshotAssertionContext<'a>, Box<dyn Error>> {
322        let tool_config = get_tool_config(workspace);
323        let snapshot_name;
324        let mut duplication_key = None;
325        let mut snapshot_file = None;
326        let mut old_snapshot = None;
327        let mut pending_snapshots_path = None;
328        let is_doctest = is_doctest(function_name);
329
330        match new_snapshot_value {
331            SnapshotValue::FileText { name, .. } | SnapshotValue::Binary { name, .. } => {
332                let name = match &name {
333                    Some(name) => add_suffix_to_snapshot_name(name.clone()),
334                    None => {
335                        if is_doctest {
336                            panic!("Cannot determine reliable names for snapshot in doctests.  Please use explicit names instead.");
337                        }
338                        detect_snapshot_name(function_name, module_path)
339                            .unwrap()
340                            .into()
341                    }
342                };
343                if allow_duplicates() {
344                    duplication_key = Some(format!("named:{module_path}|{name}"));
345                }
346                let file = get_snapshot_filename(
347                    module_path,
348                    assertion_file,
349                    &name,
350                    workspace,
351                    is_doctest,
352                );
353                if fs::metadata(&file).is_ok() {
354                    match Snapshot::from_file(&file) {
355                        Ok(snapshot) => {
356                            old_snapshot = Some(snapshot);
357                        }
358                        Err(err) => {
359                            // If we can't parse the snapshot (e.g., invalid YAML,
360                            // merge conflicts, truncated file), log a warning and
361                            // proceed. The test will generate a new pending snapshot.
362                            elog!(
363                                "{}: Failed to parse snapshot file; \
364                                 a new snapshot will be generated: {}\n  Error: {}",
365                                style("warning").yellow().bold(),
366                                file.display(),
367                                err
368                            );
369                        }
370                    }
371                }
372                snapshot_name = Some(name);
373                snapshot_file = Some(file);
374            }
375            SnapshotValue::InlineText {
376                reference_content: contents,
377                ..
378            } => {
379                if allow_duplicates() {
380                    duplication_key = Some(format!(
381                        "inline:{function_name}|{assertion_file}|{assertion_line}"
382                    ));
383                } else {
384                    prevent_inline_duplicate(function_name, assertion_file, assertion_line);
385                }
386                snapshot_name = detect_snapshot_name(function_name, module_path)
387                    .ok()
388                    .map(Cow::Owned);
389                let mut pending_file = workspace.join(assertion_file);
390                pending_file.set_file_name(format!(
391                    ".{}.pending-snap",
392                    pending_file
393                        .file_name()
394                        .expect("no filename")
395                        .to_str()
396                        .expect("non unicode filename")
397                ));
398                pending_snapshots_path = Some(pending_file);
399                old_snapshot = Some(Snapshot::from_components(
400                    module_path.replace("::", "__"),
401                    None,
402                    MetaData::default(),
403                    SnapshotContents::Text(TextSnapshotContents::from_inline_literal(contents)),
404                ));
405            }
406        };
407
408        let snapshot_type = match new_snapshot_value {
409            SnapshotValue::FileText { .. } | SnapshotValue::InlineText { .. } => SnapshotKind::Text,
410            &SnapshotValue::Binary { extension, .. } => SnapshotKind::Binary {
411                extension: extension.to_string(),
412            },
413        };
414
415        Ok(SnapshotAssertionContext {
416            tool_config,
417            workspace,
418            module_path,
419            snapshot_name,
420            snapshot_file,
421            old_snapshot,
422            pending_snapshots_path,
423            assertion_file,
424            assertion_line,
425            duplication_key,
426            is_doctest,
427            snapshot_kind: snapshot_type,
428        })
429    }
430
431    /// Given a path returns the local path within the workspace.
432    pub fn localize_path(&self, p: &Path) -> Option<PathBuf> {
433        let workspace = self.workspace.canonicalize().ok()?;
434        let p = self.workspace.join(p).canonicalize().ok()?;
435        p.strip_prefix(&workspace).ok().map(|x| x.to_path_buf())
436    }
437
438    /// Creates the new snapshot from input values.
439    pub fn new_snapshot(&self, contents: SnapshotContents, expr: &str) -> Snapshot {
440        assert_eq!(
441            contents.is_binary(),
442            matches!(self.snapshot_kind, SnapshotKind::Binary { .. })
443        );
444
445        Snapshot::from_components(
446            self.module_path.replace("::", "__"),
447            self.snapshot_name.as_ref().map(|x| x.to_string()),
448            Settings::with(|settings| MetaData {
449                source: {
450                    let source_path = Path::new(self.assertion_file);
451                    // We need to compute a relative path from the workspace to the source file.
452                    // This is necessary for workspace setups where the project is not a direct
453                    // child of the workspace root (e.g., when workspace and project are siblings).
454                    // We canonicalize paths first to properly handle symlinks.
455                    let canonicalized_base = self.workspace.canonicalize().ok();
456                    let canonicalized_path = source_path.canonicalize().ok();
457
458                    let relative = if let (Some(base), Some(path)) =
459                        (canonicalized_base, canonicalized_path)
460                    {
461                        path_relative_from(&path, &base)
462                            .unwrap_or_else(|| source_path.to_path_buf())
463                    } else {
464                        // If canonicalization fails, try with original paths
465                        path_relative_from(source_path, self.workspace)
466                            .unwrap_or_else(|| source_path.to_path_buf())
467                    };
468                    Some(path_to_storage(&relative))
469                },
470                assertion_line: Some(self.assertion_line),
471                description: settings.description().map(Into::into),
472                expression: if settings.omit_expression() {
473                    None
474                } else {
475                    Some(expr.to_string())
476                },
477                info: settings.info().map(ToOwned::to_owned),
478                input_file: settings
479                    .input_file()
480                    .and_then(|x| self.localize_path(x))
481                    .map(|x| path_to_storage(&x)),
482                snapshot_kind: self.snapshot_kind.clone(),
483            }),
484            contents,
485        )
486    }
487
488    /// Cleanup logic for passing snapshots.
489    pub fn cleanup_passing(&self) -> Result<(), Box<dyn Error>> {
490        // let's just make sure there are no more pending files lingering
491        // around.
492        if let Some(ref snapshot_file) = self.snapshot_file {
493            let snapshot_file = snapshot_file.clone().with_extension("snap.new");
494            fs::remove_file(snapshot_file).ok();
495        }
496
497        // and add a null pending snapshot to a pending snapshot file if needed
498        if let Some(ref pending_snapshots) = self.pending_snapshots_path {
499            if fs::metadata(pending_snapshots).is_ok() {
500                PendingInlineSnapshot::new(None, None, self.assertion_line)
501                    .save(pending_snapshots)?;
502            }
503        }
504        Ok(())
505    }
506
507    /// Removes any old .snap.new.* files that belonged to previous pending snapshots. This should
508    /// only ever remove maximum one file because we do this every time before we create a new
509    /// pending snapshot.
510    pub fn cleanup_previous_pending_binary_snapshots(&self) -> Result<(), Box<dyn Error>> {
511        if let Some(ref path) = self.snapshot_file {
512            // The file name to compare against has to be valid utf-8 as it is generated by this crate
513            // out of utf-8 strings.
514            let file_name_prefix = format!("{}.new.", path.file_name().unwrap().to_str().unwrap());
515
516            let read_dir = path.parent().unwrap().read_dir();
517
518            match read_dir {
519                Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
520                _ => (),
521            }
522
523            // We have to loop over where whole directory here because there is no filesystem API
524            // for getting files by prefix.
525            for entry in read_dir? {
526                let entry = entry?;
527                let entry_file_name = entry.file_name();
528
529                // We'll just skip over files with non-utf-8 names. The assumption being that those
530                // would not have been generated by this crate.
531                if entry_file_name
532                    .to_str()
533                    .map(|f| f.starts_with(&file_name_prefix))
534                    .unwrap_or(false)
535                {
536                    std::fs::remove_file(entry.path())?;
537                }
538            }
539        }
540
541        Ok(())
542    }
543
544    /// Writes the changes of the snapshot back.
545    pub fn update_snapshot(
546        &self,
547        new_snapshot: Snapshot,
548    ) -> Result<SnapshotUpdateBehavior, Box<dyn Error>> {
549        // TODO: this seems to be making `unseen` be true when there is an
550        // existing snapshot file; which seems wrong??
551        let unseen = self
552            .snapshot_file
553            .as_ref()
554            .map_or(false, |x| fs::metadata(x).is_ok());
555        let should_print = self.tool_config.output_behavior() != OutputBehavior::Nothing;
556        let snapshot_update = snapshot_update_behavior(&self.tool_config, unseen);
557
558        // If snapshot_update is `InPlace` and we have an inline snapshot, then
559        // use `NewFile`, since we can't use `InPlace` for inline. `cargo-insta`
560        // then accepts all snapshots at the end of the test.
561        let snapshot_update =
562            // TODO: could match on the snapshot kind instead of whether snapshot_file is None
563            if snapshot_update == SnapshotUpdateBehavior::InPlace && self.snapshot_file.is_none() {
564                SnapshotUpdateBehavior::NewFile
565            } else {
566                snapshot_update
567            };
568
569        match snapshot_update {
570            SnapshotUpdateBehavior::InPlace => {
571                if let Some(ref snapshot_file) = self.snapshot_file {
572                    new_snapshot.save(snapshot_file)?;
573                    if should_print {
574                        elog!(
575                            "{} {}",
576                            style("updated snapshot").green(),
577                            style(snapshot_file.display()).cyan().underlined(),
578                        );
579                    }
580                } else {
581                    // Checked self.snapshot_file.is_none() above
582                    unreachable!()
583                }
584            }
585            SnapshotUpdateBehavior::NewFile => {
586                if let Some(ref snapshot_file) = self.snapshot_file {
587                    // File snapshot
588                    let new_path = new_snapshot.save_new(snapshot_file)?;
589                    if should_print {
590                        elog!(
591                            "{} {}",
592                            style("stored new snapshot").green(),
593                            style(new_path.display()).cyan().underlined(),
594                        );
595                    }
596                } else if self.is_doctest {
597                    if should_print {
598                        elog!(
599                            "{}",
600                            style("warning: cannot update inline snapshots in doctests")
601                                .red()
602                                .bold(),
603                        );
604                    }
605                } else {
606                    PendingInlineSnapshot::new(
607                        Some(new_snapshot),
608                        self.old_snapshot.clone(),
609                        self.assertion_line,
610                    )
611                    .save(self.pending_snapshots_path.as_ref().unwrap())?;
612                }
613            }
614            SnapshotUpdateBehavior::NoUpdate => {}
615        }
616
617        Ok(snapshot_update)
618    }
619
620    /// This prints the information about the snapshot
621    fn print_snapshot_info(&self, new_snapshot: &Snapshot) {
622        let mut printer =
623            SnapshotPrinter::new(self.workspace, self.old_snapshot.as_ref(), new_snapshot);
624        printer.set_line(Some(self.assertion_line));
625        printer.set_snapshot_file(self.snapshot_file.as_deref());
626        printer.set_title(Some("Snapshot Summary"));
627        printer.set_show_info(true);
628        match self.tool_config.output_behavior() {
629            OutputBehavior::Summary => {
630                printer.print();
631            }
632            OutputBehavior::Diff => {
633                printer.set_show_diff(true);
634                printer.print();
635            }
636            _ => {}
637        }
638    }
639
640    /// Finalizes the assertion when the snapshot comparison fails, potentially
641    /// panicking to fail the test
642    fn finalize(&self, update_result: SnapshotUpdateBehavior) {
643        // if we are in glob mode, we want to adjust the finalization
644        // so that we do not show the hints immediately.
645        let fail_fast = {
646            #[cfg(feature = "glob")]
647            {
648                if let Some(top) = crate::glob::GLOB_STACK.lock().unwrap().last() {
649                    top.fail_fast
650                } else {
651                    true
652                }
653            }
654            #[cfg(not(feature = "glob"))]
655            {
656                true
657            }
658        };
659
660        if fail_fast
661            && update_result == SnapshotUpdateBehavior::NewFile
662            && self.tool_config.output_behavior() != OutputBehavior::Nothing
663            && !self.is_doctest
664        {
665            println!(
666                "{hint}",
667                hint = style("To update snapshots run `cargo insta review`").dim(),
668            );
669        }
670
671        if update_result != SnapshotUpdateBehavior::InPlace && !self.tool_config.force_pass() {
672            if fail_fast && self.tool_config.output_behavior() != OutputBehavior::Nothing {
673                let msg = if env::var("INSTA_CARGO_INSTA") == Ok("1".to_string()) {
674                    "Stopped on the first failure."
675                } else {
676                    "Stopped on the first failure. Run `cargo insta test` to run all snapshots."
677                };
678                println!("{hint}", hint = style(msg).dim(),);
679            }
680
681            // if we are in glob mode, count the failures and print the
682            // errors instead of panicking.  The glob will then panic at
683            // the end.
684            #[cfg(feature = "glob")]
685            {
686                let mut stack = crate::glob::GLOB_STACK.lock().unwrap();
687                if let Some(glob_collector) = stack.last_mut() {
688                    glob_collector.failed += 1;
689                    if update_result == SnapshotUpdateBehavior::NewFile
690                        && self.tool_config.output_behavior() != OutputBehavior::Nothing
691                    {
692                        glob_collector.show_insta_hint = true;
693                    }
694
695                    print_or_panic!(
696                        fail_fast,
697                        "snapshot assertion from glob for '{}' failed in line {}",
698                        self.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
699                        self.assertion_line
700                    );
701                    return;
702                }
703            }
704
705            panic!(
706                "snapshot assertion for '{}' failed in line {}",
707                self.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
708                self.assertion_line
709            );
710        }
711    }
712}
713
714/// Computes a relative path from `base` to `path`, returning a path with `../` components
715/// if necessary.
716///
717/// This function is vendored from the old Rust standard library implementation
718/// (pre-1.0, removed in RFC 474) and is distributed under the same terms as the
719/// Rust project (MIT/Apache-2.0 dual license).
720///
721/// Unlike `Path::strip_prefix`, this function can handle cases where `path` is not
722/// a descendant of `base`, making it suitable for finding relative paths between
723/// arbitrary directories (e.g., between sibling directories in a workspace).
724fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> {
725    use std::path::Component;
726
727    if path.is_absolute() != base.is_absolute() {
728        if path.is_absolute() {
729            Some(PathBuf::from(path))
730        } else {
731            None
732        }
733    } else {
734        let mut ita = path.components();
735        let mut itb = base.components();
736        let mut comps: Vec<Component> = vec![];
737        loop {
738            match (ita.next(), itb.next()) {
739                (None, None) => break,
740                (Some(a), None) => {
741                    comps.push(a);
742                    comps.extend(ita.by_ref());
743                    break;
744                }
745                (None, _) => comps.push(Component::ParentDir),
746                (Some(a), Some(b)) if comps.is_empty() && a == b => {}
747                (Some(a), Some(_b)) => {
748                    comps.push(Component::ParentDir);
749                    for _ in itb {
750                        comps.push(Component::ParentDir);
751                    }
752                    comps.push(a);
753                    comps.extend(ita.by_ref());
754                    break;
755                }
756            }
757        }
758        Some(comps.iter().map(|c| c.as_os_str()).collect())
759    }
760}
761
762fn prevent_inline_duplicate(function_name: &str, assertion_file: &str, assertion_line: u32) {
763    let key = format!("{function_name}|{assertion_file}|{assertion_line}");
764    let mut set = INLINE_DUPLICATES.lock().unwrap();
765    if set.contains(&key) {
766        // drop the lock so we don't poison it
767        drop(set);
768        panic!(
769            "Insta does not allow inline snapshot assertions in loops. \
770            Wrap your assertions in allow_duplicates! to change this."
771        );
772    }
773    set.insert(key);
774}
775
776fn record_snapshot_duplicate(
777    results: &mut BTreeMap<String, Snapshot>,
778    snapshot: &Snapshot,
779    ctx: &SnapshotAssertionContext,
780) {
781    let key = ctx.duplication_key.as_deref().unwrap();
782    if let Some(prev_snapshot) = results.get(key) {
783        if prev_snapshot.contents() != snapshot.contents() {
784            println!("Snapshots in allow-duplicates block do not match.");
785            let mut printer = SnapshotPrinter::new(ctx.workspace, Some(prev_snapshot), snapshot);
786            printer.set_line(Some(ctx.assertion_line));
787            printer.set_snapshot_file(ctx.snapshot_file.as_deref());
788            printer.set_title(Some("Differences in Block"));
789            printer.set_snapshot_hints("previous assertion", "current assertion");
790            if ctx.tool_config.output_behavior() == OutputBehavior::Diff {
791                printer.set_show_diff(true);
792            }
793            printer.print();
794            panic!(
795                "snapshot assertion for '{}' failed in line {}. Result \
796                    does not match previous snapshot in allow-duplicates block.",
797                ctx.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),
798                ctx.assertion_line
799            );
800        }
801    } else {
802        results.insert(key.to_string(), snapshot.clone());
803    }
804}
805
806/// Do we allow recording of duplicates?
807fn allow_duplicates() -> bool {
808    RECORDED_DUPLICATES.with(|x| !x.borrow().is_empty())
809}
810
811/// Helper function to support perfect duplicate detection.
812pub fn with_allow_duplicates<R, F>(f: F) -> R
813where
814    F: FnOnce() -> R,
815{
816    RECORDED_DUPLICATES.with(|x| x.borrow_mut().push(BTreeMap::new()));
817    let rv = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
818    RECORDED_DUPLICATES.with(|x| x.borrow_mut().pop().unwrap());
819    match rv {
820        Ok(rv) => rv,
821        Err(payload) => std::panic::resume_unwind(payload),
822    }
823}
824
825/// This function is invoked from the macros to run the main assertion logic.
826///
827/// This will create the assertion context, run the main logic to assert
828/// on snapshots and write changes to the pending snapshot files.  It will
829/// also print the necessary bits of information to the output and fail the
830/// assertion with a panic if needed.
831#[allow(clippy::too_many_arguments)]
832pub fn assert_snapshot(
833    snapshot_value: SnapshotValue<'_>,
834    workspace: &Path,
835    function_name: &str,
836    module_path: &str,
837    assertion_file: &str,
838    assertion_line: u32,
839    expr: &str,
840) -> Result<(), Box<dyn Error>> {
841    let ctx = SnapshotAssertionContext::prepare(
842        &snapshot_value,
843        workspace,
844        function_name,
845        module_path,
846        assertion_file,
847        assertion_line,
848    )?;
849
850    ctx.cleanup_previous_pending_binary_snapshots()?;
851
852    let content = match snapshot_value {
853        SnapshotValue::FileText { content, .. } | SnapshotValue::InlineText { content, .. } => {
854            // apply filters if they are available
855            #[cfg(feature = "filters")]
856            let content = Settings::with(|settings| settings.filters().apply_to(content));
857
858            let kind = match ctx.snapshot_file {
859                Some(_) => TextSnapshotKind::File,
860                None => TextSnapshotKind::Inline,
861            };
862
863            TextSnapshotContents::new(content.into(), kind).into()
864        }
865        SnapshotValue::Binary {
866            content, extension, ..
867        } => {
868            assert!(
869                extension != "new",
870                "'.new' is not allowed as a file extension"
871            );
872            assert!(
873                !extension.starts_with("new."),
874                "file extensions starting with 'new.' are not allowed",
875            );
876
877            SnapshotContents::Binary(Rc::new(content))
878        }
879    };
880
881    let new_snapshot = ctx.new_snapshot(content, expr);
882
883    // memoize the snapshot file if requested, as part of potentially removing unreferenced snapshots
884    if let Some(ref snapshot_file) = ctx.snapshot_file {
885        memoize_snapshot_file(snapshot_file);
886    }
887
888    // If we allow assertion with duplicates, we record the duplicate now.  This will
889    // in itself fail the assertion if the previous visit of the same assertion macro
890    // did not yield the same result.
891    RECORDED_DUPLICATES.with(|x| {
892        if let Some(results) = x.borrow_mut().last_mut() {
893            record_snapshot_duplicate(results, &new_snapshot, &ctx);
894        }
895    });
896
897    let pass = ctx
898        .old_snapshot
899        .as_ref()
900        .map(|x| {
901            if ctx.tool_config.require_full_match() {
902                x.matches_fully(&new_snapshot)
903            } else {
904                x.matches(&new_snapshot)
905            }
906        })
907        .unwrap_or(false);
908
909    if pass {
910        ctx.cleanup_passing()?;
911
912        if matches!(
913            ctx.tool_config.snapshot_update(),
914            crate::env::SnapshotUpdate::Force
915        ) {
916            ctx.update_snapshot(new_snapshot)?;
917        }
918    // otherwise print information and update snapshots.
919    } else {
920        ctx.print_snapshot_info(&new_snapshot);
921        let update_result = ctx.update_snapshot(new_snapshot)?;
922        ctx.finalize(update_result);
923    }
924
925    Ok(())
926}
927
928#[allow(rustdoc::private_doc_tests)]
929/// Test snapshots in doctests.
930///
931/// ```
932/// // this is only working on newer rust versions
933/// extern crate rustc_version;
934/// use rustc_version::{Version, version};
935/// if version().unwrap() > Version::parse("1.72.0").unwrap() {
936///     insta::assert_debug_snapshot!("named", vec![1, 2, 3, 4, 5]);
937/// }
938/// ```
939///
940/// ```should_panic
941/// insta::assert_debug_snapshot!(vec![1, 2, 3, 4, 5]);
942/// ```
943///
944/// ```
945/// let some_string = "Coucou je suis un joli bug";
946/// insta::assert_snapshot!(some_string, @"Coucou je suis un joli bug");
947/// ```
948///
949/// ```
950/// let some_string = "Coucou je suis un joli bug";
951/// insta::assert_snapshot!(some_string, @"Coucou je suis un joli bug");
952/// ```
953const _DOCTEST1: bool = false;