rb_sys_test_helpers_macros/
lib.rs

1#![allow(rustdoc::bare_urls)]
2#![doc = include_str!("../readme.md")]
3
4use proc_macro::{TokenStream, TokenTree};
5use quote::quote;
6use syn::{spanned::Spanned, ItemFn, ReturnType};
7
8/// A proc-macro which generates a `#[test]` function has access to a valid Ruby VM.
9///
10/// Doing this properly it is not trivial, so this function abstracts away the
11/// details. Under the hood, it ensures:
12///
13/// 1. The Ruby VM is setup and initialized once and only once.
14/// 2. All code runs on the same OS thread.
15/// 3. Exceptions are properly handled and propagated as Rust `Result<T,
16///    RubyException>` values.
17///
18/// ### Example
19///
20/// ```
21/// use rb_sys_test_helpers_macros::ruby_test;
22///
23/// #[ruby_test]
24/// fn test_it_works() {
25///    unsafe { rb_sys::rb_eval_string("1 + 1\0".as_ptr() as _) };
26/// }
27///
28/// #[ruby_test(gc_stress)]
29/// fn test_with_stress() {
30///    unsafe { rb_sys::rb_eval_string("puts 'GC is stressing me out.'\0".as_ptr() as _) };
31/// }
32/// ```
33///
34/// Tests can also return a `Result` to use the `?` operator:
35///
36/// ```
37/// use rb_sys_test_helpers_macros::ruby_test;
38/// use std::error::Error;
39///
40/// #[ruby_test]
41/// fn test_with_result() -> Result<(), Box<dyn Error>> {
42///    let value = some_fallible_operation()?;
43///    Ok(())
44/// }
45/// # fn some_fallible_operation() -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
46/// ```
47#[proc_macro_attribute]
48pub fn ruby_test(args: TokenStream, input: TokenStream) -> TokenStream {
49    let input: ItemFn = match syn::parse2(input.into()) {
50        Ok(input) => input,
51        Err(err) => return err.to_compile_error().into(),
52    };
53
54    let mut gc_stress = false;
55
56    for arg in args {
57        match arg {
58            TokenTree::Ident(ident) => match ident.to_string().as_str() {
59                "gc_stress" => gc_stress = true,
60                kw => {
61                    return syn::Error::new(kw.span(), format!("unknown argument: {}", kw))
62                        .to_compile_error()
63                        .into();
64                }
65            },
66            _ => {
67                return syn::Error::new(arg.span().into(), format!("expected identifier: {}", arg))
68                    .to_compile_error()
69                    .into();
70            }
71        }
72    }
73
74    let block = input.block;
75    let attrs = input.attrs;
76    let vis = input.vis;
77    let sig = &input.sig;
78
79    // Check if the function returns a Result type
80    let returns_result = matches!(&sig.output, ReturnType::Type(_, _));
81
82    let block = if gc_stress {
83        quote! {
84            rb_sys_test_helpers::with_gc_stress(|| {
85                #block
86            })
87        }
88    } else {
89        quote! { #block }
90    };
91
92    let block = quote! {
93        let ret = {
94            #block
95        };
96        rb_sys_test_helpers::trigger_full_gc!();
97        ret
98    };
99
100    // Helper to generate the error logging code
101    let log_ruby_exception = quote! {
102        match std::env::var("RUST_BACKTRACE") {
103            Ok(val) if val == "1" || val == "full" => {
104                eprintln!("ruby exception:");
105                let errinfo = format!("{:#?}", err);
106                let errinfo = errinfo.replace("\n", "\n    ");
107                eprintln!("    {}", errinfo);
108            },
109            _ => (),
110        }
111    };
112
113    // Generate different code based on whether the test returns a Result or not
114    let test_fn = if returns_result {
115        // For Result-returning tests, propagate errors properly
116        quote! {
117            #[test]
118            #(#attrs)*
119            #vis #sig {
120                rb_sys_test_helpers::with_ruby_vm(|| {
121                    let result = rb_sys_test_helpers::protect(|| {
122                        #block
123                    });
124
125                    match result {
126                        Err(err) => {
127                            #log_ruby_exception
128                            Err(err.into())
129                        },
130                        Ok(inner_result) => inner_result,
131                    }
132                }).expect("test execution failure")
133            }
134        }
135    } else {
136        // For unit-returning tests, use the original behavior
137        quote! {
138            #[test]
139            #(#attrs)*
140            #vis #sig {
141                rb_sys_test_helpers::with_ruby_vm(|| {
142                    let result = rb_sys_test_helpers::protect(|| {
143                        #block
144                    });
145
146                    let ret = match result {
147                        Err(err) => {
148                            #log_ruby_exception
149                            Err(err)
150                        },
151                        Ok(v) => Ok(v),
152                    };
153
154                    ret
155                }).expect("test execution failure").expect("ruby exception");
156            }
157        }
158    };
159
160    test_fn.into()
161}