test_case_core/
expr.rs

1use crate::complex_expr::ComplexTestCase;
2use crate::modifier::{parse_kws, Modifier};
3use crate::utils::fmt_syn;
4use crate::TokenStream2;
5use quote::ToTokens;
6use std::collections::HashSet;
7use std::fmt::{Display, Formatter};
8use syn::parse::{Parse, ParseStream};
9use syn::token::If;
10use syn::{parse_quote, Attribute, Expr, Pat, Token};
11
12pub mod kw {
13    syn::custom_keyword!(matches);
14    syn::custom_keyword!(using);
15    syn::custom_keyword!(with);
16    syn::custom_keyword!(it);
17    syn::custom_keyword!(is);
18    syn::custom_keyword!(panics);
19}
20
21#[derive(Clone, Debug)]
22pub struct TestCaseExpression {
23    _token: Token![=>],
24    pub extra_keywords: HashSet<Modifier>,
25    pub result: TestCaseResult,
26}
27
28#[derive(Clone, Debug)]
29pub enum TestCaseResult {
30    // test_case(a, b, c => keywords)
31    Empty,
32    // test_case(a, b, c => result)
33    Simple(Expr),
34    // test_case(a, b, c => matches Ok(_) if true)
35    Matching(Pat, Option<Box<Expr>>),
36    // test_case(a, b, c => panics "abcd")
37    Panicking(Option<Expr>),
38    // test_case(a, b, c => with |v: T| assert!(v.is_nan()))
39    With(Expr),
40    // test_case(a, b, c => using assert_nan)
41    UseFn(Expr),
42    // test_case(a, b, c => is close to 4 precision 0.1)
43    Complex(ComplexTestCase),
44}
45
46impl Parse for TestCaseExpression {
47    fn parse(input: ParseStream) -> syn::Result<Self> {
48        let token: Token![=>] = input.parse()?;
49        let extra_keywords = parse_kws(input);
50
51        if input.parse::<kw::matches>().is_ok() {
52            let pattern = Pat::parse_single(input)?;
53            let guard = if input.peek(If) {
54                let _if_kw: If = input.parse()?;
55                let guard: Box<Expr> = input.parse()?;
56                Some(guard)
57            } else {
58                None
59            };
60
61            Ok(TestCaseExpression {
62                _token: token,
63                extra_keywords,
64                result: TestCaseResult::Matching(pattern, guard),
65            })
66        } else if input.parse::<kw::it>().is_ok() || input.parse::<kw::is>().is_ok() {
67            parse_with_keyword::<_, _>(input, token, extra_keywords, TestCaseResult::Complex)
68        } else if input.parse::<kw::using>().is_ok() {
69            parse_with_keyword::<_, _>(input, token, extra_keywords, TestCaseResult::UseFn)
70        } else if input.parse::<kw::with>().is_ok() {
71            parse_with_keyword::<_, _>(input, token, extra_keywords, TestCaseResult::With)
72        } else if input.parse::<kw::panics>().is_ok() {
73            parse_with_keyword_ok::<_, _>(input, token, extra_keywords, TestCaseResult::Panicking)
74        } else {
75            let result = match input.parse::<Expr>() {
76                Ok(expr) => TestCaseResult::Simple(expr),
77                Err(_) if !extra_keywords.is_empty() => TestCaseResult::Empty,
78                Err(e) => return Err(e),
79            };
80
81            Ok(Self {
82                _token: token,
83                extra_keywords,
84                result,
85            })
86        }
87    }
88}
89
90impl Display for TestCaseExpression {
91    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
92        for kw in &self.extra_keywords {
93            write!(f, "{kw:?}")?;
94        }
95        write!(f, "{}", self.result)
96    }
97}
98
99impl Display for TestCaseResult {
100    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
101        match self {
102            TestCaseResult::Simple(expr) => write!(f, "{}", fmt_syn(expr)),
103            TestCaseResult::Matching(pat, expr) => {
104                write!(f, "matching {} {}", fmt_syn(pat), fmt_syn(expr))
105            }
106            TestCaseResult::Panicking(expr) => write!(
107                f,
108                "panicking {:?}",
109                expr.as_ref().map(|inner| fmt_syn(&inner))
110            ),
111            TestCaseResult::With(expr) => write!(f, "with {}", fmt_syn(expr)),
112            TestCaseResult::UseFn(expr) => write!(f, "use {}", fmt_syn(expr)),
113            TestCaseResult::Complex(complex) => write!(f, "complex {complex}"),
114            TestCaseResult::Empty => write!(f, "empty"),
115        }
116    }
117}
118
119impl TestCaseExpression {
120    pub fn assertion(&self) -> TokenStream2 {
121        match &self.result {
122            TestCaseResult::Simple(expr) => parse_quote! { assert_eq!(#expr, _result) },
123            TestCaseResult::Matching(pat, guard) => {
124                let pat_str = pat.to_token_stream().to_string();
125
126                if let Some(guard) = guard {
127                    let guard_str = guard.to_token_stream().to_string();
128
129                    parse_quote! {
130                        match _result {
131                            #pat if #guard => (),
132                            e => panic!("Expected `{} if {}` found {:?}", #pat_str, #guard_str, e)
133                        }
134                    }
135                } else {
136                    parse_quote! {
137                        match _result {
138                            #pat => (),
139                            e => panic!("Expected `{}` found {:?}", #pat_str, e)
140                        }
141                    }
142                }
143            }
144            TestCaseResult::Panicking(_) => TokenStream2::new(),
145            TestCaseResult::With(expr) => parse_quote! { let fun = #expr; fun(_result) },
146            TestCaseResult::UseFn(path) => parse_quote! { #path(_result) },
147            TestCaseResult::Complex(complex) => complex.assertion(),
148            TestCaseResult::Empty => TokenStream2::new(),
149        }
150    }
151
152    pub fn attributes(&self) -> Vec<Attribute> {
153        let mut attrs: Vec<Attribute> = self
154            .extra_keywords
155            .iter()
156            .map(|modifier| modifier.attribute())
157            .collect();
158        if let TestCaseResult::Panicking(opt) = &self.result {
159            if let Some(expr) = opt {
160                attrs.push(parse_quote! { #[should_panic(expected = #expr)] })
161            } else {
162                attrs.push(parse_quote! { #[should_panic] })
163            }
164        }
165        attrs
166    }
167}
168
169fn parse_with_keyword<Inner, Mapping>(
170    input: ParseStream,
171    token: Token![=>],
172    extra_keywords: HashSet<Modifier>,
173    mapping: Mapping,
174) -> syn::Result<TestCaseExpression>
175where
176    Mapping: FnOnce(Inner) -> TestCaseResult,
177    Inner: Parse,
178{
179    Ok(TestCaseExpression {
180        _token: token,
181        extra_keywords,
182        result: mapping(input.parse()?),
183    })
184}
185
186fn parse_with_keyword_ok<Inner, Mapping>(
187    input: ParseStream,
188    token: Token![=>],
189    extra_keywords: HashSet<Modifier>,
190    mapping: Mapping,
191) -> syn::Result<TestCaseExpression>
192where
193    Mapping: FnOnce(Option<Inner>) -> TestCaseResult,
194    Inner: Parse,
195{
196    let result = (!input.is_empty()).then(|| input.parse()).transpose()?;
197    Ok(TestCaseExpression {
198        _token: token,
199        extra_keywords,
200        result: mapping(result),
201    })
202}