Skip to main content

assert_snap/
lib.rs

1pub mod assert_impl;
2pub mod redaction;
3
4#[macro_export]
5macro_rules! assert_snap {
6    ($actual:expr, $expected:expr) => {
7        let actual = format!("{}", $actual);
8        let expected = format!("{}", $expected);
9        $crate::assert_snap!(@assert_str, &actual, &expected);
10    };
11
12    ($actual:expr, $expected:expr,$($tail:tt)+) => {
13        let actual = format!("{}", $actual);
14        let expected = format!("{}", $expected);
15        $crate::assert_snap!(@assert_str, &actual, &expected, $($tail)+);
16    };
17
18    (
19        @assert_str,
20        // &str
21        $actual:expr,
22        // &str
23        $expected:expr,
24        // Rules token stream
25        $($rules:tt)+
26    ) => {
27        use redaction::*;
28
29        let mut redaction_rules = Vec::new();
30        #[allow(clippy::vec_init_then_push)]
31        {
32            assert_snap!(@munch redaction_rules ; $($rules)+);
33        }
34
35        $crate::assert_impl::assert_snap(
36            $actual,
37            $expected,
38            &redaction_rules
39        );
40    };
41
42    (
43        @assert_str,
44        // &str
45        $actual:expr,
46        // &str
47        $expected:expr
48    ) => {
49        $crate::assert_impl::assert_snap(
50            $actual,
51            $expected,
52            Default::default()
53        );
54    };
55
56    (@munch $vec:ident;) => {};
57
58    // Rule WITH limit (followed by a comma and more rules)
59    (@munch $vec:ident; [$limit:expr] $pattern:expr => $replacement:expr , $($rest:tt)*) => {
60        $vec.push(RedactionRule {
61            pattern: $pattern,
62            limit: $limit,
63            replacement: $replacement,
64        });
65        assert_snap!(@munch $vec; $($rest)*);
66    };
67
68    // Rule WITH limit (last rule)
69    (@munch $vec:ident; [$limit:expr] $pattern:expr => $replacement:expr) => {
70        $vec.push(RedactionRule {
71            pattern: $pattern,
72            limit: $limit,
73            replacement: $replacement,
74        });
75    };
76
77    // Rule WITHOUT limit (followed by a comma and more rules)
78    (@munch $vec:ident; $pattern:expr => $replacement:expr , $($rest:tt)*) => {
79        $vec.push(RedactionRule {
80            pattern: $pattern,
81            limit: 0,
82            replacement: $replacement,
83        });
84        assert_snap!(@munch $vec; $($rest)*);
85    };
86
87    // Rule WITHOUT limit (last rule)
88    (@munch $vec:ident; $pattern:expr => $replacement:expr) => {
89        $vec.push(RedactionRule {
90            pattern: $pattern,
91            limit: 0,
92            replacement: $replacement,
93        });
94    };
95}
96
97#[macro_export]
98macro_rules! assert_debug_snap {
99    ($actual:expr, $expected:expr) => {
100        let actual = format!("{:#?}", $actual);
101        let expected = format!("{:#?}", $expected);
102        $crate::assert_snap!(@assert_str, &actual, &expected);
103    };
104
105    ($actual:expr, $expected:expr,$($tail:tt)+) => {
106        let actual = format!("{:#?}", $actual);
107        let expected = format!("{:#?}", $expected);
108        $crate::assert_snap!(@assert_str, &actual, &expected, $($tail)+);
109    };
110}
111
112#[cfg(test)]
113#[allow(dead_code)]
114mod tests {
115    use super::*;
116
117    // ===== assert_snap tests =====
118
119    #[test]
120    fn test_assert_snap_no_redaction() {
121        assert_snap!("Hello World!", "Hello World!");
122    }
123
124    #[test]
125    fn test_assert_snap_basic_redaction() {
126        assert_snap!(
127            "User password is secret123",
128            "User password is ****123",
129            "secret" => "****"
130        );
131    }
132
133    #[test]
134    fn test_assert_snap_with_limit() {
135        assert_snap!(
136            "secret and secret",
137            "**** and secret",
138            [1] "secret" => "****"
139        );
140    }
141
142    #[test]
143    fn test_assert_snap_multiple_rules() {
144        assert_snap!(
145            "api_key=abc123 password=xyz789",
146            "api_key=**** password=****",
147            r"api_key=.+\s" => "api_key=**** ",
148            [1] r"password=.+" => "password=****"
149        );
150    }
151
152    #[test]
153    fn test_assert_snap_regex_special_chars() {
154        assert_snap!(
155            "price=$100.50",
156            "price=****",
157            r"\$\d+\.\d+" => "****"
158        );
159    }
160
161    #[test]
162    fn test_assert_snap_no_match() {
163        assert_snap!(
164            "nothing to hide here",
165            "nothing to hide here",
166            "secret" => "****"
167        );
168    }
169
170    #[test]
171    fn test_assert_snap_limit_zero_means_unlimited() {
172        assert_snap!(
173            "secret secret secret",
174            "**** **** ****",
175            [0] "secret" => "****"
176        );
177    }
178
179    #[test]
180    fn test_assert_snap_format_args() {
181        let name = "Alice";
182        let age = 30;
183        assert_snap!(
184            format!("User: {}, Age: {}", name, age),
185            "User: Alice, Age: 30"
186        );
187    }
188
189    // ===== assert_debug_snap tests =====
190
191    #[test]
192    fn test_assert_debug_snap_basic() {
193        assert_debug_snap!(42, 42);
194    }
195
196    #[test]
197    fn test_assert_debug_snap_struct() {
198        #[derive(Debug)]
199        struct Point {
200            x: i32,
201            y: i32,
202        }
203        assert_debug_snap!(Point { x: 1, y: 2 }, Point { x: 1, y: 2 });
204    }
205
206    #[test]
207    fn test_assert_debug_snap_vec() {
208        assert_debug_snap!(vec![1, 2, 3], vec![1, 2, 3]);
209    }
210
211    #[test]
212    fn test_assert_debug_snap_option() {
213        assert_debug_snap!(Some("value"), Some("value"));
214        assert_debug_snap!(None::<String>, None::<String>);
215    }
216
217    #[test]
218    fn test_assert_debug_snap_enum() {
219        #[derive(Debug, PartialEq)]
220        enum Color {
221            Red,
222            Green,
223            Blue,
224        }
225        assert_debug_snap!(Color::Red, Color::Red);
226        assert_debug_snap!(Color::Blue, Color::Blue);
227    }
228
229    #[test]
230    fn test_assert_debug_snap_with_redaction() {
231        assert_debug_snap!(
232            "User { name: \"Alice\", password: \"secret123\" }",
233            "User { name: \"Alice\", password: \"****123\" }",
234            "secret" => "****"
235        );
236    }
237
238    #[test]
239    fn test_assert_debug_snap_with_multiple_redaction_rules() {
240        #[derive(Debug)]
241        struct Config {
242            api_key: String,
243            password: String,
244            debug: bool,
245        }
246        let config = Config {
247            api_key: "abc123".into(),
248            password: "xyz789".into(),
249            debug: true,
250        };
251        assert_debug_snap!(
252            config,
253            Config {
254                api_key: "****".into(),
255                password: "****".into(),
256                debug: true,
257            },
258            "abc123" => "****",
259            "xyz789" => "****"
260        );
261    }
262
263    #[test]
264    fn test_assert_debug_snap_with_limit() {
265        assert_debug_snap!(
266            vec!["secret", "secret", "secret"],
267            vec!["****", "secret", "secret"],
268            [1] "secret" => "****"
269        );
270    }
271
272    #[test]
273    fn test_assert_debug_snap_complex_struct_with_redaction() {
274        #[derive(Debug)]
275        struct User {
276            id: u64,
277            email: String,
278            token: String,
279        }
280        let user = User {
281            id: 1,
282            email: "user@example.com".into(),
283            token: "Bearer abc.def.ghi".into(),
284        };
285        assert_debug_snap!(
286            user,
287            User {
288                id: 1,
289                email: "user@example.com".into(),
290                token: "Bearer ****".into(),
291            },
292            r"Bearer [A-Za-z0-9._-]+" => "Bearer ****"
293        );
294    }
295
296    #[test]
297    fn test_assert_debug_snap_map() {
298        use std::collections::BTreeMap;
299        let mut map = BTreeMap::new();
300        map.insert("key1", "value1");
301        map.insert("key2", "secret");
302        let mut expected = BTreeMap::new();
303        expected.insert("key1", "value1");
304        expected.insert("key2", "****");
305        assert_debug_snap!(
306            map,
307            expected,
308            "secret" => "****"
309        );
310    }
311
312    #[test]
313    fn test_assert_debug_snap_format_args() {
314        let name = "Bob";
315        let score = 95;
316        assert_debug_snap!(
317            format!("Player: {}, Score: {}", name, score),
318            "Player: Bob, Score: 95"
319        );
320    }
321}