use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use crate::ParseResult;
use crate::util::{
KvParser, bail, extract_typename, ident, path_ends_with, retain_attributes_except,
};
pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
let func = match input_item {
venial::Item::Function(f) => f,
_ => return bail!(&input_item, "#[itest] can only be applied to functions"),
};
let mut attr = KvParser::parse_required(&func.attributes, "itest", &func.name)?;
let skipped = attr.handle_alone("skip")?;
let focused = attr.handle_alone("focus")?;
let is_async = attr.handle_alone("async")?;
attr.finish()?;
if func.generic_params.is_some()
|| func.params.len() > 1
|| (func.return_ty.is_some() && !is_async)
|| func.where_clause.is_some()
{
return bad_signature(&func);
}
if skipped && focused {
return bail!(
func.name,
"#[itest]: keys `skip` and `focus` are mutually exclusive",
);
}
let test_name = &func.name;
let test_name_str = func.name.to_string();
let param = if let Some((param, _punct)) = func.params.first() {
if let venial::FnParam::Typed(param) = param {
if path_ends_with(¶m.ty.tokens, "TestContext") {
param.to_token_stream()
} else if is_async {
return bad_async_signature(&func);
} else {
return bad_signature(&func);
}
} else if is_async {
return bad_async_signature(&func);
} else {
return bad_signature(&func);
}
} else {
quote! { __unused_context: &crate::framework::TestContext }
};
let return_ty = func.return_ty.as_ref();
if is_async
&& return_ty
.and_then(extract_typename)
.is_none_or(|segment| segment.ident != "TaskHandle")
{
return bad_async_signature(&func);
}
let body = &func.body;
let (return_tokens, test_case_ty, plugin_name);
if is_async {
let [arrow, arrow_head] = func.tk_return_arrow.unwrap();
return_tokens = quote! { #arrow #arrow_head #return_ty }; test_case_ty = quote! { crate::framework::AsyncRustTestCase };
plugin_name = ident("__GODOT_ASYNC_ITEST");
} else {
return_tokens = TokenStream::new();
test_case_ty = quote! { crate::framework::RustTestCase };
plugin_name = ident("__GODOT_ITEST");
};
let other_attributes = retain_attributes_except(&func.attributes, "itest");
Ok(quote! {
#(#other_attributes)*
pub fn #test_name(#param) #return_tokens
#body
::godot::sys::plugin_add!(crate::framework::#plugin_name; #test_case_ty {
name: #test_name_str,
skipped: #skipped,
focused: #focused,
file: std::file!(),
line: std::line!(),
function: #test_name,
});
})
}
fn bad_signature(func: &venial::Function) -> Result<TokenStream, venial::Error> {
bail!(
func,
"#[itest] function must have one of these signatures:\
\n fn {f}() {{ ... }}\
\n fn {f}(ctx: &TestContext) {{ ... }}",
f = func.name,
)
}
fn bad_async_signature(func: &venial::Function) -> Result<TokenStream, venial::Error> {
bail!(
func,
"#[itest(async)] function must have one of these signatures:\
\n fn {f}() -> TaskHandle {{ ... }}\
\n fn {f}(ctx: &TestContext) -> TaskHandle {{ ... }}",
f = func.name,
)
}