assert_unordered/
lib.rs

1//! A direct replacement for `assert_eq` for unordered collections
2//!
3//! This macro is useful for any situation where the ordering of the collection doesn't matter, even
4//! if they are always in the same order. This is because the stdlib `assert_eq` shows the entire
5//! collection for both left and right and leaves it up to the user to visually scan for differences.
6//! In contrast, this crate only works with collections (types that implement `IntoIterator`) and
7//! therefore can show only the differences (see below for an example of what the output looks like).
8//!
9//! # Which Macro?
10//!
11//! TLDR; - favor `assert_eq_unordered_sort` unless the trait requirements can't be met
12//!
13//! * [assert_eq_unordered]
14//!     * Requires only `Debug` and `PartialEq` on the elements
15//!     * Collection level equality check, and if unequal, falls back to item by item compare (O(n^2))
16//! * [assert_eq_unordered_sort]
17//!     * Requires `Debug`, `Eq` and `Ord` on the elements
18//!     * Collection level equality check, and if unequal, sorts and then compares again,
19//!       and if still unequal, falls back to item by item compare (O(n^2))
20
21//!
22//! # Example
23//! ```should_panic
24//! use assert_unordered::assert_eq_unordered;
25//!
26//! #[derive(Debug, PartialEq)]
27//! struct MyType(i32);
28//!
29//! let expected = vec![MyType(1), MyType(2), MyType(4), MyType(5)];
30//! let actual = vec![MyType(2), MyType(0), MyType(4)];
31//!
32//! assert_eq_unordered!(expected, actual);
33//! ```
34//!
35//! Output:
36//!  
37//! ![example_error](https://raw.githubusercontent.com/nu11ptr/assert_unordered/master/example_error.png)
38
39#![cfg_attr(not(feature = "std"), no_std)]
40#![cfg_attr(docsrs, feature(doc_cfg))]
41#![warn(missing_docs)]
42
43// Trick to test README samples (from: https://github.com/rust-lang/cargo/issues/383#issuecomment-720873790)
44#[cfg(doctest)]
45mod test_readme {
46    macro_rules! external_doc_test {
47        ($x:expr) => {
48            #[doc = $x]
49            extern "C" {}
50        };
51    }
52
53    external_doc_test!(include_str!("../README.md"));
54}
55
56extern crate alloc;
57extern crate core;
58
59use alloc::format;
60use alloc::string::String;
61use alloc::vec::Vec;
62use core::fmt::{Arguments, Debug};
63#[cfg(feature = "color")]
64#[cfg(windows)]
65use std::sync::Once;
66
67#[cfg(feature = "color")]
68#[cfg(windows)]
69static INIT_COLOR: Once = Once::new();
70
71#[cfg(feature = "color")]
72#[cfg(windows)]
73static mut COLOR_ENABLED: bool = false;
74
75/// Assert that `$left` and `$right` are "unordered" equal. That is, they contain the same elements,
76/// but not necessarily in the same order. If this assertion is false, a panic is raised, and the
77/// elements that are different between `$left` and `$right` are shown (when possible).
78///
79/// Both `$left` and `$right` must be of the same type and implement [PartialEq] and [Iterator] or
80/// [IntoIterator], but otherwise can be any type. The iterator `Item` type can be any type that
81/// implements [Debug] and [PartialEq]. Optional `$arg` parameters may be given to customize the
82/// error message, if any (these are the same as the parameters passed to [format!]).
83///
84/// # Efficiency
85/// If `$left` and `$right` are equal, this assertion is quite efficient just doing a regular equality
86/// check and then returning. If they are not equal, `$left` and `$right` are collected into a [Vec]
87/// and the elements compared one by one for both `$left` and `$right` (meaning it is at least
88/// O(n^2) algorithmic complexity in the non-equality path).
89///
90/// # Example
91/// ```should_panic
92/// use assert_unordered::assert_eq_unordered;
93///
94/// #[derive(Debug, PartialEq)]
95/// struct MyType(i32);
96///
97/// let expected = vec![MyType(1), MyType(2), MyType(4), MyType(5)];
98/// let actual = vec![MyType(2), MyType(0), MyType(4)];
99///
100/// assert_eq_unordered!(expected, actual);
101///  ```
102///
103/// Output:
104///
105/// ![example_error](https://raw.githubusercontent.com/nu11ptr/assert_unordered/master/example_error.png)
106#[macro_export]
107macro_rules! assert_eq_unordered {
108    ($left:expr, $right:expr $(,)?) => {
109        $crate::pass_or_panic($crate::compare_unordered($left, $right), core::option::Option::None);
110    };
111    ($left:expr, $right:expr, $($arg:tt)+) => {
112        $crate::pass_or_panic(
113            $crate::compare_unordered($left, $right),
114            core::option::Option::Some(core::format_args!($($arg)+))
115        );
116    };
117}
118
119/// Assert that `$left` and `$right` are "unordered" equal. That is, they contain the same elements,
120/// but not necessarily in the same order. If this assertion is false, a panic is raised, and the
121/// elements that are different between `$left` and `$right` are shown (when possible).
122///
123/// Both `$left` and `$right` must be of the same type and implement [PartialEq] and [Iterator] or
124/// [IntoIterator], but otherwise can be any type. The iterator `Item` type can be any type that
125/// implements [Debug], [Ord], and [Eq]. Optional `$arg` parameters may be given to customize the
126/// error message, if any (these are the same as the parameters passed to [format!]).
127///
128/// # Efficiency
129/// If `$left` and `$right` are equal, this assertion is quite efficient just doing a regular equality
130/// check and then returning. If they are not equal, `$left` and `$right` are sorted and compared again.
131/// If still not equal, the elements compared one by one for both `$left` and `$right` (meaning it
132/// is at least O(n^2) algorithmic complexity, if not equal by this point).
133///
134/// # Example
135/// ```should_panic
136/// use assert_unordered::assert_eq_unordered_sort;
137///
138/// #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
139/// struct MyType(i32);
140///
141/// let expected = vec![MyType(1), MyType(2), MyType(4), MyType(5)];
142/// let actual = vec![MyType(2), MyType(0), MyType(4)];
143///
144/// assert_eq_unordered_sort!(expected, actual);
145///  ```
146///
147/// Output:
148///
149/// ![example_error](https://raw.githubusercontent.com/nu11ptr/assert_unordered/master/example_error.png)
150#[macro_export]
151macro_rules! assert_eq_unordered_sort {
152    ($left:expr, $right:expr $(,)?) => {
153        $crate::pass_or_panic($crate::compare_unordered_sort($left, $right), core::option::Option::None);
154    };
155    ($left:expr, $right:expr, $($arg:tt)+) => {
156        $crate::pass_or_panic(
157            $crate::compare_unordered_sort($left, $right),
158            core::option::Option::Some(core::format_args!($($arg)+))
159        );
160    };
161}
162
163#[cfg(feature = "color")]
164#[cfg(windows)]
165#[inline]
166fn init_color() -> bool {
167    // SAFETY: This is the example given in stdlib docs for how to init a mutable static var
168    unsafe {
169        INIT_COLOR.call_once(|| {
170            COLOR_ENABLED = ansi_term::enable_ansi_support().is_ok();
171        });
172        COLOR_ENABLED
173    }
174}
175
176#[cfg(feature = "color")]
177#[cfg(not(windows))]
178#[inline]
179const fn init_color() -> bool {
180    true
181}
182
183#[doc(hidden)]
184pub enum CompareResult {
185    Equal,
186    NotEqualDiffElements(String, String, String),
187}
188
189#[cfg(feature = "color")]
190#[doc(hidden)]
191#[inline]
192pub fn pass_or_panic(result: CompareResult, msg: Option<Arguments>) {
193    if init_color() {
194        color_pass_or_panic(result, msg)
195    } else {
196        plain_pass_or_panic(result, msg);
197    }
198}
199
200#[cfg(not(feature = "color"))]
201#[doc(hidden)]
202#[inline]
203pub fn pass_or_panic(result: CompareResult, msg: Option<Arguments>) {
204    plain_pass_or_panic(result, msg);
205}
206
207#[cfg(feature = "color")]
208fn color_pass_or_panic(result: CompareResult, msg: Option<Arguments>) {
209    match result {
210        CompareResult::NotEqualDiffElements(in_both, in_left_not_right, in_right_not_left) => {
211            use ansi_term::Color::{Green, Red, Yellow};
212
213            let msg = match msg {
214                Some(msg) => msg.to_string(),
215                None => {
216                    format!(
217                        "The {} did not contain the {} as the {}",
218                        Red.paint("left"),
219                        Yellow.paint("same items"),
220                        Green.paint("right"),
221                    )
222                }
223            };
224
225            let both = Yellow.paint(format!("In both: {in_both}"));
226            let left = Red.paint(format!("In left: {in_left_not_right}"));
227            let right = Green.paint(format!("In right: {in_right_not_left}"));
228
229            panic!("{msg}:\n{both}\n{left}\n{right}\n");
230        }
231        CompareResult::Equal => {}
232    }
233}
234
235fn plain_pass_or_panic(result: CompareResult, msg: Option<Arguments>) {
236    match result {
237        CompareResult::NotEqualDiffElements(in_both, in_left_not_right, in_right_not_left) => {
238            let msg = match msg {
239                Some(msg) => msg,
240                // TODO: 1.60 `format_args` not yet stable on 'const fn'. Maybe soon?
241                None => format_args!("The left did not contain the same items as the right"),
242            };
243
244            panic!(
245                "{msg}:\nIn both: {in_both}\nIn left: {in_left_not_right}\nIn right: {in_right_not_left}"
246            );
247        }
248        CompareResult::Equal => {}
249    }
250}
251
252fn compare_elem_by_elem<I, T>(left: I, right: Vec<T>) -> CompareResult
253where
254    I: IntoIterator<Item = T> + PartialEq,
255    T: Debug + PartialEq,
256{
257    let mut in_right_not_left: Vec<_> = right;
258    let mut in_left_not_right = Vec::new();
259    // Optimistically assume we likely got it close to right
260    let mut in_both = Vec::with_capacity(in_right_not_left.len());
261
262    for elem1 in left {
263        match in_right_not_left.iter().position(|elem2| &elem1 == elem2) {
264            Some(idx) => {
265                in_both.push(elem1);
266                in_right_not_left.remove(idx);
267            }
268            None => {
269                in_left_not_right.push(elem1);
270            }
271        }
272    }
273
274    if !in_left_not_right.is_empty() || !in_right_not_left.is_empty() {
275        CompareResult::NotEqualDiffElements(
276            format!("{in_both:#?}"),
277            format!("{in_left_not_right:#?}"),
278            format!("{in_right_not_left:#?}"),
279        )
280    } else {
281        CompareResult::Equal
282    }
283}
284
285#[doc(hidden)]
286pub fn compare_unordered<I, T>(left: I, right: I) -> CompareResult
287where
288    I: IntoIterator<Item = T> + PartialEq,
289    T: Debug + PartialEq,
290{
291    // First, try for the easy (and faster compare)
292    if left != right {
293        // Fallback on the slow one by one compare
294        let right = right.into_iter().collect();
295        compare_elem_by_elem(left, right)
296    } else {
297        CompareResult::Equal
298    }
299}
300
301#[doc(hidden)]
302pub fn compare_unordered_sort<I, T>(left: I, right: I) -> CompareResult
303where
304    I: IntoIterator<Item = T> + PartialEq,
305    T: Debug + Ord + PartialEq,
306{
307    // First, try for the easy (and faster compare)
308    if left != right {
309        // Next, try and sort under assumption these are equal, but might be out of order
310        let mut left: Vec<_> = left.into_iter().collect();
311        let mut right: Vec<_> = right.into_iter().collect();
312
313        left.sort_unstable();
314        right.sort_unstable();
315
316        if left != right {
317            // Fallback on the slow one by one compare
318            compare_elem_by_elem(left, right)
319        } else {
320            CompareResult::Equal
321        }
322    } else {
323        CompareResult::Equal
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use crate::{compare_unordered, compare_unordered_sort, CompareResult};
330    use alloc::vec::Vec;
331    use alloc::{format, vec};
332    use core::fmt::Debug;
333
334    #[derive(Debug, PartialEq)]
335    struct MyType(i32);
336
337    #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
338    struct MyTypeSort(i32);
339
340    fn validate_results<T: Debug>(
341        result: CompareResult,
342        both_expected: Vec<T>,
343        left_expected: Vec<T>,
344        right_expected: Vec<T>,
345    ) {
346        match result {
347            CompareResult::NotEqualDiffElements(both_actual, left_actual, right_actual) => {
348                assert_eq!(format!("{both_expected:#?}"), both_actual);
349                assert_eq!(format!("{left_expected:#?}"), left_actual);
350                assert_eq!(format!("{right_expected:#?}"), right_actual);
351            }
352            _ => {
353                panic!("Left and right were expected to have have different elements");
354            }
355        }
356    }
357
358    macro_rules! make_tests {
359        ($func:ident, $type:ident) => {
360            #[test]
361            fn compare_unordered_not_equal_diff_elem() {
362                let left = vec![$type(1), $type(2), $type(4), $type(5)];
363                let right = vec![$type(2), $type(0), $type(4)];
364
365                validate_results(
366                    $func(left, right),
367                    vec![$type(2), $type(4)],
368                    vec![$type(1), $type(5)],
369                    vec![$type(0)],
370                );
371            }
372
373            #[test]
374            fn compare_unordered_not_equal_dup_elem_diff_len() {
375                let left = vec![$type(2), $type(4), $type(4)];
376                let right = vec![$type(4), $type(2)];
377
378                validate_results(
379                    $func(left, right),
380                    vec![$type(2), $type(4)],
381                    vec![$type(4)],
382                    vec![],
383                );
384            }
385
386            #[test]
387            fn compare_unordered_not_equal_dup_elem() {
388                let left = vec![$type(2), $type(2), $type(2), $type(4)];
389                let right = vec![$type(2), $type(4), $type(4), $type(4)];
390
391                validate_results(
392                    $func(left, right),
393                    vec![$type(2), $type(4)],
394                    vec![$type(2), $type(2)],
395                    vec![$type(4), $type(4)],
396                );
397            }
398
399            #[test]
400            fn compare_unordered_equal_diff_order() {
401                let left = vec![$type(1), $type(2), $type(4), $type(5)];
402                let right = vec![$type(5), $type(2), $type(1), $type(4)];
403
404                assert!(matches!($func(left, right), CompareResult::Equal));
405            }
406
407            #[test]
408            fn compare_unordered_equal_same_order() {
409                let left = vec![$type(1), $type(2), $type(4), $type(5)];
410                let right = vec![$type(1), $type(2), $type(4), $type(5)];
411
412                assert!(matches!($func(left, right), CompareResult::Equal));
413            }
414        };
415    }
416
417    mod regular {
418        use super::*;
419
420        make_tests!(compare_unordered, MyType);
421    }
422
423    mod sort {
424        use super::*;
425
426        make_tests!(compare_unordered_sort, MyTypeSort);
427    }
428}