rb_sys_test_helpers/
lib.rs

1#![allow(rustdoc::bare_urls)]
2#![doc = include_str!("../readme.md")]
3mod once_cell;
4mod ruby_exception;
5mod ruby_test_executor;
6mod utils;
7
8use rb_sys::{rb_errinfo, rb_intern, rb_set_errinfo, Qnil, VALUE};
9use ruby_test_executor::global_executor;
10use std::{error::Error, mem::MaybeUninit, panic::UnwindSafe};
11
12pub use rb_sys_test_helpers_macros::*;
13pub use ruby_exception::RubyException;
14pub use ruby_test_executor::{cleanup_ruby, setup_ruby, setup_ruby_unguarded};
15
16/// Run a given function with inside of a Ruby VM.
17///
18/// Doing this properly it is not trivial, so this function abstracts away the
19/// details. Under the hood, it ensures:
20///
21/// 1. The Ruby VM is setup and initialized once and only once.
22/// 2. All code runs on the same OS thread.
23/// 3. Exceptions are properly handled and propagated as Rust `Result<T,
24///    RubyException>` values.
25///
26/// ### Example
27///
28/// ```
29/// use rb_sys_test_helpers::with_ruby_vm;
30/// use std::ffi::CStr;
31///
32/// with_ruby_vm(|| unsafe {
33///     let mut hello = rb_sys::rb_utf8_str_new_cstr("hello \0".as_ptr() as _);
34///     rb_sys::rb_str_cat(hello, "world\0".as_ptr() as _, 5);
35///     let result = rb_sys::rb_string_value_cstr(&mut hello);
36///     let result = CStr::from_ptr(result).to_string_lossy().into_owned();
37///
38///     assert_eq!(result, "hello world");
39/// });
40/// ```
41pub fn with_ruby_vm<R, F>(f: F) -> Result<R, Box<dyn Error>>
42where
43    R: Send + 'static,
44    F: FnOnce() -> R + UnwindSafe + Send + 'static,
45{
46    global_executor().run_test(f)
47}
48
49/// Runs a test with GC stress enabled to help find GC bugs.
50///
51/// ### Example
52///
53/// ```
54/// use rb_sys_test_helpers::{with_gc_stress, with_ruby_vm};
55/// use std::ffi::CStr;
56///
57/// with_ruby_vm(|| unsafe {
58///     let hello_world = with_gc_stress(|| unsafe {
59///         let mut rstring = rb_sys::rb_utf8_str_new_cstr("hello world\0".as_ptr() as _);
60///         let result = rb_sys::rb_string_value_cstr(&mut rstring);
61///         CStr::from_ptr(result).to_string_lossy().into_owned()
62///     });
63///
64///    assert_eq!(hello_world, "hello world");
65/// });
66/// ```
67pub fn with_gc_stress<R, F>(f: F) -> R
68where
69    R: Send + 'static,
70    F: FnOnce() -> R + UnwindSafe + Send + 'static,
71{
72    unsafe {
73        let stress_intern = rb_intern("stress\0".as_ptr() as _);
74        let stress_eq_intern = rb_intern("stress=\0".as_ptr() as _);
75        let gc_module = rb_sys::rb_const_get(rb_sys::rb_cObject, rb_intern("GC\0".as_ptr() as _));
76
77        let old_gc_stress = rb_sys::rb_funcall(gc_module, stress_intern, 0);
78        rb_sys::rb_funcall(gc_module, stress_eq_intern, 1, rb_sys::Qtrue);
79        let result = std::panic::catch_unwind(f);
80        rb_sys::rb_funcall(gc_module, stress_eq_intern, 1, old_gc_stress);
81
82        match result {
83            Ok(result) => result,
84            Err(err) => std::panic::resume_unwind(err),
85        }
86    }
87}
88
89/// Catches a Ruby exception and returns it as a `Result` (using [`rb_sys::rb_protect`]).
90///
91/// ### Example
92///
93/// ```
94/// use rb_sys_test_helpers::{protect, with_ruby_vm, RubyException};
95///
96/// with_ruby_vm(|| unsafe {
97///     let result: Result<&str, RubyException> = protect(|| {
98///         rb_sys::rb_raise(rb_sys::rb_eRuntimeError, "oh no\0".as_ptr() as _);
99///         "this will never be returned"
100///     });
101///
102///     assert!(result.is_err());
103///     assert!(result.unwrap_err().message().unwrap().contains("oh no"));
104/// });
105/// ```
106pub fn protect<F, T>(f: F) -> Result<T, RubyException>
107where
108    F: FnMut() -> T + std::panic::UnwindSafe,
109{
110    unsafe extern "C" fn ffi_closure<T, F: FnMut() -> T>(args: VALUE) -> VALUE {
111        let args: *mut (Option<*mut F>, *mut Option<T>) = args as _;
112        let args = *args;
113        let (mut func, outbuf) = args;
114        let func = func.take().unwrap();
115        let func = &mut *func;
116        let result = func();
117        outbuf.write_volatile(Some(result));
118        outbuf as _
119    }
120
121    unsafe {
122        let mut state = 0;
123        let func_ref = &Some(f) as *const _;
124        let mut outbuf: MaybeUninit<Option<T>> = MaybeUninit::new(None);
125        let args = &(Some(func_ref), outbuf.as_mut_ptr() as *mut _) as *const _ as VALUE;
126        rb_sys::rb_protect(Some(ffi_closure::<T, F>), args, &mut state);
127
128        if state == 0 {
129            if outbuf.as_mut_ptr().read_volatile().is_some() {
130                Ok(outbuf.assume_init().expect("unreachable"))
131            } else {
132                Err(RubyException::new(rb_errinfo()))
133            }
134        } else {
135            let err = rb_errinfo();
136            rb_set_errinfo(Qnil as _);
137            Err(RubyException::new(err))
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_protect_returns_correct_value() -> Result<(), Box<dyn Error>> {
148        let ret = with_ruby_vm(|| protect(|| "my val"))?;
149
150        assert_eq!(ret, Ok("my val"));
151
152        Ok(())
153    }
154
155    #[test]
156    fn test_protect_capture_ruby_exception() {
157        with_ruby_vm(|| unsafe {
158            let result = protect(|| {
159                rb_sys::rb_raise(rb_sys::rb_eRuntimeError, "hello world\0".as_ptr() as _);
160            });
161
162            assert!(result.is_err());
163        })
164        .unwrap();
165    }
166}