1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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]
/// input must be the RunConfig
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()
}