Skip to main content

pretty_assertions_sorted/
lib.rs

1//! # Pretty Assertions (Sorted)
2//!
3//! This crate wraps the [pretty_assertions](https://raw.githubusercontent.com/colin-kiegel/rust-pretty-assertions) crate, which highlights differences
4//! in a test failure via a colorful diff.
5//!
6//! However, the diff is based on the Debug output of the objects. For objects that
7//! have non-deterministic output, eg. two HashMaps with close to the same contents, the diff
8//! will be polluted and obscured with with false-positive differences like here:
9//!
10//! ![standard assertion](https://raw.githubusercontent.com/DarrenTsung/rust-pretty-assertions-sorted/fe860f070bdfb29a399a32ff9d3b98ca8d958326/images/non_deterministic.png)
11//!
12//! This is much easier to understand when the diff is sorted:
13//!
14//! ![sorted assertion](https://raw.githubusercontent.com/DarrenTsung/rust-pretty-assertions-sorted/fe860f070bdfb29a399a32ff9d3b98ca8d958326/images/sorted.png)
15//!
16//! This is a pretty trivial example, you could solve this instead by converting the HashMap to
17//! a BTreeMap in your tests. But it's not always feasible to replace the types with ordered
18//! versions, especially for HashMaps that are deeply nested in types outside of your control.
19//!
20//! To use the sorted version, import like this:
21//!
22//! ```rust
23//! use pretty_assertions_sorted::{assert_eq, assert_eq_sorted};
24//! ```
25//!
26//! `assert_eq` is provided as a re-export of `pretty_assertions::assert_eq` and should
27//! be used if you don't want the Debug output to be sorted, or if the Debug output can't
28//! be sorted (not supported types, eg. f64::NEG_INFINITY, or custom Debug output).
29//!
30//! ## Tip
31//!
32//! Specify it as [`[dev-dependencies]`](http://doc.crates.io/specifying-dependencies.html#development-dependencies)
33//! and it will only be used for compiling tests, examples, and benchmarks.
34//! This way the compile time of `cargo build` won't be affected!
35use std::fmt;
36
37use darrentsung_debug_parser::*;
38pub use pretty_assertions::{assert_eq, assert_ne, Comparison};
39
40/// This is a wrapper with similar functionality to [`assert_eq`], however, the
41/// [`Debug`] representation is sorted to provide deterministic output.
42///
43/// Not all [`Debug`] representations are sortable yet and this doesn't work with
44/// custom [`Debug`] implementations that don't conform to the format that #[derive(Debug)]
45/// uses, eg. `fmt.debug_struct()`, `fmt.debug_map()`, etc.
46///
47/// Don't use this if you want to test the ordering of the types that are sorted, since
48/// sorting will clobber any previous ordering.
49///
50/// Potential use-cases that aren't implemented yet:
51/// * Blocklist for field names that shouldn't be sorted
52/// * Sorting more than just maps (struct fields, lists, etc.)
53#[macro_export]
54macro_rules! assert_eq_sorted {
55    ($left:expr, $right:expr$(,)?) => ({
56        $crate::assert_eq_sorted!(@ $left, $right, "", "");
57    });
58    ($left:expr, $right:expr, $($arg:tt)*) => ({
59        $crate::assert_eq_sorted!(@ $left, $right, ": ", $($arg)+);
60    });
61    (@ $left:expr, $right:expr, $maybe_semicolon:expr, $($arg:tt)*) => ({
62        match (&($left), &($right)) {
63            (left_val, right_val) => {
64                if !(*left_val == *right_val) {
65                    // We create the comparison string outside the panic! call
66                    // because creating the comparison string could panic itself.
67                    let comparison_string = $crate::Comparison::new(
68                        &$crate::SortedDebug::new(left_val),
69                        &$crate::SortedDebug::new(right_val)
70                    ).to_string();
71                    ::core::panic!("assertion failed: `(left == right)`{}{}\
72                       \n\
73                       \n{}\
74                       \n",
75                       $maybe_semicolon,
76                       format_args!($($arg)*),
77                       comparison_string,
78                    )
79                }
80            }
81        }
82    });
83}
84
85/// New-type wrapper around an object that sorts the fmt::Debug output when displayed for
86/// deterministic output.
87///
88/// This works through parsing the output and sorting the `debug_map()` type.
89///
90/// DISCLAIMER: This Debug implementation will panic if the inner value's Debug
91/// representation can't be sorted. This is used to notify users when used in tests. An
92/// alternative solution of falling back to non-sorted could be implemented.
93///
94/// Potential use-cases that aren't implemented yet:
95/// * Blocklist for field names that shouldn't be sorted
96/// * Sorting more than just maps (struct fields, lists, etc.)
97pub struct SortedDebug<T>(T);
98
99impl<T> SortedDebug<T> {
100    pub fn new(v: T) -> Self {
101        Self(v)
102    }
103}
104
105impl<T: fmt::Debug> fmt::Debug for SortedDebug<T> {
106    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
107        let mut value = match parse(&format!("{:?}", self.0)) {
108            Ok(value) => value,
109            Err(err) => {
110                ::core::panic!("Failed to parse Debug output for sorting (please use `assert_eq!` instead and/or file an issue for your use-case)!\nError: {}", err)
111            }
112        };
113
114        sort_maps(&mut value);
115
116        // Replace one-line non-exhaustive objects with empty brackets separated by
117        // newlines. This changes output like: "Foo { .. }" with "Foo {\n}". "Foo {\n}" is
118        // more desirable because it diffs better against some multi-line output of "Foo {
119        // value: 10.0 }" (imagine the newlines please).
120        let formatted_output = format!("{:#?}", value).replace("{ .. }", "{\n}");
121        fmt::Display::fmt(&formatted_output, f)
122    }
123}
124
125fn sort_maps(v: &mut Value) {
126    match v {
127        Value::Struct(s) => {
128            for ident_value_or_non_exhaustive in &mut s.values {
129                match ident_value_or_non_exhaustive {
130                    OrNonExhaustive::Value(ident_value) => {
131                        sort_maps(&mut ident_value.value);
132                    }
133                    OrNonExhaustive::NonExhaustive => (),
134                }
135            }
136        }
137        Value::Set(s) => {
138            for child_v in &mut s.values {
139                sort_maps(child_v);
140            }
141        }
142        Value::Map(map) => {
143            map.values.sort_by(|a, b| a.key.cmp(&b.key));
144
145            for key_value in &mut map.values {
146                sort_maps(&mut key_value.key);
147                sort_maps(&mut key_value.value);
148            }
149        }
150        Value::List(l) => {
151            for child_v in &mut l.values {
152                sort_maps(child_v);
153            }
154        }
155        Value::Tuple(t) => {
156            for child_v in &mut t.values {
157                sort_maps(child_v);
158            }
159        }
160        // No need to recurse for Term variant.
161        Value::Term(_) => (),
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use indoc::indoc;
169    use std::assert_eq;
170    use std::collections::HashMap;
171
172    const TEST_RERUNS_FOR_DETERMINISM: u32 = 100;
173
174    fn sorted_debug<T: fmt::Debug>(v: T) -> String {
175        format!("{:#?}", SortedDebug(v))
176    }
177
178    #[test]
179    fn noop_sorts() {
180        for _ in 0..TEST_RERUNS_FOR_DETERMINISM {
181            assert_eq!(sorted_debug(2), "2");
182        }
183    }
184
185    #[test]
186    fn sorts_hashmap() {
187        for _ in 0..TEST_RERUNS_FOR_DETERMINISM {
188            // Note that we have to create the HashMaps each try
189            // in order to induce non-determinism in the debug output.
190            let item = {
191                let mut map = HashMap::new();
192                map.insert(1, true);
193                map.insert(2, true);
194                map.insert(20, true);
195                map
196            };
197
198            let expected = indoc!(
199                "{
200                    1: true,
201                    2: true,
202                    20: true,
203                }"
204            );
205            assert_eq!(sorted_debug(item), expected);
206        }
207    }
208
209    #[test]
210    fn sorts_hashmaps_with_non_exhaustive_object_values() {
211        #[allow(unused)]
212        struct Foo {
213            value: f32,
214        }
215
216        impl fmt::Debug for Foo {
217            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218                f.debug_struct("Foo")
219                    .field("value", &self.value)
220                    .finish_non_exhaustive()
221            }
222        }
223
224        for _ in 0..TEST_RERUNS_FOR_DETERMINISM {
225            let item = {
226                let mut map = HashMap::new();
227                map.insert(1, Foo { value: 10.1 });
228                map.insert(32, Foo { value: 2.0 });
229                map.insert(2, Foo { value: -1.5 });
230                map
231            };
232
233            let expected = indoc!(
234                "{
235                    1: Foo {
236                        value: 10.1,
237                        ..
238                    },
239                    2: Foo {
240                        value: -1.5,
241                        ..
242                    },
243                    32: Foo {
244                        value: 2.0,
245                        ..
246                    },
247                }"
248            );
249            assert_eq!(sorted_debug(item), expected);
250        }
251    }
252
253    #[test]
254    fn sorts_object_with_hashmap() {
255        #[derive(Debug)]
256        #[allow(unused)]
257        struct Foo {
258            bar: Bar,
259        }
260
261        #[derive(Debug)]
262        #[allow(unused)]
263        struct Bar {
264            count: HashMap<&'static str, Zed>,
265            value: usize,
266        }
267
268        #[derive(Debug)]
269        struct Zed;
270
271        for _ in 0..TEST_RERUNS_FOR_DETERMINISM {
272            let item = Foo {
273                bar: Bar {
274                    count: {
275                        let mut map = HashMap::new();
276                        map.insert("hello world", Zed);
277                        map.insert("lorem ipsum", Zed);
278                        map
279                    },
280                    value: 200,
281                },
282            };
283
284            let expected = indoc!(
285                "Foo {
286                    bar: Bar {
287                        count: {
288                            \"hello world\": Zed,
289                            \"lorem ipsum\": Zed,
290                        },
291                        value: 200,
292                    },
293                }"
294            );
295            assert_eq!(sorted_debug(item), expected);
296        }
297    }
298
299    #[test]
300    fn hashmap_with_object_values() {
301        #[derive(Debug)]
302        #[allow(unused)]
303        struct Foo {
304            value: f32,
305            bar: Vec<Bar>,
306        }
307
308        #[derive(Debug)]
309        #[allow(unused)]
310        struct Bar {
311            elo: i32,
312        }
313
314        for _ in 0..TEST_RERUNS_FOR_DETERMINISM {
315            let item = {
316                let mut map = HashMap::new();
317                map.insert(
318                    "foo",
319                    Foo {
320                        value: 12.2,
321                        bar: vec![Bar { elo: 200 }, Bar { elo: -12 }],
322                    },
323                );
324                map.insert(
325                    "foo2",
326                    Foo {
327                        value: -0.2,
328                        bar: vec![],
329                    },
330                );
331                map
332            };
333
334            let expected = indoc!(
335                "{
336                    \"foo\": Foo {
337                        value: 12.2,
338                        bar: [
339                            Bar {
340                                elo: 200,
341                            },
342                            Bar {
343                                elo: -12,
344                            },
345                        ],
346                    },
347                    \"foo2\": Foo {
348                        value: -0.2,
349                        bar: [],
350                    },
351                }"
352            );
353            assert_eq!(sorted_debug(item), expected);
354        }
355    }
356
357    #[test]
358    fn hashmap_with_object_keys() {
359        #[derive(Debug, PartialEq, Eq, Hash)]
360        struct Foo {
361            value: i32,
362            bar: Vec<Bar>,
363        }
364
365        #[derive(Debug, PartialEq, Eq, Hash)]
366        struct Bar {
367            elo: i32,
368        }
369
370        for _ in 0..TEST_RERUNS_FOR_DETERMINISM {
371            let item = {
372                let mut map = HashMap::new();
373                map.insert(
374                    Foo {
375                        value: 12,
376                        bar: vec![Bar { elo: 200 }, Bar { elo: -12 }],
377                    },
378                    "foo",
379                );
380                map.insert(
381                    Foo {
382                        value: -2,
383                        bar: vec![],
384                    },
385                    "foo2",
386                );
387                map
388            };
389
390            let expected = indoc!(
391                "{
392                    Foo {
393                        value: -2,
394                        bar: [],
395                    }: \"foo2\",
396                    Foo {
397                        value: 12,
398                        bar: [
399                            Bar {
400                                elo: 200,
401                            },
402                            Bar {
403                                elo: -12,
404                            },
405                        ],
406                    }: \"foo\",
407                }"
408            );
409            assert_eq!(sorted_debug(item), expected);
410        }
411    }
412
413    #[test]
414    fn hashmap_with_chrono_naivedate() {
415        for _ in 0..TEST_RERUNS_FOR_DETERMINISM {
416            let item = {
417                let mut map = HashMap::new();
418                map.insert(chrono::NaiveDate::from_ymd(2000, 2, 14), "foo");
419                map.insert(chrono::NaiveDate::from_ymd(2001, 4, 2), "foo");
420                map
421            };
422
423            dbg!(&item);
424
425            let expected = indoc!(
426                "{
427                    2000-02-14: \"foo\",
428                    2001-04-02: \"foo\",
429                }"
430            );
431            assert_eq!(sorted_debug(item), expected);
432        }
433    }
434
435    #[test]
436    #[should_panic(
437        expected = "Failed to parse Debug output for sorting (please use `assert_eq!` instead and/or file an issue for your use-case)!
438Error: Failed to consume all of string!
439Value:
440Object
441
442Rest:
443\" {\\\"a\\\": Number(0)}\""
444    )]
445    fn panics_when_expression_cant_be_sorted() {
446        assert_eq_sorted!(serde_json::json!({"a":0}), "2");
447    }
448
449    #[derive(PartialEq)]
450    #[allow(unused)]
451    struct FooWithOptionalField {
452        value: Option<f32>,
453    }
454
455    impl fmt::Debug for FooWithOptionalField {
456        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457            let mut foo = f.debug_struct("FooWithOptionalField");
458            if let Some(value) = &self.value {
459                foo.field("value", value);
460                foo.finish()
461            } else {
462                foo.finish_non_exhaustive()
463            }
464        }
465    }
466
467    /// Test that the value field is displayed as missing (colored red) for optional fields
468    /// on non-exhaustive Debug implementations.
469    #[test]
470    #[should_panic(expected = "FooWithOptionalField {\n\u{1b}[31m<    value: 2.0,\u{1b}[0m\n }")]
471    fn ui_looks_right_for_non_exhaustive_optional_fields() {
472        assert_eq_sorted!(
473            FooWithOptionalField { value: Some(2.0) },
474            FooWithOptionalField { value: None }
475        );
476    }
477
478    #[test]
479    fn outputs_nice_output_for_non_exhaustive_objects_with_optional_fields() {
480        assert_eq!(
481            sorted_debug(FooWithOptionalField { value: Some(10.0) }),
482            indoc!(
483                "FooWithOptionalField {
484                    value: 10.0,
485                }"
486            )
487        );
488
489        assert_eq!(
490            sorted_debug(FooWithOptionalField { value: None }),
491            indoc!(
492                "FooWithOptionalField {
493                }"
494            )
495        );
496    }
497}