use proc_macro::TokenStream;
use quote::quote;
use rtest_util::TestArguments;
use std::sync::{Arc, Mutex, OnceLock};
struct TestInfo {
fn_name: String,
#[allow(dead_code)]
args: TestArguments,
input: String,
}
static FUNCTION_NAMES: OnceLock<Arc<Mutex<Vec<TestInfo>>>> = OnceLock::new();
#[proc_macro_derive(FromContext)]
pub fn from_context(input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input as syn::DeriveInput);
let name = &ast.ident;
let name_str = name.to_string();
let code = quote!(
impl rtest::FromContext for #name {
type Context = rtest::Context;
fn from_context(context: &Self::Context) -> Option<rtest::Resource<Self>> {
context.extract::<#name>()
}
fn into_context(context: &Self::Context, resource: rtest::Resource<#name>) {
context.inject(resource)
}
fn get_resource_id() -> rtest::ResourceId {
#name_str.to_string()
}
}
);
code.into()
}
#[proc_macro_attribute]
pub fn rtest(attr: TokenStream, orig_input: TokenStream) -> TokenStream {
use darling::FromMeta;
let input = orig_input.clone();
let fn_ast = syn::parse_macro_input!(input as syn::ItemFn);
let attr_args = match darling::ast::NestedMeta::parse_meta_list(attr.into()) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(darling::Error::from(e).write_errors());
},
};
let args = match TestArguments::from_list(&attr_args) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(e.write_errors());
},
};
{
let mutex = FUNCTION_NAMES.get_or_init(|| Arc::new(Mutex::new(Vec::new())));
let mut list = mutex.lock().unwrap();
list.push(TestInfo {
fn_name: fn_ast.sig.ident.to_string(),
args,
input: orig_input.to_string(),
})
}
orig_input
}
#[proc_macro]
pub fn run(input: TokenStream) -> TokenStream {
use std::str::FromStr;
let list: Vec<_> = {
let mutex = FUNCTION_NAMES.get_or_init(|| Arc::new(Mutex::new(Vec::new())));
mutex.lock().unwrap().drain(..).collect()
};
let input_struct = syn::parse_macro_input!(input as syn::Path);
let mut commands = quote!();
for (i, l) in list.into_iter().enumerate() {
let name = l.fn_name.to_string();
let input = TokenStream::from_str(&l.input).unwrap();
let ast = syn::parse_macro_input!(input as syn::ItemFn);
let fn_ident = ast.sig.ident;
let relative_fn_ident = l
.args
.module
.as_ref()
.and_then(|module| module.split_once("::"))
.map(|module| format!("crate::{}::{}", module.1, &fn_ident.to_string()))
.unwrap_or(fn_ident.to_string());
let relative_fn_ident = TokenStream::from_str(&relative_fn_ident).unwrap();
let relative_fn_ident = syn::parse_macro_input!(relative_fn_ident as syn::Path);
let testargs = l.args;
let f = quote! {
let c = #input_struct.context.clone();
let handler_params = rtest::HandlerParams::from(&#input_struct);
let (reference, inputs, outputs) = rtest::describe_handler(& #relative_fn_ident);
test_repo.add(#i, #name, Box::new(move || {
rtest::call_handler(c.clone(), &mut #relative_fn_ident, &handler_params)
}), #testargs, reference, inputs, outputs);
};
commands = quote!(
#commands
#f
);
}
let code = quote!(
{
use rtest::{Runner, Printer, TestArguments, Persister};
use clap::Parser;
let args = rtest::Args::parse();
let #input_struct: rtest::RunConfig<_> = #input_struct;
let initial_resources = #input_struct.context.get_resources();
let mut test_repo = rtest::TestRepo::new(args.test_tracing_log_level);
#commands
let (results, info) = Runner::run::<rtest::DepdencyStrategy>(test_repo, initial_resources);
let exit_code = results.exitcode();
Printer::print_to_stdout(&results, &info);
if let Err(e) = Persister::persist_to_file(results, info) {
tracing::error!(?e, "could not store to file");
}
std::process::ExitCode::from(exit_code)
}
);
code.into()
}