use std::error::Error;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
mod inline;
mod redact;
pub use inline::{
InlineLocation, InlineSnapshotFailure, assert_inline_snapshot, normalize_inline_literal,
parse_pending_patch, pending_patch_dir,
};
pub use redact::Redactions;
#[cfg(feature = "accept")]
mod accept;
#[cfg(feature = "accept")]
pub use accept::{
AcceptError, Applied, apply_inline_patch, apply_patches_from, apply_pending_patches,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotMode {
Compare,
Update,
}
impl SnapshotMode {
#[must_use]
pub fn from_env() -> Self {
match std::env::var_os("UPDATE_SNAPSHOTS") {
Some(value) if !value.is_empty() => SnapshotMode::Update,
_ => SnapshotMode::Compare,
}
}
}
#[derive(Debug)]
pub enum SnapshotFailure {
Missing {
path: PathBuf,
},
Mismatch {
path: PathBuf,
expected: String,
actual: String,
},
Io {
path: PathBuf,
action: &'static str,
source: std::io::Error,
},
}
impl fmt::Display for SnapshotFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SnapshotFailure::Missing { path } => write!(
f,
"no snapshot at {}; rerun with UPDATE_SNAPSHOTS=1 to create it",
path.display()
),
SnapshotFailure::Mismatch { path, .. } => {
write!(f, "snapshot at {} does not match", path.display())
}
SnapshotFailure::Io {
path,
action,
source,
} => write!(f, "I/O error {action} ({}): {source}", path.display()),
}
}
}
impl Error for SnapshotFailure {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
SnapshotFailure::Io { source, .. } => Some(source),
_ => None,
}
}
}
#[must_use]
pub fn snapshot_path(dir: &Path, module_path: &str, name: &str) -> PathBuf {
dir.join(format!(
"{}__{}.snap",
sanitize(module_path),
sanitize(name)
))
}
fn sanitize(raw: &str) -> String {
raw.replace("::", "__")
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect()
}
pub fn assert_snapshot_in(
dir: &Path,
module_path: &str,
name: &str,
actual: &str,
mode: SnapshotMode,
) -> Result<(), SnapshotFailure> {
let path = snapshot_path(dir, module_path, name);
match mode {
SnapshotMode::Update => {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|source| SnapshotFailure::Io {
path: path.clone(),
action: "creating the snapshot directory",
source,
})?;
}
fs::write(&path, actual).map_err(|source| SnapshotFailure::Io {
path,
action: "writing the snapshot file",
source,
})
}
SnapshotMode::Compare => match fs::read_to_string(&path) {
Ok(expected) if expected == actual => Ok(()),
Ok(expected) => Err(SnapshotFailure::Mismatch {
path,
expected,
actual: actual.to_string(),
}),
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
Err(SnapshotFailure::Missing { path })
}
Err(source) => Err(SnapshotFailure::Io {
path,
action: "reading the snapshot file",
source,
}),
},
}
}
pub fn assert_snapshot(module_path: &str, name: &str, actual: &str) -> Result<(), SnapshotFailure> {
let base = std::env::current_dir().map_err(|source| SnapshotFailure::Io {
path: PathBuf::from("tests/snapshots"),
action: "resolving the current directory",
source,
})?;
assert_snapshot_in(
&base.join("tests").join("snapshots"),
module_path,
name,
actual,
SnapshotMode::from_env(),
)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use test_better_core::{OrFail, TestResult};
use test_better_matchers::{check, contains_str, eq, is_true};
use super::*;
#[test]
fn snapshot_path_joins_module_and_name_with_a_snap_extension() -> TestResult {
let path = snapshot_path(Path::new("tests/snapshots"), "snapshot", "homepage");
check!(path).satisfies(eq(PathBuf::from("tests/snapshots/snapshot__homepage.snap")))
}
#[test]
fn snapshot_path_collapses_module_separators_and_sanitizes() -> TestResult {
let path = snapshot_path(Path::new("snaps"), "my_crate::ui::pages", "home page/v2");
check!(path).satisfies(eq(PathBuf::from(
"snaps/my_crate__ui__pages__home_page_v2.snap",
)))
}
#[test]
fn missing_file_in_compare_mode_is_a_missing_failure() -> TestResult {
let dir = scratch_dir("missing");
let outcome = assert_snapshot_in(&dir, "t", "absent", "value", SnapshotMode::Compare);
let failure = outcome
.err()
.or_fail_with("a missing snapshot should fail")?;
check!(matches!(failure, SnapshotFailure::Missing { .. })).satisfies(is_true())?;
check!(failure.to_string().as_str()).satisfies(contains_str("UPDATE_SNAPSHOTS=1"))?;
let _ = fs::remove_dir_all(&dir);
Ok(())
}
fn scratch_dir(tag: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"test-better-snapshot-{}-{}",
std::process::id(),
tag
))
}
}