blob: ea3679b0b4fe85775028af7b3336b2015e528bd5 [file] [log] [blame]
use quote::quote;
use quote::ToTokens;
use syn::export::TokenStream2;
use syn::parse::{Parse, ParseStream};
use syn::{parse_quote, Error, Expr, Ident, ItemFn, LitStr, Pat, Token};
#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct TestCase {
test_case_name: String,
args: Vec<Expr>,
expected: Option<Expected>,
case_desc: Option<LitStr>,
}
#[cfg_attr(test, derive(Debug, PartialEq))]
pub enum Expected {
Pat(Pat),
Panic(LitStr),
Ignored(Box<Expr>),
Expr(Box<Expr>),
}
mod kw {
syn::custom_keyword!(matches);
syn::custom_keyword!(panics);
syn::custom_keyword!(inconclusive);
}
impl ToString for Expected {
fn to_string(&self) -> String {
match self {
Expected::Pat(p) => format!("matches {}", fmt_syn(&p)),
Expected::Panic(p) => format!("panics {}", fmt_syn(&p)),
Expected::Ignored(e) => format!("ignored {}", fmt_syn(&e)),
Expected::Expr(e) => format!("expects {}", fmt_syn(&e)),
}
}
}
impl Parse for Expected {
fn parse(input: ParseStream) -> Result<Self, Error> {
let lookahead = input.lookahead1();
if lookahead.peek(kw::matches) {
let _kw = input.parse::<kw::matches>()?;
let pat = input.parse()?;
return Ok(Expected::Pat(pat));
}
if lookahead.peek(kw::panics) {
let _kw = input.parse::<kw::panics>()?;
let pat = input.parse()?;
return Ok(Expected::Panic(pat));
}
if lookahead.peek(kw::inconclusive) {
let _kw = input.parse::<kw::inconclusive>()?;
let expr = input.parse()?;
return Ok(Expected::Ignored(expr));
}
let expr = input.parse()?;
Ok(Expected::Expr(expr))
}
}
fn fmt_syn(syn: &(impl ToTokens + Clone)) -> String {
syn.clone().into_token_stream().to_string()
}
impl Parse for TestCase {
fn parse(input: ParseStream) -> Result<Self, Error> {
let mut test_case_name = String::new();
let mut args = vec![];
loop {
let exp: Expr = input.parse()?;
test_case_name += &format!(" {}", fmt_syn(&exp));
args.push(exp);
if !input.peek(Token![,]) {
break;
}
let _comma: Token![,] = input.parse()?;
}
let arrow: Option<Token![=>]> = input.parse()?;
let expected = if arrow.is_some() {
let expected: Expected = input.parse()?;
test_case_name += &format!(" {}", expected.to_string());
Some(expected)
} else {
None
};
let semicolon: Option<Token![;]> = input.parse()?;
let case_desc = if semicolon.is_some() {
let desc: LitStr = input.parse()?;
Some(desc)
} else {
None
};
Ok(Self {
test_case_name,
args,
expected,
case_desc,
})
}
}
impl TestCase {
pub fn test_case_name(&self) -> Ident {
let case_desc = self
.case_desc
.as_ref()
.map(|cd| cd.value())
.unwrap_or_else(|| self.test_case_name.clone());
crate::utils::escape_test_name(case_desc)
}
pub fn render(&self, mut item: ItemFn) -> TokenStream2 {
let item_name = item.sig.ident.clone();
let arg_values = self.args.iter();
let test_case_name = self.test_case_name();
let inconclusive = self
.case_desc
.as_ref()
.map(|cd| cd.value().to_lowercase().contains("inconclusive"))
.unwrap_or_default();
let mut attrs = vec![];
let expected: Expr = match &self.expected {
Some(Expected::Pat(pat)) => {
let pat_str = format!("{}", quote! { #pat });
parse_quote! {
match _result {
#pat => (),
e => panic!("Expected {} found {:?}", #pat_str, e)
}
}
}
Some(Expected::Expr(e)) => {
parse_quote! { assert_eq!(#e, _result) }
}
Some(Expected::Panic(l)) => {
attrs.push(parse_quote! { #[should_panic(expected = #l)] });
parse_quote! {()}
}
Some(Expected::Ignored(_)) => {
attrs.push(parse_quote! { #[ignore] });
parse_quote! {()}
}
None => parse_quote! {()},
};
if inconclusive {
attrs.push(parse_quote! { #[ignore] });
}
attrs.append(&mut item.attrs);
quote! {
#[test]
#(#attrs)*
fn #test_case_name() {
let _result = #item_name(#(#arg_values),*);
#expected
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
mod expected {
use super::*;
mod parse {
use super::*;
use syn::parse_quote;
#[test]
fn parses_expression() {
let actual: Expected = parse_quote! { 2 + 3 };
assert_eq!(Expected::Expr(parse_quote!(2 + 3)), actual);
}
#[test]
fn parses_panic() {
let actual: Expected = parse_quote! { panics "Error msg" };
assert_eq!(Expected::Panic(parse_quote!("Error msg")), actual);
}
#[test]
fn parses_pattern() {
let actual: Expected = parse_quote! { matches Some(_) };
assert_eq!(Expected::Pat(parse_quote!(Some(_))), actual);
}
}
}
mod test_case {
use super::*;
mod parse {
use super::*;
use syn::parse_quote;
#[test]
fn parses_basic_input() {
let actual: TestCase = parse_quote! {
2, 10
};
assert_eq!(
TestCase {
test_case_name: " 2 10".to_string(),
args: vec![parse_quote!(2), parse_quote!(10),],
expected: None,
case_desc: None,
},
actual
);
}
#[test]
fn parses_input_with_expectation() {
let actual: TestCase = parse_quote! {
2, 10 => 12
};
assert_eq!(
TestCase {
test_case_name: " 2 10 expects 12".to_string(),
args: vec![parse_quote!(2), parse_quote!(10),],
expected: Some(parse_quote!(12)),
case_desc: None,
},
actual
);
}
#[test]
fn parses_input_with_description() {
let actual: TestCase = parse_quote! {
2, 10; "basic addition"
};
assert_eq!(
TestCase {
test_case_name: " 2 10".to_string(),
args: vec![parse_quote!(2), parse_quote!(10),],
expected: None,
case_desc: parse_quote!("basic addition"),
},
actual
);
}
}
}
}