test_casing/
test_casing.rs

1//! Support types for the `test_casing` macro.
2
3use std::{fmt, iter::Fuse};
4
5/// Obtains a test case from an iterator.
6#[doc(hidden)] // used by the `#[test_casing]` macro; logically private
7pub fn case<I: IntoIterator>(iter: I, index: usize) -> I::Item
8where
9    I::Item: fmt::Debug,
10{
11    iter.into_iter().nth(index).unwrap_or_else(|| {
12        panic!("case #{index} not provided from the cases iterator");
13    })
14}
15
16#[doc(hidden)] // used by the `#[test_casing]` macro; logically private
17pub fn assert_case_count(iter: impl IntoIterator, expected_count: usize) {
18    const MAX_SIZE_HINT: usize = 1_000;
19
20    let iter = iter.into_iter();
21    let (lo, hi) = iter.size_hint();
22    let actual_count = if Some(lo) == hi {
23        lo
24    } else if hi.is_none_or(|hi| hi > MAX_SIZE_HINT) {
25        // Here, we don't have an exact-sized iterator. We should be careful to use `count()`,
26        // because it can hang with infinite-sized iterators.
27        return;
28    } else {
29        iter.count()
30    };
31
32    assert_eq!(
33        actual_count, expected_count,
34        "Unexpected number of test cases; use #[test_casing({actual_count}, ..)]"
35    );
36}
37
38/// Container for test cases based on a lazily evaluated iterator. Should be constructed
39/// using the [`cases!`](crate::cases) macro.
40///
41/// # Examples
42///
43/// ```
44/// # use test_casing::{cases, TestCases};
45/// const NUMBER_CASES: TestCases<u32> = cases!([2, 3, 5, 8]);
46/// const MORE_CASES: TestCases<u32> = cases! {
47///     NUMBER_CASES.into_iter().chain([42, 555])
48/// };
49///
50/// // The `cases!` macro can wrap a statement block:
51/// const COMPLEX_CASES: TestCases<u32> = cases!({
52///     use rand::{rngs::StdRng, Rng, SeedableRng};
53///
54///     let mut rng = StdRng::seed_from_u64(123);
55///     (0..5).map(move |_| rng.random())
56/// });
57/// ```
58pub struct TestCases<T> {
59    lazy: fn() -> Box<dyn Iterator<Item = T>>,
60}
61
62impl<T> fmt::Debug for TestCases<T> {
63    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64        formatter.debug_struct("TestCases").finish_non_exhaustive()
65    }
66}
67
68impl<T> Clone for TestCases<T> {
69    fn clone(&self) -> Self {
70        *self
71    }
72}
73
74impl<T> Copy for TestCases<T> {}
75
76impl<T> TestCases<T> {
77    /// Creates a new set of test cases.
78    pub const fn new(lazy: fn() -> Box<dyn Iterator<Item = T>>) -> Self {
79        Self { lazy }
80    }
81}
82
83impl<T> IntoIterator for TestCases<T> {
84    type Item = T;
85    type IntoIter = Box<dyn Iterator<Item = T>>;
86
87    fn into_iter(self) -> Self::IntoIter {
88        (self.lazy)()
89    }
90}
91
92/// Creates [`TestCases`] based on the provided expression implementing [`IntoIterator`]
93/// (e.g., an array, a range or an iterator).
94///
95/// # Examples
96///
97/// See [`TestCases`](TestCases#examples) docs for the examples of usage.
98#[macro_export]
99macro_rules! cases {
100    ($iter:expr) => {
101        $crate::TestCases::<_>::new(|| {
102            std::boxed::Box::new(core::iter::IntoIterator::into_iter($iter))
103        })
104    };
105}
106
107/// Cartesian product of several test cases.
108///
109/// For now, this supports products of 2..8 values. The provided [`IntoIterator`] expression
110/// for each value must implement [`Clone`]. One way to do that is using [`TestCases`], which
111/// wraps a lazy iterator initializer and is thus always [`Copy`]able.
112///
113/// # Examples
114///
115/// ```
116/// # use test_casing::Product;
117/// let product = Product((0..2, ["test", "other"]));
118/// let values: Vec<_> = product.into_iter().collect();
119/// assert_eq!(
120///     values,
121///     [(0, "test"), (0, "other"), (1, "test"), (1, "other")]
122/// );
123/// ```
124#[derive(Debug, Clone, Copy)]
125pub struct Product<Ts>(pub Ts);
126
127impl<T, U> IntoIterator for Product<(T, U)>
128where
129    T: Clone + IntoIterator,
130    U: Clone + IntoIterator,
131{
132    type Item = (T::Item, U::Item);
133    type IntoIter = ProductIter<T, U>;
134
135    fn into_iter(self) -> Self::IntoIter {
136        let (first, second) = &self.0;
137        let second = second.clone().into_iter();
138        let size_hint = product_size_hint(&first.clone().into_iter(), &second);
139        ProductIter {
140            sources: self.0,
141            size_hint,
142            first_idx: 0,
143            second_iter: second.fuse(),
144            is_finished: false,
145        }
146    }
147}
148
149fn product_size_hint(first: &impl Iterator, second: &impl Iterator) -> (usize, Option<usize>) {
150    let (first_lo, first_hi) = first.size_hint();
151    let (second_lo, second_hi) = second.size_hint();
152    let lo = first_lo.saturating_mul(second_lo);
153    let hi = if let (Some(x), Some(y)) = (first_hi, second_hi) {
154        x.checked_mul(y)
155    } else {
156        None
157    };
158    (lo, hi)
159}
160
161macro_rules! impl_product {
162    ($head:ident: $head_ty:ident, $($tail:ident: $tail_ty:ident),+) => {
163        impl<$head_ty, $($tail_ty,)+> IntoIterator for Product<($head_ty, $($tail_ty,)+)>
164        where
165            $head_ty: 'static + Clone + IntoIterator,
166            $($tail_ty: 'static + Clone + IntoIterator,)+
167        {
168            type Item = ($head_ty::Item, $($tail_ty::Item,)+);
169            type IntoIter = Box<dyn Iterator<Item = Self::Item>>;
170
171            fn into_iter(self) -> Self::IntoIter {
172                let ($head, $($tail,)+) = self.0;
173                let tail = Product(($($tail,)+));
174                let iter = Product(($head, tail))
175                    .into_iter()
176                    .map(|($head, ($($tail,)+))| ($head, $($tail,)+));
177                Box::new(iter)
178            }
179        }
180    };
181}
182
183impl_product!(t: T, u: U, v: V);
184impl_product!(t: T, u: U, v: V, w: W);
185impl_product!(t: T, u: U, v: V, w: W, x: X);
186impl_product!(t: T, u: U, v: V, w: W, x: X, y: Y);
187impl_product!(t: T, u: U, v: V, w: W, x: X, y: Y, z: Z);
188
189/// Iterator over test cases in [`Product`].
190#[derive(Debug)]
191pub struct ProductIter<T: IntoIterator, U: IntoIterator> {
192    sources: (T, U),
193    size_hint: (usize, Option<usize>),
194    first_idx: usize,
195    second_iter: Fuse<U::IntoIter>,
196    is_finished: bool,
197}
198
199impl<T, U> Iterator for ProductIter<T, U>
200where
201    T: Clone + IntoIterator,
202    U: Clone + IntoIterator,
203{
204    type Item = (T::Item, U::Item);
205
206    fn next(&mut self) -> Option<Self::Item> {
207        if self.is_finished {
208            return None;
209        }
210
211        loop {
212            if let Some(second_case) = self.second_iter.next() {
213                let mut first_iter = self.sources.0.clone().into_iter();
214                let Some(first_case) = first_iter.nth(self.first_idx) else {
215                    self.is_finished = true;
216                    return None;
217                };
218                return Some((first_case, second_case));
219            }
220            self.first_idx += 1;
221            self.second_iter = self.sources.1.clone().into_iter().fuse();
222        }
223    }
224
225    fn size_hint(&self) -> (usize, Option<usize>) {
226        self.size_hint
227    }
228}
229
230impl<T, U> ExactSizeIterator for ProductIter<T, U>
231where
232    T: Clone + IntoIterator<IntoIter: ExactSizeIterator>,
233    U: Clone + IntoIterator<IntoIter: ExactSizeIterator>,
234{
235    fn len(&self) -> usize {
236        // By construction, `size_hint` will always have `(n, Some(n))` form.
237        self.size_hint.0
238    }
239}
240
241#[cfg(doctest)]
242doc_comment::doctest!("../README.md");
243
244#[cfg(test)]
245mod tests {
246    use std::collections::HashSet;
247
248    use super::*;
249
250    #[test]
251    fn cartesian_product() {
252        let numbers = cases!(0..3);
253        let strings = cases!(["0", "1"]);
254        let prod = Product((numbers, strings)).into_iter();
255        assert_eq!(prod.size_hint(), (6, Some(6)));
256        let cases: Vec<_> = prod.into_iter().collect();
257        assert_eq!(
258            cases.as_slice(),
259            [(0, "0"), (0, "1"), (1, "0"), (1, "1"), (2, "0"), (2, "1")]
260        );
261
262        let booleans = [false, true];
263        let prod = Product((numbers, strings, booleans)).into_iter();
264        assert_eq!(prod.size_hint(), (12, Some(12)));
265        let cases: HashSet<_> = prod.collect();
266        assert_eq!(cases.len(), 12); // 3 * 2 * 2
267    }
268
269    #[test]
270    fn exact_size_iterator_for_product() {
271        let numbers = 0..3;
272        let strings = ["0", "1"];
273        let prod = Product((numbers, strings)).into_iter();
274        assert_eq!(prod.size_hint(), (6, Some(6)));
275        assert_eq!(prod.len(), 6);
276        let cases: Vec<_> = prod.into_iter().collect();
277        assert_eq!(
278            cases.as_slice(),
279            [(0, "0"), (0, "1"), (1, "0"), (1, "1"), (2, "0"), (2, "1")]
280        );
281    }
282
283    #[test]
284    fn unit_test_detection_works() {
285        assert!(option_env!("CARGO_TARGET_TMPDIR").is_none());
286    }
287}