Skip to main content

tesults_test_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, ItemFn};
4
5/// Replaces `#[test]` to add Tesults reporting. Timing, pass/fail, and panic
6/// reason are captured automatically. Use `tesults_test::description()`,
7/// `tesults_test::custom()`, `tesults_test::step()`, and `tesults_test::file()`
8/// inside the test body for enhanced reporting.
9#[proc_macro_attribute]
10pub fn test(_attr: TokenStream, item: TokenStream) -> TokenStream {
11    let input = parse_macro_input!(item as ItemFn);
12    let fn_name = &input.sig.ident;
13    let fn_name_str = fn_name.to_string();
14    let fn_body = &input.block;
15    let attrs = &input.attrs;
16    let vis = &input.vis;
17
18    let should_panic = attrs.iter().any(|a| a.path().is_ident("should_panic"));
19
20    // For should_panic tests: passed = test DID panic (result is Err).
21    // For normal tests:       passed = test did NOT panic (result is Ok).
22    let passed_expr = if should_panic {
23        quote! { __tt_result.is_err() }
24    } else {
25        quote! { __tt_result.is_ok() }
26    };
27
28    // Extract a human-readable failure reason from the panic payload.
29    // Uses `ref` pattern to borrow without consuming __tt_result.
30    let reason_expr = if should_panic {
31        quote! {
32            if __tt_result.is_ok() {
33                ::std::string::String::from("Test was expected to panic but did not")
34            } else {
35                ::std::string::String::new()
36            }
37        }
38    } else {
39        quote! {
40            match __tt_result {
41                ::std::result::Result::Err(ref __tt_err) => {
42                    if let ::std::option::Option::Some(s) =
43                        __tt_err.downcast_ref::<::std::string::String>()
44                    {
45                        s.clone()
46                    } else if let ::std::option::Option::Some(s) =
47                        __tt_err.downcast_ref::<&str>()
48                    {
49                        s.to_string()
50                    } else {
51                        ::std::string::String::from("Test panicked")
52                    }
53                }
54                _ => ::std::string::String::new(),
55            }
56        }
57    };
58
59    let expanded = quote! {
60        #[test]
61        #(#attrs)*
62        #vis fn #fn_name() {
63            let __tt_start = ::tesults_test::__private::now_ms();
64            let __tt_result = ::std::panic::catch_unwind(
65                ::std::panic::AssertUnwindSafe(|| { #fn_body })
66            );
67            let __tt_end = ::tesults_test::__private::now_ms();
68            let __tt_passed = #passed_expr;
69            let __tt_reason = #reason_expr;
70            ::tesults_test::__private::record_result(
71                #fn_name_str,
72                module_path!(),
73                __tt_passed,
74                __tt_reason,
75                __tt_start,
76                __tt_end,
77            );
78            if let ::std::result::Result::Err(__tt_err) = __tt_result {
79                ::std::panic::resume_unwind(__tt_err);
80            }
81        }
82    };
83
84    expanded.into()
85}