diff --git a/crates/snapbox/src/data/mod.rs b/crates/snapbox/src/data/mod.rs index 20e9cca3..d4c7941f 100644 --- a/crates/snapbox/src/data/mod.rs +++ b/crates/snapbox/src/data/mod.rs @@ -40,18 +40,12 @@ impl ToDebug for D { #[macro_export] macro_rules! file { [_] => {{ - let stem = ::std::path::Path::new(::std::file!()).file_stem().unwrap(); - let rel_path = ::std::format!("snapshots/{}-{}.txt", stem.to_str().unwrap(), line!()); - let mut path = $crate::current_dir!(); - path.push(rel_path); + let path = $crate::data::generate_snapshot_path($crate::fn_path!(), None); $crate::Data::read_from(&path, None) }}; [_ : $type:ident] => {{ - let stem = ::std::path::Path::new(::std::file!()).file_stem().unwrap(); - let ext = $crate::data::DataFormat:: $type.ext(); - let rel_path = ::std::format!("snapshots/{}-{}.{ext}", stem.to_str().unwrap(), line!()); - let mut path = $crate::current_dir!(); - path.push(rel_path); + let format = $crate::data::DataFormat:: $type; + let path = $crate::data::generate_snapshot_path($crate::fn_path!(), Some(format)); $crate::Data::read_from(&path, Some($crate::data::DataFormat:: $type)) }}; [$path:literal] => {{ @@ -568,3 +562,18 @@ fn is_binary(data: &[u8]) -> bool { fn is_binary(_data: &[u8]) -> bool { false } + +#[doc(hidden)] +pub fn generate_snapshot_path(fn_path: &str, format: Option) -> std::path::PathBuf { + use std::fmt::Write as _; + + let fn_path_normalized = fn_path.replace("::", "__"); + let mut path = format!("tests/snapshots/{fn_path_normalized}"); + let count = runtime::get().count(&path); + if 0 < count { + write!(&mut path, "@{count}").unwrap(); + } + path.push('.'); + path.push_str(format.unwrap_or(DataFormat::Text).ext()); + path.into() +} diff --git a/crates/snapbox/src/data/runtime.rs b/crates/snapbox/src/data/runtime.rs index 43758eea..52dad661 100644 --- a/crates/snapbox/src/data/runtime.rs +++ b/crates/snapbox/src/data/runtime.rs @@ -9,13 +9,30 @@ pub(crate) fn get() -> std::sync::MutexGuard<'static, Runtime> { #[derive(Default)] pub(crate) struct Runtime { - per_file: Vec, + per_file: Vec, + path_count: Vec, } impl Runtime { const fn new() -> Self { Self { per_file: Vec::new(), + path_count: Vec::new(), + } + } + + pub(crate) fn count(&mut self, path_prefix: &str) -> usize { + if let Some(entry) = self + .path_count + .iter_mut() + .find(|entry| entry.is(path_prefix)) + { + entry.next() + } else { + let entry = PathRuntime::new(path_prefix); + let next = entry.count(); + self.path_count.push(entry); + next } } @@ -28,7 +45,7 @@ impl Runtime { { entry.update(&actual, inline)?; } else { - let mut entry = FileRuntime::new(inline)?; + let mut entry = SourceFileRuntime::new(inline)?; entry.update(&actual, inline)?; self.per_file.push(entry); } @@ -37,18 +54,18 @@ impl Runtime { } } -struct FileRuntime { +struct SourceFileRuntime { path: std::path::PathBuf, original_text: String, patchwork: Patchwork, } -impl FileRuntime { - fn new(inline: &Inline) -> std::io::Result { +impl SourceFileRuntime { + fn new(inline: &Inline) -> std::io::Result { let path = inline.position.file.clone(); let original_text = std::fs::read_to_string(&path)?; let patchwork = Patchwork::new(original_text.clone()); - Ok(FileRuntime { + Ok(SourceFileRuntime { path, original_text, patchwork, @@ -324,6 +341,34 @@ impl StrLitKind { } } +#[derive(Clone)] +struct PathRuntime { + path_prefix: String, + count: usize, +} + +impl PathRuntime { + fn new(path_prefix: &str) -> Self { + Self { + path_prefix: path_prefix.to_owned(), + count: 0, + } + } + + fn is(&self, path_prefix: &str) -> bool { + self.path_prefix == path_prefix + } + + fn next(&mut self) -> usize { + self.count += 1; + self.count + } + + fn count(&self) -> usize { + self.count + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/snapbox/src/macros.rs b/crates/snapbox/src/macros.rs index 3baf82ba..5a72d665 100644 --- a/crates/snapbox/src/macros.rs +++ b/crates/snapbox/src/macros.rs @@ -39,3 +39,46 @@ macro_rules! cargo_rustc_current_dir { } }}; } + +/// Path to the current function +/// +/// Closures are ignored +#[doc(hidden)] +#[macro_export] +macro_rules! fn_path { + () => {{ + fn f() {} + fn type_name_of_val(_: T) -> &'static str { + std::any::type_name::() + } + let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or(""); + while let Some(rest) = name.strip_suffix("::{{closure}}") { + name = rest; + } + name + }}; +} + +#[cfg(test)] +mod test { + #[test] + fn direct_fn_path() { + assert_eq!(fn_path!(), "snapbox::macros::test::direct_fn_path"); + } + + #[test] + #[allow(clippy::redundant_closure_call)] + fn closure_fn_path() { + (|| { + assert_eq!(fn_path!(), "snapbox::macros::test::closure_fn_path"); + })() + } + + #[test] + fn nested_fn_path() { + fn nested() { + assert_eq!(fn_path!(), "snapbox::macros::test::nested_fn_path::nested"); + } + nested() + } +}