use similar::{ChangeTag, TextDiff};
use std::path::Path;
macro_rules! verbose_println {
($($arg:tt)*) => {
#[cfg(debug_assertions)]
eprintln!($($arg)*);
};
}
pub fn read_snapshot<P: AsRef<Path>>(path: P) -> Result<String, Box<dyn std::error::Error>> {
let path = path.as_ref();
let zst_path = path.with_extension("snap.zst");
if zst_path.exists() {
let compressed_data = std::fs::read(&zst_path)?;
let decompressed = zstd::decode_all(&compressed_data[..])?;
Ok(String::from_utf8(decompressed)?)
}
else if path.exists() {
std::fs::read_to_string(path).map_err(Into::into)
} else {
Err(format!(
"Snapshot file not found: {} or {}",
path.display(),
zst_path.display()
)
.into())
}
}
fn ensure_snapshots_synchronized(snapshot_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
if !snapshot_dir.exists() {
return Ok(()); }
for entry in std::fs::read_dir(snapshot_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension() == Some("zst".as_ref()) && path.to_string_lossy().ends_with(".snap.zst")
{
let snap_path = path.with_extension("");
let should_decompress = !snap_path.exists() || {
match (read_snapshot(&snap_path), read_snapshot(&path)) {
(Ok(snap_content), Ok(zst_content)) => snap_content != zst_content,
_ => true, }
};
if should_decompress {
let compressed_data = std::fs::read(&path)?;
let decompressed = zstd::decode_all(&compressed_data[..])?;
std::fs::write(&snap_path, &decompressed)?;
verbose_println!("Synchronized {} -> {}", path.display(), snap_path.display());
}
}
}
Ok(())
}
#[derive(Debug)]
pub struct SnapshotMismatch {
pub snapshot_name: String,
pub diff_path: std::path::PathBuf,
pub new_path: std::path::PathBuf,
}
impl std::fmt::Display for SnapshotMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Snapshot mismatch for '{}'\n Diff: {}\n New: {}",
self.snapshot_name,
self.diff_path.display(),
self.new_path.display()
)
}
}
pub fn check_snapshot_gz(snapshot_name: &str, value: &str) -> Result<(), SnapshotMismatch> {
let snapshot_dir = get_snapshot_dir();
let snapshot_path = snapshot_dir.join(format!("{}.snap", snapshot_name));
if let Err(e) = ensure_snapshots_synchronized(&snapshot_dir) {
eprintln!("Warning: Failed to synchronize snapshots: {}", e);
}
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path(&snapshot_dir);
settings.set_prepend_module_to_snapshot(false);
let result = std::panic::catch_unwind(|| {
settings.bind(|| {
insta::assert_snapshot!(snapshot_name, value);
});
});
match result {
Ok(_) => {
Ok(())
}
Err(_) => {
create_review_files(&snapshot_path, snapshot_name);
Err(SnapshotMismatch {
snapshot_name: snapshot_name.to_string(),
diff_path: snapshot_path.with_extension("snap.diff"),
new_path: snapshot_path.with_extension("snap.new"),
})
}
}
}
fn create_review_files(snapshot_path: &std::path::Path, snapshot_name: &str) {
let new_path = snapshot_path.with_extension("snap.new");
let diff_path = snapshot_path.with_extension("snap.diff");
let old_path = snapshot_path.with_extension("snap.old");
if !old_path.exists() {
if let Ok(expected_content) = read_snapshot(snapshot_path) {
if let Err(e) = std::fs::write(&old_path, &expected_content) {
eprintln!("Failed to write .snap.old file: {}", e);
} else {
verbose_println!("Current snapshot saved to: {}", old_path.display());
}
}
}
if new_path.exists() && !diff_path.exists() {
verbose_println!("Creating diff for {}", snapshot_name);
if let (Ok(new_content), Ok(expected_content)) = (
std::fs::read_to_string(&new_path),
read_snapshot(snapshot_path),
) {
let diff_content = create_diff(&expected_content, &new_content, snapshot_name);
if let Err(e) = std::fs::write(&diff_path, &diff_content) {
eprintln!("Failed to write diff file: {}", e);
} else {
verbose_println!("Diff written to: {}", diff_path.display());
}
} else {
verbose_println!("Could not read files for diff creation");
}
}
}
pub fn assert_snapshot_gz(snapshot_name: &str, value: &str) {
if let Err(mismatch) = check_snapshot_gz(snapshot_name, value) {
panic!(
"Snapshot mismatch for '{}':\n\nDiff available at: {}\nNew snapshot at: {}\n\nRun ./scripts/insta-zstd.sh to accept the new snapshot.\n",
mismatch.snapshot_name,
mismatch.diff_path.display(),
mismatch.new_path.display(),
);
}
}
fn create_diff(expected: &str, actual: &str, snapshot_name: &str) -> String {
let diff = TextDiff::from_lines(expected, actual);
let mut output = String::new();
output.push_str(&format!("--- {}.snap (expected)\n", snapshot_name));
output.push_str(&format!("+++ {}.snap (actual)\n", snapshot_name));
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
output.push_str("...\n");
}
for op in group {
for change in diff.iter_inline_changes(op) {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
ChangeTag::Equal => " ",
};
output.push_str(sign);
for (emphasized, value) in change.iter_strings_lossy() {
if emphasized {
output.push_str(&format!("«{}»", value));
} else {
output.push_str(&value);
}
}
if change.missing_newline() {
output.push_str("\n\\ No newline at end of file\n");
}
}
}
}
if output.lines().count() <= 2 {
output.push_str("No differences found (this shouldn't happen)\n");
}
output
}
fn get_snapshot_dir() -> std::path::PathBuf {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
panic!(
"CARGO_MANIFEST_DIR environment variable not found!\n\
\n\
This usually means you're running tests outside of Cargo.\n\
\n\
Solutions:\n\
- Run tests with: cargo test\n\
- If running from IDE, ensure it uses Cargo to run tests\n\
- If running binary directly, set CARGO_MANIFEST_DIR manually\n\
\n\
CARGO_MANIFEST_DIR should point to the directory containing Cargo.toml"
);
});
std::path::Path::new(&manifest_dir)
.join("tests")
.join("snapshots")
}