test_case_core/
test_case.rs

1use crate::comment::TestCaseComment;
2use crate::expr::{TestCaseExpression, TestCaseResult};
3use crate::utils::fmt_syn;
4use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
5use quote::quote;
6use syn::parse::{Parse, ParseStream};
7use syn::punctuated::Punctuated;
8use syn::{parse_quote, Error, Expr, Ident, ItemFn, ReturnType, Token};
9
10#[derive(Debug)]
11pub struct TestCase {
12    args: Punctuated<Expr, Token![,]>,
13    expression: Option<TestCaseExpression>,
14    name: Ident,
15}
16
17impl Parse for TestCase {
18    fn parse(input: ParseStream) -> Result<Self, Error> {
19        let args = Punctuated::parse_separated_nonempty_with(input, Expr::parse)?;
20        let expression = (!input.is_empty()).then(|| input.parse()).transpose();
21        let comment = (!input.is_empty()).then(|| input.parse()).transpose();
22        // if both are errors, pick the expression error since it is more likely to be informative.
23        //
24        // TODO(https://github.com/frondeus/test-case/issues/135): avoid Result::ok entirely.
25        let (expression, comment) = match (expression, comment) {
26            (Err(expression), Err(_comment)) => return Err(expression),
27            (expression, comment) => (expression.ok().flatten(), comment.ok().flatten()),
28        };
29
30        Ok(Self::new_from_parsed(args, expression, comment))
31    }
32}
33impl TestCase {
34    pub(crate) fn new<I: IntoIterator<Item = Expr>>(
35        args: I,
36        expression: Option<TestCaseExpression>,
37        comment: Option<TestCaseComment>,
38    ) -> Self {
39        Self::new_from_parsed(args.into_iter().collect(), expression, comment)
40    }
41
42    pub(crate) fn new_from_parsed(
43        args: Punctuated<Expr, Token![,]>,
44        expression: Option<TestCaseExpression>,
45        comment: Option<TestCaseComment>,
46    ) -> Self {
47        let name = Self::test_case_name_ident(args.iter(), expression.as_ref(), comment.as_ref());
48
49        Self {
50            args,
51            expression,
52            name,
53        }
54    }
55
56    pub(crate) fn new_with_prefixed_name<I: IntoIterator<Item = Expr>>(
57        args: I,
58        expression: Option<TestCaseExpression>,
59        prefix: &str,
60    ) -> Self {
61        let parsed_args = args.into_iter().collect::<Punctuated<Expr, Token![,]>>();
62        let name = Self::prefixed_test_case_name(parsed_args.iter(), expression.as_ref(), prefix);
63
64        Self {
65            args: parsed_args,
66            expression,
67            name,
68        }
69    }
70
71    pub fn test_case_name(&self) -> Ident {
72        // The clone is kind of annoying here, but because this is behind a reference, we must clone
73        // to preserve the signature without a breaking change
74        // TODO: return a reference?
75        self.name.clone()
76    }
77
78    pub fn render(&self, mut item: ItemFn, origin_span: Span2) -> TokenStream2 {
79        let item_name = item.sig.ident.clone();
80        let arg_values = self.args.iter();
81        let test_case_name = {
82            let mut test_case_name = self.test_case_name();
83            test_case_name.set_span(origin_span);
84            test_case_name
85        };
86
87        let mut attrs = self
88            .expression
89            .as_ref()
90            .map(|expr| expr.attributes())
91            .unwrap_or_default();
92
93        attrs.push(parse_quote! { #[allow(clippy::bool_assert_comparison)] });
94        attrs.append(&mut item.attrs);
95
96        let (mut signature, body) = if item.sig.asyncness.is_some() {
97            (
98                quote! { async },
99                quote! { let _result = super::#item_name(#(#arg_values),*).await; },
100            )
101        } else {
102            attrs.insert(0, parse_quote! { #[::core::prelude::v1::test] });
103            (
104                TokenStream2::new(),
105                quote! { let _result = super::#item_name(#(#arg_values),*); },
106            )
107        };
108
109        let expected = if let Some(expr) = self.expression.as_ref() {
110            attrs.extend(expr.attributes());
111
112            signature.extend(quote! { fn #test_case_name() });
113
114            if let TestCaseResult::Panicking(_) = expr.result {
115                TokenStream2::new()
116            } else {
117                expr.assertion()
118            }
119        } else {
120            signature.extend(if let ReturnType::Type(_, typ) = item.sig.output {
121                quote! { fn #test_case_name() -> #typ }
122            } else {
123                quote! { fn #test_case_name() }
124            });
125
126            quote! { _result }
127        };
128
129        quote! {
130            #(#attrs)*
131            #signature {
132                #body
133                #expected
134            }
135        }
136    }
137
138    fn test_case_name_ident<'a, I: Iterator<Item = &'a Expr>>(
139        args: I,
140        expression: Option<&TestCaseExpression>,
141        comment: Option<&TestCaseComment>,
142    ) -> Ident {
143        let desc = Self::test_case_name_string(args, expression, comment);
144
145        crate::utils::escape_test_name(desc)
146    }
147
148    fn prefixed_test_case_name<'a, I: Iterator<Item = &'a Expr>>(
149        args: I,
150        expression: Option<&TestCaseExpression>,
151        prefix: &str,
152    ) -> Ident {
153        let generated_name = Self::test_case_name_string(args, expression, None);
154        let full_desc = format!("{prefix}_{generated_name}");
155
156        crate::utils::escape_test_name(full_desc)
157    }
158
159    fn test_case_name_string<'a, I: Iterator<Item = &'a Expr>>(
160        args: I,
161        expression: Option<&TestCaseExpression>,
162        comment: Option<&TestCaseComment>,
163    ) -> String {
164        comment
165            .as_ref()
166            .map(|item| item.comment.value())
167            .unwrap_or_else(|| {
168                let mut acc = String::new();
169                for arg in args {
170                    acc.push_str(&fmt_syn(&arg));
171                    acc.push('_');
172                }
173                acc.push_str("expects");
174                if let Some(expression) = expression {
175                    acc.push(' ');
176                    acc.push_str(&expression.to_string())
177                }
178                acc
179            })
180    }
181}