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 $actual:expr,
22 $expected:expr,
24 $($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 $actual:expr,
46 $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 (@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 (@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 (@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 (@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 #[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 #[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}