use std::path::PathBuf;
use std::{env, fs};
use ron::ser::{PrettyConfig, to_string_pretty};
use serde::Serialize;
use serde::de::DeserializeOwned;
fn manifest_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
pub fn snapshot_path(test_dir: &str) -> PathBuf {
manifest_dir()
.join("test_resources")
.join(test_dir)
.join("snapshot.ron")
}
pub fn inline_snapshot_path(name: &str) -> PathBuf {
manifest_dir()
.join("test_resources")
.join("snapshots")
.join(format!("{name}.ron"))
}
fn to_ron<T: Serialize>(value: &T) -> String {
let cfg = PrettyConfig::new()
.depth_limit(10)
.separate_tuple_members(false)
.enumerate_arrays(false);
to_string_pretty(value, cfg).expect("RON serialization failed")
}
pub fn save_snapshot<T: Serialize>(path: &std::path::Path, value: &T) {
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
fs::write(path, to_ron(value)).unwrap_or_else(|e| panic!("Cannot write snapshot {}: {e}", path.display()));
}
pub fn load_snapshot<T: DeserializeOwned>(path: &std::path::Path) -> Option<T> {
if !path.exists() {
return None;
}
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read snapshot {}: {e}", path.display()));
let val: T = ron::from_str(&text).unwrap_or_else(|e| panic!("Cannot deserialize snapshot {}: {e}", path.display()));
Some(val)
}
pub fn assert_snapshot<T>(test_dir: &str, value: &T)
where
T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug,
{
run_snapshot_assert(&snapshot_path(test_dir), test_dir, value);
}
pub fn assert_dir_snapshot<T>(test_dir: &str, value: &T)
where
T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug,
{
run_snapshot_assert(&snapshot_path(test_dir), test_dir, value);
}
pub fn assert_inline_snapshot<T>(name: &str, value: &T)
where
T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug,
{
run_snapshot_assert(&inline_snapshot_path(name), name, value);
}
#[expect(clippy::print_stdout)]
fn run_snapshot_assert<T>(path: &std::path::Path, label: &str, value: &T)
where
T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug,
{
match load_snapshot::<T>(path) {
None => {
save_snapshot(path, value);
println!("📸 Snapshot created: {}", path.display());
}
Some(stored) => {
if value != &stored {
let current_ron = to_ron(value);
let stored_ron = to_ron(&stored);
panic!(
"\nSnapshot mismatch for `{label}`\n\
Jeśli zmiana jest zamierzona, usuń plik:\n {}\n\
i uruchom testy ponownie.\n\n\
── stored ──────────────────────────────\n{stored_ron}\n\
── current ─────────────────────────────\n{current_ron}\n",
path.display()
);
}
}
}
}