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, check_sameish, check_sameish_report, check_sameish_with,
20    check_sameish_with_report,
21};
22
23// =============================================================================
24// assert_same! - Same-type comparison (the common case)
25// =============================================================================
26
27/// Asserts that two values are structurally the same.
28///
29/// This macro does not require `PartialEq` - it uses Facet reflection to
30/// compare values structurally. Both values must have the same type, which
31/// enables type inference to flow between arguments.
32///
33/// For comparing values of different types (e.g., during migrations), use
34/// [`assert_sameish!`] instead.
35///
36/// # Panics
37///
38/// Panics if the values are not structurally same, displaying a colored diff
39/// showing exactly what differs.
40///
41/// Also panics if either value contains an opaque type that cannot be inspected.
42///
43/// # Example
44///
45/// ```
46/// use facet::Facet;
47/// use facet_assert::assert_same;
48///
49/// #[derive(Facet)]
50/// struct Person {
51///     name: String,
52///     age: u32,
53/// }
54///
55/// let a = Person { name: "Alice".into(), age: 30 };
56/// let b = Person { name: "Alice".into(), age: 30 };
57/// assert_same!(a, b);
58/// ```
59///
60/// Type inference works naturally:
61/// ```
62/// use facet_assert::assert_same;
63///
64/// let x: Option<Option<i32>> = Some(None);
65/// assert_same!(x, Some(None)); // Type of Some(None) inferred from x
66/// ```
67#[macro_export]
68macro_rules! assert_same {
69    ($left:expr, $right:expr $(,)?) => {
70        match $crate::check_same(&$left, &$right) {
71            $crate::Sameness::Same => {}
72            $crate::Sameness::Different(diff) => {
73                panic!(
74                    "assertion `assert_same!(left, right)` failed\n\n{diff}\n"
75                );
76            }
77            $crate::Sameness::Opaque { type_name } => {
78                panic!(
79                    "assertion `assert_same!(left, right)` failed: cannot compare opaque type `{type_name}`"
80                );
81            }
82        }
83    };
84    ($left:expr, $right:expr, $($arg:tt)+) => {
85        match $crate::check_same(&$left, &$right) {
86            $crate::Sameness::Same => {}
87            $crate::Sameness::Different(diff) => {
88                panic!(
89                    "assertion `assert_same!(left, right)` failed: {}\n\n{diff}\n",
90                    format_args!($($arg)+)
91                );
92            }
93            $crate::Sameness::Opaque { type_name } => {
94                panic!(
95                    "assertion `assert_same!(left, right)` failed: {}: cannot compare opaque type `{type_name}`",
96                    format_args!($($arg)+)
97                );
98            }
99        }
100    };
101}
102
103/// Asserts that two values are structurally the same with custom options.
104///
105/// Like [`assert_same!`], but allows configuring comparison behavior via [`SameOptions`].
106///
107/// # Panics
108///
109/// Panics if the values are not structurally same, displaying a colored diff.
110///
111/// # Example
112///
113/// ```
114/// use facet_assert::{assert_same_with, SameOptions};
115///
116/// let a = 1.0000001_f64;
117/// let b = 1.0000002_f64;
118///
119/// // This would fail with exact comparison:
120/// // assert_same!(a, b);
121///
122/// // But passes with tolerance:
123/// assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
124/// ```
125#[macro_export]
126macro_rules! assert_same_with {
127    ($left:expr, $right:expr, $options:expr $(,)?) => {
128        match $crate::check_same_with(&$left, &$right, $options) {
129            $crate::Sameness::Same => {}
130            $crate::Sameness::Different(diff) => {
131                panic!(
132                    "assertion `assert_same_with!(left, right, options)` failed\n\n{diff}\n"
133                );
134            }
135            $crate::Sameness::Opaque { type_name } => {
136                panic!(
137                    "assertion `assert_same_with!(left, right, options)` failed: cannot compare opaque type `{type_name}`"
138                );
139            }
140        }
141    };
142    ($left:expr, $right:expr, $options:expr, $($arg:tt)+) => {
143        match $crate::check_same_with(&$left, &$right, $options) {
144            $crate::Sameness::Same => {}
145            $crate::Sameness::Different(diff) => {
146                panic!(
147                    "assertion `assert_same_with!(left, right, options)` failed: {}\n\n{diff}\n",
148                    format_args!($($arg)+)
149                );
150            }
151            $crate::Sameness::Opaque { type_name } => {
152                panic!(
153                    "assertion `assert_same_with!(left, right, options)` failed: {}: cannot compare opaque type `{type_name}`",
154                    format_args!($($arg)+)
155                );
156            }
157        }
158    };
159}
160
161/// Asserts that two values are structurally the same (debug builds only).
162///
163/// Like [`assert_same!`], but only enabled in debug builds.
164#[macro_export]
165macro_rules! debug_assert_same {
166    ($($arg:tt)*) => {
167        if cfg!(debug_assertions) {
168            $crate::assert_same!($($arg)*);
169        }
170    };
171}
172
173/// Asserts that two values are structurally the same with custom options (debug builds only).
174///
175/// Like [`assert_same_with!`], but only enabled in debug builds.
176#[macro_export]
177macro_rules! debug_assert_same_with {
178    ($($arg:tt)*) => {
179        if cfg!(debug_assertions) {
180            $crate::assert_same_with!($($arg)*);
181        }
182    };
183}
184
185// =============================================================================
186// assert_sameish! - Cross-type comparison (for migrations, etc.)
187// =============================================================================
188
189/// Asserts that two values of potentially different types are structurally the same.
190///
191/// Unlike [`assert_same!`], this allows comparing values of different types.
192/// Two values are "sameish" if they have the same structure and values,
193/// even if they have different type names.
194///
195/// **Note:** Because the two arguments can have different types, the compiler
196/// cannot infer types from one side to the other. If you get type inference
197/// errors, either add type annotations or use [`assert_same!`] instead.
198///
199/// # Panics
200///
201/// Panics if the values are not structurally same, displaying a colored diff.
202///
203/// # Example
204///
205/// ```
206/// use facet::Facet;
207/// use facet_assert::assert_sameish;
208///
209/// #[derive(Facet)]
210/// struct PersonV1 {
211///     name: String,
212///     age: u32,
213/// }
214///
215/// #[derive(Facet)]
216/// struct PersonV2 {
217///     name: String,
218///     age: u32,
219/// }
220///
221/// let old = PersonV1 { name: "Alice".into(), age: 30 };
222/// let new = PersonV2 { name: "Alice".into(), age: 30 };
223/// assert_sameish!(old, new); // Different types, same structure
224/// ```
225#[macro_export]
226macro_rules! assert_sameish {
227    ($left:expr, $right:expr $(,)?) => {
228        match $crate::check_sameish(&$left, &$right) {
229            $crate::Sameness::Same => {}
230            $crate::Sameness::Different(diff) => {
231                panic!(
232                    "assertion `assert_sameish!(left, right)` failed\n\n{diff}\n"
233                );
234            }
235            $crate::Sameness::Opaque { type_name } => {
236                panic!(
237                    "assertion `assert_sameish!(left, right)` failed: cannot compare opaque type `{type_name}`"
238                );
239            }
240        }
241    };
242    ($left:expr, $right:expr, $($arg:tt)+) => {
243        match $crate::check_sameish(&$left, &$right) {
244            $crate::Sameness::Same => {}
245            $crate::Sameness::Different(diff) => {
246                panic!(
247                    "assertion `assert_sameish!(left, right)` failed: {}\n\n{diff}\n",
248                    format_args!($($arg)+)
249                );
250            }
251            $crate::Sameness::Opaque { type_name } => {
252                panic!(
253                    "assertion `assert_sameish!(left, right)` failed: {}: cannot compare opaque type `{type_name}`",
254                    format_args!($($arg)+)
255                );
256            }
257        }
258    };
259}
260
261/// Asserts that two values of different types are structurally the same with custom options.
262///
263/// Like [`assert_sameish!`], but allows configuring comparison behavior via [`SameOptions`].
264#[macro_export]
265macro_rules! assert_sameish_with {
266    ($left:expr, $right:expr, $options:expr $(,)?) => {
267        match $crate::check_sameish_with(&$left, &$right, $options) {
268            $crate::Sameness::Same => {}
269            $crate::Sameness::Different(diff) => {
270                panic!(
271                    "assertion `assert_sameish_with!(left, right, options)` failed\n\n{diff}\n"
272                );
273            }
274            $crate::Sameness::Opaque { type_name } => {
275                panic!(
276                    "assertion `assert_sameish_with!(left, right, options)` failed: cannot compare opaque type `{type_name}`"
277                );
278            }
279        }
280    };
281    ($left:expr, $right:expr, $options:expr, $($arg:tt)+) => {
282        match $crate::check_sameish_with(&$left, &$right, $options) {
283            $crate::Sameness::Same => {}
284            $crate::Sameness::Different(diff) => {
285                panic!(
286                    "assertion `assert_sameish_with!(left, right, options)` failed: {}\n\n{diff}\n",
287                    format_args!($($arg)+)
288                );
289            }
290            $crate::Sameness::Opaque { type_name } => {
291                panic!(
292                    "assertion `assert_sameish_with!(left, right, options)` failed: {}: cannot compare opaque type `{type_name}`",
293                    format_args!($($arg)+)
294                );
295            }
296        }
297    };
298}
299
300/// Asserts that two values of different types are structurally the same (debug builds only).
301///
302/// Like [`assert_sameish!`], but only enabled in debug builds.
303#[macro_export]
304macro_rules! debug_assert_sameish {
305    ($($arg:tt)*) => {
306        if cfg!(debug_assertions) {
307            $crate::assert_sameish!($($arg)*);
308        }
309    };
310}
311
312/// Asserts that two values of different types are structurally the same with options (debug builds only).
313///
314/// Like [`assert_sameish_with!`], but only enabled in debug builds.
315#[macro_export]
316macro_rules! debug_assert_sameish_with {
317    ($($arg:tt)*) => {
318        if cfg!(debug_assertions) {
319            $crate::assert_sameish_with!($($arg)*);
320        }
321    };
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use facet::Facet;
328
329    #[derive(Facet)]
330    struct Person {
331        name: String,
332        age: u32,
333    }
334
335    #[derive(Facet)]
336    struct PersonV2 {
337        name: String,
338        age: u32,
339    }
340
341    #[derive(Facet)]
342    struct Different {
343        name: String,
344        score: f64,
345    }
346
347    #[test]
348    fn same_type_same_values() {
349        let a = Person {
350            name: "Alice".into(),
351            age: 30,
352        };
353        let b = Person {
354            name: "Alice".into(),
355            age: 30,
356        };
357        assert_same!(a, b);
358    }
359
360    #[test]
361    fn different_types_same_structure() {
362        let a = Person {
363            name: "Alice".into(),
364            age: 30,
365        };
366        let b = PersonV2 {
367            name: "Alice".into(),
368            age: 30,
369        };
370        // Use assert_sameish! for cross-type comparison
371        assert_sameish!(a, b);
372    }
373
374    #[test]
375    fn same_type_different_values() {
376        let a = Person {
377            name: "Alice".into(),
378            age: 30,
379        };
380        let b = Person {
381            name: "Bob".into(),
382            age: 30,
383        };
384
385        match check_same(&a, &b) {
386            Sameness::Different(_) => {} // expected
387            other => panic!(
388                "expected Different, got {:?}",
389                matches!(other, Sameness::Same)
390            ),
391        }
392    }
393
394    #[test]
395    fn diff_report_renders_multiple_flavors() {
396        let a = Person {
397            name: "Alice".into(),
398            age: 30,
399        };
400        let b = Person {
401            name: "Bob".into(),
402            age: 45,
403        };
404
405        let report = match check_same_report(&a, &b) {
406            SameReport::Different(report) => report,
407            _ => panic!("expected Different"),
408        };
409
410        let rust = report.render_plain_rust();
411        assert!(rust.contains("Person"));
412
413        let json = report.render_plain_json();
414        assert!(json.contains("\"name\""));
415
416        let xml = report.render_plain_xml();
417        // Person is a proxy type (struct without xml:: attributes), so it gets @ prefix
418        assert!(xml.contains("<@Person"));
419    }
420
421    #[test]
422    fn primitives() {
423        assert_same!(42i32, 42i32);
424        assert_same!("hello", "hello");
425        assert_same!(true, true);
426    }
427
428    #[test]
429    fn vectors() {
430        let a = vec![1, 2, 3];
431        let b = vec![1, 2, 3];
432        assert_same!(a, b);
433    }
434
435    #[test]
436    fn vectors_different() {
437        let a = vec![1, 2, 3];
438        let b = vec![1, 2, 4];
439
440        match check_same(&a, &b) {
441            Sameness::Different(_) => {} // expected
442            _ => panic!("expected Different"),
443        }
444    }
445
446    #[test]
447    fn options() {
448        let a: Option<i32> = Some(42);
449        let b: Option<i32> = Some(42);
450        assert_same!(a, b);
451
452        let c: Option<i32> = None;
453        let d: Option<i32> = None;
454        assert_same!(c, d);
455    }
456
457    #[test]
458    fn float_exact_same() {
459        let a = 1.0_f64;
460        let b = 1.0_f64;
461        assert_same!(a, b);
462    }
463
464    #[test]
465    fn float_exact_different() {
466        let a = 1.0000001_f64;
467        let b = 1.0000002_f64;
468
469        match check_same(&a, &b) {
470            Sameness::Different(_) => {} // expected - exact comparison fails
471            _ => panic!("expected Different"),
472        }
473    }
474
475    #[test]
476    fn float_with_tolerance_same() {
477        let a = 1.0000001_f64;
478        let b = 1.0000002_f64;
479
480        // With tolerance, these should be considered the same
481        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
482    }
483
484    #[test]
485    fn float_with_tolerance_different() {
486        let a = 1.0_f64;
487        let b = 2.0_f64;
488
489        // Even with tolerance, 1.0 vs 2.0 are different
490        match check_same_with(&a, &b, SameOptions::new().float_tolerance(1e-6)) {
491            Sameness::Different(_) => {} // expected
492            _ => panic!("expected Different"),
493        }
494    }
495
496    #[test]
497    fn f32_with_tolerance() {
498        let a = 1.0000001_f32;
499        let b = 1.0000002_f32;
500
501        // With tolerance, these should be considered the same
502        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-5));
503    }
504
505    #[test]
506    fn struct_with_float_tolerance() {
507        #[derive(Facet)]
508        struct Measurement {
509            name: String,
510            value: f64,
511        }
512
513        let a = Measurement {
514            name: "temperature".into(),
515            value: 98.6000001,
516        };
517        let b = Measurement {
518            name: "temperature".into(),
519            value: 98.6000002,
520        };
521
522        // Exact comparison fails
523        match check_same(&a, &b) {
524            Sameness::Different(_) => {} // expected
525            _ => panic!("expected Different"),
526        }
527
528        // With tolerance, they're the same
529        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
530    }
531
532    #[test]
533    fn vec_of_floats_with_tolerance() {
534        let a = vec![1.0000001_f64, 2.0000001_f64, 3.0000001_f64];
535        let b = vec![1.0000002_f64, 2.0000002_f64, 3.0000002_f64];
536
537        // Exact comparison fails
538        match check_same(&a, &b) {
539            Sameness::Different(_) => {} // expected
540            _ => panic!("expected Different"),
541        }
542
543        // With tolerance, they're the same
544        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
545    }
546
547    #[test]
548    fn nested_struct_with_float_tolerance() {
549        #[derive(Facet)]
550        struct Point {
551            x: f64,
552            y: f64,
553        }
554
555        #[derive(Facet)]
556        struct Line {
557            start: Point,
558            end: Point,
559        }
560
561        let a = Line {
562            start: Point {
563                x: 0.0000001,
564                y: 0.0000001,
565            },
566            end: Point {
567                x: 1.0000001,
568                y: 1.0000001,
569            },
570        };
571        let b = Line {
572            start: Point {
573                x: 0.0000002,
574                y: 0.0000002,
575            },
576            end: Point {
577                x: 1.0000002,
578                y: 1.0000002,
579            },
580        };
581
582        assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
583    }
584
585    // Tests for type inference (the key fix from issue #1161)
586    mod type_inference {
587        use super::*;
588
589        #[test]
590        fn with_option() {
591            // This is the key test from issue #1161
592            // Type inference flows from x to the right-hand side
593            let x: Option<Option<i32>> = Some(None);
594            assert_same!(x, Some(None)); // Works! Type flows from x
595        }
596
597        #[test]
598        fn with_nested_option() {
599            let x: Option<Option<Option<i32>>> = Some(Some(None));
600            assert_same!(x, Some(Some(None))); // Triple nesting works too
601        }
602
603        #[test]
604        fn with_result() {
605            let x: Result<Option<i32>, ()> = Ok(None);
606            assert_same!(x, Ok(None)); // Works with Result too
607        }
608
609        #[test]
610        fn with_vec() {
611            let x: Vec<Option<i32>> = vec![Some(1), None, Some(3)];
612            assert_same!(x, vec![Some(1), None, Some(3)]);
613        }
614    }
615
616    // Tests for sameish (cross-type comparison)
617    mod sameish {
618        use super::*;
619
620        #[test]
621        fn different_types_same_structure() {
622            let a = Person {
623                name: "Alice".into(),
624                age: 30,
625            };
626            let b = PersonV2 {
627                name: "Alice".into(),
628                age: 30,
629            };
630            assert_sameish!(a, b);
631        }
632
633        #[test]
634        fn check_sameish_detects_differences() {
635            let a = Person {
636                name: "Alice".into(),
637                age: 30,
638            };
639            let b = PersonV2 {
640                name: "Bob".into(),
641                age: 30,
642            };
643
644            match check_sameish(&a, &b) {
645                Sameness::Different(_) => {} // expected
646                _ => panic!("expected Different"),
647            }
648        }
649
650        #[test]
651        fn with_options_float_tolerance() {
652            #[derive(Facet)]
653            struct MeasurementV1 {
654                value: f64,
655            }
656
657            #[derive(Facet)]
658            struct MeasurementV2 {
659                value: f64,
660            }
661
662            let a = MeasurementV1 { value: 1.0000001 };
663            let b = MeasurementV2 { value: 1.0000002 };
664
665            assert_sameish_with!(a, b, SameOptions::new().float_tolerance(1e-6));
666        }
667
668        #[test]
669        fn with_custom_message() {
670            let a = Person {
671                name: "Alice".into(),
672                age: 30,
673            };
674            let b = PersonV2 {
675                name: "Alice".into(),
676                age: 30,
677            };
678            assert_sameish!(a, b, "custom message: {} vs {}", "Person", "PersonV2");
679        }
680    }
681}