rb_sys_test_helpers/
ruby_test_executor.rs1use std::error::Error;
2use std::panic;
3use std::ptr::addr_of_mut;
4use std::sync::mpsc::{self, SyncSender};
5use std::sync::Once;
6use std::thread::{self, JoinHandle};
7use std::time::Duration;
8
9use crate::once_cell::OnceCell;
10#[cfg(ruby_gte_3_0)]
11use rb_sys::rb_ext_ractor_safe;
12use rb_sys::{
13 rb_errinfo, rb_inspect, rb_protect, rb_set_errinfo, rb_string_value_cstr, ruby_exec_node,
14 ruby_init_stack, ruby_process_options, ruby_setup, Qnil, VALUE,
15};
16
17static mut GLOBAL_EXECUTOR: OnceCell<RubyTestExecutor> = OnceCell::new();
18
19pub struct RubyTestExecutor {
20 #[allow(clippy::type_complexity)]
21 sender: Option<SyncSender<Box<dyn FnOnce() -> Result<(), Box<dyn Error>> + Send>>>,
22 handle: Option<JoinHandle<Result<(), std::boxed::Box<dyn Error + Send>>>>,
23 timeout: Duration,
24}
25
26impl RubyTestExecutor {
27 pub fn start() -> Self {
28 let (sender, receiver) =
29 mpsc::sync_channel::<Box<dyn FnOnce() -> Result<(), Box<dyn Error>> + Send>>(0);
30
31 let handle = thread::spawn(move || -> Result<(), Box<dyn Error + Send>> {
32 for closure in receiver {
33 match closure() {
34 Ok(()) => {}
35 Err(err) => {
36 let err: Box<dyn Error + Send> = unsafe { std::mem::transmute(err) };
38 return Err(err);
39 }
40 }
41 }
42 Ok(())
43 });
44
45 let executor = Self {
46 sender: Some(sender),
47 handle: Some(handle),
48 timeout: Duration::from_secs(10),
49 };
50
51 executor
52 .run(|| {
53 static INIT: Once = Once::new();
54
55 INIT.call_once(|| unsafe {
56 setup_ruby_unguarded();
57 })
58 })
59 .expect("Failed to setup Ruby");
60
61 executor
62 }
63
64 pub fn set_test_timeout(&mut self, timeout: Duration) {
65 self.timeout = timeout;
66 }
67
68 pub fn shutdown(&mut self) {
69 self.set_test_timeout(Duration::from_secs(3));
70
71 let _ = self.run(|| unsafe {
72 cleanup_ruby();
73 });
74
75 if let Some(sender) = self.sender.take() {
76 drop(sender);
77 }
78
79 if let Some(handle) = self.handle.take() {
80 let _ = handle.join().expect("Failed to join executor thread");
81 }
82 }
83
84 pub fn run<F, R>(&self, f: F) -> Result<R, Box<dyn Error>>
85 where
86 F: FnOnce() -> R + Send + 'static,
87 R: Send + 'static,
88 {
89 let (result_sender, result_receiver) = mpsc::sync_channel(1);
90
91 let closure = Box::new(move || -> Result<(), Box<dyn Error>> {
92 let result = panic::catch_unwind(panic::AssertUnwindSafe(f));
93 result_sender.send(result).map_err(Into::into)
94 });
95
96 if let Some(sender) = self.sender.as_ref() {
97 sender.send(closure)?;
98 } else {
99 return Err("Ruby test executor is shutdown".into());
100 }
101
102 match result_receiver.recv_timeout(self.timeout) {
103 Ok(Ok(result)) => Ok(result),
104 Ok(Err(err)) => std::panic::resume_unwind(err),
105 Err(_err) => Err(format!("Ruby test timed out after {:?}", self.timeout).into()),
106 }
107 }
108
109 pub fn run_test<F, R>(&self, f: F) -> Result<R, Box<dyn Error>>
110 where
111 F: FnOnce() -> R + Send + 'static,
112 R: Send + 'static,
113 {
114 self.run(f)
115 }
116}
117
118impl Drop for RubyTestExecutor {
119 fn drop(&mut self) {
120 self.shutdown();
121 }
122}
123
124pub fn global_executor() -> &'static RubyTestExecutor {
125 #[allow(unknown_lints)]
126 #[allow(static_mut_refs)]
127 unsafe { &GLOBAL_EXECUTOR }.get_or_init(RubyTestExecutor::start)
128}
129
130pub unsafe fn setup_ruby_unguarded() {
135 trick_the_linker();
136
137 #[cfg(windows)]
138 {
139 let mut argc = 0;
140 let mut argv: [*mut std::os::raw::c_char; 0] = [];
141 let mut argv = argv.as_mut_ptr();
142 rb_sys::rb_w32_sysinit(&mut argc, &mut argv);
143 }
144
145 let mut stack_marker: VALUE = 0;
146 ruby_init_stack(addr_of_mut!(stack_marker) as *mut _);
147
148 match ruby_setup() {
149 0 => {}
150 code => panic!("Failed to setup Ruby (error code: {})", code),
151 };
152
153 unsafe extern "C" fn do_ruby_process_options(_: VALUE) -> VALUE {
154 let mut argv: [*mut i8; 3] = [
155 "ruby\0".as_ptr() as _,
156 "-e\0".as_ptr() as _,
157 "\0".as_ptr() as _,
158 ];
159
160 ruby_process_options(argv.len() as _, argv.as_mut_ptr() as _) as _
161 }
162
163 let mut protect_status = 0;
164
165 let node = rb_protect(
166 Some(do_ruby_process_options),
167 Qnil as _,
168 &mut protect_status as _,
169 );
170
171 if protect_status != 0 {
172 let err = rb_errinfo();
173 let mut msg = rb_inspect(err);
174 let msg = rb_string_value_cstr(&mut msg);
175
176 let msg = std::ffi::CStr::from_ptr(msg).to_string_lossy().into_owned();
177 rb_set_errinfo(Qnil as _);
178 panic!("Failed to process Ruby options: {}", msg);
179 }
180
181 match ruby_exec_node(node as _) {
182 0 => {}
183 code => panic!("Failed to execute Ruby (error code: {})", code),
184 };
185}
186
187pub unsafe fn cleanup_ruby() {
192 let ret = rb_sys::ruby_cleanup(0);
193
194 if ret != 0 {
195 panic!("Failed to cleanup Ruby (error code: {})", ret);
196 }
197}
198
199pub struct RubyCleanupGuard;
200
201impl Drop for RubyCleanupGuard {
202 fn drop(&mut self) {
203 unsafe { cleanup_ruby() };
204 }
205}
206
207#[must_use]
212pub unsafe fn setup_ruby() -> RubyCleanupGuard {
213 setup_ruby_unguarded();
214
215 RubyCleanupGuard
216}
217
218fn trick_the_linker() {
219 #[cfg(ruby_gte_3_0)]
221 {
222 #[allow(clippy::cmp_null)]
223 let ensure_ractor_safe = rb_ext_ractor_safe as *const () != std::ptr::null();
224 assert!(ensure_ractor_safe);
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use rb_sys::ruby_vm_at_exit;
232 use rusty_fork::rusty_fork_test;
233
234 rusty_fork_test! {
235 #[test]
236 fn test_shutdown() {
237 static mut RUBY_VM_AT_EXIT_CALLED: Option<&str> = None;
238
239 let executor = RubyTestExecutor::start();
240
241 unsafe extern "C" fn set_called(_: *mut rb_sys::ruby_vm_t) {
242 RUBY_VM_AT_EXIT_CALLED = Some("hell yeah it was");
243 }
244
245 executor.run_test(|| {
246 unsafe { ruby_vm_at_exit(Some(set_called))}
247 }).unwrap();
248
249 drop(executor);
250
251 assert_eq!(Some("hell yeah it was"), unsafe { RUBY_VM_AT_EXIT_CALLED });
252 }
253 }
254
255 rusty_fork_test! {
256 #[test]
257 fn test_timeout() {
258 let mut executor = RubyTestExecutor::start();
259 executor.set_test_timeout(Duration::from_millis(10));
260
261 let result = executor
262 .run_test(|| {
263 std::thread::sleep(Duration::from_millis(1000));
264 });
265
266 assert_eq!("Ruby test timed out after 10ms", format!("{}", result.unwrap_err()));
267 }
268 }
269}