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//! 
11//!
12//! This is much easier to understand when the diff is sorted:
13//!
14//! 
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}