facet_assert/
lib.rs

1#![warn(missing_docs)]
2#![forbid(unsafe_code)]
3#![doc = include_str!("../README.md")]
4
5//! Pretty assertions for Facet types.
6//!
7//! Unlike `assert_eq!` which requires `PartialEq`, `assert_same!` works with any
8//! `Facet` type by doing structural comparison via reflection.
9
10mod same;
11
12pub use facet_diff::DiffReport;
13pub use facet_diff_core::layout::{
14    AnsiBackend, BuildOptions, ColorBackend, DiffFlavor, JsonFlavor, PlainBackend, RenderOptions,
15    RustFlavor, XmlFlavor,
16};
17pub use same::{
18    SameOptions, SameReport, Sameness, check_same, check_same_report, check_same_with,
19    check_same_with_report,
20};
21
22/// Asserts that two values are structurally the same.
23///
24/// This macro does not require `PartialEq` - it uses Facet reflection to
25/// compare values structurally. Two values are "same" if they have the same
26/// structure and the same field values, even if they have different type names.
27///
28/// # Panics
29///
30/// Panics if the values are not structurally same, displaying a colored diff
31/// showing exactly what differs.
32///
33/// Also panics if either value contains an opaque type that cannot be inspected.
34///
35/// # Example
36///
37/// ```
38/// use facet::Facet;
39/// use facet_assert::assert_same;
40///
41/// #[derive(Facet)]
42/// struct Person {
43///     name: String,
44///     age: u32,
45/// }
46///
47/// let a = Person { name: "Alice".into(), age: 30 };
48/// let b = Person { name: "Alice".into(), age: 30 };
49/// assert_same!(a, b);
50/// ```
51#[macro_export]
52macro_rules! assert_same {
53    ($left:expr, $right:expr $(,)?) => {
54        match $crate::check_same(&$left, &$right) {
55            $crate::Sameness::Same => {}
56            $crate::Sameness::Different(diff) => {
57                panic!(
58                    "assertion `assert_same!(left, right)` failed\n\n{diff}\n"
59                );
60            }
61            $crate::Sameness::Opaque { type_name } => {
62                panic!(
63                    "assertion `assert_same!(left, right)` failed: cannot compare opaque type `{type_name}`"
64                );
65            }
66        }
67    };
68    ($left:expr, $right:expr, $($arg:tt)+) => {
69        match $crate::check_same(&$left, &$right) {
70            $crate::Sameness::Same => {}
71            $crate::Sameness::Different(diff) => {
72                panic!(
73                    "assertion `assert_same!(left, right)` failed: {}\n\n{diff}\n",
74                    format_args!($($arg)+)
75                );
76            }
77            $crate::Sameness::Opaque { type_name } => {
78                panic!(
79                    "assertion `assert_same!(left, right)` failed: {}: cannot compare opaque type `{type_name}`",
80                    format_args!($($arg)+)
81                );
82            }
83        }
84    };
85}
86
87/// Asserts that two values are structurally the same with custom options.
88///
89/// Like [`assert_same!`], but allows configuring comparison behavior via [`SameOptions`].
90///
91/// # Panics
92///
93/// Panics if the values are not structurally same, displaying a colored diff
94/// showing exactly what differs.
95///
96/// Also panics if either value contains an opaque type that cannot be inspected.
97///
98/// # Example
99///
100/// ```
101/// use facet_assert::{assert_same_with, SameOptions};
102///
103/// let a = 1.0000001_f64;
104/// let b = 1.0000002_f64;
105///
106/// // This would fail with exact comparison:
107/// // assert_same!(a, b);
108///
109/// // But passes with tolerance:
110/// assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
111/// ```
112#[macro_export]
113macro_rules! assert_same_with {
114    ($left:expr, $right:expr, $options:expr $(,)?) => {
115        match $crate::check_same_with(&$left, &$right, $options) {
116            $crate::Sameness::Same => {}
117            $crate::Sameness::Different(diff) => {
118                panic!(
119                    "assertion `assert_same_with!(left, right, options)` failed\n\n{diff}\n"
120                );
121            }
122            $crate::Sameness::Opaque { type_name } => {
123                panic!(
124                    "assertion `assert_same_with!(left, right, options)` failed: cannot compare opaque type `{type_name}`"
125                );
126            }
127        }
128    };
129    ($left:expr, $right:expr, $options:expr, $($arg:tt)+) => {
130        match $crate::check_same_with(&$left, &$right, $options) {
131            $crate::Sameness::Same => {}
132            $crate::Sameness::Different(diff) => {
133                panic!(
134                    "assertion `assert_same_with!(left, right, options)` failed: {}\n\n{diff}\n",
135                    format_args!($($arg)+)
136                );
137            }
138            $crate::Sameness::Opaque { type_name } => {
139                panic!(
140                    "assertion `assert_same_with!(left, right, options)` failed: {}: cannot compare opaque type `{type_name}`",
141                    format_args!($($arg)+)
142                );
143            }
144        }
145    };
146}
147
148/// Asserts that two values are structurally the same (debug builds only).
149///
150/// Like [`assert_same!`], but only enabled in debug builds.
151#[macro_export]
152macro_rules! debug_assert_same {
153    ($($arg:tt)*) => {
154        if cfg!(debug_assertions) {
155            $crate::assert_same!($($arg)*);
156        }
157    };
158}
159
160/// Asserts that two values are structurally the same with custom options (debug builds only).
161///
162/// Like [`assert_same_with!`], but only enabled in debug builds.
163#[macro_export]
164macro_rules! debug_assert_same_with {
165    ($($arg:tt)*) => {
166        if cfg!(debug_assertions) {
167            $crate::assert_same_with!($($arg)*);
168        }
169    };
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use facet::Facet;
176
177    #[derive(Facet)]
178    struct Person {
179        name: String,
180        age: u32,
181    }
182
183    #[derive(Facet)]
184    struct PersonV2 {
185        name: String,
186        age: u32,
187    }
188
189    #[derive(Facet)]
190    struct Different {
191        name: String,
192        score: f64,
193    }
194
195    #[test]
196    fn same_type_same_values() {
197        let a = Person {
198            name: "Alice".into(),
199            age: 30,
200        };
201        let b = Person {
202            name: "Alice".into(),
203            age: 30,
204        };
205        assert_same!(a, b);
206    }
207
208    #[test]
209    fn different_types_same_structure() {
210        let a = Person {
211            name: "Alice".into(),
212            age: 30,
213        };
214        let b = PersonV2 {
215            name: "Alice".into(),
216            age: 30,
217        };
218        assert_same!(a, b);
219    }
220
221    #[test]
222    fn same_type_different_values() {
223        let a = Person {
224            name: "Alice".into(),
225            age: 30,
226        };
227        let b = Person {
228            name: "Bob".into(),
229            age: 30,
230        };
231
232        match check_same(&a, &b) {
233            Sameness::Different(_) => {} // expected
234            other => panic!(
235                "expected Different, got {:?}",
236                matches!(other, Sameness::Same)
237            ),
238        }
239    }
240
241    #[test]
242    fn diff_report_renders_multiple_flavors() {
243        let a = Person {
244            name: "Alice".into(),
245            age: 30,
246        };
247        let b = Person {
248            name: "Bob".into(),
249            age: 45,
250        };
251
252        let report = match check_same_report(&a, &b) {
253            SameReport::Different(report) => report,
254            _ => panic!("expected Different"),
255        };
256
257        let rust = report.render_plain_rust();
258        assert!(rust.contains("Person"));
259
260        let json = report.render_plain_json();
261        assert!(json.contains("\"name\""));
262
263        let xml = report.render_plain_xml();
264        // Person is a proxy type (struct without xml:: attributes), so it gets @ prefix
265        assert!(xml.contains("<@Person"));
266    }
267
268    #[test]
269    fn primitives() {
270        assert_same!(42i32, 42i32);
271        assert_same!("hello", "hello");
272        assert_same!(true, true);
273    }
274
275    #[test]
276    fn vectors() {
277        let a = vec![1, 2, 3];
278        let b = vec![1, 2, 3];
279        assert_same!(a, b);
280    }
281
282    #[test]
283    fn vectors_different() {
284        let a = vec![1, 2, 3];
285        let b = vec![1, 2, 4];
286
287        match check_same(&a, &b) {
288            Sameness::Different(_) => {} // expected
289            _ => panic!("expected Different"),
290        }
291    }
292
293    #[test]
294    fn options() {
295        let a: Option<i32> = Some(42);
296        let b: Option<i32> = Some(42);
297        assert_same!(a, b);
298
299        let c: Option<i32> = None;
300        let d: Option<i32> = None;
301        assert_same!(c, d);
302    }
303
304    #[test]
305    fn float_exact_same() {
306        let a = 1.0_f64;
307        let b = 1.0_f64;
308        assert_same!(a, b);
309    }
310
311    #[test]
312    fn float_exact_different() {
313        let a = 1.0000001_f64;
314        let b = 1.0000002_f64;
315
316        match check_same(&a, &b) {
317            Sameness::Different(_) => {} // expected - exact comparison fails
318            _ => panic!("expected Different"),
319        }
320    }
321
322    #[test]
323    fn float_with_tolerance_same() {
324        let a = 1.0000001_f64;
325        let b = 1.0000002_f64;
326
327        // With tolerance, these should be considered the same
328        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
329    }
330
331    #[test]
332    fn float_with_tolerance_different() {
333        let a = 1.0_f64;
334        let b = 2.0_f64;
335
336        // Even with tolerance, 1.0 vs 2.0 are different
337        match check_same_with(&a, &b, SameOptions::new().float_tolerance(1e-6)) {
338            Sameness::Different(_) => {} // expected
339            _ => panic!("expected Different"),
340        }
341    }
342
343    #[test]
344    fn f32_with_tolerance() {
345        let a = 1.0000001_f32;
346        let b = 1.0000002_f32;
347
348        // With tolerance, these should be considered the same
349        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-5));
350    }
351
352    #[test]
353    fn struct_with_float_tolerance() {
354        #[derive(Facet)]
355        struct Measurement {
356            name: String,
357            value: f64,
358        }
359
360        let a = Measurement {
361            name: "temperature".into(),
362            value: 98.6000001,
363        };
364        let b = Measurement {
365            name: "temperature".into(),
366            value: 98.6000002,
367        };
368
369        // Exact comparison fails
370        match check_same(&a, &b) {
371            Sameness::Different(_) => {} // expected
372            _ => panic!("expected Different"),
373        }
374
375        // With tolerance, they're the same
376        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
377    }
378
379    #[test]
380    fn vec_of_floats_with_tolerance() {
381        let a = vec![1.0000001_f64, 2.0000001_f64, 3.0000001_f64];
382        let b = vec![1.0000002_f64, 2.0000002_f64, 3.0000002_f64];
383
384        // Exact comparison fails
385        match check_same(&a, &b) {
386            Sameness::Different(_) => {} // expected
387            _ => panic!("expected Different"),
388        }
389
390        // With tolerance, they're the same
391        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
392    }
393
394    #[test]
395    fn nested_struct_with_float_tolerance() {
396        #[derive(Facet)]
397        struct Point {
398            x: f64,
399            y: f64,
400        }
401
402        #[derive(Facet)]
403        struct Line {
404            start: Point,
405            end: Point,
406        }
407
408        let a = Line {
409            start: Point {
410                x: 0.0000001,
411                y: 0.0000001,
412            },
413            end: Point {
414                x: 1.0000001,
415                y: 1.0000001,
416            },
417        };
418        let b = Line {
419            start: Point {
420                x: 0.0000002,
421                y: 0.0000002,
422            },
423            end: Point {
424                x: 1.0000002,
425                y: 1.0000002,
426            },
427        };
428
429        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
430    }
431}