Skip to main content

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//! TL;DR - favor `assert_eq_unordered_sort` unless the trait requirements can't be met.
12//! Use the regular versions for collections, and the `*_iter` versions for iterators
13//! (or differing collection types that don't support direct equality comparison).
14//!
15//! * [assert_eq_unordered]
16//!     * Requires only `Debug` and `PartialEq` on the elements
17//!     * Collection level equality check, and if unequal, falls back to item by item compare (O(n^2))
18//! * [assert_eq_unordered_sort]
19//!     * Requires `Debug` and `Ord` on the elements
20//!     * Collection level equality check, and if unequal, sorts and then compares again,
21//!       and if still unequal, falls back to item by item compare (O(n^2))
22//! * [assert_eq_unordered_iter]
23//!     * Requires only `Debug` and `PartialEq` on the elements
24//!     * Does only item by item compare (O(n^2))
25//! * [assert_eq_unordered_sort_iter]
26//!     * Requires `Debug` and `Ord` on the elements
27//!     * Sorts and then compares. If unequal, falls back to item by item compare (O(n^2))
28
29//!
30//! # Example
31//! ```should_panic
32//! use assert_unordered::assert_eq_unordered;
33//!
34//! #[derive(Debug, PartialEq)]
35//! struct MyType(i32);
36//!
37//! let expected = vec![MyType(1), MyType(2), MyType(4), MyType(5)];
38//! let actual = vec![MyType(2), MyType(0), MyType(4)];
39//!
40//! assert_eq_unordered!(expected, actual);
41//! ```
42//!
43//! Output:
44//!  
45//! ![example_error](https://raw.githubusercontent.com/nu11ptr/assert_unordered/master/example_error.png)
46
47#![cfg_attr(not(feature = "std"), no_std)]
48#![cfg_attr(docsrs, feature(doc_cfg))]
49#![warn(missing_docs)]
50
51// Trick to test README samples (from: https://github.com/rust-lang/cargo/issues/383#issuecomment-720873790)
52#[cfg(doctest)]
53mod test_readme {
54    macro_rules! external_doc_test {
55        ($x:expr) => {
56            #[doc = $x]
57            extern "C" {}
58        };
59    }
60
61    external_doc_test!(include_str!("../README.md"));
62}
63
64extern crate alloc;
65extern crate core;
66
67use alloc::format;
68use alloc::string::String;
69use alloc::vec::Vec;
70use core::fmt::{Arguments, Debug};
71#[cfg(feature = "color")]
72#[cfg(windows)]
73use std::sync::Once;
74
75#[cfg(feature = "color")]
76#[cfg(windows)]
77static INIT_COLOR: Once = Once::new();
78
79#[cfg(feature = "color")]
80#[cfg(windows)]
81static mut COLOR_ENABLED: bool = false;
82
83/// Assert that `$left` and `$right` are "unordered" equal. That is, they contain the same elements,
84/// but not necessarily in the same order. If this assertion is false, a panic is raised, and the
85/// elements that are different between `$left` and `$right` are shown (when possible).
86///
87/// Both `$left` and `$right` must implement [Iterator] or [IntoIterator], and `$left` must implement
88/// [PartialEq] for `$right`, but otherwise can be any type. The iterator `Item` type must be the
89/// same for both sides and can be any type that implements [Debug] and [PartialEq]. Optional `$arg`
90/// parameters may be given to customize the error message, if any (these are the same as the
91/// parameters passed to [format!]).
92///
93/// # Efficiency
94/// If `$left` and `$right` are equal, this assertion is quite efficient just doing a regular equality
95/// check and then returning. If they are not equal, `$left` and `$right` are collected into a [Vec]
96/// and the elements compared one by one for both `$left` and `$right` (meaning it is at least
97/// O(n^2) algorithmic complexity in the non-equality path).
98///
99/// # Example
100/// ```should_panic
101/// use assert_unordered::assert_eq_unordered;
102///
103/// #[derive(Debug, PartialEq)]
104/// struct MyType(i32);
105///
106/// let expected = vec![MyType(1), MyType(2), MyType(4), MyType(5)];
107/// let actual = vec![MyType(2), MyType(0), MyType(4)];
108///
109/// assert_eq_unordered!(expected, actual);
110///  ```
111///
112/// Output:
113///
114/// ![example_error](https://raw.githubusercontent.com/nu11ptr/assert_unordered/master/example_error.png)
115#[macro_export]
116macro_rules! assert_eq_unordered {
117    ($left:expr, $right:expr $(,)?) => {
118        $crate::pass_or_panic($crate::compare_unordered($left, $right), core::option::Option::None);
119    };
120    ($left:expr, $right:expr, $($arg:tt)+) => {
121        $crate::pass_or_panic(
122            $crate::compare_unordered($left, $right),
123            core::option::Option::Some(core::format_args!($($arg)+))
124        );
125    };
126}
127
128/// The same as [assert_eq_unordered], but for types that implement [Iterator] or [IntoIterator]
129/// without requiring [PartialEq]. The left and right sides do not need to have the same type.
130/// It will be less efficient if the collections are equal, as it skips the initial equality check.
131///
132/// # Example
133/// ```should_panic
134/// use assert_unordered::assert_eq_unordered_iter;
135///
136/// #[derive(Debug, PartialEq)]
137/// struct MyType(i32);
138///
139/// let expected = vec![MyType(1), MyType(2), MyType(4), MyType(5)].into_iter();
140/// let actual = vec![MyType(2), MyType(0), MyType(4)].into_iter();
141///
142/// assert_eq_unordered_iter!(expected, actual);
143///  ```
144///
145/// Output:
146///
147/// ![example_error](https://raw.githubusercontent.com/nu11ptr/assert_unordered/master/example_error.png)
148#[macro_export]
149macro_rules! assert_eq_unordered_iter {
150    ($left:expr, $right:expr $(,)?) => {
151        $crate::pass_or_panic($crate::compare_unordered_iter($left, $right), core::option::Option::None);
152    };
153    ($left:expr, $right:expr, $($arg:tt)+) => {
154        $crate::pass_or_panic(
155            $crate::compare_unordered_iter($left, $right),
156            core::option::Option::Some(core::format_args!($($arg)+))
157        );
158    };
159}
160
161/// Assert that `$left` and `$right` are "unordered" equal. That is, they contain the same elements,
162/// but not necessarily in the same order. If this assertion is false, a panic is raised, and the
163/// elements that are different between `$left` and `$right` are shown (when possible).
164///
165/// Both `$left` and `$right` must implement [Iterator] or [IntoIterator], and `$left` must implement
166/// [PartialEq] for `$right`, but otherwise can be any type. The iterator `Item` type must be the
167/// same for both sides and can be any type that implements [Debug] and [Ord]. Optional `$arg`
168/// parameters may be given to customize the error message, if any (these are the same as the
169/// parameters passed to [format!]).
170///
171/// # Efficiency
172/// If `$left` and `$right` are equal, this assertion is quite efficient just doing a regular equality
173/// check and then returning. If they are not equal, `$left` and `$right` are sorted and compared again.
174/// If still not equal, the elements compared one by one for both `$left` and `$right` (meaning it
175/// is at least O(n^2) algorithmic complexity, if not equal by this point).
176///
177/// # Example
178/// ```should_panic
179/// use assert_unordered::assert_eq_unordered_sort;
180///
181/// #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
182/// struct MyType(i32);
183///
184/// let expected = vec![MyType(1), MyType(2), MyType(4), MyType(5)];
185/// let actual = vec![MyType(2), MyType(0), MyType(4)];
186///
187/// assert_eq_unordered_sort!(expected, actual);
188///  ```
189///
190/// Output:
191///
192/// ![example_error](https://raw.githubusercontent.com/nu11ptr/assert_unordered/master/example_error.png)
193#[macro_export]
194macro_rules! assert_eq_unordered_sort {
195    ($left:expr, $right:expr $(,)?) => {
196        $crate::pass_or_panic($crate::compare_unordered_sort($left, $right), core::option::Option::None);
197    };
198    ($left:expr, $right:expr, $($arg:tt)+) => {
199        $crate::pass_or_panic(
200            $crate::compare_unordered_sort($left, $right),
201            core::option::Option::Some(core::format_args!($($arg)+))
202        );
203    };
204}
205
206/// The same as [assert_eq_unordered_sort], but for types that implement [Iterator] or [IntoIterator]
207/// without requiring [PartialEq]. The left and right sides do not need to have the same type.
208/// It will be less efficient if the collections are equal, as it skips the initial equality check.
209///
210/// # Example
211/// ```should_panic
212/// use assert_unordered::assert_eq_unordered_sort_iter;
213///
214/// #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
215/// struct MyType(i32);
216///
217/// let expected = vec![MyType(1), MyType(2), MyType(4), MyType(5)].into_iter();
218/// let actual = vec![MyType(2), MyType(0), MyType(4)].into_iter();
219///
220/// assert_eq_unordered_sort_iter!(expected, actual);
221///  ```
222///
223/// Output:
224///
225/// ![example_error](https://raw.githubusercontent.com/nu11ptr/assert_unordered/master/example_error.png)
226#[macro_export]
227macro_rules! assert_eq_unordered_sort_iter {
228    ($left:expr, $right:expr $(,)?) => {
229        $crate::pass_or_panic($crate::compare_unordered_sort_iter($left, $right), core::option::Option::None);
230    };
231    ($left:expr, $right:expr, $($arg:tt)+) => {
232        $crate::pass_or_panic(
233            $crate::compare_unordered_sort_iter($left, $right),
234            core::option::Option::Some(core::format_args!($($arg)+))
235        );
236    };
237}
238
239#[cfg(feature = "color")]
240#[cfg(windows)]
241#[inline]
242fn init_color() -> bool {
243    // SAFETY: This is the example given in stdlib docs for how to init a mutable static var
244    unsafe {
245        INIT_COLOR.call_once(|| {
246            COLOR_ENABLED = ansi_term::enable_ansi_support().is_ok();
247        });
248        COLOR_ENABLED
249    }
250}
251
252#[cfg(feature = "color")]
253#[cfg(not(windows))]
254#[inline]
255const fn init_color() -> bool {
256    true
257}
258
259#[doc(hidden)]
260pub enum CompareResult {
261    Equal,
262    NotEqualDiffElements(String, String, String),
263}
264
265#[cfg(feature = "color")]
266#[doc(hidden)]
267#[inline]
268pub fn pass_or_panic(result: CompareResult, msg: Option<Arguments>) {
269    if init_color() {
270        color_pass_or_panic(result, msg)
271    } else {
272        plain_pass_or_panic(result, msg);
273    }
274}
275
276#[cfg(not(feature = "color"))]
277#[doc(hidden)]
278#[inline]
279pub fn pass_or_panic(result: CompareResult, msg: Option<Arguments>) {
280    plain_pass_or_panic(result, msg);
281}
282
283#[cfg(feature = "color")]
284fn color_pass_or_panic(result: CompareResult, msg: Option<Arguments>) {
285    match result {
286        CompareResult::NotEqualDiffElements(in_both, in_left_not_right, in_right_not_left) => {
287            use ansi_term::Color::{Green, Red, Yellow};
288
289            let msg = match msg {
290                Some(msg) => msg.to_string(),
291                None => {
292                    format!(
293                        "The {} did not contain the {} as the {}",
294                        Red.paint("left"),
295                        Yellow.paint("same items"),
296                        Green.paint("right"),
297                    )
298                }
299            };
300
301            let both = Yellow.paint(format!("In both: {in_both}"));
302            let left = Red.paint(format!("In left: {in_left_not_right}"));
303            let right = Green.paint(format!("In right: {in_right_not_left}"));
304
305            panic!("{msg}:\n{both}\n{left}\n{right}\n");
306        }
307        CompareResult::Equal => {}
308    }
309}
310
311fn plain_pass_or_panic(result: CompareResult, msg: Option<Arguments>) {
312    match result {
313        CompareResult::NotEqualDiffElements(in_both, in_left_not_right, in_right_not_left) => {
314            let msg = match msg {
315                Some(msg) => msg,
316                // TODO: 1.60 `format_args` not yet stable on 'const fn'. Maybe soon?
317                None => format_args!("The left did not contain the same items as the right"),
318            };
319
320            panic!(
321                "{msg}:\nIn both: {in_both}\nIn left: {in_left_not_right}\nIn right: {in_right_not_left}"
322            );
323        }
324        CompareResult::Equal => {}
325    }
326}
327
328fn compare_elem_by_elem<I, T>(left: I, right: Vec<T>) -> CompareResult
329where
330    I: IntoIterator<Item = T>,
331    T: Debug + PartialEq,
332{
333    let mut in_right_not_left: Vec<_> = right;
334    let mut in_left_not_right = Vec::new();
335    // Optimistically assume we likely got it close to right
336    let mut in_both = Vec::with_capacity(in_right_not_left.len());
337
338    for elem1 in left {
339        match in_right_not_left.iter().position(|elem2| &elem1 == elem2) {
340            Some(idx) => {
341                in_both.push(elem1);
342                in_right_not_left.remove(idx);
343            }
344            None => {
345                in_left_not_right.push(elem1);
346            }
347        }
348    }
349
350    if !in_left_not_right.is_empty() || !in_right_not_left.is_empty() {
351        CompareResult::NotEqualDiffElements(
352            format!("{in_both:#?}"),
353            format!("{in_left_not_right:#?}"),
354            format!("{in_right_not_left:#?}"),
355        )
356    } else {
357        CompareResult::Equal
358    }
359}
360
361#[doc(hidden)]
362pub fn compare_unordered_iter<L, R, T>(left: L, right: R) -> CompareResult
363where
364    L: IntoIterator<Item = T>,
365    R: IntoIterator<Item = T>,
366    T: Debug + PartialEq,
367{
368    let right = right.into_iter().collect();
369    compare_elem_by_elem(left, right)
370}
371
372#[doc(hidden)]
373pub fn compare_unordered<L, R, T>(left: L, right: R) -> CompareResult
374where
375    L: IntoIterator<Item = T> + PartialEq<R>,
376    R: IntoIterator<Item = T>,
377    T: Debug + PartialEq,
378{
379    // First, try for the easy (and faster compare)
380    if left != right {
381        // Fallback on the slow one by one compare
382        compare_unordered_iter(left, right)
383    } else {
384        CompareResult::Equal
385    }
386}
387
388#[doc(hidden)]
389pub fn compare_unordered_sort_iter<L, R, T>(left: L, right: R) -> CompareResult
390where
391    L: IntoIterator<Item = T>,
392    R: IntoIterator<Item = T>,
393    T: Debug + Ord,
394{
395    // Try and sort under assumption these are equal, but might be out of order
396    let mut left: Vec<_> = left.into_iter().collect();
397    let mut right: Vec<_> = right.into_iter().collect();
398
399    left.sort_unstable();
400    right.sort_unstable();
401
402    if left != right {
403        // Fallback on the slow one by one compare
404        compare_elem_by_elem(left, right)
405    } else {
406        CompareResult::Equal
407    }
408}
409
410#[doc(hidden)]
411pub fn compare_unordered_sort<L, R, T>(left: L, right: R) -> CompareResult
412where
413    L: IntoIterator<Item = T> + PartialEq<R>,
414    R: IntoIterator<Item = T>,
415    T: Debug + Ord,
416{
417    // First, try for the easy (and faster compare)
418    if left != right {
419        // Fallback on the slow one by one compare
420        compare_unordered_sort_iter(left, right)
421    } else {
422        CompareResult::Equal
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use crate::{
429        compare_unordered, compare_unordered_iter, compare_unordered_sort,
430        compare_unordered_sort_iter, CompareResult,
431    };
432    use alloc::vec::Vec;
433    use alloc::{format, vec};
434    use core::fmt::Debug;
435
436    #[derive(Debug, PartialEq)]
437    struct MyType(i32);
438
439    #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
440    struct MyTypeSort(i32);
441
442    fn validate_results<T: Debug>(
443        result: CompareResult,
444        both_expected: Vec<T>,
445        left_expected: Vec<T>,
446        right_expected: Vec<T>,
447    ) {
448        match result {
449            CompareResult::NotEqualDiffElements(both_actual, left_actual, right_actual) => {
450                assert_eq!(format!("{both_expected:#?}"), both_actual);
451                assert_eq!(format!("{left_expected:#?}"), left_actual);
452                assert_eq!(format!("{right_expected:#?}"), right_actual);
453            }
454            _ => {
455                panic!("Left and right were expected to have have different elements");
456            }
457        }
458    }
459
460    macro_rules! make_tests {
461        ($func:ident, $type:ident) => {
462            #[test]
463            fn compare_unordered_not_equal_diff_elem() {
464                let left = vec![$type(1), $type(2), $type(4), $type(5)];
465                let right = vec![$type(2), $type(0), $type(4)];
466
467                validate_results(
468                    $func(left, right),
469                    vec![$type(2), $type(4)],
470                    vec![$type(1), $type(5)],
471                    vec![$type(0)],
472                );
473            }
474
475            #[test]
476            fn compare_unordered_not_equal_dup_elem_diff_len() {
477                let left = vec![$type(2), $type(4), $type(4)];
478                let right = vec![$type(4), $type(2)];
479
480                validate_results(
481                    $func(left, right),
482                    vec![$type(2), $type(4)],
483                    vec![$type(4)],
484                    vec![],
485                );
486            }
487
488            #[test]
489            fn compare_unordered_not_equal_dup_elem() {
490                let left = vec![$type(2), $type(2), $type(2), $type(4)];
491                let right = vec![$type(2), $type(4), $type(4), $type(4)];
492
493                validate_results(
494                    $func(left, right),
495                    vec![$type(2), $type(4)],
496                    vec![$type(2), $type(2)],
497                    vec![$type(4), $type(4)],
498                );
499            }
500
501            #[test]
502            fn compare_unordered_equal_diff_order() {
503                let left = vec![$type(1), $type(2), $type(4), $type(5)];
504                let right = vec![$type(5), $type(2), $type(1), $type(4)];
505
506                assert!(matches!($func(left, right), CompareResult::Equal));
507            }
508
509            #[test]
510            fn compare_unordered_equal_same_order() {
511                let left = vec![$type(1), $type(2), $type(4), $type(5)];
512                let right = vec![$type(1), $type(2), $type(4), $type(5)];
513
514                assert!(matches!($func(left, right), CompareResult::Equal));
515            }
516
517            #[test]
518            fn compare_unordered_equal_different_collection_types() {
519                let left = vec![$type(1), $type(2), $type(4), $type(5)];
520                let right = [$type(5), $type(2), $type(1), $type(4)];
521
522                assert!(matches!($func(left, right), CompareResult::Equal));
523            }
524        };
525    }
526
527    macro_rules! make_tests_iter {
528        ($func:ident, $type:ident) => {
529            #[test]
530            fn compare_unordered_not_equal_diff_elem() {
531                let left = vec![$type(1), $type(2), $type(4), $type(5)].into_iter();
532                let right = vec![$type(2), $type(0), $type(4)].into_iter();
533
534                validate_results(
535                    $func(left, right),
536                    vec![$type(2), $type(4)],
537                    vec![$type(1), $type(5)],
538                    vec![$type(0)],
539                );
540            }
541
542            #[test]
543            fn compare_unordered_not_equal_dup_elem_diff_len() {
544                let left = vec![$type(2), $type(4), $type(4)].into_iter();
545                let right = vec![$type(4), $type(2)].into_iter();
546
547                validate_results(
548                    $func(left, right),
549                    vec![$type(2), $type(4)],
550                    vec![$type(4)],
551                    vec![],
552                );
553            }
554
555            #[test]
556            fn compare_unordered_not_equal_dup_elem() {
557                let left = vec![$type(2), $type(2), $type(2), $type(4)].into_iter();
558                let right = vec![$type(2), $type(4), $type(4), $type(4)].into_iter();
559
560                validate_results(
561                    $func(left, right),
562                    vec![$type(2), $type(4)],
563                    vec![$type(2), $type(2)],
564                    vec![$type(4), $type(4)],
565                );
566            }
567
568            #[test]
569            fn compare_unordered_equal_diff_order() {
570                let left = vec![$type(1), $type(2), $type(4), $type(5)].into_iter();
571                let right = vec![$type(5), $type(2), $type(1), $type(4)].into_iter();
572
573                assert!(matches!($func(left, right), CompareResult::Equal));
574            }
575
576            #[test]
577            fn compare_unordered_equal_same_order() {
578                let left = vec![$type(1), $type(2), $type(4), $type(5)].into_iter();
579                let right = vec![$type(1), $type(2), $type(4), $type(5)].into_iter();
580
581                assert!(matches!($func(left, right), CompareResult::Equal));
582            }
583
584            #[test]
585            fn compare_unordered_equal_different_iterator_types() {
586                let left = vec![$type(1), $type(2), $type(4), $type(5)];
587                let right = [$type(5), $type(2), $type(1), $type(4)].into_iter();
588
589                assert!(matches!($func(left, right), CompareResult::Equal));
590            }
591        };
592    }
593
594    mod regular {
595        use super::*;
596
597        make_tests!(compare_unordered, MyType);
598    }
599
600    mod regular_iter {
601        use super::*;
602
603        make_tests_iter!(compare_unordered_iter, MyType);
604    }
605
606    mod sort {
607        use super::*;
608
609        make_tests!(compare_unordered_sort, MyTypeSort);
610    }
611
612    mod sort_iter {
613        use super::*;
614
615        make_tests_iter!(compare_unordered_sort_iter, MyTypeSort);
616    }
617}