diff --git a/src/glob.rs b/src/glob.rs index 6c247e5e..b30a5724 100644 --- a/src/glob.rs +++ b/src/glob.rs @@ -1,11 +1,23 @@ use std::env; use std::path::Path; +use std::sync::Mutex; use globset::{GlobBuilder, GlobMatcher}; use once_cell::sync::Lazy; use walkdir::WalkDir; use crate::settings::Settings; +use crate::utils::style; + +pub(crate) struct GlobCollector { + pub(crate) fail_fast: bool, + pub(crate) failed: usize, + pub(crate) show_insta_hint: bool, +} + +// the glob stack holds failure count + an indication if cargo insta review +// should be run. +pub(crate) static GLOB_STACK: Lazy>> = Lazy::new(Mutex::default); static GLOB_FILTER: Lazy> = Lazy::new(|| { env::var("INSTA_GLOB_FILTER") @@ -34,6 +46,12 @@ pub fn glob_exec(base: &Path, pattern: &str, mut f: F) { let mut glob_found_matches = false; let mut settings = Settings::clone_current(); + GLOB_STACK.lock().unwrap().push(GlobCollector { + failed: 0, + show_insta_hint: false, + fail_fast: std::env::var("INSTA_GLOB_FAIL_FAST").as_deref() == Ok("1"), + }); + for file in walker { let file = file.unwrap(); let path = file.path(); @@ -58,7 +76,28 @@ pub fn glob_exec(base: &Path, pattern: &str, mut f: F) { }); } + let top = GLOB_STACK.lock().unwrap().pop().unwrap(); if !glob_found_matches && !settings.allow_empty_glob() { panic!("the glob! macro did not match any files."); } + + if top.failed > 0 { + if top.show_insta_hint { + println!( + "{hint}", + hint = style("To update snapshots run `cargo insta review`").dim(), + ); + } + if top.failed > 1 { + println!( + "{hint}", + hint = style("To enable fast failing for glob! export INSTA_GLOB_FAIL_FAST=1 as environment variable.").dim() + ); + } + panic!( + "glob! resulted in {} snapshot assertion failure{}s", + top.failed, + if top.failed == 1 { "" } else { "s" }, + ); + } } diff --git a/src/macros.rs b/src/macros.rs index d420e98c..63b9ac77 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -488,6 +488,11 @@ macro_rules! with_settings { /// `INSTA_GLOB_FILTER` to `foo-*txt;bar-*.txt` only files starting with `foo-` or `bar-` /// end ending in `.txt` will be executed. When using `cargo-insta` the `--glob-filter` /// option can be used instead. +/// +/// Another effect of the globbing system is that snapshot failures within the glob macro +/// are deferred until the end of of it. In other words this means that each snapshot +/// assertion within the `glob!` block are reported. It can be disabled by setting +/// `INSTA_GLOB_FAIL_FAST` environment variable to `1`. #[cfg(feature = "glob")] #[cfg_attr(docsrs, doc(cfg(feature = "glob")))] #[macro_export] diff --git a/src/runtime.rs b/src/runtime.rs index 07f0d16b..e7e606bc 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -436,9 +436,39 @@ fn print_snapshot_info(ctx: &SnapshotAssertionContext, new_snapshot: &Snapshot) } } +#[cfg(feature = "glob")] +macro_rules! print_or_panic { + ($fail_fast:expr, $($tokens:tt)*) => {{ + if (!$fail_fast) { + eprintln!($($tokens)*); + eprintln!(); + } else { + panic!($($tokens)*); + } + }} +} + /// Finalizes the assertion based on the update result. fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpdate) { - if update_result == SnapshotUpdate::NewFile + // if we are in glob mode, we want to adjust the finalization + // so that we do not show the hints immediately. + let fail_fast = { + #[cfg(feature = "glob")] + { + if let Some(top) = crate::glob::GLOB_STACK.lock().unwrap().last() { + top.fail_fast + } else { + true + } + } + #[cfg(not(feature = "glob"))] + { + true + } + }; + + if fail_fast + && update_result == SnapshotUpdate::NewFile && get_output_behavior() != OutputBehavior::Nothing && !ctx.is_doctest { @@ -449,7 +479,7 @@ fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpd } if update_result != SnapshotUpdate::InPlace && !force_pass() { - if get_output_behavior() != OutputBehavior::Nothing { + if fail_fast && get_output_behavior() != OutputBehavior::Nothing { println!( "{hint}", hint = style( @@ -458,6 +488,31 @@ fn finalize_assertion(ctx: &SnapshotAssertionContext, update_result: SnapshotUpd .dim(), ); } + + // if we are in glob mode, count the failures and print the + // errors instead of panicking. The glob will then panic at + // the end. + #[cfg(feature = "glob")] + { + let mut stack = crate::glob::GLOB_STACK.lock().unwrap(); + if let Some(glob_collector) = stack.last_mut() { + glob_collector.failed += 1; + if update_result == SnapshotUpdate::NewFile + && get_output_behavior() != OutputBehavior::Nothing + { + glob_collector.show_insta_hint = true; + } + + print_or_panic!( + fail_fast, + "snapshot assertion from glob for '{}' failed in line {}", + ctx.snapshot_name.as_deref().unwrap_or("unnamed snapshot"), + ctx.assertion_line + ); + return; + } + } + panic!( "snapshot assertion for '{}' failed in line {}", ctx.snapshot_name.as_deref().unwrap_or("unnamed snapshot"),