Skip to main content

assert_snap/
lib.rs

1//! # `assert-snap`
2//!
3//! A snapshot testing and assertion library for Rust that supports flexible, regex-based dynamic data redactions and detailed unified diff output.
4//!
5//! ## Main Macros
6//!
7//! - [`assert_snap!`]: Asserts any types implementing [`Display`](std::fmt::Display).
8//! - [`assert_debug_snap!`]: Asserts any types implementing [`Debug`](std::fmt::Debug).
9//!
10//! ## Quick Example
11//!
12//! ```rust
13//! use assert_snap::{assert_snap, assert_debug_snap};
14//!
15//! // Simple string assertion with regex redaction
16//! assert_snap!(
17//!     "User id: 12345, status: active",
18//!     "User id: [ID], status: active",
19//!     r"\d+" => "[ID]"
20//! );
21//!
22//! // Debug format assertion
23//! assert_debug_snap!(Some("secret"), Some("secret"));
24//! ```
25
26pub mod assert_impl;
27pub mod redaction;
28
29/// Asserts any types implementing [`Display`](std::fmt::Display).
30///
31/// # Syntax
32///
33/// ```rust,ignore
34/// assert_snap!(actual, expected);
35/// assert_snap!(actual, expected, "pattern" => "replacement");
36/// assert_snap!(actual, expected, [limit] "pattern" => "replacement");
37/// assert_snap!(actual, expected, rule1, rule2, ...);
38/// ```
39///
40/// # Parameters
41///
42/// - `$actual`: An expression implementing [`Display`](std::fmt::Display) or convertible via `format!("{actual}")`.
43/// - `$expected`: An expression implementing [`Display`](std::fmt::Display) or convertible via `format!("{expected}")`.
44/// - `[limit] pattern => replacement`: Optional redaction rules to scrub dynamic data (e.g. timestamps, UUIDs) before comparison.
45///   - `limit` (optional): `[n]` limits replacement to the first `n` matches. If omitted or `[0]`, all matches are replaced.
46///   - `pattern`: A regex pattern string.
47///   - `replacement`: Replacement string or regex group reference (e.g. `"$1"`).
48///
49/// # Examples
50///
51/// Basic comparison:
52/// ```rust
53/// use assert_snap::assert_snap;
54///
55/// assert_snap!("hello world", "hello world");
56/// ```
57///
58/// Comparison with redaction rules:
59/// ```rust
60/// use assert_snap::assert_snap;
61///
62/// assert_snap!(
63///     "User id is 12345",
64///     "User id is [ID]",
65///     r"\d+" => "[ID]"
66/// );
67/// ```
68///
69/// Comparison with redaction rules with match limits:
70/// ```rust
71/// use assert_snap::assert_snap;
72///
73/// assert_snap!(
74///     "secret and secret",
75///     "**** and secret",
76///     [1] "secret" => "****"
77/// );
78/// ```
79#[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        // &str
96        $actual:expr,
97        // &str
98        $expected:expr,
99        // Rules token stream
100        $($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        // &str
118        $actual:expr,
119        // &str
120        $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    // Rule WITH limit (followed by a comma and more rules)
132    (@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    // Rule WITH limit (last rule)
142    (@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    // Rule WITHOUT limit (followed by a comma and more rules)
151    (@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    // Rule WITHOUT limit (last rule)
161    (@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/// Asserts any types implementing [`Debug`](std::fmt::Debug).
171///
172/// The macro formats `actual` and `expected` using `{:#?}` pretty-printing prior to comparison.
173///
174/// # Syntax
175///
176/// ```rust,ignore
177/// assert_debug_snap!(actual, expected);
178/// assert_debug_snap!(actual, expected, "pattern" => "replacement");
179/// assert_debug_snap!(actual, expected, [limit] "pattern" => "replacement");
180/// assert_debug_snap!(actual, expected, rule1, rule2, ...);
181/// ```
182///
183/// # Examples
184///
185/// Basic debug assertion:
186/// ```rust
187/// use assert_snap::assert_debug_snap;
188///
189/// assert_debug_snap!(Some(42), Some(42));
190/// ```
191///
192/// Debug assertion with redaction rules:
193/// ```rust
194/// use assert_snap::assert_debug_snap;
195///
196/// #[derive(Debug)]
197/// struct User {
198///     name: String,
199///     token: String,
200/// }
201///
202/// let user = User { name: "Alice".into(), token: "secret_123".into() };
203/// assert_debug_snap!(
204///     user,
205///     User {
206///         name: "Alice".to_string(),
207///         token: "****".to_string(),
208///     },
209///     "secret_123" => "****"
210/// );
211/// ```
212#[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    // ===== assert_snap tests =====
232
233    #[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    // ===== assert_debug_snap tests =====
304
305    #[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}