use crate::complex_expr::ComplexTestCase;
use crate::modifier::{parse_kws, Modifier};
use crate::utils::fmt_syn;
use crate::TokenStream2;
use quote::ToTokens;
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use syn::parse::{Parse, ParseStream};
use syn::{parse_quote, Attribute, Expr, Pat, Token};
pub mod kw {
syn::custom_keyword!(matches);
syn::custom_keyword!(using);
syn::custom_keyword!(with);
syn::custom_keyword!(it);
syn::custom_keyword!(is);
syn::custom_keyword!(panics);
}
#[derive(Debug)]
pub struct TestCaseExpression {
_token: Token![=>],
pub extra_keywords: HashSet<Modifier>,
pub result: TestCaseResult,
}
#[derive(Debug)]
pub enum TestCaseResult {
Simple(Expr),
Matching(Pat),
Panicking(Option<Expr>),
With(Expr),
UseFn(Expr),
Complex(ComplexTestCase),
}
impl Parse for TestCaseExpression {
fn parse(input: ParseStream) -> syn::Result<Self> {
let token: Token![=>] = input.parse()?;
let extra_keywords = parse_kws(input);
if input.parse::<kw::matches>().is_ok() {
parse_with_keyword::<_, _>(input, token, extra_keywords, TestCaseResult::Matching)
} else if input.parse::<kw::it>().is_ok() || input.parse::<kw::is>().is_ok() {
parse_with_keyword::<_, _>(input, token, extra_keywords, TestCaseResult::Complex)
} else if input.parse::<kw::using>().is_ok() {
parse_with_keyword::<_, _>(input, token, extra_keywords, TestCaseResult::UseFn)
} else if input.parse::<kw::with>().is_ok() {
parse_with_keyword::<_, _>(input, token, extra_keywords, TestCaseResult::With)
} else if input.parse::<kw::panics>().is_ok() {
parse_with_keyword_ok::<_, _>(input, token, extra_keywords, TestCaseResult::Panicking)
} else {
Ok(Self {
_token: token,
extra_keywords,
result: TestCaseResult::Simple(input.parse()?),
})
}
}
}
impl Display for TestCaseExpression {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for kw in &self.extra_keywords {
write!(f, "{:?}", kw)?;
}
write!(f, "{}", self.result)
}
}
impl Display for TestCaseResult {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TestCaseResult::Simple(expr) => write!(f, "{}", fmt_syn(expr)),
TestCaseResult::Matching(expr) => write!(f, "matching {}", fmt_syn(expr)),
TestCaseResult::Panicking(expr) => write!(
f,
"panicking {:?}",
expr.as_ref().map(|inner| fmt_syn(&inner))
),
TestCaseResult::With(expr) => write!(f, "with {}", fmt_syn(expr)),
TestCaseResult::UseFn(expr) => write!(f, "use {}", fmt_syn(expr)),
TestCaseResult::Complex(complex) => write!(f, "complex {}", complex),
}
}
}
impl TestCaseExpression {
pub fn assertion(&self) -> TokenStream2 {
match &self.result {
TestCaseResult::Simple(expr) => parse_quote! { assert_eq!(#expr, _result) },
TestCaseResult::Matching(pat) => {
let pat_str = pat.to_token_stream().to_string();
parse_quote! {
match _result {
#pat => (),
e => panic!("Expected {} found {:?}", #pat_str, e)
}
}
}
TestCaseResult::Panicking(_) => TokenStream2::new(),
TestCaseResult::With(expr) => parse_quote! { let fun = #expr; fun(_result) },
TestCaseResult::UseFn(path) => parse_quote! { #path(_result) },
TestCaseResult::Complex(complex) => complex.assertion(),
}
}
pub fn attributes(&self) -> Vec<Attribute> {
let mut attrs: Vec<Attribute> = self
.extra_keywords
.iter()
.map(|modifier| modifier.attribute())
.collect();
if let TestCaseResult::Panicking(opt) = &self.result {
if let Some(expr) = opt {
attrs.push(parse_quote! { #[should_panic(expected = #expr)] })
} else {
attrs.push(parse_quote! { #[should_panic] })
}
}
attrs
}
}
fn parse_with_keyword<Inner, Mapping>(
input: ParseStream,
token: Token![=>],
extra_keywords: HashSet<Modifier>,
mapping: Mapping,
) -> syn::Result<TestCaseExpression>
where
Mapping: FnOnce(Inner) -> TestCaseResult,
Inner: Parse,
{
Ok(TestCaseExpression {
_token: token,
extra_keywords,
result: mapping(input.parse()?),
})
}
fn parse_with_keyword_ok<Inner, Mapping>(
input: ParseStream,
token: Token![=>],
extra_keywords: HashSet<Modifier>,
mapping: Mapping,
) -> syn::Result<TestCaseExpression>
where
Mapping: FnOnce(Option<Inner>) -> TestCaseResult,
Inner: Parse,
{
Ok(TestCaseExpression {
_token: token,
extra_keywords,
result: mapping(input.parse::<Inner>().ok()),
})
}