1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use std::{iter, mem};

use proc_macro2::{Literal, Span};
use syn::{
    parse::{Parse, ParseStream},
    punctuated::Punctuated,
    spanned::Spanned,
    Expr, ExprLit, ExprRange, Lit, RangeLimits, Token,
};

use crate::{comment::TestCaseComment, expr::TestCaseExpression, TestCase};

mod matrix_product;

#[derive(Debug, Default)]
pub struct TestMatrix {
    variables: Vec<Vec<Expr>>,
    expression: Option<TestCaseExpression>,
    comment: Option<TestCaseComment>,
}

impl TestMatrix {
    pub fn push_argument(&mut self, values: Vec<Expr>) {
        self.variables.push(values);
    }

    pub fn cases(&self) -> impl Iterator<Item = TestCase> {
        let expression = self.expression.clone();
        let comment = self.comment.clone();

        matrix_product::multi_cartesian_product(self.variables.iter().cloned()).map(move |v| {
            if let Some(comment) = comment.clone() {
                TestCase::new_with_prefixed_name(
                    v,
                    expression.clone(),
                    comment.comment.value().as_ref(),
                )
            } else {
                TestCase::new(v, expression.clone(), None)
            }
        })
    }
}

impl Parse for TestMatrix {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let args: Punctuated<Expr, Token![,]> = Punctuated::parse_separated_nonempty(input)?;

        let expression = (!input.is_empty()).then(|| input.parse()).transpose();
        let comment = (!input.is_empty()).then(|| input.parse()).transpose();
        // if both are errors, pick the expression error since it is more likely to be informative.
        //
        // TODO(https://github.com/frondeus/test-case/issues/135): avoid Result::ok entirely.
        let (expression, comment) = match (expression, comment) {
            (Err(expression), Err(_comment)) => return Err(expression),
            (expression, comment) => (expression.ok().flatten(), comment.ok().flatten()),
        };

        let mut matrix = TestMatrix {
            expression,
            comment,
            ..Default::default()
        };

        for arg in args {
            let values: Vec<Expr> = match &arg {
                Expr::Array(v) => v.elems.iter().cloned().collect(),
                Expr::Tuple(v) => v.elems.iter().cloned().collect(),
                Expr::Range(ExprRange {
                    start, limits, end, ..
                }) => {
                    let start = isize_from_range_expr(limits.span(), start.as_deref())?;
                    let end = isize_from_range_expr(limits.span(), end.as_deref())?;
                    let range: Box<dyn Iterator<Item = isize>> = match limits {
                        RangeLimits::HalfOpen(_) => Box::from(start..end),
                        RangeLimits::Closed(_) => Box::from(start..=end),
                    };
                    range
                        .map(|n| {
                            let mut lit = Lit::new(Literal::isize_unsuffixed(n));
                            lit.set_span(arg.span());
                            Expr::from(ExprLit { lit, attrs: vec![] })
                        })
                        .collect()
                }
                v => iter::once(v.clone()).collect(),
            };

            let mut value_literal_type = None;
            for expr in &values {
                if let Expr::Lit(ExprLit { lit, .. }) = expr {
                    let first_literal_type =
                        *value_literal_type.get_or_insert_with(|| mem::discriminant(lit));
                    if first_literal_type != mem::discriminant(lit) {
                        return Err(syn::Error::new(
                            lit.span(),
                            "All literal values must be of the same type",
                        ));
                    }
                }
            }
            matrix.push_argument(values);
        }

        Ok(matrix)
    }
}

fn isize_from_range_expr(limits_span: Span, expr: Option<&Expr>) -> syn::Result<isize> {
    match expr {
        Some(Expr::Lit(ExprLit {
            lit: Lit::Int(n), ..
        })) => n.base10_parse(),
        Some(e) => Err(syn::Error::new(
            e.span(),
            "Range bounds can only be an integer literal",
        )),
        None => Err(syn::Error::new(
            limits_span,
            "Unbounded ranges are not supported",
        )),
    }
}