rb_sys_test_helpers/
lib.rs1#![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::{any::Any, 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
16pub 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
49pub struct GcStressGuard {
73 old_gc_stress: VALUE,
74}
75
76impl GcStressGuard {
77 pub fn new() -> Self {
82 unsafe {
83 let stress_intern = rb_intern("stress\0".as_ptr() as _);
84 let stress_eq_intern = rb_intern("stress=\0".as_ptr() as _);
85 let gc_module =
86 rb_sys::rb_const_get(rb_sys::rb_cObject, rb_intern("GC\0".as_ptr() as _));
87
88 let old_gc_stress = rb_sys::rb_funcall(gc_module, stress_intern, 0);
89 rb_sys::rb_funcall(gc_module, stress_eq_intern, 1, rb_sys::Qtrue);
90
91 Self { old_gc_stress }
92 }
93 }
94}
95
96impl Default for GcStressGuard {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl Drop for GcStressGuard {
103 fn drop(&mut self) {
104 unsafe {
105 let stress_eq_intern = rb_intern("stress=\0".as_ptr() as _);
106 let gc_module =
107 rb_sys::rb_const_get(rb_sys::rb_cObject, rb_intern("GC\0".as_ptr() as _));
108 rb_sys::rb_funcall(gc_module, stress_eq_intern, 1, self.old_gc_stress);
109 }
110 }
111}
112
113pub fn with_gc_stress<R, F>(f: F) -> R
136where
137 F: FnOnce() -> R,
138{
139 let _guard = GcStressGuard::new();
140 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
141
142 match result {
143 Ok(result) => result,
144 Err(err) => std::panic::resume_unwind(err),
145 }
146}
147
148type PanicPayload = Box<dyn Any + Send + 'static>;
150
151enum ClosureResult<T> {
155 Ok(T),
157 Panic(PanicPayload),
159}
160
161pub fn protect<F, T>(f: F) -> Result<T, RubyException>
183where
184 F: FnMut() -> T + std::panic::UnwindSafe,
185{
186 unsafe extern "C" fn ffi_closure<T, F: FnMut() -> T>(args: VALUE) -> VALUE {
187 let args: *mut (Option<*mut F>, *mut Option<ClosureResult<T>>) = args as _;
188 let args = *args;
189 let (mut func, outbuf) = args;
190 let func = func.take().unwrap();
191 let func = &mut *func;
192
193 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
196
197 let closure_result = match result {
198 Ok(value) => ClosureResult::Ok(value),
199 Err(panic_payload) => ClosureResult::Panic(panic_payload),
200 };
201
202 outbuf.write_volatile(Some(closure_result));
203 outbuf as _
204 }
205
206 unsafe {
207 let mut state = 0;
208 let func_ref = &Some(f) as *const _;
209 let mut outbuf: MaybeUninit<Option<ClosureResult<T>>> = MaybeUninit::new(None);
210 let args = &(Some(func_ref), outbuf.as_mut_ptr() as *mut _) as *const _ as VALUE;
211 rb_sys::rb_protect(Some(ffi_closure::<T, F>), args, &mut state);
212
213 if state == 0 {
214 match outbuf.assume_init() {
215 Some(ClosureResult::Ok(value)) => Ok(value),
216 Some(ClosureResult::Panic(payload)) => {
217 std::panic::resume_unwind(payload)
219 }
220 None => Err(RubyException::new(rb_errinfo())),
221 }
222 } else {
223 let err = rb_errinfo();
224 rb_set_errinfo(Qnil as _);
225 Err(RubyException::new(err))
226 }
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use rusty_fork::rusty_fork_test;
234
235 #[test]
236 fn test_protect_returns_correct_value() -> Result<(), Box<dyn Error>> {
237 let ret = with_ruby_vm(|| protect(|| "my val"))?;
238
239 assert_eq!(ret, Ok("my val"));
240
241 Ok(())
242 }
243
244 #[test]
245 fn test_protect_capture_ruby_exception() {
246 with_ruby_vm(|| unsafe {
247 let result = protect(|| {
248 rb_sys::rb_raise(rb_sys::rb_eRuntimeError, "hello world\0".as_ptr() as _);
249 });
250
251 assert!(result.is_err());
252 })
253 .unwrap();
254 }
255
256 rusty_fork_test! {
257 #[test]
258 fn test_protect_propagates_rust_panic_with_readable_output() {
259 use std::panic;
260
261 let result = with_ruby_vm(|| {
262 panic::catch_unwind(panic::AssertUnwindSafe(|| {
263 protect(|| {
264 panic!("this is a test panic message that should be visible");
265 })
266 }))
267 });
268
269 let outer_result = result.expect("with_ruby_vm should not fail");
271
272 assert!(outer_result.is_err(), "panic should have been caught by catch_unwind");
274
275 let panic_payload = outer_result.unwrap_err();
277 let panic_msg = panic_payload
278 .downcast_ref::<&str>()
279 .map(|s| s.to_string())
280 .or_else(|| panic_payload.downcast_ref::<String>().cloned())
281 .unwrap_or_else(|| "unknown panic".to_string());
282
283 assert!(
284 panic_msg.contains("test panic message"),
285 "panic message should be preserved, got: {}",
286 panic_msg
287 );
288 }
289 }
290
291 #[test]
293 fn test_protect_with_result_ok() {
294 with_ruby_vm(|| {
295 let result = protect(|| -> Result<i32, &'static str> { Ok(42) });
296
297 match result {
298 Ok(Ok(value)) => assert_eq!(value, 42),
299 Ok(Err(e)) => panic!("inner error: {}", e),
300 Err(e) => panic!("ruby exception: {:?}", e),
301 }
302 })
303 .unwrap();
304 }
305
306 #[test]
307 fn test_protect_with_result_err() {
308 with_ruby_vm(|| {
309 let result = protect(|| -> Result<i32, &'static str> { Err("test error") });
310
311 match result {
312 Ok(Ok(_)) => panic!("expected error"),
313 Ok(Err(e)) => assert_eq!(e, "test error"),
314 Err(e) => panic!("ruby exception: {:?}", e),
315 }
316 })
317 .unwrap();
318 }
319
320 #[test]
321 fn test_gc_stress_guard() {
322 with_ruby_vm(|| unsafe {
323 let stress_intern = rb_intern("stress\0".as_ptr() as _);
324 let gc_module =
325 rb_sys::rb_const_get(rb_sys::rb_cObject, rb_intern("GC\0".as_ptr() as _));
326
327 let initial_stress = rb_sys::rb_funcall(gc_module, stress_intern, 0);
329 assert_eq!(initial_stress, rb_sys::Qfalse as VALUE);
330
331 {
332 let _guard = GcStressGuard::new();
333
334 let stress_during = rb_sys::rb_funcall(gc_module, stress_intern, 0);
336 assert_eq!(stress_during, rb_sys::Qtrue as VALUE);
337 }
338
339 let final_stress = rb_sys::rb_funcall(gc_module, stress_intern, 0);
341 assert_eq!(final_stress, rb_sys::Qfalse as VALUE);
342 })
343 .unwrap();
344 }
345
346 #[test]
347 fn test_with_gc_stress_restores_on_panic() {
348 with_ruby_vm(|| unsafe {
349 let stress_intern = rb_intern("stress\0".as_ptr() as _);
350 let gc_module =
351 rb_sys::rb_const_get(rb_sys::rb_cObject, rb_intern("GC\0".as_ptr() as _));
352
353 let initial_stress = rb_sys::rb_funcall(gc_module, stress_intern, 0);
355 assert_eq!(initial_stress, rb_sys::Qfalse as VALUE);
356
357 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
359 with_gc_stress(|| {
360 panic!("test panic");
361 })
362 }));
363
364 assert!(result.is_err(), "should have panicked");
365
366 let final_stress = rb_sys::rb_funcall(gc_module, stress_intern, 0);
368 assert_eq!(final_stress, rb_sys::Qfalse as VALUE);
369 })
370 .unwrap();
371 }
372}