test_case_core/test_matrix/
mod.rs

1use std::{iter, mem};
2
3use proc_macro2::{Literal, Span};
4use syn::{
5    parse::{Parse, ParseStream},
6    punctuated::Punctuated,
7    spanned::Spanned,
8    Expr, ExprLit, ExprRange, Lit, RangeLimits, Token,
9};
10
11use crate::{comment::TestCaseComment, expr::TestCaseExpression, TestCase};
12
13mod matrix_product;
14
15#[derive(Debug, Default)]
16pub struct TestMatrix {
17    variables: Vec<Vec<Expr>>,
18    expression: Option<TestCaseExpression>,
19    comment: Option<TestCaseComment>,
20}
21
22impl TestMatrix {
23    pub fn push_argument(&mut self, values: Vec<Expr>) {
24        self.variables.push(values);
25    }
26
27    pub fn cases(&self) -> impl Iterator<Item = TestCase> {
28        let expression = self.expression.clone();
29        let comment = self.comment.clone();
30
31        matrix_product::multi_cartesian_product(self.variables.iter().cloned()).map(move |v| {
32            if let Some(comment) = comment.clone() {
33                TestCase::new_with_prefixed_name(
34                    v,
35                    expression.clone(),
36                    comment.comment.value().as_ref(),
37                )
38            } else {
39                TestCase::new(v, expression.clone(), None)
40            }
41        })
42    }
43}
44
45impl Parse for TestMatrix {
46    fn parse(input: ParseStream) -> syn::Result<Self> {
47        let args: Punctuated<Expr, Token![,]> = Punctuated::parse_separated_nonempty(input)?;
48
49        let expression = (!input.is_empty()).then(|| input.parse()).transpose();
50        let comment = (!input.is_empty()).then(|| input.parse()).transpose();
51        // if both are errors, pick the expression error since it is more likely to be informative.
52        //
53        // TODO(https://github.com/frondeus/test-case/issues/135): avoid Result::ok entirely.
54        let (expression, comment) = match (expression, comment) {
55            (Err(expression), Err(_comment)) => return Err(expression),
56            (expression, comment) => (expression.ok().flatten(), comment.ok().flatten()),
57        };
58
59        let mut matrix = TestMatrix {
60            expression,
61            comment,
62            ..Default::default()
63        };
64
65        for arg in args {
66            let values: Vec<Expr> = match &arg {
67                Expr::Array(v) => v.elems.iter().cloned().collect(),
68                Expr::Tuple(v) => v.elems.iter().cloned().collect(),
69                Expr::Range(ExprRange {
70                    start, limits, end, ..
71                }) => {
72                    let start = isize_from_range_expr(limits.span(), start.as_deref())?;
73                    let end = isize_from_range_expr(limits.span(), end.as_deref())?;
74                    let range: Box<dyn Iterator<Item = isize>> = match limits {
75                        RangeLimits::HalfOpen(_) => Box::from(start..end),
76                        RangeLimits::Closed(_) => Box::from(start..=end),
77                    };
78                    range
79                        .map(|n| {
80                            let mut lit = Lit::new(Literal::isize_unsuffixed(n));
81                            lit.set_span(arg.span());
82                            Expr::from(ExprLit { lit, attrs: vec![] })
83                        })
84                        .collect()
85                }
86                v => iter::once(v.clone()).collect(),
87            };
88
89            let mut value_literal_type = None;
90            for expr in &values {
91                if let Expr::Lit(ExprLit { lit, .. }) = expr {
92                    let first_literal_type =
93                        *value_literal_type.get_or_insert_with(|| mem::discriminant(lit));
94                    if first_literal_type != mem::discriminant(lit) {
95                        return Err(syn::Error::new(
96                            lit.span(),
97                            "All literal values must be of the same type",
98                        ));
99                    }
100                }
101            }
102            matrix.push_argument(values);
103        }
104
105        Ok(matrix)
106    }
107}
108
109fn isize_from_range_expr(limits_span: Span, expr: Option<&Expr>) -> syn::Result<isize> {
110    match expr {
111        Some(Expr::Lit(ExprLit {
112            lit: Lit::Int(n), ..
113        })) => n.base10_parse(),
114        Some(e) => Err(syn::Error::new(
115            e.span(),
116            "Range bounds can only be an integer literal",
117        )),
118        None => Err(syn::Error::new(
119            limits_span,
120            "Unbounded ranges are not supported",
121        )),
122    }
123}