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