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