rubyx-py 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Cargo.toml +19 -0
- data/README.md +469 -0
- data/ext/rubyx/Cargo.toml +19 -0
- data/ext/rubyx/extconf.rb +22 -0
- data/ext/rubyx/src/async_gen.rs +1298 -0
- data/ext/rubyx/src/context.rs +812 -0
- data/ext/rubyx/src/convert.rs +1498 -0
- data/ext/rubyx/src/eval.rs +377 -0
- data/ext/rubyx/src/exception.rs +184 -0
- data/ext/rubyx/src/future.rs +126 -0
- data/ext/rubyx/src/import.rs +34 -0
- data/ext/rubyx/src/lib.rs +4212 -0
- data/ext/rubyx/src/nonblocking_stream.rs +1422 -0
- data/ext/rubyx/src/pipe_notify.rs +232 -0
- data/ext/rubyx/src/python/sync_adapter.py +31 -0
- data/ext/rubyx/src/python_api.rs +6029 -0
- data/ext/rubyx/src/python_ffi.rs +18 -0
- data/ext/rubyx/src/python_finder.rs +119 -0
- data/ext/rubyx/src/python_guard.rs +25 -0
- data/ext/rubyx/src/ruby_helpers.rs +74 -0
- data/ext/rubyx/src/rubyx_object.rs +1931 -0
- data/ext/rubyx/src/rubyx_stream.rs +950 -0
- data/ext/rubyx/src/stream.rs +713 -0
- data/ext/rubyx/src/test_helpers.rs +351 -0
- data/lib/generators/rubyx/install_generator.rb +24 -0
- data/lib/generators/rubyx/templates/rubyx_initializer.rb +17 -0
- data/lib/rubyx/context.rb +27 -0
- data/lib/rubyx/error.rb +30 -0
- data/lib/rubyx/rails.rb +105 -0
- data/lib/rubyx/railtie.rb +20 -0
- data/lib/rubyx/uv.rb +261 -0
- data/lib/rubyx/version.rb +4 -0
- data/lib/rubyx-py.rb +1 -0
- data/lib/rubyx.rb +136 -0
- metadata +123 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
use crate::eval::{await_eval_with_globals, eval_with_globals, make_globals};
|
|
2
|
+
use crate::python_api::PythonApi;
|
|
3
|
+
use crate::python_ffi::PyObject;
|
|
4
|
+
use crate::rubyx_object::ruby_to_python;
|
|
5
|
+
use magnus::r_hash::ForEach;
|
|
6
|
+
use magnus::{RHash, Value};
|
|
7
|
+
|
|
8
|
+
#[magnus::wrap(class = "Rubyx::Context", free_immediately)]
|
|
9
|
+
pub(crate) struct RubyxContext {
|
|
10
|
+
globals: *mut PyObject,
|
|
11
|
+
api: &'static PythonApi,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
unsafe impl Send for RubyxContext {}
|
|
15
|
+
unsafe impl Sync for RubyxContext {}
|
|
16
|
+
|
|
17
|
+
impl RubyxContext {
|
|
18
|
+
pub(crate) fn new() -> Result<Self, magnus::Error> {
|
|
19
|
+
let api = crate::api();
|
|
20
|
+
let gil = api.ensure_gil();
|
|
21
|
+
|
|
22
|
+
let guard = make_globals(api);
|
|
23
|
+
let globals = guard.ptr();
|
|
24
|
+
api.incref(globals);
|
|
25
|
+
|
|
26
|
+
api.release_gil(gil);
|
|
27
|
+
Ok(Self { globals, api })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub(crate) fn eval(&self, code: String) -> Result<magnus::Value, magnus::Error> {
|
|
31
|
+
let gil = self.api.ensure_gil();
|
|
32
|
+
let result = eval_with_globals(&code, self.globals, self.api);
|
|
33
|
+
self.api.release_gil(gil);
|
|
34
|
+
result
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub(crate) fn eval_with_globals(
|
|
38
|
+
&self,
|
|
39
|
+
code: String,
|
|
40
|
+
globals_hash: RHash,
|
|
41
|
+
) -> Result<magnus::Value, magnus::Error> {
|
|
42
|
+
let gil = self.api.ensure_gil();
|
|
43
|
+
let result = match self.inject_globals(globals_hash) {
|
|
44
|
+
Ok(()) => eval_with_globals(&code, self.globals, self.api),
|
|
45
|
+
Err(e) => Err(e),
|
|
46
|
+
};
|
|
47
|
+
self.api.release_gil(gil);
|
|
48
|
+
result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub(crate) fn await_eval(&self, code: String) -> Result<magnus::Value, magnus::Error> {
|
|
52
|
+
let gil = self.api.ensure_gil();
|
|
53
|
+
let result = await_eval_with_globals(&code, self.globals, self.api);
|
|
54
|
+
self.api.release_gil(gil);
|
|
55
|
+
result
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub(crate) fn await_eval_with_globals(
|
|
59
|
+
&self,
|
|
60
|
+
code: String,
|
|
61
|
+
globals_hash: RHash,
|
|
62
|
+
) -> Result<magnus::Value, magnus::Error> {
|
|
63
|
+
let gil = self.api.ensure_gil();
|
|
64
|
+
let result = match self.inject_globals(globals_hash) {
|
|
65
|
+
Ok(()) => await_eval_with_globals(&code, self.globals, self.api),
|
|
66
|
+
Err(e) => Err(e),
|
|
67
|
+
};
|
|
68
|
+
self.api.release_gil(gil);
|
|
69
|
+
result
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Eval code to get a coroutine, then run it on a background thread.
|
|
73
|
+
/// Returns a Rubyx::Future immediately.
|
|
74
|
+
pub(crate) fn async_await_eval(
|
|
75
|
+
&self,
|
|
76
|
+
code: String,
|
|
77
|
+
) -> Result<crate::future::RubyxFuture, magnus::Error> {
|
|
78
|
+
let gil = self.api.ensure_gil();
|
|
79
|
+
|
|
80
|
+
// Eval the code in context globals to get the coroutine
|
|
81
|
+
let py_coroutine = match self.api.run_string(&code, 258, self.globals, self.globals) {
|
|
82
|
+
Ok(obj) if !obj.is_null() => obj,
|
|
83
|
+
Ok(_) => {
|
|
84
|
+
let err = if self.api.has_error() {
|
|
85
|
+
crate::python_api::PythonApi::extract_exception(self.api)
|
|
86
|
+
.map(magnus::Error::from)
|
|
87
|
+
.unwrap_or_else(|| {
|
|
88
|
+
magnus::Error::new(
|
|
89
|
+
crate::ruby_helpers::runtime_error(),
|
|
90
|
+
"Python eval failed",
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
} else {
|
|
94
|
+
magnus::Error::new(
|
|
95
|
+
crate::ruby_helpers::runtime_error(),
|
|
96
|
+
"Python eval returned null",
|
|
97
|
+
)
|
|
98
|
+
};
|
|
99
|
+
self.api.release_gil(gil);
|
|
100
|
+
return Err(err);
|
|
101
|
+
}
|
|
102
|
+
Err(e) => {
|
|
103
|
+
self.api.release_gil(gil);
|
|
104
|
+
return Err(magnus::Error::new(crate::ruby_helpers::runtime_error(), e));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let future = crate::future::RubyxFuture::from_coroutine(py_coroutine, self.api);
|
|
109
|
+
self.api.decref(py_coroutine);
|
|
110
|
+
self.api.release_gil(gil);
|
|
111
|
+
|
|
112
|
+
Ok(future)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pub(crate) fn async_await_eval_with_globals(
|
|
116
|
+
&self,
|
|
117
|
+
code: String,
|
|
118
|
+
globals_hash: RHash,
|
|
119
|
+
) -> Result<crate::future::RubyxFuture, magnus::Error> {
|
|
120
|
+
let gil = self.api.ensure_gil();
|
|
121
|
+
|
|
122
|
+
if let Err(e) = self.inject_globals(globals_hash) {
|
|
123
|
+
self.api.release_gil(gil);
|
|
124
|
+
return Err(e);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let py_coroutine = match self.api.run_string(&code, 258, self.globals, self.globals) {
|
|
128
|
+
Ok(obj) if !obj.is_null() => obj,
|
|
129
|
+
Ok(_) => {
|
|
130
|
+
let err = if self.api.has_error() {
|
|
131
|
+
crate::python_api::PythonApi::extract_exception(self.api)
|
|
132
|
+
.map(magnus::Error::from)
|
|
133
|
+
.unwrap_or_else(|| {
|
|
134
|
+
magnus::Error::new(
|
|
135
|
+
crate::ruby_helpers::runtime_error(),
|
|
136
|
+
"Python eval failed",
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
} else {
|
|
140
|
+
magnus::Error::new(
|
|
141
|
+
crate::ruby_helpers::runtime_error(),
|
|
142
|
+
"Python eval returned null",
|
|
143
|
+
)
|
|
144
|
+
};
|
|
145
|
+
self.api.release_gil(gil);
|
|
146
|
+
return Err(err);
|
|
147
|
+
}
|
|
148
|
+
Err(e) => {
|
|
149
|
+
self.api.release_gil(gil);
|
|
150
|
+
return Err(magnus::Error::new(crate::ruby_helpers::runtime_error(), e));
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let future = crate::future::RubyxFuture::from_coroutine(py_coroutine, self.api);
|
|
155
|
+
self.api.decref(py_coroutine);
|
|
156
|
+
self.api.release_gil(gil);
|
|
157
|
+
|
|
158
|
+
Ok(future)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Merge a Ruby Hash into the persistent globals dict.
|
|
162
|
+
/// Caller must hold the GIL.
|
|
163
|
+
fn inject_globals(&self, globals_hash: RHash) -> Result<(), magnus::Error> {
|
|
164
|
+
let api = self.api;
|
|
165
|
+
let globals = self.globals;
|
|
166
|
+
let mut err: Option<magnus::Error> = None;
|
|
167
|
+
globals_hash.foreach(|key: Value, val: Value| {
|
|
168
|
+
let py_key = match ruby_to_python(key, api) {
|
|
169
|
+
Ok(k) => k,
|
|
170
|
+
Err(e) => {
|
|
171
|
+
err = Some(e);
|
|
172
|
+
return Ok(ForEach::Stop);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
let py_val = match ruby_to_python(val, api) {
|
|
176
|
+
Ok(v) => v,
|
|
177
|
+
Err(e) => {
|
|
178
|
+
api.decref(py_key);
|
|
179
|
+
err = Some(e);
|
|
180
|
+
return Ok(ForEach::Stop);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
api.dict_set_item(globals, py_key, py_val);
|
|
184
|
+
api.decref(py_key);
|
|
185
|
+
api.decref(py_val);
|
|
186
|
+
Ok(ForEach::Continue)
|
|
187
|
+
})?;
|
|
188
|
+
if let Some(e) = err {
|
|
189
|
+
return Err(e);
|
|
190
|
+
}
|
|
191
|
+
Ok(())
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
impl Drop for RubyxContext {
|
|
196
|
+
fn drop(&mut self) {
|
|
197
|
+
if self.globals.is_null() {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if !self.api.is_initialized() {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let gil = self.api.ensure_gil();
|
|
204
|
+
self.api.decref(self.globals);
|
|
205
|
+
self.api.release_gil(gil);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[cfg(test)]
|
|
210
|
+
mod tests {
|
|
211
|
+
use crate::eval::make_globals;
|
|
212
|
+
use crate::test_helpers::skip_if_no_python;
|
|
213
|
+
use serial_test::serial;
|
|
214
|
+
|
|
215
|
+
// ========== Construction & Globals Lifecycle ==========
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
#[serial]
|
|
219
|
+
fn test_make_globals_and_incref_keeps_dict_alive() {
|
|
220
|
+
let Some(guard) = skip_if_no_python() else {
|
|
221
|
+
return;
|
|
222
|
+
};
|
|
223
|
+
let api = guard.api();
|
|
224
|
+
|
|
225
|
+
let globals_guard = make_globals(api);
|
|
226
|
+
let globals = globals_guard.ptr();
|
|
227
|
+
|
|
228
|
+
// incref so the dict survives the PyGuard drop
|
|
229
|
+
api.incref(globals);
|
|
230
|
+
drop(globals_guard); // decrefs once — refcount should be 1
|
|
231
|
+
|
|
232
|
+
// dict should still be usable
|
|
233
|
+
let size = api.dict_size(globals);
|
|
234
|
+
assert!(size >= 1, "globals should have at least __builtins__");
|
|
235
|
+
|
|
236
|
+
// cleanup
|
|
237
|
+
api.decref(globals);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
#[serial]
|
|
242
|
+
fn test_globals_has_builtins() {
|
|
243
|
+
let Some(guard) = skip_if_no_python() else {
|
|
244
|
+
return;
|
|
245
|
+
};
|
|
246
|
+
let api = guard.api();
|
|
247
|
+
|
|
248
|
+
let globals_guard = make_globals(api);
|
|
249
|
+
let globals = globals_guard.ptr();
|
|
250
|
+
|
|
251
|
+
let key = api.string_from_str("__builtins__");
|
|
252
|
+
assert!(!key.is_null());
|
|
253
|
+
let builtins = api.dict_get_item(globals, key);
|
|
254
|
+
assert!(!builtins.is_null(), "globals should contain __builtins__");
|
|
255
|
+
|
|
256
|
+
api.decref(key);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ========== eval_with_globals: State Persistence ==========
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
#[serial]
|
|
263
|
+
fn test_eval_with_globals_state_persists() {
|
|
264
|
+
let Some(guard) = skip_if_no_python() else {
|
|
265
|
+
return;
|
|
266
|
+
};
|
|
267
|
+
let api = guard.api();
|
|
268
|
+
|
|
269
|
+
let globals_guard = make_globals(api);
|
|
270
|
+
let globals = globals_guard.ptr();
|
|
271
|
+
|
|
272
|
+
// Set a variable
|
|
273
|
+
api.run_simple_string("x = 42").ok(); // this uses its own globals
|
|
274
|
+
// Instead, use run_string with our globals
|
|
275
|
+
let set_result = api.run_string("x = 42", 257, globals, globals);
|
|
276
|
+
assert!(set_result.is_ok(), "setting x = 42 should succeed");
|
|
277
|
+
|
|
278
|
+
// Read it back from the same globals
|
|
279
|
+
let get_result = api.run_string("x", 258, globals, globals);
|
|
280
|
+
assert!(get_result.is_ok(), "reading x should succeed");
|
|
281
|
+
let py_obj = get_result.unwrap();
|
|
282
|
+
assert!(!py_obj.is_null());
|
|
283
|
+
|
|
284
|
+
let value = api.long_to_i64(py_obj);
|
|
285
|
+
assert_eq!(value, 42);
|
|
286
|
+
api.decref(py_obj);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#[test]
|
|
290
|
+
#[serial]
|
|
291
|
+
fn test_eval_with_globals_accumulates_state() {
|
|
292
|
+
let Some(guard) = skip_if_no_python() else {
|
|
293
|
+
return;
|
|
294
|
+
};
|
|
295
|
+
let api = guard.api();
|
|
296
|
+
|
|
297
|
+
let globals_guard = make_globals(api);
|
|
298
|
+
let globals = globals_guard.ptr();
|
|
299
|
+
|
|
300
|
+
// Multiple assignments accumulate
|
|
301
|
+
let _ = api.run_string("a = 10", 257, globals, globals);
|
|
302
|
+
let _ = api.run_string("b = 20", 257, globals, globals);
|
|
303
|
+
let _ = api.run_string("c = a + b", 257, globals, globals);
|
|
304
|
+
|
|
305
|
+
let result = api.run_string("c", 258, globals, globals);
|
|
306
|
+
assert!(result.is_ok());
|
|
307
|
+
let py_obj = result.unwrap();
|
|
308
|
+
assert!(!py_obj.is_null());
|
|
309
|
+
assert_eq!(api.long_to_i64(py_obj), 30);
|
|
310
|
+
api.decref(py_obj);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[test]
|
|
314
|
+
#[serial]
|
|
315
|
+
fn test_eval_with_globals_functions_persist() {
|
|
316
|
+
let Some(guard) = skip_if_no_python() else {
|
|
317
|
+
return;
|
|
318
|
+
};
|
|
319
|
+
let api = guard.api();
|
|
320
|
+
|
|
321
|
+
let globals_guard = make_globals(api);
|
|
322
|
+
let globals = globals_guard.ptr();
|
|
323
|
+
|
|
324
|
+
// Define a function
|
|
325
|
+
let _ = api.run_string("def double(n): return n * 2", 257, globals, globals);
|
|
326
|
+
|
|
327
|
+
// Call it
|
|
328
|
+
let result = api.run_string("double(21)", 258, globals, globals);
|
|
329
|
+
assert!(result.is_ok());
|
|
330
|
+
let py_obj = result.unwrap();
|
|
331
|
+
assert!(!py_obj.is_null());
|
|
332
|
+
assert_eq!(api.long_to_i64(py_obj), 42);
|
|
333
|
+
api.decref(py_obj);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#[test]
|
|
337
|
+
#[serial]
|
|
338
|
+
fn test_eval_with_globals_imports_persist() {
|
|
339
|
+
let Some(guard) = skip_if_no_python() else {
|
|
340
|
+
return;
|
|
341
|
+
};
|
|
342
|
+
let api = guard.api();
|
|
343
|
+
|
|
344
|
+
let globals_guard = make_globals(api);
|
|
345
|
+
let globals = globals_guard.ptr();
|
|
346
|
+
|
|
347
|
+
// Import a module
|
|
348
|
+
let _ = api.run_string("import math", 257, globals, globals);
|
|
349
|
+
|
|
350
|
+
// Use it
|
|
351
|
+
let result = api.run_string("math.factorial(5)", 258, globals, globals);
|
|
352
|
+
assert!(result.is_ok());
|
|
353
|
+
let py_obj = result.unwrap();
|
|
354
|
+
assert!(!py_obj.is_null());
|
|
355
|
+
assert_eq!(api.long_to_i64(py_obj), 120);
|
|
356
|
+
api.decref(py_obj);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ========== Isolation Between Globals Dicts ==========
|
|
360
|
+
|
|
361
|
+
#[test]
|
|
362
|
+
#[serial]
|
|
363
|
+
fn test_separate_globals_are_isolated() {
|
|
364
|
+
let Some(guard) = skip_if_no_python() else {
|
|
365
|
+
return;
|
|
366
|
+
};
|
|
367
|
+
let api = guard.api();
|
|
368
|
+
|
|
369
|
+
let globals1 = make_globals(api);
|
|
370
|
+
let globals2 = make_globals(api);
|
|
371
|
+
|
|
372
|
+
// Set variable in globals1
|
|
373
|
+
let _ = api.run_string("isolated_var = 999", 257, globals1.ptr(), globals1.ptr());
|
|
374
|
+
|
|
375
|
+
// Should NOT be visible in globals2
|
|
376
|
+
let result = api.run_string("isolated_var", 258, globals2.ptr(), globals2.ptr());
|
|
377
|
+
// This should fail (NameError) or return null
|
|
378
|
+
match result {
|
|
379
|
+
Ok(obj) if obj.is_null() => {
|
|
380
|
+
// Expected: Python set an error
|
|
381
|
+
if api.has_error() {
|
|
382
|
+
crate::python_api::PythonApi::extract_exception(api);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
Ok(_obj) => {
|
|
386
|
+
panic!("isolated_var should NOT be visible in a separate globals dict");
|
|
387
|
+
}
|
|
388
|
+
Err(_) => {
|
|
389
|
+
// Also expected — run_string returned an error
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ========== Error Recovery ==========
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
#[serial]
|
|
398
|
+
fn test_error_does_not_corrupt_globals() {
|
|
399
|
+
let Some(guard) = skip_if_no_python() else {
|
|
400
|
+
return;
|
|
401
|
+
};
|
|
402
|
+
let api = guard.api();
|
|
403
|
+
|
|
404
|
+
let globals_guard = make_globals(api);
|
|
405
|
+
let globals = globals_guard.ptr();
|
|
406
|
+
|
|
407
|
+
// Set a variable
|
|
408
|
+
let _ = api.run_string("x = 10", 257, globals, globals);
|
|
409
|
+
|
|
410
|
+
// Cause an error
|
|
411
|
+
let err_result = api.run_string("1 / 0", 258, globals, globals);
|
|
412
|
+
match err_result {
|
|
413
|
+
Ok(obj) if obj.is_null() => {
|
|
414
|
+
if api.has_error() {
|
|
415
|
+
crate::python_api::PythonApi::extract_exception(api);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
Err(_) => {}
|
|
419
|
+
_ => {}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// x should still be accessible
|
|
423
|
+
let result = api.run_string("x", 258, globals, globals);
|
|
424
|
+
assert!(result.is_ok());
|
|
425
|
+
let py_obj = result.unwrap();
|
|
426
|
+
assert!(!py_obj.is_null());
|
|
427
|
+
assert_eq!(api.long_to_i64(py_obj), 10);
|
|
428
|
+
api.decref(py_obj);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ========== Drop Safety ==========
|
|
432
|
+
|
|
433
|
+
#[test]
|
|
434
|
+
#[serial]
|
|
435
|
+
fn test_drop_with_null_globals_does_not_crash() {
|
|
436
|
+
let Some(_guard) = skip_if_no_python() else {
|
|
437
|
+
return;
|
|
438
|
+
};
|
|
439
|
+
let api = crate::api();
|
|
440
|
+
|
|
441
|
+
// Manually construct with null globals to test the guard in Drop
|
|
442
|
+
let ctx = super::RubyxContext {
|
|
443
|
+
globals: std::ptr::null_mut(),
|
|
444
|
+
api,
|
|
445
|
+
};
|
|
446
|
+
drop(ctx); // Should not crash
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
#[serial]
|
|
451
|
+
fn test_drop_decrefs_globals() {
|
|
452
|
+
let Some(guard) = skip_if_no_python() else {
|
|
453
|
+
return;
|
|
454
|
+
};
|
|
455
|
+
let api = guard.api();
|
|
456
|
+
|
|
457
|
+
// Create globals with an extra incref (simulating what new() does)
|
|
458
|
+
let globals_guard = make_globals(api);
|
|
459
|
+
let globals = globals_guard.ptr();
|
|
460
|
+
api.incref(globals); // refcount = 2
|
|
461
|
+
drop(globals_guard); // refcount = 1
|
|
462
|
+
|
|
463
|
+
// incref again so we can observe the decref from Drop
|
|
464
|
+
api.incref(globals); // refcount = 2
|
|
465
|
+
|
|
466
|
+
let ctx = super::RubyxContext { globals, api };
|
|
467
|
+
drop(ctx); // Drop calls decref → refcount = 1
|
|
468
|
+
|
|
469
|
+
// globals should still be valid (refcount = 1, our extra ref)
|
|
470
|
+
let size = api.dict_size(globals);
|
|
471
|
+
assert!(
|
|
472
|
+
size >= 1,
|
|
473
|
+
"globals should still be alive after context drop"
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// Final cleanup
|
|
477
|
+
api.decref(globals);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ========== Original Eval Isolation ==========
|
|
481
|
+
|
|
482
|
+
#[test]
|
|
483
|
+
#[serial]
|
|
484
|
+
fn test_rubyx_eval_still_isolated() {
|
|
485
|
+
let Some(guard) = skip_if_no_python() else {
|
|
486
|
+
return;
|
|
487
|
+
};
|
|
488
|
+
let api = guard.api();
|
|
489
|
+
|
|
490
|
+
// Create two separate globals — each should be independent
|
|
491
|
+
let g1 = make_globals(api);
|
|
492
|
+
let g2 = make_globals(api);
|
|
493
|
+
|
|
494
|
+
let _ = api.run_string("leak_test = 123", 257, g1.ptr(), g1.ptr());
|
|
495
|
+
|
|
496
|
+
// leak_test should not be in g2
|
|
497
|
+
let result = api.run_string("leak_test", 258, g2.ptr(), g2.ptr());
|
|
498
|
+
let leaked = match result {
|
|
499
|
+
Ok(obj) if !obj.is_null() => {
|
|
500
|
+
api.decref(obj);
|
|
501
|
+
true
|
|
502
|
+
}
|
|
503
|
+
_ => {
|
|
504
|
+
if api.has_error() {
|
|
505
|
+
crate::python_api::PythonApi::extract_exception(api);
|
|
506
|
+
}
|
|
507
|
+
false
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
assert!(
|
|
511
|
+
!leaked,
|
|
512
|
+
"state should not leak between separate globals dicts"
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ========== Multiple Contexts ==========
|
|
517
|
+
|
|
518
|
+
#[test]
|
|
519
|
+
#[serial]
|
|
520
|
+
fn test_multiple_globals_independent_values() {
|
|
521
|
+
let Some(guard) = skip_if_no_python() else {
|
|
522
|
+
return;
|
|
523
|
+
};
|
|
524
|
+
let api = guard.api();
|
|
525
|
+
|
|
526
|
+
let g1 = make_globals(api);
|
|
527
|
+
let g2 = make_globals(api);
|
|
528
|
+
let g3 = make_globals(api);
|
|
529
|
+
|
|
530
|
+
let _ = api.run_string("val = 0", 257, g1.ptr(), g1.ptr());
|
|
531
|
+
let _ = api.run_string("val = 10", 257, g2.ptr(), g2.ptr());
|
|
532
|
+
let _ = api.run_string("val = 20", 257, g3.ptr(), g3.ptr());
|
|
533
|
+
|
|
534
|
+
let r1 = api.run_string("val", 258, g1.ptr(), g1.ptr()).unwrap();
|
|
535
|
+
let r2 = api.run_string("val", 258, g2.ptr(), g2.ptr()).unwrap();
|
|
536
|
+
let r3 = api.run_string("val", 258, g3.ptr(), g3.ptr()).unwrap();
|
|
537
|
+
|
|
538
|
+
assert_eq!(api.long_to_i64(r1), 0);
|
|
539
|
+
assert_eq!(api.long_to_i64(r2), 10);
|
|
540
|
+
assert_eq!(api.long_to_i64(r3), 20);
|
|
541
|
+
|
|
542
|
+
api.decref(r1);
|
|
543
|
+
api.decref(r2);
|
|
544
|
+
api.decref(r3);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ========== Context inject_globals tests ==========
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
#[serial]
|
|
551
|
+
fn test_context_inject_globals_simple() {
|
|
552
|
+
use crate::test_helpers::with_ruby_python;
|
|
553
|
+
use magnus::IntoValue;
|
|
554
|
+
with_ruby_python(|ruby, api| {
|
|
555
|
+
let globals_guard = make_globals(api);
|
|
556
|
+
let globals = globals_guard.ptr();
|
|
557
|
+
|
|
558
|
+
let hash = magnus::RHash::new();
|
|
559
|
+
hash.aset(ruby.sym_new("x"), 10_i64.into_value_with(ruby))
|
|
560
|
+
.unwrap();
|
|
561
|
+
hash.aset(ruby.sym_new("y"), 20_i64.into_value_with(ruby))
|
|
562
|
+
.unwrap();
|
|
563
|
+
|
|
564
|
+
// Inject into globals
|
|
565
|
+
let ctx = super::RubyxContext { globals, api };
|
|
566
|
+
ctx.inject_globals(hash).expect("inject should succeed");
|
|
567
|
+
|
|
568
|
+
// Verify x and y are in globals
|
|
569
|
+
let key_x = api.string_from_str("x");
|
|
570
|
+
let val_x = api.dict_get_item(globals, key_x);
|
|
571
|
+
assert!(!val_x.is_null());
|
|
572
|
+
assert_eq!(api.long_to_i64(val_x), 10);
|
|
573
|
+
api.decref(key_x);
|
|
574
|
+
|
|
575
|
+
let key_y = api.string_from_str("y");
|
|
576
|
+
let val_y = api.dict_get_item(globals, key_y);
|
|
577
|
+
assert!(!val_y.is_null());
|
|
578
|
+
assert_eq!(api.long_to_i64(val_y), 20);
|
|
579
|
+
api.decref(key_y);
|
|
580
|
+
|
|
581
|
+
// Prevent Drop from double-decref
|
|
582
|
+
std::mem::forget(ctx);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#[test]
|
|
587
|
+
#[serial]
|
|
588
|
+
fn test_context_eval_with_globals() {
|
|
589
|
+
use crate::test_helpers::with_ruby_python;
|
|
590
|
+
use magnus::{IntoValue, TryConvert};
|
|
591
|
+
with_ruby_python(|ruby, api| {
|
|
592
|
+
let ctx = super::RubyxContext::new().expect("context should create");
|
|
593
|
+
|
|
594
|
+
let hash = magnus::RHash::new();
|
|
595
|
+
hash.aset(ruby.sym_new("a"), 5_i64.into_value_with(ruby))
|
|
596
|
+
.unwrap();
|
|
597
|
+
hash.aset(ruby.sym_new("b"), 7_i64.into_value_with(ruby))
|
|
598
|
+
.unwrap();
|
|
599
|
+
|
|
600
|
+
let result = ctx
|
|
601
|
+
.eval_with_globals("a * b".to_string(), hash)
|
|
602
|
+
.expect("eval should succeed");
|
|
603
|
+
|
|
604
|
+
let obj =
|
|
605
|
+
magnus::typed_data::Obj::<crate::rubyx_object::RubyxObject>::try_convert(result)
|
|
606
|
+
.expect("should be RubyxObject");
|
|
607
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 35);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
#[test]
|
|
612
|
+
#[serial]
|
|
613
|
+
fn test_context_globals_persist_after_inject() {
|
|
614
|
+
use crate::test_helpers::with_ruby_python;
|
|
615
|
+
use magnus::{IntoValue, TryConvert};
|
|
616
|
+
with_ruby_python(|ruby, api| {
|
|
617
|
+
let ctx = super::RubyxContext::new().expect("context should create");
|
|
618
|
+
|
|
619
|
+
// Inject x=100
|
|
620
|
+
let hash = magnus::RHash::new();
|
|
621
|
+
hash.aset(ruby.sym_new("x"), 100_i64.into_value_with(ruby))
|
|
622
|
+
.unwrap();
|
|
623
|
+
let _ = ctx
|
|
624
|
+
.eval_with_globals("y = x + 1".to_string(), hash)
|
|
625
|
+
.expect("eval should succeed");
|
|
626
|
+
|
|
627
|
+
// x and y should persist in context without re-injecting
|
|
628
|
+
let result = ctx
|
|
629
|
+
.eval("x + y".to_string())
|
|
630
|
+
.expect("should access persisted globals");
|
|
631
|
+
let obj =
|
|
632
|
+
magnus::typed_data::Obj::<crate::rubyx_object::RubyxObject>::try_convert(result)
|
|
633
|
+
.expect("should be RubyxObject");
|
|
634
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 201); // 100 + 101
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
#[test]
|
|
639
|
+
#[serial]
|
|
640
|
+
fn test_context_eval_with_globals_string_values() {
|
|
641
|
+
use crate::test_helpers::with_ruby_python;
|
|
642
|
+
use magnus::{IntoValue, TryConvert};
|
|
643
|
+
with_ruby_python(|ruby, api| {
|
|
644
|
+
let ctx = super::RubyxContext::new().expect("context should create");
|
|
645
|
+
|
|
646
|
+
let hash = magnus::RHash::new();
|
|
647
|
+
hash.aset(ruby.sym_new("greeting"), "hello".into_value_with(ruby))
|
|
648
|
+
.unwrap();
|
|
649
|
+
hash.aset(ruby.sym_new("name"), "world".into_value_with(ruby))
|
|
650
|
+
.unwrap();
|
|
651
|
+
|
|
652
|
+
let result = ctx
|
|
653
|
+
.eval_with_globals("f'{greeting}, {name}!'".to_string(), hash)
|
|
654
|
+
.expect("eval should succeed");
|
|
655
|
+
|
|
656
|
+
let obj =
|
|
657
|
+
magnus::typed_data::Obj::<crate::rubyx_object::RubyxObject>::try_convert(result)
|
|
658
|
+
.expect("should be RubyxObject");
|
|
659
|
+
assert_eq!(
|
|
660
|
+
api.string_to_string(obj.as_ptr()),
|
|
661
|
+
Some("hello, world!".to_string())
|
|
662
|
+
);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
#[test]
|
|
667
|
+
#[serial]
|
|
668
|
+
fn test_context_eval_with_globals_list() {
|
|
669
|
+
use crate::test_helpers::with_ruby_python;
|
|
670
|
+
use magnus::{IntoValue, TryConvert};
|
|
671
|
+
with_ruby_python(|ruby, api| {
|
|
672
|
+
let ctx = super::RubyxContext::new().expect("context should create");
|
|
673
|
+
|
|
674
|
+
let arr = magnus::RArray::new();
|
|
675
|
+
arr.push(1_i64.into_value_with(ruby)).unwrap();
|
|
676
|
+
arr.push(2_i64.into_value_with(ruby)).unwrap();
|
|
677
|
+
arr.push(3_i64.into_value_with(ruby)).unwrap();
|
|
678
|
+
|
|
679
|
+
let hash = magnus::RHash::new();
|
|
680
|
+
hash.aset(ruby.sym_new("items"), arr.into_value_with(ruby))
|
|
681
|
+
.unwrap();
|
|
682
|
+
|
|
683
|
+
let result = ctx
|
|
684
|
+
.eval_with_globals("sum(items)".to_string(), hash)
|
|
685
|
+
.expect("eval should succeed");
|
|
686
|
+
|
|
687
|
+
let obj =
|
|
688
|
+
magnus::typed_data::Obj::<crate::rubyx_object::RubyxObject>::try_convert(result)
|
|
689
|
+
.expect("should be RubyxObject");
|
|
690
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 6);
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
#[test]
|
|
695
|
+
#[serial]
|
|
696
|
+
fn test_context_await_with_globals() {
|
|
697
|
+
use crate::test_helpers::with_ruby_python;
|
|
698
|
+
use magnus::{IntoValue, TryConvert};
|
|
699
|
+
with_ruby_python(|ruby, api| {
|
|
700
|
+
let ctx = super::RubyxContext::new().expect("context should create");
|
|
701
|
+
|
|
702
|
+
// Define async function in context
|
|
703
|
+
ctx.eval("import asyncio\nasync def multiply(a, b): return a * b".to_string())
|
|
704
|
+
.expect("should define function");
|
|
705
|
+
|
|
706
|
+
let hash = magnus::RHash::new();
|
|
707
|
+
hash.aset(ruby.sym_new("a"), 6_i64.into_value_with(ruby))
|
|
708
|
+
.unwrap();
|
|
709
|
+
hash.aset(ruby.sym_new("b"), 7_i64.into_value_with(ruby))
|
|
710
|
+
.unwrap();
|
|
711
|
+
|
|
712
|
+
let result = ctx
|
|
713
|
+
.await_eval_with_globals("multiply(a, b)".to_string(), hash)
|
|
714
|
+
.expect("await should succeed");
|
|
715
|
+
|
|
716
|
+
let obj =
|
|
717
|
+
magnus::typed_data::Obj::<crate::rubyx_object::RubyxObject>::try_convert(result)
|
|
718
|
+
.expect("should be RubyxObject");
|
|
719
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 42);
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
#[test]
|
|
724
|
+
#[serial]
|
|
725
|
+
fn test_context_await_with_globals_error() {
|
|
726
|
+
use crate::test_helpers::with_ruby_python;
|
|
727
|
+
use magnus::IntoValue;
|
|
728
|
+
with_ruby_python(|ruby, _api| {
|
|
729
|
+
let ctx = super::RubyxContext::new().expect("context should create");
|
|
730
|
+
|
|
731
|
+
ctx.eval(
|
|
732
|
+
"import asyncio\nasync def fail_if_neg(n):\n if n < 0: raise ValueError('neg')\n return n"
|
|
733
|
+
.to_string(),
|
|
734
|
+
)
|
|
735
|
+
.expect("should define function");
|
|
736
|
+
|
|
737
|
+
let hash = magnus::RHash::new();
|
|
738
|
+
hash.aset(ruby.sym_new("n"), (-5_i64).into_value_with(ruby))
|
|
739
|
+
.unwrap();
|
|
740
|
+
|
|
741
|
+
let result = ctx.await_eval_with_globals("fail_if_neg(n)".to_string(), hash);
|
|
742
|
+
assert!(result.is_err(), "should propagate ValueError");
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
#[test]
|
|
747
|
+
#[serial]
|
|
748
|
+
fn test_context_async_await_with_globals() {
|
|
749
|
+
use crate::test_helpers::with_ruby_python;
|
|
750
|
+
use magnus::{IntoValue, TryConvert};
|
|
751
|
+
with_ruby_python(|ruby, api| {
|
|
752
|
+
let ctx = super::RubyxContext::new().expect("context should create");
|
|
753
|
+
|
|
754
|
+
ctx.eval("import asyncio\nasync def add(x, y): return x + y".to_string())
|
|
755
|
+
.expect("should define function");
|
|
756
|
+
|
|
757
|
+
let hash = magnus::RHash::new();
|
|
758
|
+
hash.aset(ruby.sym_new("x"), 15_i64.into_value_with(ruby))
|
|
759
|
+
.unwrap();
|
|
760
|
+
hash.aset(ruby.sym_new("y"), 27_i64.into_value_with(ruby))
|
|
761
|
+
.unwrap();
|
|
762
|
+
|
|
763
|
+
// Need to release GIL for the background thread
|
|
764
|
+
let gil = api.ensure_gil();
|
|
765
|
+
let future = ctx
|
|
766
|
+
.async_await_eval_with_globals("add(x, y)".to_string(), hash)
|
|
767
|
+
.expect("async_await should succeed");
|
|
768
|
+
api.release_gil(gil);
|
|
769
|
+
|
|
770
|
+
let tstate = api.save_thread();
|
|
771
|
+
let result = future.value().expect("future should resolve");
|
|
772
|
+
drop(future);
|
|
773
|
+
api.restore_thread(tstate);
|
|
774
|
+
|
|
775
|
+
assert_eq!(i64::try_convert(result).unwrap(), 42);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
#[test]
|
|
780
|
+
#[serial]
|
|
781
|
+
fn test_context_globals_override() {
|
|
782
|
+
use crate::test_helpers::with_ruby_python;
|
|
783
|
+
use magnus::{IntoValue, TryConvert};
|
|
784
|
+
with_ruby_python(|ruby, api| {
|
|
785
|
+
let ctx = super::RubyxContext::new().expect("context should create");
|
|
786
|
+
|
|
787
|
+
// Inject x=10
|
|
788
|
+
let hash1 = magnus::RHash::new();
|
|
789
|
+
hash1
|
|
790
|
+
.aset(ruby.sym_new("x"), 10_i64.into_value_with(ruby))
|
|
791
|
+
.unwrap();
|
|
792
|
+
let r1 = ctx
|
|
793
|
+
.eval_with_globals("x".to_string(), hash1)
|
|
794
|
+
.expect("eval should succeed");
|
|
795
|
+
let obj1 = magnus::typed_data::Obj::<crate::rubyx_object::RubyxObject>::try_convert(r1)
|
|
796
|
+
.unwrap();
|
|
797
|
+
assert_eq!(api.long_to_i64(obj1.as_ptr()), 10);
|
|
798
|
+
|
|
799
|
+
// Override x=99
|
|
800
|
+
let hash2 = magnus::RHash::new();
|
|
801
|
+
hash2
|
|
802
|
+
.aset(ruby.sym_new("x"), 99_i64.into_value_with(ruby))
|
|
803
|
+
.unwrap();
|
|
804
|
+
let r2 = ctx
|
|
805
|
+
.eval_with_globals("x".to_string(), hash2)
|
|
806
|
+
.expect("eval should succeed");
|
|
807
|
+
let obj2 = magnus::typed_data::Obj::<crate::rubyx_object::RubyxObject>::try_convert(r2)
|
|
808
|
+
.unwrap();
|
|
809
|
+
assert_eq!(api.long_to_i64(obj2.as_ptr()), 99);
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|