ustar_test_utils/
snapshot_utils.rs1use similar::{ChangeTag, TextDiff};
2use std::path::Path;
3
4macro_rules! verbose_println {
6 ($($arg:tt)*) => {
7 #[cfg(debug_assertions)]
10 eprintln!($($arg)*);
11 };
12}
13
14pub fn read_snapshot<P: AsRef<Path>>(path: P) -> Result<String, Box<dyn std::error::Error>> {
17 let path = path.as_ref();
18 let zst_path = path.with_extension("snap.zst");
19
20 if zst_path.exists() {
22 let compressed_data = std::fs::read(&zst_path)?;
23 let decompressed = zstd::decode_all(&compressed_data[..])?;
24 Ok(String::from_utf8(decompressed)?)
25 }
26 else if path.exists() {
28 std::fs::read_to_string(path).map_err(Into::into)
29 } else {
30 Err(format!(
31 "Snapshot file not found: {} or {}",
32 path.display(),
33 zst_path.display()
34 )
35 .into())
36 }
37}
38
39fn ensure_snapshots_synchronized(snapshot_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
42 if !snapshot_dir.exists() {
44 return Ok(()); }
46
47 for entry in std::fs::read_dir(snapshot_dir)? {
48 let entry = entry?;
49 let path = entry.path();
50
51 if path.extension() == Some("zst".as_ref()) && path.to_string_lossy().ends_with(".snap.zst")
53 {
54 let snap_path = path.with_extension(""); let should_decompress = !snap_path.exists() || {
59 match (read_snapshot(&snap_path), read_snapshot(&path)) {
61 (Ok(snap_content), Ok(zst_content)) => snap_content != zst_content,
62 _ => true, }
64 };
65
66 if should_decompress {
67 let compressed_data = std::fs::read(&path)?;
69 let decompressed = zstd::decode_all(&compressed_data[..])?;
70 std::fs::write(&snap_path, &decompressed)?;
71 verbose_println!("Synchronized {} -> {}", path.display(), snap_path.display());
72 }
73 }
74 }
75
76 Ok(())
77}
78
79#[derive(Debug)]
81pub struct SnapshotMismatch {
82 pub snapshot_name: String,
83 pub diff_path: std::path::PathBuf,
84 pub new_path: std::path::PathBuf,
85}
86
87impl std::fmt::Display for SnapshotMismatch {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(
90 f,
91 "Snapshot mismatch for '{}'\n Diff: {}\n New: {}",
92 self.snapshot_name,
93 self.diff_path.display(),
94 self.new_path.display()
95 )
96 }
97}
98
99pub fn check_snapshot_gz(snapshot_name: &str, value: &str) -> Result<(), SnapshotMismatch> {
108 let snapshot_dir = get_snapshot_dir();
109 let snapshot_path = snapshot_dir.join(format!("{}.snap", snapshot_name));
110
111 if let Err(e) = ensure_snapshots_synchronized(&snapshot_dir) {
113 eprintln!("Warning: Failed to synchronize snapshots: {}", e);
114 }
115
116 let mut settings = insta::Settings::clone_current();
118 settings.set_snapshot_path(&snapshot_dir);
119 settings.set_prepend_module_to_snapshot(false);
120
121 let result = std::panic::catch_unwind(|| {
122 settings.bind(|| {
123 insta::assert_snapshot!(snapshot_name, value);
124 });
125 });
126
127 match result {
128 Ok(_) => {
129 Ok(())
131 }
132 Err(_) => {
133 create_review_files(&snapshot_path, snapshot_name);
136
137 Err(SnapshotMismatch {
138 snapshot_name: snapshot_name.to_string(),
139 diff_path: snapshot_path.with_extension("snap.diff"),
140 new_path: snapshot_path.with_extension("snap.new"),
141 })
142 }
143 }
144}
145
146fn create_review_files(snapshot_path: &std::path::Path, snapshot_name: &str) {
148 let new_path = snapshot_path.with_extension("snap.new");
150 let diff_path = snapshot_path.with_extension("snap.diff");
151 let old_path = snapshot_path.with_extension("snap.old");
152
153 if !old_path.exists() {
155 if let Ok(expected_content) = read_snapshot(snapshot_path) {
156 if let Err(e) = std::fs::write(&old_path, &expected_content) {
157 eprintln!("Failed to write .snap.old file: {}", e);
158 } else {
159 verbose_println!("Current snapshot saved to: {}", old_path.display());
160 }
161 }
162 }
163
164 if new_path.exists() && !diff_path.exists() {
166 verbose_println!("Creating diff for {}", snapshot_name);
167 if let (Ok(new_content), Ok(expected_content)) = (
168 std::fs::read_to_string(&new_path),
169 read_snapshot(snapshot_path),
170 ) {
171 let diff_content = create_diff(&expected_content, &new_content, snapshot_name);
173 if let Err(e) = std::fs::write(&diff_path, &diff_content) {
174 eprintln!("Failed to write diff file: {}", e);
175 } else {
176 verbose_println!("Diff written to: {}", diff_path.display());
177 }
178 } else {
179 verbose_println!("Could not read files for diff creation");
180 }
181 }
182}
183
184pub fn assert_snapshot_gz(snapshot_name: &str, value: &str) {
192 if let Err(mismatch) = check_snapshot_gz(snapshot_name, value) {
193 panic!(
194 "Snapshot mismatch for '{}':\n\nDiff available at: {}\nNew snapshot at: {}\n\nRun ./scripts/insta-zstd.sh to accept the new snapshot.\n",
195 mismatch.snapshot_name,
196 mismatch.diff_path.display(),
197 mismatch.new_path.display(),
198 );
199 }
200}
201
202fn create_diff(expected: &str, actual: &str, snapshot_name: &str) -> String {
204 let diff = TextDiff::from_lines(expected, actual);
205 let mut output = String::new();
206
207 output.push_str(&format!("--- {}.snap (expected)\n", snapshot_name));
208 output.push_str(&format!("+++ {}.snap (actual)\n", snapshot_name));
209
210 for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
211 if idx > 0 {
212 output.push_str("...\n");
213 }
214
215 for op in group {
216 for change in diff.iter_inline_changes(op) {
217 let sign = match change.tag() {
218 ChangeTag::Delete => "-",
219 ChangeTag::Insert => "+",
220 ChangeTag::Equal => " ",
221 };
222
223 output.push_str(sign);
224 for (emphasized, value) in change.iter_strings_lossy() {
225 if emphasized {
226 output.push_str(&format!("«{}»", value));
227 } else {
228 output.push_str(&value);
229 }
230 }
231 if change.missing_newline() {
232 output.push_str("\n\\ No newline at end of file\n");
233 }
234 }
235 }
236 }
237
238 if output.lines().count() <= 2 {
239 output.push_str("No differences found (this shouldn't happen)\n");
240 }
241
242 output
243}
244
245fn get_snapshot_dir() -> std::path::PathBuf {
252 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
253 panic!(
254 "CARGO_MANIFEST_DIR environment variable not found!\n\
255 \n\
256 This usually means you're running tests outside of Cargo.\n\
257 \n\
258 Solutions:\n\
259 - Run tests with: cargo test\n\
260 - If running from IDE, ensure it uses Cargo to run tests\n\
261 - If running binary directly, set CARGO_MANIFEST_DIR manually\n\
262 \n\
263 CARGO_MANIFEST_DIR should point to the directory containing Cargo.toml"
264 );
265 });
266
267 std::path::Path::new(&manifest_dir)
268 .join("tests")
269 .join("snapshots")
270}