Skip to main content

ironbeam/testing/
assertions.rs

1//! Assertion functions for testing pipeline outputs.
2//!
3//! This module provides specialized assertion functions for comparing
4//! collections produced by pipelines with expected results.
5
6use std::collections::{HashMap, HashSet};
7use std::fmt::Debug;
8use std::hash::{BuildHasher, Hash};
9
10/// Assert that two collections are equal in order and content.
11///
12/// This function compares two vectors element-by-element and panics with
13/// a detailed message if they differ.
14///
15/// # Panics
16///
17/// Panics if the collections differ in length or content.
18///
19/// # Example
20///
21/// ```
22/// use ironbeam::testing::assert_collections_equal;
23///
24/// let actual = vec![1, 2, 3];
25/// let expected = vec![1, 2, 3];
26/// assert_collections_equal(&actual, &expected);
27/// ```
28pub fn assert_collections_equal<T: Debug + PartialEq>(actual: &[T], expected: &[T]) {
29    assert_eq!(
30        actual.len(),
31        expected.len(),
32        "Collection length mismatch:\n  Expected length: {}\n  Actual length: {}\n  Expected: {expected:?}\n  Actual: {actual:?}",
33        expected.len(),
34        actual.len()
35    );
36
37    for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
38        assert_eq!(
39            a, e,
40            "Collection mismatch at index {i}:\n  Expected: {e:?}\n  Actual: {a:?}\n  Full expected: {expected:?}\n  Full actual: {actual:?}"
41        );
42    }
43}
44
45/// Assert that two collections contain the same elements, ignoring order.
46///
47/// This function converts both collections to sets and compares them.
48/// Useful for testing transformations that may not preserve element order.
49///
50/// # Panics
51///
52/// Panics if the collections differ in content (ignoring order).
53///
54/// # Example
55///
56/// ```
57/// use ironbeam::testing::assert_collections_unordered_equal;
58///
59/// let actual = vec![3, 1, 2];
60/// let expected = vec![1, 2, 3];
61/// assert_collections_unordered_equal(&actual, &expected);
62/// ```
63pub fn assert_collections_unordered_equal<T: Debug + Eq + Hash>(actual: &[T], expected: &[T]) {
64    let actual_set: HashSet<_> = actual.iter().collect();
65    let expected_set: HashSet<_> = expected.iter().collect();
66
67    assert_eq!(
68        actual.len(),
69        expected.len(),
70        "Collection length mismatch:\n  Expected length: {}\n  Actual length: {}\n  Expected: {expected:?}\n  Actual: {actual:?}",
71        expected.len(),
72        actual.len()
73    );
74
75    if actual_set != expected_set {
76        let missing: Vec<_> = expected_set.difference(&actual_set).collect();
77        let extra: Vec<_> = actual_set.difference(&expected_set).collect();
78
79        panic!(
80            "Collection content mismatch:\n  Missing elements: {missing:?}\n  Extra elements: {extra:?}\n  Expected: {expected:?}\n  Actual: {actual:?}"
81        );
82    }
83}
84
85/// Assert that two collections of key-value pairs are equal after sorting by key.
86///
87/// This function is specifically designed for testing grouped or aggregated data
88/// where the order of keys may vary between runs but should be consistent when sorted.
89///
90/// # Panics
91///
92/// Panics if the collections differ after sorting by key.
93///
94/// # Example
95///
96/// ```
97/// use ironbeam::testing::assert_kv_collections_equal;
98///
99/// let actual = vec![("b", 2), ("a", 1)];
100/// let expected = vec![("a", 1), ("b", 2)];
101/// assert_kv_collections_equal(actual, expected);
102/// ```
103pub fn assert_kv_collections_equal<K, V>(mut actual: Vec<(K, V)>, mut expected: Vec<(K, V)>)
104where
105    K: Debug + Ord,
106    V: Debug + PartialEq,
107{
108    actual.sort_by(|a, b| a.0.cmp(&b.0));
109    expected.sort_by(|a, b| a.0.cmp(&b.0));
110
111    assert_eq!(
112        actual.len(),
113        expected.len(),
114        "Collection length mismatch:\n  Expected length: {}\n  Actual length: {}\n  Expected: {expected:?}\n  Actual: {actual:?}",
115        expected.len(),
116        actual.len()
117    );
118
119    for (i, ((ak, av), (ek, ev))) in actual.iter().zip(expected.iter()).enumerate() {
120        assert!(
121            !(ak != ek || av != ev),
122            "Collection mismatch at index {i} after sorting:\n  Expected: ({ek:?}, {ev:?})\n  Actual: ({ak:?}, {av:?})\n  Full expected: {expected:?}\n  Full actual: {actual:?}"
123        );
124    }
125}
126
127/// Assert that two collections of key-value pairs with grouped values are equal.
128///
129/// This function compares grouped data (e.g., output from `group_by_key`) where
130/// each key maps to a vector of values. The keys are compared in sorted order,
131/// and the value vectors are compared as unordered sets.
132///
133/// # Panics
134///
135/// Panics if the collections differ in keys or values.
136///
137/// # Example
138///
139/// ```
140/// use ironbeam::testing::assert_grouped_kv_equal;
141///
142/// let actual = vec![("a", vec![1, 2]), ("b", vec![3])];
143/// let expected = vec![("a", vec![2, 1]), ("b", vec![3])];
144/// assert_grouped_kv_equal(actual, expected);
145/// ```
146pub fn assert_grouped_kv_equal<K, V>(mut actual: Vec<(K, Vec<V>)>, mut expected: Vec<(K, Vec<V>)>)
147where
148    K: Debug + Ord,
149    V: Debug + Eq + Hash,
150{
151    actual.sort_by(|a, b| a.0.cmp(&b.0));
152    expected.sort_by(|a, b| a.0.cmp(&b.0));
153
154    assert_eq!(
155        actual.len(),
156        expected.len(),
157        "Collection length mismatch:\n  Expected length: {}\n  Actual length: {}",
158        expected.len(),
159        actual.len()
160    );
161
162    for (i, ((ak, av), (ek, ev))) in actual.iter().zip(expected.iter()).enumerate() {
163        assert_eq!(
164            ak, ek,
165            "Key mismatch at index {i}:\n  Expected: {ek:?}\n  Actual: {ak:?}"
166        );
167
168        let av_set: HashSet<_> = av.iter().collect();
169        let ev_set: HashSet<_> = ev.iter().collect();
170
171        assert_eq!(
172            av_set, ev_set,
173            "Value mismatch for key {ak:?} at index {i}:\n  Expected values: {ev:?}\n  Actual values: {av:?}"
174        );
175    }
176}
177
178/// Assert that all elements in a collection satisfy a predicate.
179///
180/// # Panics
181///
182/// Panics if any element does not satisfy the predicate.
183///
184/// # Example
185///
186/// ```
187/// use ironbeam::testing::assert_all;
188///
189/// let data = vec![2, 4, 6, 8];
190/// assert_all(&data, |x| x % 2 == 0);
191/// ```
192pub fn assert_all<T: Debug>(collection: &[T], predicate: impl Fn(&T) -> bool) {
193    for (i, item) in collection.iter().enumerate() {
194        assert!(
195            predicate(item),
196            "Predicate failed for element at index {i}:\n  Element: {item:?}\n  Collection: {collection:?}"
197        );
198    }
199}
200
201/// Assert that at least one element in a collection satisfies a predicate.
202///
203/// # Panics
204///
205/// Panics if no elements satisfy the predicate.
206///
207/// # Example
208///
209/// ```
210/// use ironbeam::testing::assert_any;
211///
212/// let data = vec![1, 2, 3, 4];
213/// assert_any(&data, |x| x % 2 == 0);
214/// ```
215pub fn assert_any<T: Debug>(collection: &[T], predicate: impl Fn(&T) -> bool) {
216    assert!(
217        collection.iter().any(&predicate),
218        "No elements satisfied the predicate:\n  Collection: {collection:?}"
219    );
220}
221
222/// Assert that no elements in a collection satisfy a predicate.
223///
224/// # Panics
225///
226/// Panics if any element satisfies the predicate.
227///
228/// # Example
229///
230/// ```
231/// use ironbeam::testing::assert_none;
232///
233/// let data = vec![1, 3, 5, 7];
234/// assert_none(&data, |x| x % 2 == 0);
235/// ```
236pub fn assert_none<T: Debug>(collection: &[T], predicate: impl Fn(&T) -> bool) {
237    for (i, item) in collection.iter().enumerate() {
238        assert!(
239            !predicate(item),
240            "Predicate unexpectedly succeeded for element at index {i}:\n  Element: {item:?}\n  Collection: {collection:?}"
241        );
242    }
243}
244
245/// Assert that a collection has the expected size.
246///
247/// # Panics
248///
249/// Panics if the collection size doesn't match the expected size.
250///
251/// # Example
252///
253/// ```
254/// use ironbeam::testing::assert_collection_size;
255///
256/// let data = vec![1, 2, 3];
257/// assert_collection_size(&data, 3);
258/// ```
259pub fn assert_collection_size<T>(collection: &[T], expected_size: usize) {
260    assert_eq!(
261        collection.len(),
262        expected_size,
263        "Collection size mismatch:\n  Expected: {expected_size}\n  Actual: {}",
264        collection.len()
265    );
266}
267
268/// Assert that a collection contains a specific element.
269///
270/// # Panics
271///
272/// Panics if the element is not found in the collection.
273///
274/// # Example
275///
276/// ```
277/// use ironbeam::testing::assert_contains;
278///
279/// let data = vec![1, 2, 3, 4];
280/// assert_contains(&data, &3);
281/// ```
282pub fn assert_contains<T: Debug + PartialEq>(collection: &[T], element: &T) {
283    assert!(
284        collection.contains(element),
285        "Element not found in collection:\n  Looking for: {element:?}\n  Collection: {collection:?}"
286    );
287}
288
289/// Assert that two hashmaps are equal.
290///
291/// # Panics
292///
293/// Panics if the hashmaps differ in keys or values.
294///
295/// # Example
296///
297/// ```
298/// use ironbeam::testing::assert_maps_equal;
299/// use std::collections::HashMap;
300///
301/// let mut actual = HashMap::new();
302/// actual.insert("a", 1);
303/// actual.insert("b", 2);
304///
305/// let mut expected = HashMap::new();
306/// expected.insert("a", 1);
307/// expected.insert("b", 2);
308///
309/// assert_maps_equal(&actual, &expected);
310/// ```
311pub fn assert_maps_equal<K, V, S: BuildHasher>(
312    actual: &HashMap<K, V, S>,
313    expected: &HashMap<K, V, S>,
314) where
315    K: Debug + Eq + Hash,
316    V: Debug + PartialEq,
317{
318    assert_eq!(
319        actual.len(),
320        expected.len(),
321        "HashMap size mismatch:\n  Expected size: {}\n  Actual size: {}\n  Expected: {expected:?}\n  Actual: {actual:?}",
322        expected.len(),
323        actual.len()
324    );
325
326    for (key, expected_value) in expected {
327        match actual.get(key) {
328            Some(actual_value) if actual_value == expected_value => {}
329            Some(actual_value) => {
330                panic!(
331                    "HashMap value mismatch for key {key:?}:\n  Expected: {expected_value:?}\n  Actual: {actual_value:?}"
332                );
333            }
334            None => {
335                panic!("HashMap missing key: {key:?}");
336            }
337        }
338    }
339}