1pub mod assert_impl;
27pub mod redaction;
28
29#[macro_export]
80macro_rules! assert_snap {
81 ($actual:expr, $expected:expr) => {
82 let actual = format!("{}", $actual);
83 let expected = format!("{}", $expected);
84 $crate::assert_snap!(@assert_str, &actual, &expected);
85 };
86
87 ($actual:expr, $expected:expr,$($tail:tt)+) => {
88 let actual = format!("{}", $actual);
89 let expected = format!("{}", $expected);
90 $crate::assert_snap!(@assert_str, &actual, &expected, $($tail)+);
91 };
92
93 (
94 @assert_str,
95 $actual:expr,
97 $expected:expr,
99 $($rules:tt)+
101 ) => {
102 let mut redaction_rules = Vec::new();
103 #[allow(clippy::vec_init_then_push)]
104 {
105 $crate::assert_snap!(@munch redaction_rules ; $($rules)+);
106 }
107
108 $crate::assert_impl::assert_snap(
109 $actual,
110 $expected,
111 &redaction_rules
112 );
113 };
114
115 (
116 @assert_str,
117 $actual:expr,
119 $expected:expr
121 ) => {
122 $crate::assert_impl::assert_snap(
123 $actual,
124 $expected,
125 Default::default()
126 );
127 };
128
129 (@munch $vec:ident;) => {};
130
131 (@munch $vec:ident; [$limit:expr] $pattern:expr => $replacement:expr , $($rest:tt)*) => {
133 $vec.push($crate::redaction::RedactionRule {
134 pattern: $pattern,
135 limit: $limit,
136 replacement: $replacement,
137 });
138 $crate::assert_snap!(@munch $vec; $($rest)*);
139 };
140
141 (@munch $vec:ident; [$limit:expr] $pattern:expr => $replacement:expr) => {
143 $vec.push($crate::redaction::RedactionRule {
144 pattern: $pattern,
145 limit: $limit,
146 replacement: $replacement,
147 });
148 };
149
150 (@munch $vec:ident; $pattern:expr => $replacement:expr , $($rest:tt)*) => {
152 $vec.push($crate::redaction::RedactionRule {
153 pattern: $pattern,
154 limit: 0,
155 replacement: $replacement,
156 });
157 $crate::assert_snap!(@munch $vec; $($rest)*);
158 };
159
160 (@munch $vec:ident; $pattern:expr => $replacement:expr) => {
162 $vec.push($crate::redaction::RedactionRule {
163 pattern: $pattern,
164 limit: 0,
165 replacement: $replacement,
166 });
167 };
168}
169
170#[macro_export]
213macro_rules! assert_debug_snap {
214 ($actual:expr, $expected:expr) => {
215 let actual = format!("{:#?}", $actual);
216 let expected = format!("{:#?}", $expected);
217 $crate::assert_snap!(@assert_str, &actual, &expected);
218 };
219
220 ($actual:expr, $expected:expr,$($tail:tt)+) => {
221 let actual = format!("{:#?}", $actual);
222 let expected = format!("{:#?}", $expected);
223 $crate::assert_snap!(@assert_str, &actual, &expected, $($tail)+);
224 };
225}
226
227#[cfg(test)]
228#[allow(dead_code)]
229mod tests {
230
231 #[test]
234 fn test_assert_snap_no_redaction() {
235 assert_snap!("Hello World!", "Hello World!");
236 }
237
238 #[test]
239 fn test_assert_snap_basic_redaction() {
240 assert_snap!(
241 "User password is secret123",
242 "User password is ****123",
243 "secret" => "****"
244 );
245 }
246
247 #[test]
248 fn test_assert_snap_with_limit() {
249 assert_snap!(
250 "secret and secret",
251 "**** and secret",
252 [1] "secret" => "****"
253 );
254 }
255
256 #[test]
257 fn test_assert_snap_multiple_rules() {
258 assert_snap!(
259 "api_key=abc123 password=xyz789",
260 "api_key=**** password=****",
261 r"api_key=.+\s" => "api_key=**** ",
262 [1] r"password=.+" => "password=****"
263 );
264 }
265
266 #[test]
267 fn test_assert_snap_regex_special_chars() {
268 assert_snap!(
269 "price=$100.50",
270 "price=****",
271 r"\$\d+\.\d+" => "****"
272 );
273 }
274
275 #[test]
276 fn test_assert_snap_no_match() {
277 assert_snap!(
278 "nothing to hide here",
279 "nothing to hide here",
280 "secret" => "****"
281 );
282 }
283
284 #[test]
285 fn test_assert_snap_limit_zero_means_unlimited() {
286 assert_snap!(
287 "secret secret secret",
288 "**** **** ****",
289 [0] "secret" => "****"
290 );
291 }
292
293 #[test]
294 fn test_assert_snap_format_args() {
295 let name = "Alice";
296 let age = 30;
297 assert_snap!(
298 format!("User: {}, Age: {}", name, age),
299 "User: Alice, Age: 30"
300 );
301 }
302
303 #[test]
306 fn test_assert_debug_snap_basic() {
307 assert_debug_snap!(42, 42);
308 }
309
310 #[test]
311 fn test_assert_debug_snap_struct() {
312 #[derive(Debug)]
313 struct Point {
314 x: i32,
315 y: i32,
316 }
317 assert_debug_snap!(Point { x: 1, y: 2 }, Point { x: 1, y: 2 });
318 }
319
320 #[test]
321 fn test_assert_debug_snap_vec() {
322 assert_debug_snap!(vec![1, 2, 3], vec![1, 2, 3]);
323 }
324
325 #[test]
326 fn test_assert_debug_snap_option() {
327 assert_debug_snap!(Some("value"), Some("value"));
328 assert_debug_snap!(None::<String>, None::<String>);
329 }
330
331 #[test]
332 fn test_assert_debug_snap_enum() {
333 #[derive(Debug, PartialEq)]
334 enum Color {
335 Red,
336 Green,
337 Blue,
338 }
339 assert_debug_snap!(Color::Red, Color::Red);
340 assert_debug_snap!(Color::Blue, Color::Blue);
341 }
342
343 #[test]
344 fn test_assert_debug_snap_with_redaction() {
345 assert_debug_snap!(
346 "User { name: \"Alice\", password: \"secret123\" }",
347 "User { name: \"Alice\", password: \"****123\" }",
348 "secret" => "****"
349 );
350 }
351
352 #[test]
353 fn test_assert_debug_snap_with_multiple_redaction_rules() {
354 #[derive(Debug)]
355 struct Config {
356 api_key: String,
357 password: String,
358 debug: bool,
359 }
360 let config = Config {
361 api_key: "abc123".into(),
362 password: "xyz789".into(),
363 debug: true,
364 };
365 assert_debug_snap!(
366 config,
367 Config {
368 api_key: "****".into(),
369 password: "****".into(),
370 debug: true,
371 },
372 "abc123" => "****",
373 "xyz789" => "****"
374 );
375 }
376
377 #[test]
378 fn test_assert_debug_snap_with_limit() {
379 assert_debug_snap!(
380 vec!["secret", "secret", "secret"],
381 vec!["****", "secret", "secret"],
382 [1] "secret" => "****"
383 );
384 }
385
386 #[test]
387 fn test_assert_debug_snap_complex_struct_with_redaction() {
388 #[derive(Debug)]
389 struct User {
390 id: u64,
391 email: String,
392 token: String,
393 }
394 let user = User {
395 id: 1,
396 email: "user@example.com".into(),
397 token: "Bearer abc.def.ghi".into(),
398 };
399 assert_debug_snap!(
400 user,
401 User {
402 id: 1,
403 email: "user@example.com".into(),
404 token: "Bearer ****".into(),
405 },
406 r"Bearer [A-Za-z0-9._-]+" => "Bearer ****"
407 );
408 }
409
410 #[test]
411 fn test_assert_debug_snap_map() {
412 use std::collections::BTreeMap;
413 let mut map = BTreeMap::new();
414 map.insert("key1", "value1");
415 map.insert("key2", "secret");
416 let mut expected = BTreeMap::new();
417 expected.insert("key1", "value1");
418 expected.insert("key2", "****");
419 assert_debug_snap!(
420 map,
421 expected,
422 "secret" => "****"
423 );
424 }
425
426 #[test]
427 fn test_assert_debug_snap_format_args() {
428 let name = "Bob";
429 let score = 95;
430 assert_debug_snap!(
431 format!("Player: {}, Score: {}", name, score),
432 "Player: Bob, Score: 95"
433 );
434 }
435}