Skip to content

Commit

Permalink
Add glob support (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Apr 13, 2020
1 parent 8593c9f commit 61c6140
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 9 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ default = []
# snapshots.
redactions = ["pest", "pest_derive"]

# Glob support
glob = ["globwalk"]

# This feature is now just always enabled because we use yaml internally now.
serialization = []

Expand All @@ -40,3 +43,4 @@ pest = { version = "2.1.0", optional = true }
pest_derive = { version = "2.1.0", optional = true }
ron = { version = "0.5.1", optional = true }
backtrace = { version = "0.3.42", optional = true }
globwalk = { version = "0.8.0", optional = true }
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,31 @@ assert_yaml_snapshot!(&User {
});
```

## Globbing

**Feature:** `glob`

Sometimes it can be useful to run code against multiple input files.
The easiest way to accomplish this is to use the `glob!` macro which
runs a closure for each input file that matches. Before the closure
is executed the settings are updated to set a reference to the input
file and the appropriate snapshot suffix.

Example:

```rust
use std::fs;

glob!("inputs/*.txt", |path| {
let input = fs::read_to_string(path).unwrap();
assert_json_snapshot!(input.to_uppercase());
});
```

The path to the glob macro is relative to the location of the test
file. It uses the [`globset`](https://crates.io/crates/globset) crate
for actual glob operations.

## Inline Snapshots

Additionally snapshots can also be stored inline. In that case the format
Expand Down Expand Up @@ -300,6 +325,7 @@ The following features exist:

* `ron`: enables RON support (`assert_ron_snapshot!`)
* `redactions`: enables support for redactions
* `glob`: enables support for globbing (`glob!`)

## Settings

Expand Down
26 changes: 26 additions & 0 deletions src/glob.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use std::path::Path;

use globwalk::{FileType, GlobWalkerBuilder};

use crate::settings::Settings;

pub fn glob_exec<F: FnMut(&Path)>(base: &Path, pattern: &str, mut f: F) {
let walker = GlobWalkerBuilder::new(base, pattern)
.case_insensitive(true)
.file_type(FileType::FILE)
.build()
.unwrap();

for file in walker {
let file = file.unwrap();
let path = file.path();

let mut settings = Settings::clone_current();
settings.set_input_file(&path);
settings.set_snapshot_suffix(path.file_name().unwrap().to_str().unwrap());

settings.bind(|| {
f(path);
});
}
}
34 changes: 33 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@
//!
//! * `diff` (default): prints the diffs
//! * `summary`: prints only summaries (name of snapshot files etc.)
//! * `mimimal`: like `summary` but more minimal
//! * `minimal`: like `summary` but more minimal
//! * `none`: insta will not output any extra information
//!
//! # Redactions
Expand Down Expand Up @@ -273,6 +273,31 @@
//! });
//! ```
//!
//! # Globbing
//!
//! **Feature:** `glob`
//!
//! Sometimes it can be useful to run code against multiple input files.
//! The easiest way to accomplish this is to use the `glob!` macro which
//! runs a closure for each input file that matches. Before the closure
//! is executed the settings are updated to set a reference to the input
//! file and the appropriate snapshot suffix.
//!
//! Example:
//!
//! ```rust,ignore
//! use std::fs;
//!
//! glob!("inputs/*.txt", |path| {
//! let input = fs::read_to_string(path).unwrap();
//! assert_json_snapshot!(input.to_uppercase());
//! });
//! ```
//!
//! The path to the glob macro is relative to the location of the test
//! file. It uses the [`globset`](https://crates.io/crates/globset) crate
//! for actual glob operations.
//!
//! # Inline Snapshots
//!
//! Additionally snapshots can also be stored inline. In that case the format
Expand Down Expand Up @@ -303,6 +328,7 @@
//!
//! * `ron`: enables RON support (`assert_ron_snapshot!`)
//! * `redactions`: enables support for redactions
//! * `glob`: enables support for globbing (`glob!`)
//!
//! # Settings
//!
Expand Down Expand Up @@ -334,6 +360,9 @@ mod legacy_macros;
#[cfg(feature = "redactions")]
mod redaction;

#[cfg(feature = "glob")]
mod glob;

#[cfg(test)]
mod test;

Expand Down Expand Up @@ -372,6 +401,9 @@ pub mod _macro_support {
pub use crate::runtime::{assert_snapshot, AutoName, ReferenceValue};
pub use crate::serialization::{serialize_value, SerializationFormat, SnapshotLocation};

#[cfg(feature = "glob")]
pub use crate::glob::glob_exec;

#[cfg(feature = "redactions")]
pub use crate::{
redaction::Redaction, redaction::Selector, serialization::serialize_value_redacted,
Expand Down
17 changes: 17 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,20 @@ macro_rules! with_settings {
settings.bind(|| $body)
}}
}

/// Executes a closure for all input files matching a glob.
///
/// The closure is passed the path to the file.
#[cfg(feature = "glob")]
#[macro_export]
macro_rules! glob {
($glob:expr, $closure:expr) => {{
let base = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join(file!())
.parent()
.unwrap()
.canonicalize()
.unwrap();
$crate::_macro_support::glob_exec(&base, $glob, $closure);
}};
}
47 changes: 40 additions & 7 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ pub fn get_snapshot_filename(
.join(format!(
"{}__{}.snap",
module_path.replace("::", "__"),
snapshot_name
snapshot_name.replace("/", "__").replace("\\", "__")
))
})
}
Expand Down Expand Up @@ -354,6 +354,10 @@ pub fn print_snapshot_summary(
}
);
}

if let Some(ref value) = snapshot.metadata().input_file() {
println!("Input file: {}", style(value).cyan());
}
}

/// Prints a diff against an old snapshot.
Expand Down Expand Up @@ -515,20 +519,28 @@ fn generate_snapshot_name_for_thread(module_path: &str) -> Result<String, &'stat
names from the call stack.");
}
}

// clean test name first
let mut name = name.rsplit("::").next().unwrap();
// we really do not care about poisoning here.
let key = format!("{}::{}", module_path.replace("::", "__"), name);
let mut counters = TEST_NAME_COUNTERS.lock().unwrap_or_else(|x| x.into_inner());
let test_idx = counters.get(&key).cloned().unwrap_or(0) + 1;
if name.starts_with("test_") {
name = &name[5..];
}

// next check if we need to add a suffix
let name = add_suffix_to_snapshot_name(Cow::Borrowed(name));
let key = format!("{}::{}", module_path.replace("::", "__"), name);

// 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());
let test_idx = counters.get(&key).cloned().unwrap_or(0) + 1;
let rv = if test_idx == 1 {
name.to_string()
} else {
format!("{}-{}", name, test_idx)
};
counters.insert(key, test_idx);

Ok(rv)
}

Expand Down Expand Up @@ -820,6 +832,16 @@ fn update_snapshots(
Ok(())
}

/// If there is a suffix on the settings, append it to the snapshot name.
fn add_suffix_to_snapshot_name(name: Cow<'_, str>) -> Cow<'_, str> {
Settings::with(|settings| {
settings
.snapshot_suffix()
.map(|suffix| Cow::Owned(format!("{}@{}", name, suffix)))
.unwrap_or_else(|| name)
})
}

#[allow(clippy::too_many_arguments)]
pub fn assert_snapshot(
refval: ReferenceValue<'_>,
Expand All @@ -835,8 +857,8 @@ pub fn assert_snapshot(

let (snapshot_name, snapshot_file, old, pending_snapshots) = match refval {
ReferenceValue::Named(snapshot_name) => {
let snapshot_name: Cow<str> = match snapshot_name {
Some(snapshot_name) => snapshot_name,
let snapshot_name = match snapshot_name {
Some(snapshot_name) => add_suffix_to_snapshot_name(snapshot_name),
None => generate_snapshot_name_for_thread(module_path)
.unwrap()
.into(),
Expand Down Expand Up @@ -884,6 +906,17 @@ pub fn assert_snapshot(
MetaData {
source: Some(path_to_storage(file)),
expression: Some(expr.to_string()),
input_file: Settings::with(|settings| {
settings
.input_file()
.and_then(|x| cargo_workspace.join(x).canonicalize().ok())
.and_then(|s| {
s.strip_prefix(cargo_workspace)
.ok()
.map(|x| x.to_path_buf())
})
.map(path_to_storage)
}),
},
new_snapshot_contents,
);
Expand Down
59 changes: 58 additions & 1 deletion src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ lazy_static! {
static ref DEFAULT_SETTINGS: Arc<ActualSettings> = Arc::new(ActualSettings {
sort_maps: false,
snapshot_path: "snapshots".into(),
snapshot_suffix: "".into(),
input_file: None,
#[cfg(feature = "redactions")]
redactions: Redactions::default(),
});
Expand Down Expand Up @@ -41,6 +43,8 @@ impl<'a> From<Vec<(&'a str, Redaction)>> for Redactions {
pub struct ActualSettings {
pub sort_maps: bool,
pub snapshot_path: PathBuf,
pub snapshot_suffix: String,
pub input_file: Option<PathBuf>,
#[cfg(feature = "redactions")]
pub redactions: Redactions,
}
Expand All @@ -62,7 +66,7 @@ pub struct ActualSettings {
/// ```rust,ignore
/// use insta;
///
/// let mut settings = insta::Settings::new();
/// let mut settings = insta::Settings::clone_current();
/// settings.set_sort_maps(true);
/// settings.bind(|| {
/// insta::assert_snapshot!(...);
Expand All @@ -83,10 +87,18 @@ impl Default for Settings {

impl Settings {
/// Returns the default settings.
///
/// It's recommended to use `clone_current` instead so that
/// already applied modifications are not discarded.
pub fn new() -> Settings {
Settings::default()
}

/// Returns a copy of the current settings.
pub fn clone_current() -> Settings {
Settings::with(|x| x.clone())
}

/// Internal helper for macros
#[doc(hidden)]
pub fn _private_inner_mut(&mut self) -> &mut ActualSettings {
Expand All @@ -108,6 +120,51 @@ impl Settings {
self.inner.sort_maps
}

/// Sets the snapshot suffix.
///
/// The snapshot suffix is added to all snapshot names with an `@` sign
/// between. For instance if the snapshot suffix is set to `"foo"` and
/// the snapshot would be named `"snapshot"` it turns into `"snapshot@foo"`.
/// This is useful to separate snapshots if you want to use test
/// parameterization.
pub fn set_snapshot_suffix<I: Into<String>>(&mut self, suffix: I) {
self._private_inner_mut().snapshot_suffix = suffix.into();
}

/// Removes the snapshot suffix.
pub fn remove_snapshot_suffix(&mut self) {
self.set_snapshot_suffix("");
}

/// Returns the current snapshot suffix.
pub fn snapshot_suffix(&self) -> Option<&str> {
if self.inner.snapshot_suffix.is_empty() {
None
} else {
Some(&self.inner.snapshot_suffix)
}
}

/// Sets the input file reference.
///
/// This value is completely unused by the snapshot testing system but
/// it lets you store some meta data with a snapshot that refers you back
/// to the input file. The path stored here is made relative to the
/// workspace root before storing with the snapshot.
pub fn set_input_file<P: AsRef<Path>>(&mut self, p: P) {
self._private_inner_mut().input_file = Some(p.as_ref().to_path_buf());
}

/// Removes the input file reference.
pub fn remove_input_file(&mut self) {
self._private_inner_mut().input_file = None;
}

/// Returns the current input file reference.
pub fn input_file(&self) -> Option<&Path> {
self.inner.input_file.as_deref()
}

/// Registers redactions that should be applied.
///
/// This can be useful if redactions must be shared across multiple
Expand Down
8 changes: 8 additions & 0 deletions src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ pub struct MetaData {
/// Optionally the expression that created the snapshot.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) expression: Option<String>,
/// Reference to the input file.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) input_file: Option<String>,
}

impl MetaData {
Expand All @@ -99,6 +102,11 @@ impl MetaData {
.unwrap_or_else(|| base.to_path_buf())
})
}

/// Returns the input file reference.
pub fn input_file(&self) -> Option<&str> {
self.input_file.as_deref()
}
}

/// A helper to work with stored snapshots.
Expand Down
1 change: 1 addition & 0 deletions tests/inputs/hello.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Contents of hello
Loading

0 comments on commit 61c6140

Please sign in to comment.