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,4212 @@
|
|
|
1
|
+
use crate::async_gen::{AsyncGeneratorStream, AsyncStrategy};
|
|
2
|
+
use crate::nonblocking_stream::NonBlockingStream;
|
|
3
|
+
use crate::pipe_notify::PipeNotify;
|
|
4
|
+
use crate::python_api::PythonApi;
|
|
5
|
+
use crate::rubyx_object::python_to_sendable;
|
|
6
|
+
use crate::rubyx_object::RubyxObject;
|
|
7
|
+
use crate::rubyx_stream::RubyxStream;
|
|
8
|
+
use crate::stream::StreamItem;
|
|
9
|
+
use crossbeam_channel::unbounded;
|
|
10
|
+
use magnus::typed_data::Obj;
|
|
11
|
+
use magnus::{function, method, Module, Object, RArray, Ruby, TryConvert, Value};
|
|
12
|
+
use std::sync::Arc;
|
|
13
|
+
use std::sync::OnceLock;
|
|
14
|
+
|
|
15
|
+
mod async_gen;
|
|
16
|
+
mod context;
|
|
17
|
+
mod convert;
|
|
18
|
+
mod eval;
|
|
19
|
+
mod exception;
|
|
20
|
+
mod future;
|
|
21
|
+
mod import;
|
|
22
|
+
mod nonblocking_stream;
|
|
23
|
+
mod pipe_notify;
|
|
24
|
+
#[allow(dead_code)]
|
|
25
|
+
mod python_api;
|
|
26
|
+
mod python_ffi;
|
|
27
|
+
#[cfg(test)]
|
|
28
|
+
mod python_finder;
|
|
29
|
+
mod python_guard;
|
|
30
|
+
mod ruby_helpers;
|
|
31
|
+
mod rubyx_object;
|
|
32
|
+
mod rubyx_stream;
|
|
33
|
+
mod stream;
|
|
34
|
+
#[cfg(test)]
|
|
35
|
+
pub mod test_helpers;
|
|
36
|
+
|
|
37
|
+
// Shared Python API Instance
|
|
38
|
+
static API: OnceLock<PythonApi> = OnceLock::new();
|
|
39
|
+
|
|
40
|
+
/// Global accessor for the Python API.
|
|
41
|
+
/// Panics if `Rubyx.init` has not been called yet.
|
|
42
|
+
fn api() -> &'static PythonApi {
|
|
43
|
+
API.get().expect("Python not initialized — call Rubyx.init(python_dl, python_home, python_exe, sys_paths) first")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[magnus::init]
|
|
47
|
+
fn init(ruby: &magnus::Ruby) -> Result<(), magnus::Error> {
|
|
48
|
+
// Define Rubyx Module
|
|
49
|
+
let rubyx_module = ruby.define_module("Rubyx")?;
|
|
50
|
+
|
|
51
|
+
rubyx_module.define_singleton_method("init", function!(rubyx_init, 4))?;
|
|
52
|
+
rubyx_module.define_singleton_method("initialized?", function!(rubyx_initialized, 0))?;
|
|
53
|
+
|
|
54
|
+
// Module Class Methods
|
|
55
|
+
rubyx_module.define_singleton_method("_import", function!(crate::import::rubyx_import, 1))?;
|
|
56
|
+
rubyx_module.define_singleton_method("_eval", function!(crate::eval::rubyx_eval, 1))?;
|
|
57
|
+
rubyx_module.define_singleton_method(
|
|
58
|
+
"_eval_with_globals",
|
|
59
|
+
function!(crate::eval::rubyx_eval_with_globals, 2),
|
|
60
|
+
)?;
|
|
61
|
+
rubyx_module.define_singleton_method("_await", function!(crate::eval::rubyx_await, 1))?;
|
|
62
|
+
rubyx_module.define_singleton_method(
|
|
63
|
+
"_await_with_globals",
|
|
64
|
+
function!(crate::eval::rubyx_await_with_globals, 2),
|
|
65
|
+
)?;
|
|
66
|
+
rubyx_module.define_singleton_method("_async_await", function!(rubyx_async_await, 1))?;
|
|
67
|
+
rubyx_module.define_singleton_method(
|
|
68
|
+
"_async_await_with_globals",
|
|
69
|
+
function!(crate::eval::rubyx_async_await_with_globals, 2),
|
|
70
|
+
)?;
|
|
71
|
+
|
|
72
|
+
// RubyxObject class for wrapped Python objects
|
|
73
|
+
let py_object = ruby.define_class("RubyxObject", ruby.class_object())?;
|
|
74
|
+
py_object.define_method(
|
|
75
|
+
"method_missing",
|
|
76
|
+
method!(crate::rubyx_object::RubyxObject::method_missing, -1),
|
|
77
|
+
)?;
|
|
78
|
+
py_object.define_method("to_s", method!(crate::rubyx_object::RubyxObject::to_s, 0))?;
|
|
79
|
+
py_object.define_method(
|
|
80
|
+
"inspect",
|
|
81
|
+
method!(crate::rubyx_object::RubyxObject::inspect, 0),
|
|
82
|
+
)?;
|
|
83
|
+
py_object.define_method(
|
|
84
|
+
"to_ruby",
|
|
85
|
+
method!(crate::rubyx_object::RubyxObject::to_ruby, 0),
|
|
86
|
+
)?;
|
|
87
|
+
py_object.define_method("[]", method!(RubyxObject::getitem, 1))?;
|
|
88
|
+
py_object.define_method("[]=", method!(RubyxObject::setitem, 2))?;
|
|
89
|
+
py_object.define_method("delete", method!(RubyxObject::delitem, 1))?;
|
|
90
|
+
py_object.define_method(
|
|
91
|
+
"respond_to_missing?",
|
|
92
|
+
method!(RubyxObject::respond_to_missing, -1),
|
|
93
|
+
)?;
|
|
94
|
+
py_object.define_method("truthy?", method!(RubyxObject::is_truthy, 0))?;
|
|
95
|
+
py_object.define_method("falsy?", method!(RubyxObject::is_falsy, 0))?;
|
|
96
|
+
py_object.define_method("callable?", method!(RubyxObject::is_callable, 0))?;
|
|
97
|
+
py_object.define_method("py_type", method!(RubyxObject::py_type, 0))?;
|
|
98
|
+
py_object.define_method("each", method!(RubyxObject::each, 0))?;
|
|
99
|
+
py_object.include_module(ruby.module_enumerable())?;
|
|
100
|
+
|
|
101
|
+
// RubyxStream class with Enumerable
|
|
102
|
+
let stream_class = rubyx_module.define_class("Stream", ruby.class_object())?;
|
|
103
|
+
stream_class.define_method("each", method!(crate::rubyx_stream::RubyxStream::each, 0))?;
|
|
104
|
+
stream_class.define_method(
|
|
105
|
+
"next",
|
|
106
|
+
method!(crate::rubyx_stream::RubyxStream::next_item, 0),
|
|
107
|
+
)?;
|
|
108
|
+
stream_class.include_module(ruby.module_enumerable())?;
|
|
109
|
+
|
|
110
|
+
// Rubyx.stream(iterable) — creates a RubyxStream from a Python iterable
|
|
111
|
+
rubyx_module.define_singleton_method("stream", function!(create_stream, -1))?;
|
|
112
|
+
// Rubyx.async_stream(iterable) - creates a RubyxStream from rust event loop
|
|
113
|
+
rubyx_module.define_singleton_method("async_stream", function!(create_async_stream, -1))?;
|
|
114
|
+
|
|
115
|
+
// NonBlockingStream class with Enumerable
|
|
116
|
+
let nb_stream_class = rubyx_module.define_class("NonBlockingStream", ruby.class_object())?;
|
|
117
|
+
nb_stream_class.define_method(
|
|
118
|
+
"each",
|
|
119
|
+
method!(crate::nonblocking_stream::NonBlockingStream::each, 0),
|
|
120
|
+
)?;
|
|
121
|
+
nb_stream_class.include_module(ruby.module_enumerable())?;
|
|
122
|
+
|
|
123
|
+
// Rubyx.nb_stream(iterable) — creates a NonBlockingStream from a Python iterable
|
|
124
|
+
rubyx_module.define_singleton_method("nb_stream", function!(create_nb_stream, 1))?;
|
|
125
|
+
|
|
126
|
+
let context_class = rubyx_module.define_class("Context", ruby.class_object())?;
|
|
127
|
+
context_class
|
|
128
|
+
.define_singleton_method("new", function!(crate::context::RubyxContext::new, 0))?;
|
|
129
|
+
context_class.define_method("_eval", method!(crate::context::RubyxContext::eval, 1))?;
|
|
130
|
+
context_class.define_method(
|
|
131
|
+
"_eval_with_globals",
|
|
132
|
+
method!(crate::context::RubyxContext::eval_with_globals, 2),
|
|
133
|
+
)?;
|
|
134
|
+
context_class.define_method(
|
|
135
|
+
"_await",
|
|
136
|
+
method!(crate::context::RubyxContext::await_eval, 1),
|
|
137
|
+
)?;
|
|
138
|
+
context_class.define_method(
|
|
139
|
+
"_await_with_globals",
|
|
140
|
+
method!(crate::context::RubyxContext::await_eval_with_globals, 2),
|
|
141
|
+
)?;
|
|
142
|
+
context_class.define_method(
|
|
143
|
+
"_async_await",
|
|
144
|
+
method!(crate::context::RubyxContext::async_await_eval, 1),
|
|
145
|
+
)?;
|
|
146
|
+
context_class.define_method(
|
|
147
|
+
"_async_await_with_globals",
|
|
148
|
+
method!(
|
|
149
|
+
crate::context::RubyxContext::async_await_eval_with_globals,
|
|
150
|
+
2
|
|
151
|
+
),
|
|
152
|
+
)?;
|
|
153
|
+
rubyx_module
|
|
154
|
+
.define_singleton_method("context", function!(crate::context::RubyxContext::new, 0))?;
|
|
155
|
+
|
|
156
|
+
// Rubyx::Future class
|
|
157
|
+
let future_class = rubyx_module.define_class("Future", ruby.class_object())?;
|
|
158
|
+
future_class.define_method("value", method!(crate::future::RubyxFuture::value, 0))?;
|
|
159
|
+
future_class.define_method("ready?", method!(crate::future::RubyxFuture::is_ready, 0))?;
|
|
160
|
+
|
|
161
|
+
Ok(())
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Rubyx.async_await(coroutine) — runs a Python coroutine on a background thread.
|
|
165
|
+
/// Returns a Rubyx::Future immediately. Call future.value to get the result.
|
|
166
|
+
fn rubyx_async_await(coroutine: Value) -> Result<future::RubyxFuture, magnus::Error> {
|
|
167
|
+
let obj = Obj::<RubyxObject>::try_convert(coroutine).map_err(|_| {
|
|
168
|
+
magnus::Error::new(
|
|
169
|
+
ruby_helpers::type_error(),
|
|
170
|
+
"Rubyx.async_await requires a Python coroutine (RubyxObject)",
|
|
171
|
+
)
|
|
172
|
+
})?;
|
|
173
|
+
let api = crate::api();
|
|
174
|
+
let gil = api.ensure_gil();
|
|
175
|
+
|
|
176
|
+
let future = future::RubyxFuture::from_coroutine(obj.as_ptr(), api);
|
|
177
|
+
|
|
178
|
+
api.release_gil(gil);
|
|
179
|
+
Ok(future)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn rubyx_initialized() -> bool {
|
|
183
|
+
API.get().is_some()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// `rubyx_init`: accept config paths and initialize from ruby
|
|
187
|
+
fn rubyx_init(
|
|
188
|
+
python_dl: String,
|
|
189
|
+
python_home: String,
|
|
190
|
+
python_exe: String,
|
|
191
|
+
sys_paths: RArray,
|
|
192
|
+
) -> Result<bool, magnus::Error> {
|
|
193
|
+
if API.get().is_some() {
|
|
194
|
+
return Err(magnus::Error::new(
|
|
195
|
+
ruby_helpers::runtime_error(),
|
|
196
|
+
"Python Interpreter already initialized",
|
|
197
|
+
));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let mut api = unsafe {
|
|
201
|
+
PythonApi::load(std::path::Path::new(&python_dl)).map_err(|e| {
|
|
202
|
+
magnus::Error::new(
|
|
203
|
+
ruby_helpers::runtime_error(),
|
|
204
|
+
format!("Error loading Python interpreter: {e}"),
|
|
205
|
+
)
|
|
206
|
+
})?
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
api.set_python_home(&python_home);
|
|
210
|
+
api.set_program_name(&python_exe);
|
|
211
|
+
|
|
212
|
+
api.initialize_ex(0);
|
|
213
|
+
|
|
214
|
+
if !api.is_initialized() {
|
|
215
|
+
return Err(magnus::Error::new(
|
|
216
|
+
ruby_helpers::runtime_error(),
|
|
217
|
+
"Python Interpreter failed to initialize",
|
|
218
|
+
));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
inject_sys_paths(&api, &sys_paths)?;
|
|
222
|
+
|
|
223
|
+
let _ = api.install_async_to_sync_class();
|
|
224
|
+
|
|
225
|
+
// Release the GIL that Py_InitializeEx() acquired on the main thread.
|
|
226
|
+
// This allows worker threads to acquire it via ensure_gil()/release_gil().
|
|
227
|
+
// Without this, the main thread permanently holds the GIL and any
|
|
228
|
+
// background thread calling ensure_gil() will deadlock.
|
|
229
|
+
api.save_thread();
|
|
230
|
+
|
|
231
|
+
API.set(api).map_err(|_| {
|
|
232
|
+
magnus::Error::new(ruby_helpers::runtime_error(), "Failed to store Python API")
|
|
233
|
+
})?;
|
|
234
|
+
|
|
235
|
+
Ok(true)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fn inject_sys_paths(api: &PythonApi, sys_paths: &RArray) -> Result<(), magnus::Error> {
|
|
239
|
+
let sys_module = api.import_module("sys").map_err(|e| {
|
|
240
|
+
magnus::Error::new(
|
|
241
|
+
ruby_helpers::runtime_error(),
|
|
242
|
+
format!("Failed to import sys module: {e}"),
|
|
243
|
+
)
|
|
244
|
+
})?;
|
|
245
|
+
let sys = api.object_get_attr_string(sys_module, "path");
|
|
246
|
+
if sys.is_null() {
|
|
247
|
+
api.decref(sys_module);
|
|
248
|
+
return Err(magnus::Error::new(
|
|
249
|
+
ruby_helpers::runtime_error(),
|
|
250
|
+
"Failed to get sys.path",
|
|
251
|
+
));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Append sys_paths to sys.path
|
|
255
|
+
let len = sys_paths.len();
|
|
256
|
+
for i in 0..len {
|
|
257
|
+
let path: String = sys_paths.entry(i as isize).map_err(|e| {
|
|
258
|
+
magnus::Error::new(
|
|
259
|
+
ruby_helpers::runtime_error(),
|
|
260
|
+
format!("Failed to get path at index {i}: {e}"),
|
|
261
|
+
)
|
|
262
|
+
})?;
|
|
263
|
+
let py_str = api.string_from_str(&path);
|
|
264
|
+
if py_str.is_null() {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
let result = api.list_append(sys, py_str);
|
|
268
|
+
if result == -1 {
|
|
269
|
+
api.decref(py_str);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
api.decref(py_str);
|
|
273
|
+
}
|
|
274
|
+
api.decref(sys);
|
|
275
|
+
api.decref(sys_module);
|
|
276
|
+
|
|
277
|
+
api.clear_error();
|
|
278
|
+
Ok(())
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn create_stream(args: &[Value]) -> Result<rubyx_stream::RubyxStream, magnus::Error> {
|
|
282
|
+
let ruby = Ruby::get().map_err(|e| {
|
|
283
|
+
magnus::Error::new(
|
|
284
|
+
ruby_helpers::runtime_error(),
|
|
285
|
+
format!("Error getting Ruby: {e}"),
|
|
286
|
+
)
|
|
287
|
+
})?;
|
|
288
|
+
let has_block = ruby.block_given();
|
|
289
|
+
if args.len() == 1 && !has_block {
|
|
290
|
+
create_stream_from_iterable(args[0])
|
|
291
|
+
} else if args.is_empty() && has_block {
|
|
292
|
+
// Get proc
|
|
293
|
+
let proc = ruby.block_proc().map_err(|e| {
|
|
294
|
+
magnus::Error::new(
|
|
295
|
+
ruby_helpers::runtime_error(),
|
|
296
|
+
format!("Error getting block proc: {e}"),
|
|
297
|
+
)
|
|
298
|
+
})?;
|
|
299
|
+
// Run Proc
|
|
300
|
+
let iterable: Value = proc.call(())?;
|
|
301
|
+
create_stream_from_iterable(iterable)
|
|
302
|
+
} else {
|
|
303
|
+
Err(magnus::Error::new(
|
|
304
|
+
ruby_helpers::arg_error(),
|
|
305
|
+
"Rubyx.stream takes either 0 or 1 arguments",
|
|
306
|
+
))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn create_nb_stream(
|
|
311
|
+
iterable: Value,
|
|
312
|
+
) -> Result<nonblocking_stream::NonBlockingStream, magnus::Error> {
|
|
313
|
+
let obj = Obj::<RubyxObject>::try_convert(iterable).map_err(|_| {
|
|
314
|
+
magnus::Error::new(
|
|
315
|
+
ruby_helpers::type_error(),
|
|
316
|
+
"Rubyx.nb_stream requires a Python object (RubyxObject)",
|
|
317
|
+
)
|
|
318
|
+
})?;
|
|
319
|
+
|
|
320
|
+
let api = crate::api();
|
|
321
|
+
let gil = api.ensure_gil();
|
|
322
|
+
|
|
323
|
+
// Get a Python iterator — handle both sync and async iterables
|
|
324
|
+
let py_iter = if api.is_async_iterable(obj.as_ptr()) {
|
|
325
|
+
let sync_iter = api.wrap_async_generator(obj.as_ptr());
|
|
326
|
+
if sync_iter.is_null() {
|
|
327
|
+
api.clear_error();
|
|
328
|
+
api.release_gil(gil);
|
|
329
|
+
return Err(magnus::Error::new(
|
|
330
|
+
ruby_helpers::runtime_error(),
|
|
331
|
+
"Failed to wrap async generator",
|
|
332
|
+
));
|
|
333
|
+
}
|
|
334
|
+
sync_iter
|
|
335
|
+
} else {
|
|
336
|
+
let iter = api.object_get_iter(obj.as_ptr());
|
|
337
|
+
if iter.is_null() {
|
|
338
|
+
api.clear_error();
|
|
339
|
+
api.release_gil(gil);
|
|
340
|
+
return Err(magnus::Error::new(
|
|
341
|
+
ruby_helpers::type_error(),
|
|
342
|
+
"Object is not iterable",
|
|
343
|
+
));
|
|
344
|
+
}
|
|
345
|
+
iter
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
api.release_gil(gil);
|
|
349
|
+
|
|
350
|
+
// Use unbounded channel so the producer never blocks on send().
|
|
351
|
+
// With a bounded channel, the producer blocks when the channel is
|
|
352
|
+
// full. If the consumer is in IO.select (fiber-aware path) waiting
|
|
353
|
+
// for a pipe notification, and the producer can't reach notify()
|
|
354
|
+
// because it's blocked on send(), both sides deadlock.
|
|
355
|
+
let (tx, rx) = unbounded();
|
|
356
|
+
let pipe = Arc::new(PipeNotify::new().map_err(|e| {
|
|
357
|
+
magnus::Error::new(
|
|
358
|
+
ruby_helpers::runtime_error(),
|
|
359
|
+
format!("Failed to create pipe: {e}"),
|
|
360
|
+
)
|
|
361
|
+
})?);
|
|
362
|
+
let pipe_clone = pipe.clone();
|
|
363
|
+
let py_iter_addr = py_iter as usize;
|
|
364
|
+
|
|
365
|
+
std::thread::spawn(move || {
|
|
366
|
+
let py_iter = py_iter_addr as *mut crate::python_ffi::PyObject;
|
|
367
|
+
let api = crate::api();
|
|
368
|
+
let gil = api.ensure_gil();
|
|
369
|
+
|
|
370
|
+
loop {
|
|
371
|
+
let item = api.iter_next(py_iter);
|
|
372
|
+
if item.is_null() {
|
|
373
|
+
if api.has_error() {
|
|
374
|
+
if let Some(exc) = crate::python_api::PythonApi::extract_exception(api) {
|
|
375
|
+
let _ = tx.send(StreamItem::Error(exc.to_string()));
|
|
376
|
+
} else {
|
|
377
|
+
let _ = tx.send(StreamItem::End);
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
let _ = tx.send(StreamItem::End);
|
|
381
|
+
}
|
|
382
|
+
pipe_clone.notify();
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let ruby_value = python_to_sendable(item, api)
|
|
387
|
+
.map_err(|e| format!("Error converting Python value: {e}"));
|
|
388
|
+
api.decref(item);
|
|
389
|
+
|
|
390
|
+
match ruby_value {
|
|
391
|
+
Ok(value) => {
|
|
392
|
+
if tx.send(StreamItem::Value(value)).is_err() {
|
|
393
|
+
break; // Consumer dropped
|
|
394
|
+
}
|
|
395
|
+
pipe_clone.notify();
|
|
396
|
+
}
|
|
397
|
+
Err(e) => {
|
|
398
|
+
let _ = tx.send(StreamItem::Error(e));
|
|
399
|
+
pipe_clone.notify();
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
api.decref(py_iter);
|
|
406
|
+
api.release_gil(gil);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
Ok(NonBlockingStream::new(rx, pipe))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
fn create_async_stream(args: &[Value]) -> Result<rubyx_stream::RubyxStream, magnus::Error> {
|
|
413
|
+
let ruby = Ruby::get().map_err(|e| {
|
|
414
|
+
magnus::Error::new(
|
|
415
|
+
ruby_helpers::runtime_error(),
|
|
416
|
+
format!("Error getting Ruby: {e}"),
|
|
417
|
+
)
|
|
418
|
+
})?;
|
|
419
|
+
let has_block = ruby.block_given();
|
|
420
|
+
if args.len() == 1 && !has_block {
|
|
421
|
+
create_async_stream_from_iterable(args[0])
|
|
422
|
+
} else if args.is_empty() && has_block {
|
|
423
|
+
// Get proc
|
|
424
|
+
let proc = ruby.block_proc().map_err(|e| {
|
|
425
|
+
magnus::Error::new(
|
|
426
|
+
ruby_helpers::runtime_error(),
|
|
427
|
+
format!("Error getting block proc: {e}"),
|
|
428
|
+
)
|
|
429
|
+
})?;
|
|
430
|
+
// Run Proc
|
|
431
|
+
let iterable: Value = proc.call(())?;
|
|
432
|
+
create_async_stream_from_iterable(iterable)
|
|
433
|
+
} else {
|
|
434
|
+
Err(magnus::Error::new(
|
|
435
|
+
ruby_helpers::arg_error(),
|
|
436
|
+
"Rubyx.stream takes either 0 or 1 arguments",
|
|
437
|
+
))
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// Create a RubyxStream from a Python iterable (RubyxObject).
|
|
442
|
+
///
|
|
443
|
+
/// Acquires the GIL, calls PyObject_GetIter on the wrapped Python object,
|
|
444
|
+
/// and passes the resulting iterator to AsyncStream::from_python_iterator.
|
|
445
|
+
fn create_stream_from_iterable(iterable: Value) -> Result<RubyxStream, magnus::Error> {
|
|
446
|
+
let obj = Obj::<RubyxObject>::try_convert(iterable).map_err(|_| {
|
|
447
|
+
magnus::Error::new(
|
|
448
|
+
ruby_helpers::type_error(),
|
|
449
|
+
"Rubyx.stream requires a Python object (RubyxObject)",
|
|
450
|
+
)
|
|
451
|
+
})?;
|
|
452
|
+
|
|
453
|
+
let stream =
|
|
454
|
+
AsyncGeneratorStream::from_python_object(obj.as_ptr(), AsyncStrategy::PythonAdapter)
|
|
455
|
+
.map_err(|e| magnus::Error::new(ruby_helpers::runtime_error(), e))?;
|
|
456
|
+
|
|
457
|
+
Ok(RubyxStream::from_stream(stream))
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
fn create_async_stream_from_iterable(iterable: Value) -> Result<RubyxStream, magnus::Error> {
|
|
461
|
+
let obj = Obj::<RubyxObject>::try_convert(iterable).map_err(|_| {
|
|
462
|
+
magnus::Error::new(
|
|
463
|
+
ruby_helpers::type_error(),
|
|
464
|
+
"Rubyx.stream requires a Python object (RubyxObject)",
|
|
465
|
+
)
|
|
466
|
+
})?;
|
|
467
|
+
|
|
468
|
+
// Verify the object is actually an async iterable before using RustDriving
|
|
469
|
+
let api = crate::api();
|
|
470
|
+
let gil = api.ensure_gil();
|
|
471
|
+
let is_async = api.is_async_iterable(obj.as_ptr());
|
|
472
|
+
api.release_gil(gil);
|
|
473
|
+
|
|
474
|
+
if !is_async {
|
|
475
|
+
return Err(magnus::Error::new(
|
|
476
|
+
ruby_helpers::type_error(),
|
|
477
|
+
"Object is not an async iterable (missing __aiter__/__anext__)",
|
|
478
|
+
));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let stream = AsyncGeneratorStream::from_python_object(obj.as_ptr(), AsyncStrategy::RustDriving)
|
|
482
|
+
.map_err(|e| magnus::Error::new(ruby_helpers::runtime_error(), e))?;
|
|
483
|
+
|
|
484
|
+
Ok(RubyxStream::from_stream(stream))
|
|
485
|
+
}
|
|
486
|
+
// export LD_LIBRARY_PATH="~/.asdf/installs/ruby/3.4.7/lib:$LD_LIBRARY_PATH"
|
|
487
|
+
// cargo test
|
|
488
|
+
|
|
489
|
+
#[cfg(test)]
|
|
490
|
+
mod tests {
|
|
491
|
+
use crate::rubyx_object::RubyxObject;
|
|
492
|
+
use crate::test_helpers::{skip_if_no_python, with_ruby_python};
|
|
493
|
+
use magnus::typed_data::Obj;
|
|
494
|
+
use magnus::value::ReprValue;
|
|
495
|
+
use magnus::{IntoValue, TryConvert};
|
|
496
|
+
use serial_test::serial;
|
|
497
|
+
|
|
498
|
+
#[test]
|
|
499
|
+
#[serial]
|
|
500
|
+
fn test_rubyx_object_wraps_pyobject() {
|
|
501
|
+
let Some(guard) = skip_if_no_python() else {
|
|
502
|
+
return;
|
|
503
|
+
};
|
|
504
|
+
let api = guard.api();
|
|
505
|
+
|
|
506
|
+
let py_int = api.long_from_i64(42);
|
|
507
|
+
assert!(!py_int.is_null());
|
|
508
|
+
|
|
509
|
+
let wrapper = RubyxObject::new(py_int, api).expect("Should wrap non-null PyObject");
|
|
510
|
+
assert_eq!(
|
|
511
|
+
wrapper.as_ptr(),
|
|
512
|
+
py_int,
|
|
513
|
+
"as_ptr should return the original pointer"
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// Drop wrapper (decrefs: refcount 2 → 1), then decref original (1 → 0)
|
|
517
|
+
drop(wrapper);
|
|
518
|
+
api.decref(py_int);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#[test]
|
|
522
|
+
#[serial]
|
|
523
|
+
fn test_rubyx_object_null_returns_none() {
|
|
524
|
+
let Some(guard) = skip_if_no_python() else {
|
|
525
|
+
return;
|
|
526
|
+
};
|
|
527
|
+
let api = guard.api();
|
|
528
|
+
|
|
529
|
+
let result = RubyxObject::new(std::ptr::null_mut(), api);
|
|
530
|
+
assert!(result.is_none(), "null pointer should return None");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
#[test]
|
|
534
|
+
#[serial]
|
|
535
|
+
fn test_rubyx_object_increfs_on_create() {
|
|
536
|
+
let Some(guard) = skip_if_no_python() else {
|
|
537
|
+
return;
|
|
538
|
+
};
|
|
539
|
+
let api = guard.api();
|
|
540
|
+
|
|
541
|
+
// long_from_i64 returns a new reference (refcount = 1)
|
|
542
|
+
let py_int = api.long_from_i64(42);
|
|
543
|
+
|
|
544
|
+
// Wrapping increfs (refcount = 2)
|
|
545
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
546
|
+
|
|
547
|
+
// Release our original reference (refcount = 1, wrapper still holds it)
|
|
548
|
+
api.decref(py_int);
|
|
549
|
+
|
|
550
|
+
// Object must still be alive — the wrapper's incref keeps it alive
|
|
551
|
+
let value = api.long_to_i64(wrapper.as_ptr());
|
|
552
|
+
assert_eq!(
|
|
553
|
+
value, 42,
|
|
554
|
+
"Object should still be alive after decref'ing original ref"
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
// wrapper drops here → decref → refcount 0 → freed
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
#[test]
|
|
561
|
+
#[serial]
|
|
562
|
+
fn test_rubyx_object_decrefs_on_drop() {
|
|
563
|
+
let Some(guard) = skip_if_no_python() else {
|
|
564
|
+
return;
|
|
565
|
+
};
|
|
566
|
+
let api = guard.api();
|
|
567
|
+
|
|
568
|
+
// Create object (refcount = 1)
|
|
569
|
+
let py_int = api.long_from_i64(99);
|
|
570
|
+
|
|
571
|
+
// Take an extra ref so we can safely observe after wrapper drops (refcount = 2)
|
|
572
|
+
api.incref(py_int);
|
|
573
|
+
|
|
574
|
+
{
|
|
575
|
+
// Wrap it (incref → refcount = 3)
|
|
576
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
577
|
+
assert_eq!(api.long_to_i64(wrapper.as_ptr()), 99);
|
|
578
|
+
// wrapper drops here → decref → refcount = 2
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Object should still be alive (refcount = 2)
|
|
582
|
+
let value = api.long_to_i64(py_int);
|
|
583
|
+
assert_eq!(value, 99, "Object should survive after wrapper drop");
|
|
584
|
+
|
|
585
|
+
// Clean up our two remaining references
|
|
586
|
+
api.decref(py_int);
|
|
587
|
+
api.decref(py_int);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
#[test]
|
|
591
|
+
#[serial]
|
|
592
|
+
fn test_multiple_wrappers_same_object() {
|
|
593
|
+
let Some(guard) = skip_if_no_python() else {
|
|
594
|
+
return;
|
|
595
|
+
};
|
|
596
|
+
let api = guard.api();
|
|
597
|
+
|
|
598
|
+
// refcount = 1
|
|
599
|
+
let py_int = api.long_from_i64(77);
|
|
600
|
+
|
|
601
|
+
// Each wrapper increfs: refcount 1 → 2 → 3 → 4
|
|
602
|
+
let w1 = RubyxObject::new(py_int, api).unwrap();
|
|
603
|
+
let w2 = RubyxObject::new(py_int, api).unwrap();
|
|
604
|
+
let w3 = RubyxObject::new(py_int, api).unwrap();
|
|
605
|
+
|
|
606
|
+
// All point to the same object
|
|
607
|
+
assert_eq!(w1.as_ptr(), py_int);
|
|
608
|
+
assert_eq!(w2.as_ptr(), py_int);
|
|
609
|
+
assert_eq!(w3.as_ptr(), py_int);
|
|
610
|
+
|
|
611
|
+
// Drop wrappers: each decrefs (refcount 4 → 3 → 2 → 1)
|
|
612
|
+
drop(w1);
|
|
613
|
+
drop(w2);
|
|
614
|
+
drop(w3);
|
|
615
|
+
|
|
616
|
+
// Object still alive (refcount = 1, our original ref)
|
|
617
|
+
assert_eq!(api.long_to_i64(py_int), 77);
|
|
618
|
+
|
|
619
|
+
// Clean up
|
|
620
|
+
api.decref(py_int);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
#[test]
|
|
624
|
+
#[serial]
|
|
625
|
+
fn test_gc_stress() {
|
|
626
|
+
let Some(guard) = skip_if_no_python() else {
|
|
627
|
+
return;
|
|
628
|
+
};
|
|
629
|
+
let api = guard.api();
|
|
630
|
+
|
|
631
|
+
// Rapid create-and-drop: 1000 wrappers sequentially
|
|
632
|
+
for i in 0..1000 {
|
|
633
|
+
let py_int = api.long_from_i64(i);
|
|
634
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
635
|
+
|
|
636
|
+
assert_eq!(api.long_to_i64(wrapper.as_ptr()), i);
|
|
637
|
+
|
|
638
|
+
drop(wrapper);
|
|
639
|
+
api.decref(py_int);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Many wrappers alive simultaneously
|
|
643
|
+
let mut wrappers = Vec::new();
|
|
644
|
+
for i in 0..100 {
|
|
645
|
+
let py_int = api.long_from_i64(i);
|
|
646
|
+
wrappers.push((py_int, RubyxObject::new(py_int, api).unwrap()));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Verify all still valid while all alive
|
|
650
|
+
for (i, (_ptr, wrapper)) in wrappers.iter().enumerate() {
|
|
651
|
+
assert_eq!(api.long_to_i64(wrapper.as_ptr()), i as i64);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Drop all wrappers and clean up
|
|
655
|
+
for (ptr, wrapper) in wrappers {
|
|
656
|
+
drop(wrapper);
|
|
657
|
+
api.decref(ptr);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ========== Multi-threading safety tests ==========
|
|
662
|
+
//
|
|
663
|
+
// These tests validate the `unsafe impl Send` and `unsafe impl Sync`
|
|
664
|
+
// on RubyxObject. They exercise cross-thread GIL acquisition, concurrent
|
|
665
|
+
// Drop, and shared-reference access from multiple threads.
|
|
666
|
+
//
|
|
667
|
+
// Python's GIL serialises interpreter access, so "thread safety" here means:
|
|
668
|
+
// 1. No crashes / UB when operations happen from different OS threads.
|
|
669
|
+
// 2. Reference counts remain consistent after concurrent create/drop.
|
|
670
|
+
// 3. ensure_gil / release_gil work correctly from non-main threads.
|
|
671
|
+
|
|
672
|
+
/// SAFETY: the wrapped pointer is only dereferenced while holding
|
|
673
|
+
/// the Python GIL, which serialises all interpreter access.
|
|
674
|
+
struct SendPtr(*mut crate::python_ffi::PyObject);
|
|
675
|
+
unsafe impl Send for SendPtr {}
|
|
676
|
+
unsafe impl Sync for SendPtr {}
|
|
677
|
+
|
|
678
|
+
fn get_static_api() -> Option<&'static crate::python_api::PythonApi> {
|
|
679
|
+
crate::test_helpers::get_api()
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
#[test]
|
|
683
|
+
#[serial]
|
|
684
|
+
fn test_send_wrapper_to_another_thread() {
|
|
685
|
+
use std::thread;
|
|
686
|
+
|
|
687
|
+
let Some(guard) = skip_if_no_python() else {
|
|
688
|
+
return;
|
|
689
|
+
};
|
|
690
|
+
let api = guard.api();
|
|
691
|
+
|
|
692
|
+
let py_int = api.long_from_i64(42);
|
|
693
|
+
api.incref(py_int);
|
|
694
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
695
|
+
|
|
696
|
+
let saved = api.save_thread();
|
|
697
|
+
let static_api = get_static_api().unwrap();
|
|
698
|
+
|
|
699
|
+
let handle = thread::spawn(move || {
|
|
700
|
+
let gil = static_api.ensure_gil();
|
|
701
|
+
let val = static_api.long_to_i64(wrapper.as_ptr());
|
|
702
|
+
assert_eq!(val, 42, "Value should survive cross-thread move");
|
|
703
|
+
drop(wrapper);
|
|
704
|
+
static_api.release_gil(gil);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
handle.join().expect("Worker thread panicked");
|
|
708
|
+
api.restore_thread(saved);
|
|
709
|
+
|
|
710
|
+
let val = api.long_to_i64(py_int);
|
|
711
|
+
assert_eq!(
|
|
712
|
+
val, 42,
|
|
713
|
+
"Object should survive after cross-thread wrapper drop"
|
|
714
|
+
);
|
|
715
|
+
api.decref(py_int);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
#[test]
|
|
719
|
+
#[serial]
|
|
720
|
+
fn test_concurrent_create_and_drop_different_objects() {
|
|
721
|
+
use std::sync::{Arc, Barrier};
|
|
722
|
+
use std::thread;
|
|
723
|
+
|
|
724
|
+
let Some(guard) = skip_if_no_python() else {
|
|
725
|
+
return;
|
|
726
|
+
};
|
|
727
|
+
let api = guard.api();
|
|
728
|
+
let num_threads = 4;
|
|
729
|
+
let objects_per_thread = 100;
|
|
730
|
+
|
|
731
|
+
let saved = api.save_thread();
|
|
732
|
+
let static_api = get_static_api().unwrap();
|
|
733
|
+
|
|
734
|
+
let barrier = Arc::new(Barrier::new(num_threads));
|
|
735
|
+
let mut handles = Vec::new();
|
|
736
|
+
|
|
737
|
+
for t in 0..num_threads {
|
|
738
|
+
let barrier = Arc::clone(&barrier);
|
|
739
|
+
handles.push(thread::spawn(move || {
|
|
740
|
+
barrier.wait();
|
|
741
|
+
|
|
742
|
+
for i in 0..objects_per_thread {
|
|
743
|
+
let gil = static_api.ensure_gil();
|
|
744
|
+
let value = (t * objects_per_thread + i) as i64;
|
|
745
|
+
let py_int = static_api.long_from_i64(value);
|
|
746
|
+
let wrapper = RubyxObject::new(py_int, static_api).unwrap();
|
|
747
|
+
assert_eq!(static_api.long_to_i64(wrapper.as_ptr()), value);
|
|
748
|
+
drop(wrapper);
|
|
749
|
+
static_api.decref(py_int);
|
|
750
|
+
static_api.release_gil(gil);
|
|
751
|
+
}
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
for h in handles {
|
|
756
|
+
h.join().expect("Worker thread panicked");
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
api.restore_thread(saved);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
#[test]
|
|
763
|
+
#[serial]
|
|
764
|
+
fn test_concurrent_wrappers_same_object() {
|
|
765
|
+
use std::sync::{Arc, Barrier};
|
|
766
|
+
use std::thread;
|
|
767
|
+
|
|
768
|
+
let Some(guard) = skip_if_no_python() else {
|
|
769
|
+
return;
|
|
770
|
+
};
|
|
771
|
+
let api = guard.api();
|
|
772
|
+
|
|
773
|
+
let py_int = api.long_from_i64(999);
|
|
774
|
+
api.incref(py_int);
|
|
775
|
+
|
|
776
|
+
let num_threads = 4;
|
|
777
|
+
let wraps_per_thread = 50;
|
|
778
|
+
let shared_ptr = Arc::new(SendPtr(py_int));
|
|
779
|
+
|
|
780
|
+
let saved = api.save_thread();
|
|
781
|
+
let static_api = get_static_api().unwrap();
|
|
782
|
+
|
|
783
|
+
let barrier = Arc::new(Barrier::new(num_threads));
|
|
784
|
+
let mut handles = Vec::new();
|
|
785
|
+
|
|
786
|
+
for _ in 0..num_threads {
|
|
787
|
+
let barrier = Arc::clone(&barrier);
|
|
788
|
+
let shared_ptr = Arc::clone(&shared_ptr);
|
|
789
|
+
handles.push(thread::spawn(move || {
|
|
790
|
+
barrier.wait();
|
|
791
|
+
|
|
792
|
+
for _ in 0..wraps_per_thread {
|
|
793
|
+
let gil = static_api.ensure_gil();
|
|
794
|
+
let wrapper = RubyxObject::new(shared_ptr.0, static_api).unwrap();
|
|
795
|
+
assert_eq!(static_api.long_to_i64(wrapper.as_ptr()), 999);
|
|
796
|
+
drop(wrapper);
|
|
797
|
+
static_api.release_gil(gil);
|
|
798
|
+
}
|
|
799
|
+
}));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
for h in handles {
|
|
803
|
+
h.join().expect("Worker thread panicked");
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
api.restore_thread(saved);
|
|
807
|
+
|
|
808
|
+
let val = api.long_to_i64(py_int);
|
|
809
|
+
assert_eq!(
|
|
810
|
+
val, 999,
|
|
811
|
+
"Object should survive concurrent wrap/drop cycles"
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
api.decref(py_int);
|
|
815
|
+
api.decref(py_int);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
#[test]
|
|
819
|
+
#[serial]
|
|
820
|
+
fn test_drop_on_different_thread_than_create() {
|
|
821
|
+
use std::thread;
|
|
822
|
+
|
|
823
|
+
let Some(guard) = skip_if_no_python() else {
|
|
824
|
+
return;
|
|
825
|
+
};
|
|
826
|
+
let api = guard.api();
|
|
827
|
+
|
|
828
|
+
let py_ints: Vec<_> = (0..10)
|
|
829
|
+
.map(|i| {
|
|
830
|
+
let p = api.long_from_i64(i);
|
|
831
|
+
api.incref(p);
|
|
832
|
+
let wrapper = RubyxObject::new(p, api).unwrap();
|
|
833
|
+
(SendPtr(p), wrapper)
|
|
834
|
+
})
|
|
835
|
+
.collect();
|
|
836
|
+
|
|
837
|
+
let saved = api.save_thread();
|
|
838
|
+
let static_api = get_static_api().unwrap();
|
|
839
|
+
|
|
840
|
+
let mut handles = Vec::new();
|
|
841
|
+
for (send_ptr, wrapper) in py_ints.into_iter() {
|
|
842
|
+
handles.push(thread::spawn(move || {
|
|
843
|
+
let gil = static_api.ensure_gil();
|
|
844
|
+
drop(wrapper);
|
|
845
|
+
static_api.release_gil(gil);
|
|
846
|
+
send_ptr
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
let returned_ptrs: Vec<_> = handles
|
|
851
|
+
.into_iter()
|
|
852
|
+
.map(|h| h.join().expect("Worker panicked"))
|
|
853
|
+
.collect();
|
|
854
|
+
|
|
855
|
+
let gil = static_api.ensure_gil();
|
|
856
|
+
for (i, send_ptr) in returned_ptrs.iter().enumerate() {
|
|
857
|
+
let val = static_api.long_to_i64(send_ptr.0);
|
|
858
|
+
assert_eq!(val, i as i64, "Object {i} should survive cross-thread drop");
|
|
859
|
+
static_api.decref(send_ptr.0);
|
|
860
|
+
}
|
|
861
|
+
static_api.release_gil(gil);
|
|
862
|
+
|
|
863
|
+
api.restore_thread(saved);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
#[test]
|
|
867
|
+
#[serial]
|
|
868
|
+
fn test_shared_ref_across_threads() {
|
|
869
|
+
use std::sync::{Arc, Barrier};
|
|
870
|
+
use std::thread;
|
|
871
|
+
|
|
872
|
+
let Some(guard) = skip_if_no_python() else {
|
|
873
|
+
return;
|
|
874
|
+
};
|
|
875
|
+
let api = guard.api();
|
|
876
|
+
|
|
877
|
+
let py_int = api.long_from_i64(777);
|
|
878
|
+
// Arc<RubyxObject> exercises Sync — multiple threads hold &RubyxObject
|
|
879
|
+
let wrapper = Arc::new(RubyxObject::new(py_int, api).unwrap());
|
|
880
|
+
|
|
881
|
+
let num_threads = 4;
|
|
882
|
+
let reads_per_thread = 100;
|
|
883
|
+
|
|
884
|
+
let saved = api.save_thread();
|
|
885
|
+
let static_api = get_static_api().unwrap();
|
|
886
|
+
|
|
887
|
+
let barrier = Arc::new(Barrier::new(num_threads));
|
|
888
|
+
let mut handles = Vec::new();
|
|
889
|
+
|
|
890
|
+
for _ in 0..num_threads {
|
|
891
|
+
let wrapper = Arc::clone(&wrapper);
|
|
892
|
+
let barrier = Arc::clone(&barrier);
|
|
893
|
+
handles.push(thread::spawn(move || {
|
|
894
|
+
barrier.wait();
|
|
895
|
+
for _ in 0..reads_per_thread {
|
|
896
|
+
let gil = static_api.ensure_gil();
|
|
897
|
+
let val = static_api.long_to_i64(wrapper.as_ptr());
|
|
898
|
+
assert_eq!(val, 777, "Concurrent reads should be consistent");
|
|
899
|
+
static_api.release_gil(gil);
|
|
900
|
+
}
|
|
901
|
+
}));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
for h in handles {
|
|
905
|
+
h.join().expect("Worker panicked");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
api.restore_thread(saved);
|
|
909
|
+
|
|
910
|
+
drop(wrapper);
|
|
911
|
+
api.decref(py_int);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
#[test]
|
|
915
|
+
#[serial]
|
|
916
|
+
fn test_concurrent_stress_mixed_operations() {
|
|
917
|
+
use std::sync::{Arc, Barrier};
|
|
918
|
+
use std::thread;
|
|
919
|
+
|
|
920
|
+
let Some(guard) = skip_if_no_python() else {
|
|
921
|
+
return;
|
|
922
|
+
};
|
|
923
|
+
let api = guard.api();
|
|
924
|
+
|
|
925
|
+
let num_threads = 8;
|
|
926
|
+
let iterations = 50;
|
|
927
|
+
|
|
928
|
+
let saved = api.save_thread();
|
|
929
|
+
let static_api = get_static_api().unwrap();
|
|
930
|
+
|
|
931
|
+
let barrier = Arc::new(Barrier::new(num_threads));
|
|
932
|
+
let mut handles = Vec::new();
|
|
933
|
+
|
|
934
|
+
for t in 0..num_threads {
|
|
935
|
+
let barrier = Arc::clone(&barrier);
|
|
936
|
+
handles.push(thread::spawn(move || {
|
|
937
|
+
barrier.wait();
|
|
938
|
+
|
|
939
|
+
let mut wrappers = Vec::new();
|
|
940
|
+
|
|
941
|
+
for i in 0..iterations {
|
|
942
|
+
let gil = static_api.ensure_gil();
|
|
943
|
+
|
|
944
|
+
let val = (t * 1000 + i) as i64;
|
|
945
|
+
let py_int = static_api.long_from_i64(val);
|
|
946
|
+
let w = RubyxObject::new(py_int, static_api).unwrap();
|
|
947
|
+
assert_eq!(static_api.long_to_i64(w.as_ptr()), val);
|
|
948
|
+
wrappers.push((py_int, w));
|
|
949
|
+
|
|
950
|
+
if wrappers.len() > 5 {
|
|
951
|
+
let (ptr, w) = wrappers.remove(0);
|
|
952
|
+
drop(w);
|
|
953
|
+
static_api.decref(ptr);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
static_api.release_gil(gil);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
let gil = static_api.ensure_gil();
|
|
960
|
+
for (ptr, w) in wrappers {
|
|
961
|
+
drop(w);
|
|
962
|
+
static_api.decref(ptr);
|
|
963
|
+
}
|
|
964
|
+
static_api.release_gil(gil);
|
|
965
|
+
}));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
for h in handles {
|
|
969
|
+
h.join().expect("Worker panicked");
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
api.restore_thread(saved);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
#[test]
|
|
976
|
+
#[serial]
|
|
977
|
+
fn test_gil_ensure_is_reentrant() {
|
|
978
|
+
let Some(guard) = skip_if_no_python() else {
|
|
979
|
+
return;
|
|
980
|
+
};
|
|
981
|
+
let api = guard.api();
|
|
982
|
+
|
|
983
|
+
// RubyxObject::new calls ensure_gil internally.
|
|
984
|
+
// The guard already holds the GIL, so this exercises reentrant acquire.
|
|
985
|
+
let py_int = api.long_from_i64(123);
|
|
986
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
987
|
+
assert_eq!(api.long_to_i64(wrapper.as_ptr()), 123);
|
|
988
|
+
|
|
989
|
+
let inner_gil = api.ensure_gil();
|
|
990
|
+
let py_int2 = api.long_from_i64(456);
|
|
991
|
+
let wrapper2 = RubyxObject::new(py_int2, api).unwrap();
|
|
992
|
+
assert_eq!(api.long_to_i64(wrapper2.as_ptr()), 456);
|
|
993
|
+
drop(wrapper2);
|
|
994
|
+
api.decref(py_int2);
|
|
995
|
+
api.release_gil(inner_gil);
|
|
996
|
+
|
|
997
|
+
drop(wrapper);
|
|
998
|
+
api.decref(py_int);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ========== Import tests ==========
|
|
1002
|
+
|
|
1003
|
+
#[test]
|
|
1004
|
+
#[serial]
|
|
1005
|
+
fn test_import_builtin_module() {
|
|
1006
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1007
|
+
return;
|
|
1008
|
+
};
|
|
1009
|
+
let api = guard.api();
|
|
1010
|
+
|
|
1011
|
+
// Import 'sys' — a builtin module that's always available
|
|
1012
|
+
let module = api.import_module("sys");
|
|
1013
|
+
assert!(module.is_ok(), "Should import 'sys' module");
|
|
1014
|
+
let module = module.unwrap();
|
|
1015
|
+
assert!(!module.is_null(), "sys module should be non-null");
|
|
1016
|
+
|
|
1017
|
+
// Wrap in RubyxObject to verify the full pipeline
|
|
1018
|
+
let wrapper = RubyxObject::new(module, api);
|
|
1019
|
+
assert!(wrapper.is_some(), "Should wrap imported module");
|
|
1020
|
+
let wrapper = wrapper.unwrap();
|
|
1021
|
+
assert_eq!(wrapper.as_ptr(), module);
|
|
1022
|
+
|
|
1023
|
+
drop(wrapper);
|
|
1024
|
+
api.decref(module);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#[test]
|
|
1028
|
+
#[serial]
|
|
1029
|
+
fn test_import_nonexistent_raises() {
|
|
1030
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1031
|
+
return;
|
|
1032
|
+
};
|
|
1033
|
+
let api = guard.api();
|
|
1034
|
+
|
|
1035
|
+
// Importing a non-existent module should fail
|
|
1036
|
+
let result = api.import_module("nonexistent_module_xyz_12345");
|
|
1037
|
+
assert!(result.is_err(), "Importing nonexistent module should fail");
|
|
1038
|
+
|
|
1039
|
+
// Python should have set an error (ModuleNotFoundError / ImportError)
|
|
1040
|
+
// import_module returns Err on null, but the error state may or may not
|
|
1041
|
+
// still be set depending on implementation. Clear to be safe.
|
|
1042
|
+
if api.has_error() {
|
|
1043
|
+
let exc = crate::python_api::PythonApi::extract_exception(api);
|
|
1044
|
+
assert!(exc.is_some(), "Should have a Python exception");
|
|
1045
|
+
if let Some(crate::exception::PythonException::Exception { kind, .. }) = &exc {
|
|
1046
|
+
// Python 3.6+ raises ModuleNotFoundError (subclass of ImportError)
|
|
1047
|
+
assert!(
|
|
1048
|
+
kind == "ModuleNotFoundError" || kind == "ImportError",
|
|
1049
|
+
"Expected ModuleNotFoundError or ImportError, got: {}",
|
|
1050
|
+
kind
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
#[test]
|
|
1057
|
+
#[serial]
|
|
1058
|
+
fn test_import_json_module_and_use() {
|
|
1059
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1060
|
+
return;
|
|
1061
|
+
};
|
|
1062
|
+
let api = guard.api();
|
|
1063
|
+
|
|
1064
|
+
// json is a pure-Python module — always safe to import via libloading
|
|
1065
|
+
let module = api.import_module("json").expect("json should import");
|
|
1066
|
+
assert!(!module.is_null());
|
|
1067
|
+
|
|
1068
|
+
// Wrap in RubyxObject like rubyx_import does
|
|
1069
|
+
let wrapper = RubyxObject::new(module, api).expect("Should wrap json module");
|
|
1070
|
+
assert!(!wrapper.as_ptr().is_null());
|
|
1071
|
+
|
|
1072
|
+
drop(wrapper);
|
|
1073
|
+
api.decref(module);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
#[test]
|
|
1077
|
+
#[serial]
|
|
1078
|
+
fn test_import_os_module() {
|
|
1079
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1080
|
+
return;
|
|
1081
|
+
};
|
|
1082
|
+
let api = guard.api();
|
|
1083
|
+
|
|
1084
|
+
let module = api.import_module("os").expect("os should import");
|
|
1085
|
+
assert!(!module.is_null());
|
|
1086
|
+
|
|
1087
|
+
let wrapper = RubyxObject::new(module, api).expect("Should wrap os module");
|
|
1088
|
+
drop(wrapper);
|
|
1089
|
+
api.decref(module);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
#[test]
|
|
1093
|
+
#[serial]
|
|
1094
|
+
fn test_import_same_module_twice() {
|
|
1095
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1096
|
+
return;
|
|
1097
|
+
};
|
|
1098
|
+
let api = guard.api();
|
|
1099
|
+
|
|
1100
|
+
let module1 = api
|
|
1101
|
+
.import_module("sys")
|
|
1102
|
+
.expect("First sys import should work");
|
|
1103
|
+
let module2 = api
|
|
1104
|
+
.import_module("sys")
|
|
1105
|
+
.expect("Second sys import should work");
|
|
1106
|
+
|
|
1107
|
+
// Python caches modules — both should be the same object
|
|
1108
|
+
assert_eq!(
|
|
1109
|
+
module1, module2,
|
|
1110
|
+
"Importing the same module twice should return the same object"
|
|
1111
|
+
);
|
|
1112
|
+
|
|
1113
|
+
api.decref(module1);
|
|
1114
|
+
api.decref(module2);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// ========== Eval tests ==========
|
|
1118
|
+
//
|
|
1119
|
+
// Same constraint as import: rubyx_eval() returns magnus::Value and uses
|
|
1120
|
+
// crate::api(), so we test the underlying operations directly.
|
|
1121
|
+
// We replicate what eval.rs does: make_globals → run_string → wrap result.
|
|
1122
|
+
|
|
1123
|
+
/// Py_eval_input = 258 (for expressions)
|
|
1124
|
+
const EVAL_INPUT: i64 = 258;
|
|
1125
|
+
/// Py_file_input = 257 (for statements)
|
|
1126
|
+
const FILE_INPUT: i64 = 257;
|
|
1127
|
+
|
|
1128
|
+
/// Helper: create globals dict with __builtins__ (mirrors eval::make_globals)
|
|
1129
|
+
fn test_make_globals(api: &crate::python_api::PythonApi) -> *mut crate::python_ffi::PyObject {
|
|
1130
|
+
let globals = api.dict_new();
|
|
1131
|
+
let builtins_key = api.string_from_str("__builtins__");
|
|
1132
|
+
let builtins = api
|
|
1133
|
+
.import_module("builtins")
|
|
1134
|
+
.expect("builtins should exist");
|
|
1135
|
+
api.dict_set_item(globals, builtins_key, builtins);
|
|
1136
|
+
api.decref(builtins_key);
|
|
1137
|
+
api.decref(builtins);
|
|
1138
|
+
globals
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
#[test]
|
|
1142
|
+
#[serial]
|
|
1143
|
+
fn test_eval_expression() {
|
|
1144
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1145
|
+
return;
|
|
1146
|
+
};
|
|
1147
|
+
let api = guard.api();
|
|
1148
|
+
let globals = test_make_globals(api);
|
|
1149
|
+
|
|
1150
|
+
// Evaluate "1 + 2" as expression
|
|
1151
|
+
let result = api.run_string("1 + 2", EVAL_INPUT, globals, globals);
|
|
1152
|
+
assert!(result.is_ok(), "run_string should succeed");
|
|
1153
|
+
let py_obj = result.unwrap();
|
|
1154
|
+
assert!(!py_obj.is_null(), "Expression result should be non-null");
|
|
1155
|
+
assert_eq!(api.long_to_i64(py_obj), 3, "1 + 2 should equal 3");
|
|
1156
|
+
|
|
1157
|
+
// Wrap in RubyxObject like rubyx_eval does
|
|
1158
|
+
let wrapper = RubyxObject::new(py_obj, api).expect("Should wrap eval result");
|
|
1159
|
+
assert_eq!(api.long_to_i64(wrapper.as_ptr()), 3);
|
|
1160
|
+
|
|
1161
|
+
drop(wrapper);
|
|
1162
|
+
api.decref(py_obj);
|
|
1163
|
+
api.decref(globals);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#[test]
|
|
1167
|
+
#[serial]
|
|
1168
|
+
fn test_eval_string_expression() {
|
|
1169
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1170
|
+
return;
|
|
1171
|
+
};
|
|
1172
|
+
let api = guard.api();
|
|
1173
|
+
let globals = test_make_globals(api);
|
|
1174
|
+
|
|
1175
|
+
let result = api.run_string("'hello' + ' ' + 'world'", EVAL_INPUT, globals, globals);
|
|
1176
|
+
let py_obj = result.unwrap();
|
|
1177
|
+
assert!(!py_obj.is_null());
|
|
1178
|
+
assert_eq!(
|
|
1179
|
+
api.string_to_string(py_obj),
|
|
1180
|
+
Some("hello world".to_string())
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
let wrapper = RubyxObject::new(py_obj, api).expect("Should wrap string result");
|
|
1184
|
+
assert_eq!(
|
|
1185
|
+
api.string_to_string(wrapper.as_ptr()),
|
|
1186
|
+
Some("hello world".to_string())
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
drop(wrapper);
|
|
1190
|
+
api.decref(py_obj);
|
|
1191
|
+
api.decref(globals);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
#[test]
|
|
1195
|
+
#[serial]
|
|
1196
|
+
fn test_eval_with_syntax_error() {
|
|
1197
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1198
|
+
return;
|
|
1199
|
+
};
|
|
1200
|
+
let api = guard.api();
|
|
1201
|
+
let globals = test_make_globals(api);
|
|
1202
|
+
|
|
1203
|
+
// "def" alone is invalid as an expression (Py_eval_input)
|
|
1204
|
+
let result = api.run_string("def", EVAL_INPUT, globals, globals);
|
|
1205
|
+
let py_obj = result.unwrap();
|
|
1206
|
+
assert!(py_obj.is_null(), "Invalid expression should return null");
|
|
1207
|
+
assert!(api.has_error(), "Python error should be set");
|
|
1208
|
+
|
|
1209
|
+
// Verify it's a SyntaxError
|
|
1210
|
+
let exc = crate::python_api::PythonApi::extract_exception(api);
|
|
1211
|
+
assert!(exc.is_some(), "Should have extracted an exception");
|
|
1212
|
+
assert!(
|
|
1213
|
+
matches!(
|
|
1214
|
+
exc,
|
|
1215
|
+
Some(crate::exception::PythonException::SyntaxError { .. })
|
|
1216
|
+
),
|
|
1217
|
+
"Expected SyntaxError, got: {:?}",
|
|
1218
|
+
exc
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
api.decref(globals);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
#[test]
|
|
1225
|
+
#[serial]
|
|
1226
|
+
fn test_eval_syntax_error_then_retry_as_statement() {
|
|
1227
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1228
|
+
return;
|
|
1229
|
+
};
|
|
1230
|
+
let api = guard.api();
|
|
1231
|
+
let globals = test_make_globals(api);
|
|
1232
|
+
|
|
1233
|
+
// "x = 42" is invalid as expression but valid as statement
|
|
1234
|
+
let result = api.run_string("x = 42", EVAL_INPUT, globals, globals);
|
|
1235
|
+
let py_obj = result.unwrap();
|
|
1236
|
+
assert!(
|
|
1237
|
+
py_obj.is_null(),
|
|
1238
|
+
"Assignment should fail as expression (eval_input)"
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
// Check it's a syntax error
|
|
1242
|
+
let is_syntax = api.has_error() && {
|
|
1243
|
+
let exc = crate::python_api::PythonApi::extract_exception(api);
|
|
1244
|
+
matches!(
|
|
1245
|
+
exc,
|
|
1246
|
+
Some(crate::exception::PythonException::SyntaxError { .. })
|
|
1247
|
+
)
|
|
1248
|
+
};
|
|
1249
|
+
assert!(
|
|
1250
|
+
is_syntax,
|
|
1251
|
+
"Assignment should produce SyntaxError as expression"
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
// Retry as statement (Py_file_input) — this is what rubyx_eval does
|
|
1255
|
+
let result = api.run_string("x = 42", FILE_INPUT, globals, globals);
|
|
1256
|
+
let stmt_obj = result.unwrap();
|
|
1257
|
+
assert!(
|
|
1258
|
+
!stmt_obj.is_null(),
|
|
1259
|
+
"Assignment should succeed as statement"
|
|
1260
|
+
);
|
|
1261
|
+
assert!(api.is_none(stmt_obj), "Statement should return Py_None");
|
|
1262
|
+
|
|
1263
|
+
// Verify the variable was set
|
|
1264
|
+
let key = api.string_from_str("x");
|
|
1265
|
+
let val = api.dict_get_item(globals, key);
|
|
1266
|
+
assert!(!val.is_null(), "x should exist in globals");
|
|
1267
|
+
assert_eq!(api.long_to_i64(val), 42, "x should be 42");
|
|
1268
|
+
|
|
1269
|
+
api.decref(key);
|
|
1270
|
+
api.decref(stmt_obj);
|
|
1271
|
+
api.decref(globals);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
#[test]
|
|
1275
|
+
#[serial]
|
|
1276
|
+
fn test_eval_name_error() {
|
|
1277
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1278
|
+
return;
|
|
1279
|
+
};
|
|
1280
|
+
let api = guard.api();
|
|
1281
|
+
let globals = test_make_globals(api);
|
|
1282
|
+
|
|
1283
|
+
let result = api.run_string("undefined_variable_xyz", EVAL_INPUT, globals, globals);
|
|
1284
|
+
let py_obj = result.unwrap();
|
|
1285
|
+
assert!(py_obj.is_null(), "Undefined variable should return null");
|
|
1286
|
+
assert!(api.has_error(), "NameError should be set");
|
|
1287
|
+
|
|
1288
|
+
let exc = crate::python_api::PythonApi::extract_exception(api);
|
|
1289
|
+
assert!(exc.is_some());
|
|
1290
|
+
if let Some(crate::exception::PythonException::Exception { kind, .. }) = &exc {
|
|
1291
|
+
assert_eq!(kind, "NameError", "Should be a NameError");
|
|
1292
|
+
} else {
|
|
1293
|
+
panic!("Expected Exception variant with NameError, got: {:?}", exc);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
api.decref(globals);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
#[test]
|
|
1300
|
+
#[serial]
|
|
1301
|
+
fn test_eval_division_by_zero() {
|
|
1302
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1303
|
+
return;
|
|
1304
|
+
};
|
|
1305
|
+
let api = guard.api();
|
|
1306
|
+
let globals = test_make_globals(api);
|
|
1307
|
+
|
|
1308
|
+
let result = api.run_string("1 / 0", EVAL_INPUT, globals, globals);
|
|
1309
|
+
let py_obj = result.unwrap();
|
|
1310
|
+
assert!(py_obj.is_null(), "Division by zero should return null");
|
|
1311
|
+
assert!(api.has_error());
|
|
1312
|
+
|
|
1313
|
+
let exc = crate::python_api::PythonApi::extract_exception(api);
|
|
1314
|
+
if let Some(crate::exception::PythonException::Exception { kind, .. }) = &exc {
|
|
1315
|
+
assert_eq!(kind, "ZeroDivisionError");
|
|
1316
|
+
} else {
|
|
1317
|
+
panic!("Expected ZeroDivisionError, got: {:?}", exc);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
api.decref(globals);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
#[test]
|
|
1324
|
+
#[serial]
|
|
1325
|
+
fn test_eval_builtin_function() {
|
|
1326
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1327
|
+
return;
|
|
1328
|
+
};
|
|
1329
|
+
let api = guard.api();
|
|
1330
|
+
let globals = test_make_globals(api);
|
|
1331
|
+
|
|
1332
|
+
let result = api.run_string("len([1, 2, 3, 4, 5])", EVAL_INPUT, globals, globals);
|
|
1333
|
+
let py_obj = result.unwrap();
|
|
1334
|
+
assert!(!py_obj.is_null());
|
|
1335
|
+
assert_eq!(api.long_to_i64(py_obj), 5, "len([1,2,3,4,5]) should be 5");
|
|
1336
|
+
|
|
1337
|
+
api.decref(py_obj);
|
|
1338
|
+
api.decref(globals);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
#[test]
|
|
1342
|
+
#[serial]
|
|
1343
|
+
fn test_eval_list_expression() {
|
|
1344
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1345
|
+
return;
|
|
1346
|
+
};
|
|
1347
|
+
let api = guard.api();
|
|
1348
|
+
let globals = test_make_globals(api);
|
|
1349
|
+
|
|
1350
|
+
let result = api.run_string("[x * 2 for x in range(5)]", EVAL_INPUT, globals, globals);
|
|
1351
|
+
let py_obj = result.unwrap();
|
|
1352
|
+
assert!(!py_obj.is_null());
|
|
1353
|
+
assert!(api.list_check(py_obj), "Should return a list");
|
|
1354
|
+
assert_eq!(api.list_size(py_obj), 5, "List should have 5 elements");
|
|
1355
|
+
|
|
1356
|
+
// Check values: [0, 2, 4, 6, 8]
|
|
1357
|
+
assert_eq!(api.long_to_i64(api.list_get_item(py_obj, 0)), 0);
|
|
1358
|
+
assert_eq!(api.long_to_i64(api.list_get_item(py_obj, 2)), 4);
|
|
1359
|
+
assert_eq!(api.long_to_i64(api.list_get_item(py_obj, 4)), 8);
|
|
1360
|
+
|
|
1361
|
+
api.decref(py_obj);
|
|
1362
|
+
api.decref(globals);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
#[test]
|
|
1366
|
+
#[serial]
|
|
1367
|
+
fn test_eval_dict_expression() {
|
|
1368
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1369
|
+
return;
|
|
1370
|
+
};
|
|
1371
|
+
let api = guard.api();
|
|
1372
|
+
let globals = test_make_globals(api);
|
|
1373
|
+
|
|
1374
|
+
let result = api.run_string("{'a': 1, 'b': 2}", EVAL_INPUT, globals, globals);
|
|
1375
|
+
let py_obj = result.unwrap();
|
|
1376
|
+
assert!(!py_obj.is_null());
|
|
1377
|
+
assert!(api.dict_check(py_obj), "Should return a dict");
|
|
1378
|
+
assert_eq!(api.dict_size(py_obj), 2);
|
|
1379
|
+
|
|
1380
|
+
let key_a = api.string_from_str("a");
|
|
1381
|
+
let val_a = api.dict_get_item(py_obj, key_a);
|
|
1382
|
+
assert_eq!(api.long_to_i64(val_a), 1);
|
|
1383
|
+
api.decref(key_a);
|
|
1384
|
+
|
|
1385
|
+
api.decref(py_obj);
|
|
1386
|
+
api.decref(globals);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
#[test]
|
|
1390
|
+
#[serial]
|
|
1391
|
+
fn test_eval_none_result() {
|
|
1392
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1393
|
+
return;
|
|
1394
|
+
};
|
|
1395
|
+
let api = guard.api();
|
|
1396
|
+
let globals = test_make_globals(api);
|
|
1397
|
+
|
|
1398
|
+
// print() returns None
|
|
1399
|
+
let result = api.run_string("print('hello from test')", FILE_INPUT, globals, globals);
|
|
1400
|
+
let py_obj = result.unwrap();
|
|
1401
|
+
assert!(!py_obj.is_null());
|
|
1402
|
+
assert!(api.is_none(py_obj), "Statement result should be Py_None");
|
|
1403
|
+
|
|
1404
|
+
api.decref(py_obj);
|
|
1405
|
+
api.decref(globals);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
#[test]
|
|
1409
|
+
#[serial]
|
|
1410
|
+
fn test_eval_bool_expression() {
|
|
1411
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1412
|
+
return;
|
|
1413
|
+
};
|
|
1414
|
+
let api = guard.api();
|
|
1415
|
+
let globals = test_make_globals(api);
|
|
1416
|
+
|
|
1417
|
+
let result = api.run_string("10 > 5", EVAL_INPUT, globals, globals);
|
|
1418
|
+
let py_obj = result.unwrap();
|
|
1419
|
+
assert!(!py_obj.is_null());
|
|
1420
|
+
assert!(api.is_true(py_obj), "10 > 5 should be True");
|
|
1421
|
+
|
|
1422
|
+
let result2 = api.run_string("10 < 5", EVAL_INPUT, globals, globals);
|
|
1423
|
+
let py_obj2 = result2.unwrap();
|
|
1424
|
+
assert!(!py_obj2.is_null());
|
|
1425
|
+
assert!(api.is_false(py_obj2), "10 < 5 should be False");
|
|
1426
|
+
|
|
1427
|
+
api.decref(py_obj);
|
|
1428
|
+
api.decref(py_obj2);
|
|
1429
|
+
api.decref(globals);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
#[test]
|
|
1433
|
+
#[serial]
|
|
1434
|
+
fn test_eval_float_expression() {
|
|
1435
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1436
|
+
return;
|
|
1437
|
+
};
|
|
1438
|
+
let api = guard.api();
|
|
1439
|
+
let globals = test_make_globals(api);
|
|
1440
|
+
|
|
1441
|
+
let result = api.run_string("3.14 * 2", EVAL_INPUT, globals, globals);
|
|
1442
|
+
let py_obj = result.unwrap();
|
|
1443
|
+
assert!(!py_obj.is_null());
|
|
1444
|
+
assert!(api.is_float(py_obj), "Should return a float");
|
|
1445
|
+
let value = api.float_to_f64(py_obj);
|
|
1446
|
+
assert!(
|
|
1447
|
+
(value - 6.28).abs() < 0.001,
|
|
1448
|
+
"3.14 * 2 should be ~6.28, got {}",
|
|
1449
|
+
value
|
|
1450
|
+
);
|
|
1451
|
+
|
|
1452
|
+
api.decref(py_obj);
|
|
1453
|
+
api.decref(globals);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
#[test]
|
|
1457
|
+
#[serial]
|
|
1458
|
+
fn test_eval_multiline_statement() {
|
|
1459
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1460
|
+
return;
|
|
1461
|
+
};
|
|
1462
|
+
let api = guard.api();
|
|
1463
|
+
let globals = test_make_globals(api);
|
|
1464
|
+
|
|
1465
|
+
let code = "def factorial(n):\n return 1 if n <= 1 else n * factorial(n - 1)\nresult = factorial(5)";
|
|
1466
|
+
let result = api.run_string(code, FILE_INPUT, globals, globals);
|
|
1467
|
+
let py_obj = result.unwrap();
|
|
1468
|
+
assert!(!py_obj.is_null(), "Multiline statement should succeed");
|
|
1469
|
+
|
|
1470
|
+
let key = api.string_from_str("result");
|
|
1471
|
+
let val = api.dict_get_item(globals, key);
|
|
1472
|
+
assert!(!val.is_null(), "result should exist in globals");
|
|
1473
|
+
assert_eq!(api.long_to_i64(val), 120, "factorial(5) should be 120");
|
|
1474
|
+
|
|
1475
|
+
api.decref(key);
|
|
1476
|
+
api.decref(py_obj);
|
|
1477
|
+
api.decref(globals);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
#[test]
|
|
1481
|
+
#[serial]
|
|
1482
|
+
fn test_eval_with_import_in_expression() {
|
|
1483
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1484
|
+
return;
|
|
1485
|
+
};
|
|
1486
|
+
let api = guard.api();
|
|
1487
|
+
let globals = test_make_globals(api);
|
|
1488
|
+
|
|
1489
|
+
// First set up the import as a statement
|
|
1490
|
+
let setup = api.run_string("import json", FILE_INPUT, globals, globals);
|
|
1491
|
+
assert!(!setup.unwrap().is_null());
|
|
1492
|
+
|
|
1493
|
+
// Then evaluate using the imported module
|
|
1494
|
+
let result = api.run_string(
|
|
1495
|
+
"json.loads('{\"key\": 42}')['key']",
|
|
1496
|
+
EVAL_INPUT,
|
|
1497
|
+
globals,
|
|
1498
|
+
globals,
|
|
1499
|
+
);
|
|
1500
|
+
let py_obj = result.unwrap();
|
|
1501
|
+
assert!(!py_obj.is_null());
|
|
1502
|
+
assert_eq!(api.long_to_i64(py_obj), 42);
|
|
1503
|
+
|
|
1504
|
+
api.decref(py_obj);
|
|
1505
|
+
api.decref(globals);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
#[test]
|
|
1509
|
+
#[serial]
|
|
1510
|
+
fn test_eval_result_wraps_in_rubyx_object() {
|
|
1511
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1512
|
+
return;
|
|
1513
|
+
};
|
|
1514
|
+
let api = guard.api();
|
|
1515
|
+
let globals = test_make_globals(api);
|
|
1516
|
+
|
|
1517
|
+
let result = api.run_string("42 * 10", EVAL_INPUT, globals, globals);
|
|
1518
|
+
let py_obj = result.unwrap();
|
|
1519
|
+
assert!(!py_obj.is_null());
|
|
1520
|
+
|
|
1521
|
+
// This is what rubyx_eval does: wrap in RubyxObject
|
|
1522
|
+
let wrapper = RubyxObject::new(py_obj, api).expect("Should wrap result");
|
|
1523
|
+
assert_eq!(api.long_to_i64(wrapper.as_ptr()), 420);
|
|
1524
|
+
|
|
1525
|
+
// After wrapping, the original ref can be decref'd safely
|
|
1526
|
+
api.decref(py_obj);
|
|
1527
|
+
|
|
1528
|
+
// Wrapper still holds a valid reference
|
|
1529
|
+
assert_eq!(api.long_to_i64(wrapper.as_ptr()), 420);
|
|
1530
|
+
|
|
1531
|
+
drop(wrapper);
|
|
1532
|
+
api.decref(globals);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
#[test]
|
|
1536
|
+
#[serial]
|
|
1537
|
+
fn test_eval_error_does_not_leak_globals() {
|
|
1538
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1539
|
+
return;
|
|
1540
|
+
};
|
|
1541
|
+
let api = guard.api();
|
|
1542
|
+
|
|
1543
|
+
// Run multiple eval-like cycles with errors — should not leak
|
|
1544
|
+
for _ in 0..100 {
|
|
1545
|
+
let globals = test_make_globals(api);
|
|
1546
|
+
let result = api.run_string("undefined_var", EVAL_INPUT, globals, globals);
|
|
1547
|
+
let py_obj = result.unwrap();
|
|
1548
|
+
assert!(py_obj.is_null());
|
|
1549
|
+
api.clear_error();
|
|
1550
|
+
api.decref(globals);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// If we get here without crash, no memory corruption from repeated cycles
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
#[test]
|
|
1557
|
+
#[serial]
|
|
1558
|
+
fn test_eval_sequential_expressions_share_globals() {
|
|
1559
|
+
let Some(guard) = skip_if_no_python() else {
|
|
1560
|
+
return;
|
|
1561
|
+
};
|
|
1562
|
+
let api = guard.api();
|
|
1563
|
+
let globals = test_make_globals(api);
|
|
1564
|
+
|
|
1565
|
+
// Set a variable via statement
|
|
1566
|
+
let r1 = api.run_string("x = 10", FILE_INPUT, globals, globals);
|
|
1567
|
+
assert!(!r1.unwrap().is_null());
|
|
1568
|
+
|
|
1569
|
+
// Use it in a subsequent expression
|
|
1570
|
+
let r2 = api.run_string("x * 5", EVAL_INPUT, globals, globals);
|
|
1571
|
+
let py_obj = r2.unwrap();
|
|
1572
|
+
assert!(!py_obj.is_null());
|
|
1573
|
+
assert_eq!(api.long_to_i64(py_obj), 50, "x * 5 should be 50");
|
|
1574
|
+
|
|
1575
|
+
api.decref(py_obj);
|
|
1576
|
+
api.decref(globals);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
#[test]
|
|
1580
|
+
#[serial]
|
|
1581
|
+
fn test_method_call() {
|
|
1582
|
+
use magnus::typed_data::Obj;
|
|
1583
|
+
use magnus::{IntoValue, TryConvert};
|
|
1584
|
+
|
|
1585
|
+
with_ruby_python(|ruby, api| {
|
|
1586
|
+
let json = api.import_module("json").expect("json should import");
|
|
1587
|
+
let wrapper = RubyxObject::new(json, api).expect("wrapper should be created");
|
|
1588
|
+
|
|
1589
|
+
let args = vec![
|
|
1590
|
+
"loads".into_value_with(ruby),
|
|
1591
|
+
r#"{"x": 42}"#.into_value_with(ruby),
|
|
1592
|
+
];
|
|
1593
|
+
let result = wrapper
|
|
1594
|
+
.method_missing(&args)
|
|
1595
|
+
.expect("loads call should succeed");
|
|
1596
|
+
let py_result = Obj::<RubyxObject>::try_convert(result)
|
|
1597
|
+
.expect("result should be wrapped Python object");
|
|
1598
|
+
assert!(
|
|
1599
|
+
api.dict_check(py_result.as_ptr()),
|
|
1600
|
+
"json.loads should return a dict"
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
let key = api.string_from_str("x");
|
|
1604
|
+
let val = api.dict_get_item(py_result.as_ptr(), key);
|
|
1605
|
+
assert_eq!(api.long_to_i64(val), 42);
|
|
1606
|
+
api.decref(key);
|
|
1607
|
+
|
|
1608
|
+
drop(wrapper);
|
|
1609
|
+
api.decref(json);
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
#[test]
|
|
1614
|
+
#[serial]
|
|
1615
|
+
fn test_attribute_get() {
|
|
1616
|
+
use magnus::typed_data::Obj;
|
|
1617
|
+
use magnus::{IntoValue, TryConvert};
|
|
1618
|
+
|
|
1619
|
+
with_ruby_python(|ruby, api| {
|
|
1620
|
+
let sys = api.import_module("sys").expect("sys should import");
|
|
1621
|
+
let wrapper = RubyxObject::new(sys, api).expect("wrapper should be created");
|
|
1622
|
+
|
|
1623
|
+
let args = vec!["version".into_value_with(ruby)];
|
|
1624
|
+
let result = wrapper
|
|
1625
|
+
.method_missing(&args)
|
|
1626
|
+
.expect("attribute read should succeed");
|
|
1627
|
+
let py_result = Obj::<RubyxObject>::try_convert(result)
|
|
1628
|
+
.expect("result should be wrapped Python object");
|
|
1629
|
+
assert!(
|
|
1630
|
+
api.is_string(py_result.as_ptr()),
|
|
1631
|
+
"sys.version should be a string"
|
|
1632
|
+
);
|
|
1633
|
+
let version = api
|
|
1634
|
+
.string_to_string(py_result.as_ptr())
|
|
1635
|
+
.expect("version should decode as string");
|
|
1636
|
+
assert!(!version.is_empty(), "version string should not be empty");
|
|
1637
|
+
|
|
1638
|
+
drop(wrapper);
|
|
1639
|
+
api.decref(sys);
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
#[test]
|
|
1644
|
+
#[serial]
|
|
1645
|
+
fn test_attribute_set() {
|
|
1646
|
+
use magnus::typed_data::Obj;
|
|
1647
|
+
use magnus::{IntoValue, TryConvert};
|
|
1648
|
+
|
|
1649
|
+
with_ruby_python(|ruby, api| {
|
|
1650
|
+
let types = api.import_module("types").expect("types should import");
|
|
1651
|
+
let wrapper = RubyxObject::new(types, api).expect("wrapper should be created");
|
|
1652
|
+
|
|
1653
|
+
// ns = types.SimpleNamespace()
|
|
1654
|
+
let args = vec!["SimpleNamespace".into_value_with(ruby)];
|
|
1655
|
+
let ns_result = wrapper
|
|
1656
|
+
.method_missing(&args)
|
|
1657
|
+
.expect("SimpleNamespace() should succeed");
|
|
1658
|
+
let ns = Obj::<RubyxObject>::try_convert(ns_result)
|
|
1659
|
+
.expect("result should be wrapped Python object");
|
|
1660
|
+
|
|
1661
|
+
// ns.foo = 99
|
|
1662
|
+
let ns_obj = RubyxObject::new(ns.as_ptr(), api).expect("rewrap should succeed");
|
|
1663
|
+
let set_args = vec!["foo=".into_value_with(ruby), 99_i64.into_value_with(ruby)];
|
|
1664
|
+
ns_obj
|
|
1665
|
+
.method_missing(&set_args)
|
|
1666
|
+
.expect("setter should succeed");
|
|
1667
|
+
|
|
1668
|
+
// ns.foo → 99
|
|
1669
|
+
let get_args = vec!["foo".into_value_with(ruby)];
|
|
1670
|
+
let get_result = ns_obj
|
|
1671
|
+
.method_missing(&get_args)
|
|
1672
|
+
.expect("getter should succeed");
|
|
1673
|
+
let py_val = Obj::<RubyxObject>::try_convert(get_result)
|
|
1674
|
+
.expect("result should be wrapped Python object");
|
|
1675
|
+
assert_eq!(api.long_to_i64(py_val.as_ptr()), 99);
|
|
1676
|
+
|
|
1677
|
+
drop(ns_obj);
|
|
1678
|
+
drop(wrapper);
|
|
1679
|
+
api.decref(types);
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
#[test]
|
|
1684
|
+
#[serial]
|
|
1685
|
+
fn test_keyword_arguments() {
|
|
1686
|
+
use magnus::typed_data::Obj;
|
|
1687
|
+
use magnus::{IntoValue, TryConvert};
|
|
1688
|
+
|
|
1689
|
+
with_ruby_python(|ruby, api| {
|
|
1690
|
+
let json = api.import_module("json").expect("json should import");
|
|
1691
|
+
let wrapper = RubyxObject::new(json, api).expect("wrapper should be created");
|
|
1692
|
+
|
|
1693
|
+
// Build a Python dict to serialize: {"a": 1}
|
|
1694
|
+
let py_dict = api.dict_new();
|
|
1695
|
+
let py_key = api.string_from_str("a");
|
|
1696
|
+
let py_val = api.long_from_i64(1);
|
|
1697
|
+
api.dict_set_item(py_dict, py_key, py_val);
|
|
1698
|
+
api.decref(py_key);
|
|
1699
|
+
api.decref(py_val);
|
|
1700
|
+
let dict_wrapper =
|
|
1701
|
+
RubyxObject::new(py_dict, api).expect("dict wrapper should be created");
|
|
1702
|
+
let dict_value = magnus::IntoValue::into_value_with(dict_wrapper, ruby);
|
|
1703
|
+
|
|
1704
|
+
// Build kwargs hash: { sort_keys: true }
|
|
1705
|
+
let kwargs = ruby.hash_new();
|
|
1706
|
+
let _ = kwargs.aset(ruby.to_symbol("sort_keys"), true.into_value_with(ruby));
|
|
1707
|
+
|
|
1708
|
+
let args = vec![
|
|
1709
|
+
"dumps".into_value_with(ruby),
|
|
1710
|
+
dict_value,
|
|
1711
|
+
kwargs.into_value_with(ruby),
|
|
1712
|
+
];
|
|
1713
|
+
let result = wrapper
|
|
1714
|
+
.method_missing(&args)
|
|
1715
|
+
.expect("dumps with kwargs should succeed");
|
|
1716
|
+
let py_result = Obj::<RubyxObject>::try_convert(result)
|
|
1717
|
+
.expect("result should be wrapped Python object");
|
|
1718
|
+
assert!(
|
|
1719
|
+
api.is_string(py_result.as_ptr()),
|
|
1720
|
+
"json.dumps should return a string"
|
|
1721
|
+
);
|
|
1722
|
+
let json_str = api
|
|
1723
|
+
.string_to_string(py_result.as_ptr())
|
|
1724
|
+
.expect("result should decode as string");
|
|
1725
|
+
assert!(
|
|
1726
|
+
json_str.contains("\"a\""),
|
|
1727
|
+
"JSON should contain key 'a': {}",
|
|
1728
|
+
json_str
|
|
1729
|
+
);
|
|
1730
|
+
|
|
1731
|
+
drop(wrapper);
|
|
1732
|
+
api.decref(py_dict);
|
|
1733
|
+
api.decref(json);
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
// ========== Async Streaming Integration Tests ==========
|
|
1737
|
+
//
|
|
1738
|
+
// GIL choreography: with_ruby_python holds both Ruby GVL + Python GIL.
|
|
1739
|
+
// AsyncStream::from_python_iterator spawns a worker that needs the GIL.
|
|
1740
|
+
// To avoid deadlock: save_thread() releases the GIL before consuming the
|
|
1741
|
+
// stream, then restore_thread() re-acquires it for cleanup.
|
|
1742
|
+
|
|
1743
|
+
const PY_EVAL_INPUT: i64 = 258;
|
|
1744
|
+
const PY_FILE_INPUT: i64 = 257;
|
|
1745
|
+
|
|
1746
|
+
use crate::python_api::PythonApi;
|
|
1747
|
+
use crate::python_ffi::PyObject;
|
|
1748
|
+
|
|
1749
|
+
fn make_globals(api: &PythonApi) -> *mut PyObject {
|
|
1750
|
+
let globals = api.dict_new();
|
|
1751
|
+
let builtins_key = api.string_from_str("__builtins__");
|
|
1752
|
+
let builtins = api
|
|
1753
|
+
.import_module("builtins")
|
|
1754
|
+
.expect("builtins should exist");
|
|
1755
|
+
api.dict_set_item(globals, builtins_key, builtins);
|
|
1756
|
+
api.decref(builtins_key);
|
|
1757
|
+
api.decref(builtins);
|
|
1758
|
+
globals
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
#[test]
|
|
1762
|
+
#[serial]
|
|
1763
|
+
fn test_stream_python_generator() {
|
|
1764
|
+
with_ruby_python(|_ruby, api| {
|
|
1765
|
+
let globals = make_globals(api);
|
|
1766
|
+
|
|
1767
|
+
// Define a Python generator
|
|
1768
|
+
api.run_string(
|
|
1769
|
+
"def gen():\n yield 10\n yield 20\n yield 30\n",
|
|
1770
|
+
PY_FILE_INPUT,
|
|
1771
|
+
globals,
|
|
1772
|
+
globals,
|
|
1773
|
+
)
|
|
1774
|
+
.expect("should define generator");
|
|
1775
|
+
|
|
1776
|
+
let py_iter = api
|
|
1777
|
+
.run_string("gen()", PY_EVAL_INPUT, globals, globals)
|
|
1778
|
+
.expect("should create generator iterator");
|
|
1779
|
+
assert!(!py_iter.is_null());
|
|
1780
|
+
|
|
1781
|
+
// Release GIL so worker thread can acquire it
|
|
1782
|
+
let tstate = api.save_thread();
|
|
1783
|
+
|
|
1784
|
+
let mut stream = crate::stream::AsyncStream::from_python_iterator(py_iter);
|
|
1785
|
+
|
|
1786
|
+
let v1 = stream.next().unwrap().unwrap();
|
|
1787
|
+
assert_eq!(i64::try_convert(v1).unwrap(), 10_i64);
|
|
1788
|
+
|
|
1789
|
+
let v2 = stream.next().unwrap().unwrap();
|
|
1790
|
+
assert_eq!(i64::try_convert(v2).unwrap(), 20_i64);
|
|
1791
|
+
|
|
1792
|
+
let v3 = stream.next().unwrap().unwrap();
|
|
1793
|
+
assert_eq!(i64::try_convert(v3).unwrap(), 30_i64);
|
|
1794
|
+
|
|
1795
|
+
assert!(stream.next().is_none());
|
|
1796
|
+
|
|
1797
|
+
// Drop stream before re-acquiring GIL (join waits for worker to release GIL)
|
|
1798
|
+
drop(stream);
|
|
1799
|
+
api.restore_thread(tstate);
|
|
1800
|
+
api.decref(globals);
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
#[test]
|
|
1805
|
+
#[serial]
|
|
1806
|
+
fn test_stream_is_lazy() {
|
|
1807
|
+
with_ruby_python(|_ruby, api| {
|
|
1808
|
+
let globals = make_globals(api);
|
|
1809
|
+
|
|
1810
|
+
// Create a large range iterator — if not lazy, would try to materialize all items
|
|
1811
|
+
let py_iter = api
|
|
1812
|
+
.run_string("iter(range(1000000))", PY_EVAL_INPUT, globals, globals)
|
|
1813
|
+
.expect("should create range iterator");
|
|
1814
|
+
|
|
1815
|
+
let tstate = api.save_thread();
|
|
1816
|
+
|
|
1817
|
+
let mut stream = crate::stream::AsyncStream::from_python_iterator(py_iter);
|
|
1818
|
+
|
|
1819
|
+
// Only consume 5 items from a million-item stream
|
|
1820
|
+
for expected in 0..5_i64 {
|
|
1821
|
+
let val = stream.next().unwrap().unwrap();
|
|
1822
|
+
assert_eq!(i64::try_convert(val).unwrap(), expected);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Drop without consuming the rest — should not hang or OOM
|
|
1826
|
+
let start = std::time::Instant::now();
|
|
1827
|
+
drop(stream);
|
|
1828
|
+
let elapsed = start.elapsed();
|
|
1829
|
+
|
|
1830
|
+
api.restore_thread(tstate);
|
|
1831
|
+
api.decref(globals);
|
|
1832
|
+
|
|
1833
|
+
assert!(
|
|
1834
|
+
elapsed < std::time::Duration::from_secs(2),
|
|
1835
|
+
"dropping lazy stream should be fast, took {:?}",
|
|
1836
|
+
elapsed
|
|
1837
|
+
);
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
#[test]
|
|
1842
|
+
#[serial]
|
|
1843
|
+
fn test_stream_cancellation() {
|
|
1844
|
+
with_ruby_python(|_ruby, api| {
|
|
1845
|
+
let globals = make_globals(api);
|
|
1846
|
+
|
|
1847
|
+
// Infinite generator — only cancellation can stop it
|
|
1848
|
+
api.run_string(
|
|
1849
|
+
"def infinite():\n i = 0\n while True:\n yield i\n i += 1\n",
|
|
1850
|
+
PY_FILE_INPUT,
|
|
1851
|
+
globals,
|
|
1852
|
+
globals,
|
|
1853
|
+
)
|
|
1854
|
+
.expect("should define infinite generator");
|
|
1855
|
+
|
|
1856
|
+
let py_iter = api
|
|
1857
|
+
.run_string("infinite()", PY_EVAL_INPUT, globals, globals)
|
|
1858
|
+
.expect("should create infinite iterator");
|
|
1859
|
+
|
|
1860
|
+
let tstate = api.save_thread();
|
|
1861
|
+
|
|
1862
|
+
let mut stream = crate::stream::AsyncStream::from_python_iterator(py_iter);
|
|
1863
|
+
|
|
1864
|
+
// Read a few items
|
|
1865
|
+
for expected in 0..3_i64 {
|
|
1866
|
+
let val = stream.next().unwrap().unwrap();
|
|
1867
|
+
assert_eq!(i64::try_convert(val).unwrap(), expected);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// Drop triggers cancellation — must not hang on an infinite generator
|
|
1871
|
+
let start = std::time::Instant::now();
|
|
1872
|
+
drop(stream);
|
|
1873
|
+
let elapsed = start.elapsed();
|
|
1874
|
+
|
|
1875
|
+
api.restore_thread(tstate);
|
|
1876
|
+
api.decref(globals);
|
|
1877
|
+
|
|
1878
|
+
assert!(
|
|
1879
|
+
elapsed < std::time::Duration::from_secs(2),
|
|
1880
|
+
"cancelling infinite stream should be fast, took {:?}",
|
|
1881
|
+
elapsed
|
|
1882
|
+
);
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
#[test]
|
|
1887
|
+
#[serial]
|
|
1888
|
+
fn test_concurrent_streams() {
|
|
1889
|
+
with_ruby_python(|_ruby, api| {
|
|
1890
|
+
let globals = make_globals(api);
|
|
1891
|
+
|
|
1892
|
+
// Create two separate iterators
|
|
1893
|
+
let py_iter1 = api
|
|
1894
|
+
.run_string("iter([100, 200, 300])", PY_EVAL_INPUT, globals, globals)
|
|
1895
|
+
.expect("should create first iterator");
|
|
1896
|
+
|
|
1897
|
+
let py_iter2 = api
|
|
1898
|
+
.run_string("iter([400, 500, 600])", PY_EVAL_INPUT, globals, globals)
|
|
1899
|
+
.expect("should create second iterator");
|
|
1900
|
+
|
|
1901
|
+
let tstate = api.save_thread();
|
|
1902
|
+
|
|
1903
|
+
let mut stream1 = crate::stream::AsyncStream::from_python_iterator(py_iter1);
|
|
1904
|
+
let mut stream2 = crate::stream::AsyncStream::from_python_iterator(py_iter2);
|
|
1905
|
+
|
|
1906
|
+
// Interleave consumption from both streams
|
|
1907
|
+
let v1a = stream1.next().unwrap().unwrap();
|
|
1908
|
+
assert_eq!(i64::try_convert(v1a).unwrap(), 100_i64);
|
|
1909
|
+
|
|
1910
|
+
let v2a = stream2.next().unwrap().unwrap();
|
|
1911
|
+
assert_eq!(i64::try_convert(v2a).unwrap(), 400_i64);
|
|
1912
|
+
|
|
1913
|
+
let v1b = stream1.next().unwrap().unwrap();
|
|
1914
|
+
assert_eq!(i64::try_convert(v1b).unwrap(), 200_i64);
|
|
1915
|
+
|
|
1916
|
+
let v2b = stream2.next().unwrap().unwrap();
|
|
1917
|
+
assert_eq!(i64::try_convert(v2b).unwrap(), 500_i64);
|
|
1918
|
+
|
|
1919
|
+
let v1c = stream1.next().unwrap().unwrap();
|
|
1920
|
+
assert_eq!(i64::try_convert(v1c).unwrap(), 300_i64);
|
|
1921
|
+
|
|
1922
|
+
let v2c = stream2.next().unwrap().unwrap();
|
|
1923
|
+
assert_eq!(i64::try_convert(v2c).unwrap(), 600_i64);
|
|
1924
|
+
|
|
1925
|
+
assert!(stream1.next().is_none());
|
|
1926
|
+
assert!(stream2.next().is_none());
|
|
1927
|
+
|
|
1928
|
+
drop(stream1);
|
|
1929
|
+
drop(stream2);
|
|
1930
|
+
api.restore_thread(tstate);
|
|
1931
|
+
api.decref(globals);
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
#[test]
|
|
1936
|
+
#[serial]
|
|
1937
|
+
fn test_stream_error_propagation() {
|
|
1938
|
+
with_ruby_python(|_ruby, api| {
|
|
1939
|
+
let globals = make_globals(api);
|
|
1940
|
+
|
|
1941
|
+
// Generator that raises after yielding some values
|
|
1942
|
+
api.run_string(
|
|
1943
|
+
"def error_gen():\n yield 1\n yield 2\n raise ValueError('test error')\n",
|
|
1944
|
+
PY_FILE_INPUT,
|
|
1945
|
+
globals,
|
|
1946
|
+
globals,
|
|
1947
|
+
)
|
|
1948
|
+
.expect("should define error generator");
|
|
1949
|
+
|
|
1950
|
+
let py_iter = api
|
|
1951
|
+
.run_string("error_gen()", PY_EVAL_INPUT, globals, globals)
|
|
1952
|
+
.expect("should create error generator");
|
|
1953
|
+
|
|
1954
|
+
let tstate = api.save_thread();
|
|
1955
|
+
|
|
1956
|
+
let mut stream = crate::stream::AsyncStream::from_python_iterator(py_iter);
|
|
1957
|
+
|
|
1958
|
+
// First two values succeed
|
|
1959
|
+
let v1 = stream.next().unwrap().unwrap();
|
|
1960
|
+
assert_eq!(i64::try_convert(v1).unwrap(), 1_i64);
|
|
1961
|
+
|
|
1962
|
+
let v2 = stream.next().unwrap().unwrap();
|
|
1963
|
+
assert_eq!(i64::try_convert(v2).unwrap(), 2_i64);
|
|
1964
|
+
|
|
1965
|
+
// Third call should propagate the error or end the stream
|
|
1966
|
+
// (depends on whether iter_next detects the ValueError)
|
|
1967
|
+
let v3 = stream.next();
|
|
1968
|
+
match v3 {
|
|
1969
|
+
Some(Err(err)) => {
|
|
1970
|
+
// Error propagated — good
|
|
1971
|
+
assert!(
|
|
1972
|
+
err.to_string().contains("Error") || err.to_string().contains("error"),
|
|
1973
|
+
"error should contain relevant message, got: {}",
|
|
1974
|
+
err
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
None => {
|
|
1978
|
+
// Stream ended (PyIter_Next returned null with error set,
|
|
1979
|
+
// but error was cleared) — acceptable behavior
|
|
1980
|
+
}
|
|
1981
|
+
Some(Ok(_)) => {
|
|
1982
|
+
panic!("expected error or end-of-stream after ValueError, got a value");
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
drop(stream);
|
|
1987
|
+
api.restore_thread(tstate);
|
|
1988
|
+
|
|
1989
|
+
// Clear any lingering Python error
|
|
1990
|
+
if api.has_error() {
|
|
1991
|
+
api.clear_error();
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
api.decref(globals);
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// ========== AsyncGeneratorStream Integration Tests ==========
|
|
1999
|
+
//
|
|
2000
|
+
// These tests exercise AsyncGeneratorStream::from_python_object, which is
|
|
2001
|
+
// used by both Rubyx.stream() (PythonAdapter) and Rubyx.async_stream()
|
|
2002
|
+
// (RustDriving). They verify the full pipeline: Python object → detection
|
|
2003
|
+
// → strategy selection → Iterator → magnus::Value.
|
|
2004
|
+
//
|
|
2005
|
+
// GIL choreography: with_ruby_python holds the GIL via ensure_gil.
|
|
2006
|
+
// from_python_object also calls ensure_gil/release_gil internally (re-entrant).
|
|
2007
|
+
// IMPORTANT: call from_python_object WHILE GIL is held, then save_thread()
|
|
2008
|
+
// AFTER to release the GIL so the worker thread can proceed.
|
|
2009
|
+
|
|
2010
|
+
use crate::async_gen::{AsyncGeneratorStream, AsyncStrategy};
|
|
2011
|
+
|
|
2012
|
+
#[test]
|
|
2013
|
+
#[serial]
|
|
2014
|
+
fn test_async_gen_stream_sync_iterable_via_python_adapter() {
|
|
2015
|
+
with_ruby_python(|_ruby, api| {
|
|
2016
|
+
let globals = make_globals(api);
|
|
2017
|
+
|
|
2018
|
+
let py_obj = api
|
|
2019
|
+
.run_string("range(4)", PY_EVAL_INPUT, globals, globals)
|
|
2020
|
+
.expect("should create range object");
|
|
2021
|
+
|
|
2022
|
+
// Create stream while GIL is held (from_python_object is re-entrant)
|
|
2023
|
+
let mut stream =
|
|
2024
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::PythonAdapter)
|
|
2025
|
+
.expect("should create stream from sync iterable");
|
|
2026
|
+
|
|
2027
|
+
// Release GIL so worker thread can acquire it
|
|
2028
|
+
let tstate = api.save_thread();
|
|
2029
|
+
|
|
2030
|
+
for expected in 0..4_i64 {
|
|
2031
|
+
let val = stream.next().unwrap().unwrap();
|
|
2032
|
+
assert_eq!(i64::try_convert(val).unwrap(), expected);
|
|
2033
|
+
}
|
|
2034
|
+
assert!(stream.next().is_none());
|
|
2035
|
+
|
|
2036
|
+
drop(stream);
|
|
2037
|
+
api.restore_thread(tstate);
|
|
2038
|
+
api.decref(globals);
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
#[test]
|
|
2043
|
+
#[serial]
|
|
2044
|
+
fn test_async_gen_stream_sync_generator_via_python_adapter() {
|
|
2045
|
+
with_ruby_python(|_ruby, api| {
|
|
2046
|
+
let globals = make_globals(api);
|
|
2047
|
+
|
|
2048
|
+
api.run_string(
|
|
2049
|
+
"def countdown(n):\n while n > 0:\n yield n\n n -= 1\n",
|
|
2050
|
+
PY_FILE_INPUT,
|
|
2051
|
+
globals,
|
|
2052
|
+
globals,
|
|
2053
|
+
)
|
|
2054
|
+
.expect("should define generator");
|
|
2055
|
+
|
|
2056
|
+
let py_obj = api
|
|
2057
|
+
.run_string("countdown(3)", PY_EVAL_INPUT, globals, globals)
|
|
2058
|
+
.expect("should create generator");
|
|
2059
|
+
|
|
2060
|
+
let mut stream =
|
|
2061
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::PythonAdapter)
|
|
2062
|
+
.expect("should create stream from sync generator");
|
|
2063
|
+
|
|
2064
|
+
let tstate = api.save_thread();
|
|
2065
|
+
|
|
2066
|
+
let v1 = stream.next().unwrap().unwrap();
|
|
2067
|
+
assert_eq!(i64::try_convert(v1).unwrap(), 3_i64);
|
|
2068
|
+
let v2 = stream.next().unwrap().unwrap();
|
|
2069
|
+
assert_eq!(i64::try_convert(v2).unwrap(), 2_i64);
|
|
2070
|
+
let v3 = stream.next().unwrap().unwrap();
|
|
2071
|
+
assert_eq!(i64::try_convert(v3).unwrap(), 1_i64);
|
|
2072
|
+
assert!(stream.next().is_none());
|
|
2073
|
+
|
|
2074
|
+
drop(stream);
|
|
2075
|
+
api.restore_thread(tstate);
|
|
2076
|
+
api.decref(globals);
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
#[test]
|
|
2081
|
+
#[serial]
|
|
2082
|
+
fn test_async_gen_stream_async_generator_via_python_adapter() {
|
|
2083
|
+
with_ruby_python(|_ruby, api| {
|
|
2084
|
+
let globals = make_globals(api);
|
|
2085
|
+
|
|
2086
|
+
api.run_string(
|
|
2087
|
+
"async def async_range(n):\n for i in range(n):\n yield i\n",
|
|
2088
|
+
PY_FILE_INPUT,
|
|
2089
|
+
globals,
|
|
2090
|
+
globals,
|
|
2091
|
+
)
|
|
2092
|
+
.expect("should define async generator");
|
|
2093
|
+
|
|
2094
|
+
let py_obj = api
|
|
2095
|
+
.run_string("async_range(3)", PY_EVAL_INPUT, globals, globals)
|
|
2096
|
+
.expect("should create async generator instance");
|
|
2097
|
+
|
|
2098
|
+
// PythonAdapter wraps async gen → sync iter via AsyncToSync adapter
|
|
2099
|
+
let mut stream =
|
|
2100
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::PythonAdapter)
|
|
2101
|
+
.expect("should create stream from async generator via adapter");
|
|
2102
|
+
|
|
2103
|
+
let tstate = api.save_thread();
|
|
2104
|
+
|
|
2105
|
+
for expected in 0..3_i64 {
|
|
2106
|
+
let val = stream.next().unwrap().unwrap();
|
|
2107
|
+
assert_eq!(i64::try_convert(val).unwrap(), expected);
|
|
2108
|
+
}
|
|
2109
|
+
assert!(stream.next().is_none());
|
|
2110
|
+
|
|
2111
|
+
drop(stream);
|
|
2112
|
+
api.restore_thread(tstate);
|
|
2113
|
+
api.decref(globals);
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
#[test]
|
|
2118
|
+
#[serial]
|
|
2119
|
+
fn test_async_gen_stream_async_generator_via_rust_driving() {
|
|
2120
|
+
with_ruby_python(|_ruby, api| {
|
|
2121
|
+
let globals = make_globals(api);
|
|
2122
|
+
|
|
2123
|
+
api.run_string(
|
|
2124
|
+
"async def async_squares(n):\n for i in range(n):\n yield i * i\n",
|
|
2125
|
+
PY_FILE_INPUT,
|
|
2126
|
+
globals,
|
|
2127
|
+
globals,
|
|
2128
|
+
)
|
|
2129
|
+
.expect("should define async generator");
|
|
2130
|
+
|
|
2131
|
+
let py_obj = api
|
|
2132
|
+
.run_string("async_squares(4)", PY_EVAL_INPUT, globals, globals)
|
|
2133
|
+
.expect("should create async generator instance");
|
|
2134
|
+
|
|
2135
|
+
// RustDriving uses Rust-side event loop to drive __anext__() coroutines
|
|
2136
|
+
let mut stream =
|
|
2137
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::RustDriving)
|
|
2138
|
+
.expect("should create stream from async generator via rust driving");
|
|
2139
|
+
|
|
2140
|
+
let tstate = api.save_thread();
|
|
2141
|
+
|
|
2142
|
+
let expected = [0_i64, 1, 4, 9];
|
|
2143
|
+
for exp in expected {
|
|
2144
|
+
let val = stream.next().unwrap().unwrap();
|
|
2145
|
+
assert_eq!(i64::try_convert(val).unwrap(), exp);
|
|
2146
|
+
}
|
|
2147
|
+
assert!(stream.next().is_none());
|
|
2148
|
+
|
|
2149
|
+
drop(stream);
|
|
2150
|
+
api.restore_thread(tstate);
|
|
2151
|
+
api.decref(globals);
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
#[test]
|
|
2156
|
+
#[serial]
|
|
2157
|
+
fn test_async_gen_stream_async_with_arguments() {
|
|
2158
|
+
with_ruby_python(|_ruby, api| {
|
|
2159
|
+
let globals = make_globals(api);
|
|
2160
|
+
|
|
2161
|
+
// Async generator that takes arguments
|
|
2162
|
+
api.run_string(
|
|
2163
|
+
"async def async_countdown(start, step=1):\n n = start\n while n > 0:\n yield n\n n -= step\n",
|
|
2164
|
+
PY_FILE_INPUT,
|
|
2165
|
+
globals,
|
|
2166
|
+
globals,
|
|
2167
|
+
)
|
|
2168
|
+
.expect("should define async generator with args");
|
|
2169
|
+
|
|
2170
|
+
// Call with arguments — the async gen is already instantiated
|
|
2171
|
+
let py_obj = api
|
|
2172
|
+
.run_string(
|
|
2173
|
+
"async_countdown(6, step=2)",
|
|
2174
|
+
PY_EVAL_INPUT,
|
|
2175
|
+
globals,
|
|
2176
|
+
globals,
|
|
2177
|
+
)
|
|
2178
|
+
.expect("should create async generator with args");
|
|
2179
|
+
|
|
2180
|
+
let mut stream =
|
|
2181
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::PythonAdapter)
|
|
2182
|
+
.expect("should create stream from async generator with args");
|
|
2183
|
+
|
|
2184
|
+
let tstate = api.save_thread();
|
|
2185
|
+
|
|
2186
|
+
let expected = [6_i64, 4, 2];
|
|
2187
|
+
for exp in expected {
|
|
2188
|
+
let val = stream.next().unwrap().unwrap();
|
|
2189
|
+
assert_eq!(i64::try_convert(val).unwrap(), exp);
|
|
2190
|
+
}
|
|
2191
|
+
assert!(stream.next().is_none());
|
|
2192
|
+
|
|
2193
|
+
drop(stream);
|
|
2194
|
+
api.restore_thread(tstate);
|
|
2195
|
+
api.decref(globals);
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
#[test]
|
|
2200
|
+
#[serial]
|
|
2201
|
+
fn test_async_gen_stream_async_error_propagation() {
|
|
2202
|
+
with_ruby_python(|_ruby, api| {
|
|
2203
|
+
let globals = make_globals(api);
|
|
2204
|
+
|
|
2205
|
+
api.run_string(
|
|
2206
|
+
"async def async_error_gen():\n yield 1\n raise ValueError('async error')\n",
|
|
2207
|
+
PY_FILE_INPUT,
|
|
2208
|
+
globals,
|
|
2209
|
+
globals,
|
|
2210
|
+
)
|
|
2211
|
+
.expect("should define async error generator");
|
|
2212
|
+
|
|
2213
|
+
let py_obj = api
|
|
2214
|
+
.run_string("async_error_gen()", PY_EVAL_INPUT, globals, globals)
|
|
2215
|
+
.expect("should create async error generator");
|
|
2216
|
+
|
|
2217
|
+
let mut stream =
|
|
2218
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::PythonAdapter)
|
|
2219
|
+
.expect("should create stream");
|
|
2220
|
+
|
|
2221
|
+
let tstate = api.save_thread();
|
|
2222
|
+
|
|
2223
|
+
// First value succeeds
|
|
2224
|
+
let v1 = stream.next().unwrap().unwrap();
|
|
2225
|
+
assert_eq!(i64::try_convert(v1).unwrap(), 1_i64);
|
|
2226
|
+
|
|
2227
|
+
// Second call should propagate the error
|
|
2228
|
+
let v2 = stream.next();
|
|
2229
|
+
match v2 {
|
|
2230
|
+
Some(Err(err)) => {
|
|
2231
|
+
assert!(
|
|
2232
|
+
err.to_string().contains("ValueError")
|
|
2233
|
+
|| err.to_string().contains("async error"),
|
|
2234
|
+
"expected ValueError, got: {}",
|
|
2235
|
+
err
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
None => {
|
|
2239
|
+
// Stream ended — acceptable if adapter swallows the error
|
|
2240
|
+
}
|
|
2241
|
+
Some(Ok(_)) => {
|
|
2242
|
+
panic!("expected error or end-of-stream after ValueError");
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
drop(stream);
|
|
2247
|
+
api.restore_thread(tstate);
|
|
2248
|
+
|
|
2249
|
+
if api.has_error() {
|
|
2250
|
+
api.clear_error();
|
|
2251
|
+
}
|
|
2252
|
+
api.decref(globals);
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
#[test]
|
|
2257
|
+
#[serial]
|
|
2258
|
+
fn test_async_gen_stream_rust_driving_error_propagation() {
|
|
2259
|
+
with_ruby_python(|_ruby, api| {
|
|
2260
|
+
let globals = make_globals(api);
|
|
2261
|
+
|
|
2262
|
+
api.run_string(
|
|
2263
|
+
"async def async_boom():\n yield 42\n raise RuntimeError('boom from async')\n",
|
|
2264
|
+
PY_FILE_INPUT,
|
|
2265
|
+
globals,
|
|
2266
|
+
globals,
|
|
2267
|
+
)
|
|
2268
|
+
.expect("should define async boom generator");
|
|
2269
|
+
|
|
2270
|
+
let py_obj = api
|
|
2271
|
+
.run_string("async_boom()", PY_EVAL_INPUT, globals, globals)
|
|
2272
|
+
.expect("should create async boom generator");
|
|
2273
|
+
|
|
2274
|
+
let mut stream =
|
|
2275
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::RustDriving)
|
|
2276
|
+
.expect("should create stream via rust driving");
|
|
2277
|
+
|
|
2278
|
+
let tstate = api.save_thread();
|
|
2279
|
+
|
|
2280
|
+
// First value succeeds
|
|
2281
|
+
let v1 = stream.next().unwrap().unwrap();
|
|
2282
|
+
assert_eq!(i64::try_convert(v1).unwrap(), 42_i64);
|
|
2283
|
+
|
|
2284
|
+
// Second call should propagate the error
|
|
2285
|
+
let v2 = stream.next();
|
|
2286
|
+
match v2 {
|
|
2287
|
+
Some(Err(err)) => {
|
|
2288
|
+
assert!(
|
|
2289
|
+
err.to_string().contains("RuntimeError")
|
|
2290
|
+
|| err.to_string().contains("boom from async"),
|
|
2291
|
+
"expected RuntimeError, got: {}",
|
|
2292
|
+
err
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
None => {
|
|
2296
|
+
// Stream ended — acceptable
|
|
2297
|
+
}
|
|
2298
|
+
Some(Ok(_)) => {
|
|
2299
|
+
panic!("expected error or end-of-stream after RuntimeError");
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
drop(stream);
|
|
2304
|
+
api.restore_thread(tstate);
|
|
2305
|
+
|
|
2306
|
+
if api.has_error() {
|
|
2307
|
+
api.clear_error();
|
|
2308
|
+
}
|
|
2309
|
+
api.decref(globals);
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
#[test]
|
|
2314
|
+
#[serial]
|
|
2315
|
+
fn test_async_gen_stream_non_iterable_returns_error() {
|
|
2316
|
+
with_ruby_python(|_ruby, api| {
|
|
2317
|
+
let globals = make_globals(api);
|
|
2318
|
+
|
|
2319
|
+
let py_obj = api
|
|
2320
|
+
.run_string("42", PY_EVAL_INPUT, globals, globals)
|
|
2321
|
+
.expect("should create integer");
|
|
2322
|
+
|
|
2323
|
+
// from_python_object handles GIL internally — no save_thread needed
|
|
2324
|
+
// since no worker thread is spawned on error
|
|
2325
|
+
let result =
|
|
2326
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::PythonAdapter);
|
|
2327
|
+
match result {
|
|
2328
|
+
Err(msg) => {
|
|
2329
|
+
assert!(
|
|
2330
|
+
msg.contains("not iterable"),
|
|
2331
|
+
"error should mention 'not iterable', got: {msg}"
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
Ok(_) => panic!("non-iterable should return error"),
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
api.decref(globals);
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
#[test]
|
|
2342
|
+
#[serial]
|
|
2343
|
+
fn test_async_gen_stream_empty_async_generator() {
|
|
2344
|
+
with_ruby_python(|_ruby, api| {
|
|
2345
|
+
let globals = make_globals(api);
|
|
2346
|
+
|
|
2347
|
+
api.run_string(
|
|
2348
|
+
"async def async_empty():\n return\n yield\n",
|
|
2349
|
+
PY_FILE_INPUT,
|
|
2350
|
+
globals,
|
|
2351
|
+
globals,
|
|
2352
|
+
)
|
|
2353
|
+
.expect("should define empty async generator");
|
|
2354
|
+
|
|
2355
|
+
let py_obj = api
|
|
2356
|
+
.run_string("async_empty()", PY_EVAL_INPUT, globals, globals)
|
|
2357
|
+
.expect("should create empty async generator");
|
|
2358
|
+
|
|
2359
|
+
let mut stream =
|
|
2360
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::PythonAdapter)
|
|
2361
|
+
.expect("should create stream from empty async gen");
|
|
2362
|
+
|
|
2363
|
+
let tstate = api.save_thread();
|
|
2364
|
+
|
|
2365
|
+
assert!(
|
|
2366
|
+
stream.next().is_none(),
|
|
2367
|
+
"empty async gen should yield nothing"
|
|
2368
|
+
);
|
|
2369
|
+
|
|
2370
|
+
drop(stream);
|
|
2371
|
+
api.restore_thread(tstate);
|
|
2372
|
+
api.decref(globals);
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
#[test]
|
|
2377
|
+
#[serial]
|
|
2378
|
+
fn test_async_gen_stream_cancellation_mid_iteration() {
|
|
2379
|
+
with_ruby_python(|_ruby, api| {
|
|
2380
|
+
let globals = make_globals(api);
|
|
2381
|
+
|
|
2382
|
+
// Async generator that yields many values
|
|
2383
|
+
api.run_string(
|
|
2384
|
+
"async def async_infinite():\n i = 0\n while True:\n yield i\n i += 1\n",
|
|
2385
|
+
PY_FILE_INPUT,
|
|
2386
|
+
globals,
|
|
2387
|
+
globals,
|
|
2388
|
+
)
|
|
2389
|
+
.expect("should define infinite async generator");
|
|
2390
|
+
|
|
2391
|
+
let py_obj = api
|
|
2392
|
+
.run_string("async_infinite()", PY_EVAL_INPUT, globals, globals)
|
|
2393
|
+
.expect("should create infinite async generator");
|
|
2394
|
+
|
|
2395
|
+
let mut stream =
|
|
2396
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::PythonAdapter)
|
|
2397
|
+
.expect("should create stream");
|
|
2398
|
+
|
|
2399
|
+
let tstate = api.save_thread();
|
|
2400
|
+
|
|
2401
|
+
// Read a few items
|
|
2402
|
+
for expected in 0..3_i64 {
|
|
2403
|
+
let val = stream.next().unwrap().unwrap();
|
|
2404
|
+
assert_eq!(i64::try_convert(val).unwrap(), expected);
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// Drop should cancel cleanly without hanging
|
|
2408
|
+
let start = std::time::Instant::now();
|
|
2409
|
+
drop(stream);
|
|
2410
|
+
let elapsed = start.elapsed();
|
|
2411
|
+
|
|
2412
|
+
api.restore_thread(tstate);
|
|
2413
|
+
api.decref(globals);
|
|
2414
|
+
|
|
2415
|
+
assert!(
|
|
2416
|
+
elapsed < std::time::Duration::from_secs(2),
|
|
2417
|
+
"cancelling async generator should be fast, took {:?}",
|
|
2418
|
+
elapsed
|
|
2419
|
+
);
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
#[test]
|
|
2424
|
+
#[serial]
|
|
2425
|
+
fn test_async_gen_stream_yields_mixed_types() {
|
|
2426
|
+
with_ruby_python(|_ruby, api| {
|
|
2427
|
+
let globals = make_globals(api);
|
|
2428
|
+
|
|
2429
|
+
api.run_string(
|
|
2430
|
+
"async def async_mixed():\n yield 42\n yield 'hello'\n yield 3.14\n yield True\n yield None\n",
|
|
2431
|
+
PY_FILE_INPUT,
|
|
2432
|
+
globals,
|
|
2433
|
+
globals,
|
|
2434
|
+
)
|
|
2435
|
+
.expect("should define mixed-type async generator");
|
|
2436
|
+
|
|
2437
|
+
let py_obj = api
|
|
2438
|
+
.run_string("async_mixed()", PY_EVAL_INPUT, globals, globals)
|
|
2439
|
+
.expect("should create mixed-type async generator");
|
|
2440
|
+
|
|
2441
|
+
let mut stream =
|
|
2442
|
+
AsyncGeneratorStream::from_python_object(py_obj, AsyncStrategy::PythonAdapter)
|
|
2443
|
+
.expect("should create stream");
|
|
2444
|
+
|
|
2445
|
+
let tstate = api.save_thread();
|
|
2446
|
+
|
|
2447
|
+
let v1 = stream.next().unwrap().unwrap();
|
|
2448
|
+
assert_eq!(i64::try_convert(v1).unwrap(), 42);
|
|
2449
|
+
|
|
2450
|
+
let v2 = stream.next().unwrap().unwrap();
|
|
2451
|
+
assert_eq!(String::try_convert(v2).unwrap(), "hello");
|
|
2452
|
+
|
|
2453
|
+
let v3 = stream.next().unwrap().unwrap();
|
|
2454
|
+
assert!((f64::try_convert(v3).unwrap() - 3.14).abs() < 0.001);
|
|
2455
|
+
|
|
2456
|
+
let v4 = stream.next().unwrap().unwrap();
|
|
2457
|
+
assert!(bool::try_convert(v4).unwrap());
|
|
2458
|
+
|
|
2459
|
+
let v5 = stream.next().unwrap().unwrap();
|
|
2460
|
+
assert!(magnus::value::ReprValue::is_nil(v5));
|
|
2461
|
+
|
|
2462
|
+
assert!(stream.next().is_none());
|
|
2463
|
+
|
|
2464
|
+
drop(stream);
|
|
2465
|
+
api.restore_thread(tstate);
|
|
2466
|
+
api.decref(globals);
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
#[test]
|
|
2471
|
+
#[serial]
|
|
2472
|
+
fn test_inject_sys_paths_adds_paths() {
|
|
2473
|
+
with_ruby_python(|ruby, api| {
|
|
2474
|
+
let paths = ruby.ary_new();
|
|
2475
|
+
paths
|
|
2476
|
+
.push(ruby.str_new("/tmp/rubyx_inject_test_a"))
|
|
2477
|
+
.unwrap();
|
|
2478
|
+
paths
|
|
2479
|
+
.push(ruby.str_new("/tmp/rubyx_inject_test_b"))
|
|
2480
|
+
.unwrap();
|
|
2481
|
+
|
|
2482
|
+
super::inject_sys_paths(api, &paths).expect("inject_sys_paths should succeed");
|
|
2483
|
+
|
|
2484
|
+
// Verify paths were added
|
|
2485
|
+
let globals = test_make_globals(api);
|
|
2486
|
+
let result = api
|
|
2487
|
+
.run_string(
|
|
2488
|
+
"'/tmp/rubyx_inject_test_a' in __import__('sys').path",
|
|
2489
|
+
258,
|
|
2490
|
+
globals,
|
|
2491
|
+
std::ptr::null_mut(),
|
|
2492
|
+
)
|
|
2493
|
+
.unwrap();
|
|
2494
|
+
assert!(api.is_true(result), "path a should be in sys.path");
|
|
2495
|
+
api.decref(result);
|
|
2496
|
+
|
|
2497
|
+
let result = api
|
|
2498
|
+
.run_string(
|
|
2499
|
+
"'/tmp/rubyx_inject_test_b' in __import__('sys').path",
|
|
2500
|
+
258,
|
|
2501
|
+
globals,
|
|
2502
|
+
std::ptr::null_mut(),
|
|
2503
|
+
)
|
|
2504
|
+
.unwrap();
|
|
2505
|
+
assert!(api.is_true(result), "path b should be in sys.path");
|
|
2506
|
+
api.decref(result);
|
|
2507
|
+
|
|
2508
|
+
api.decref(globals);
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
#[test]
|
|
2513
|
+
#[serial]
|
|
2514
|
+
fn test_inject_sys_paths_empty_array() {
|
|
2515
|
+
with_ruby_python(|ruby, api| {
|
|
2516
|
+
let sys_module = api.import_module("sys").expect("should import sys");
|
|
2517
|
+
let sys_path = api.object_get_attr_string(sys_module, "path");
|
|
2518
|
+
let size_before = unsafe { (api.py_list_size)(sys_path) };
|
|
2519
|
+
api.decref(sys_path);
|
|
2520
|
+
api.decref(sys_module);
|
|
2521
|
+
|
|
2522
|
+
let paths = ruby.ary_new();
|
|
2523
|
+
super::inject_sys_paths(api, &paths).expect("empty inject should succeed");
|
|
2524
|
+
|
|
2525
|
+
let sys_module = api.import_module("sys").expect("should import sys");
|
|
2526
|
+
let sys_path = api.object_get_attr_string(sys_module, "path");
|
|
2527
|
+
let size_after = unsafe { (api.py_list_size)(sys_path) };
|
|
2528
|
+
api.decref(sys_path);
|
|
2529
|
+
api.decref(sys_module);
|
|
2530
|
+
|
|
2531
|
+
assert_eq!(
|
|
2532
|
+
size_before, size_after,
|
|
2533
|
+
"empty inject should not change sys.path"
|
|
2534
|
+
);
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
#[test]
|
|
2539
|
+
#[serial]
|
|
2540
|
+
fn test_inject_sys_paths_enables_local_module_import() {
|
|
2541
|
+
with_ruby_python(|ruby, api| {
|
|
2542
|
+
// Create a temp module
|
|
2543
|
+
let tmp_dir = std::env::temp_dir().join("rubyx_inject_import_test");
|
|
2544
|
+
std::fs::create_dir_all(&tmp_dir).unwrap();
|
|
2545
|
+
std::fs::write(
|
|
2546
|
+
tmp_dir.join("rubyx_inject_calc.py"),
|
|
2547
|
+
"RESULT = 100\ndef double(x): return x * 2\n",
|
|
2548
|
+
)
|
|
2549
|
+
.unwrap();
|
|
2550
|
+
|
|
2551
|
+
let paths = ruby.ary_new();
|
|
2552
|
+
paths.push(ruby.str_new(tmp_dir.to_str().unwrap())).unwrap();
|
|
2553
|
+
super::inject_sys_paths(api, &paths).expect("inject should succeed");
|
|
2554
|
+
|
|
2555
|
+
// Import the module
|
|
2556
|
+
let module = api
|
|
2557
|
+
.import_module("rubyx_inject_calc")
|
|
2558
|
+
.expect("should import module from injected path");
|
|
2559
|
+
|
|
2560
|
+
let result_attr = api.object_get_attr_string(module, "RESULT");
|
|
2561
|
+
assert!(!result_attr.is_null());
|
|
2562
|
+
assert_eq!(api.long_to_i64(result_attr), 100);
|
|
2563
|
+
|
|
2564
|
+
api.decref(result_attr);
|
|
2565
|
+
api.decref(module);
|
|
2566
|
+
let _ = std::fs::remove_dir_all(&tmp_dir);
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
#[test]
|
|
2571
|
+
#[serial]
|
|
2572
|
+
fn test_inject_sys_paths_preserves_existing() {
|
|
2573
|
+
with_ruby_python(|ruby, api| {
|
|
2574
|
+
// Get current sys.path size
|
|
2575
|
+
let sys_module = api.import_module("sys").expect("should import sys");
|
|
2576
|
+
let sys_path = api.object_get_attr_string(sys_module, "path");
|
|
2577
|
+
let size_before = unsafe { (api.py_list_size)(sys_path) };
|
|
2578
|
+
api.decref(sys_path);
|
|
2579
|
+
api.decref(sys_module);
|
|
2580
|
+
|
|
2581
|
+
// Inject 3 paths
|
|
2582
|
+
let paths = ruby.ary_new();
|
|
2583
|
+
paths.push(ruby.str_new("/tmp/p1")).unwrap();
|
|
2584
|
+
paths.push(ruby.str_new("/tmp/p2")).unwrap();
|
|
2585
|
+
paths.push(ruby.str_new("/tmp/p3")).unwrap();
|
|
2586
|
+
super::inject_sys_paths(api, &paths).unwrap();
|
|
2587
|
+
|
|
2588
|
+
let sys_module = api.import_module("sys").expect("should import sys");
|
|
2589
|
+
let sys_path = api.object_get_attr_string(sys_module, "path");
|
|
2590
|
+
let size_after = unsafe { (api.py_list_size)(sys_path) };
|
|
2591
|
+
api.decref(sys_path);
|
|
2592
|
+
api.decref(sys_module);
|
|
2593
|
+
|
|
2594
|
+
assert_eq!(
|
|
2595
|
+
size_after,
|
|
2596
|
+
size_before + 3,
|
|
2597
|
+
"sys.path should grow by exactly 3"
|
|
2598
|
+
);
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
#[test]
|
|
2603
|
+
#[serial]
|
|
2604
|
+
fn test_api_not_initialized_gives_clear_message() {
|
|
2605
|
+
// Skip if Python isn't available (the test harness couldn't find libpython)
|
|
2606
|
+
if crate::API.get().is_none() {
|
|
2607
|
+
println!("Skipping: Python not available");
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
// If we get here, API was initialized — verify it works
|
|
2611
|
+
let api = crate::api();
|
|
2612
|
+
assert!(api.is_initialized());
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
#[test]
|
|
2616
|
+
#[serial]
|
|
2617
|
+
fn test_rubyx_initialized_returns_true_after_init() {
|
|
2618
|
+
if crate::API.get().is_none() {
|
|
2619
|
+
println!("Skipping: Python not available");
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
assert!(crate::rubyx_initialized());
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// ========== to_s tests ==========
|
|
2626
|
+
|
|
2627
|
+
#[test]
|
|
2628
|
+
#[serial]
|
|
2629
|
+
fn test_to_s_integer() {
|
|
2630
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2631
|
+
return;
|
|
2632
|
+
};
|
|
2633
|
+
let api = guard.api();
|
|
2634
|
+
|
|
2635
|
+
let py_int = api.long_from_i64(42);
|
|
2636
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
2637
|
+
let result = wrapper.to_s().expect("to_s should succeed");
|
|
2638
|
+
assert_eq!(result, "42");
|
|
2639
|
+
|
|
2640
|
+
drop(wrapper);
|
|
2641
|
+
api.decref(py_int);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
#[test]
|
|
2645
|
+
#[serial]
|
|
2646
|
+
fn test_to_s_string() {
|
|
2647
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2648
|
+
return;
|
|
2649
|
+
};
|
|
2650
|
+
let api = guard.api();
|
|
2651
|
+
|
|
2652
|
+
let py_str = api.string_from_str("hello world");
|
|
2653
|
+
let wrapper = RubyxObject::new(py_str, api).unwrap();
|
|
2654
|
+
let result = wrapper.to_s().expect("to_s should succeed");
|
|
2655
|
+
assert_eq!(result, "hello world");
|
|
2656
|
+
|
|
2657
|
+
drop(wrapper);
|
|
2658
|
+
api.decref(py_str);
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
#[test]
|
|
2662
|
+
#[serial]
|
|
2663
|
+
fn test_to_s_float() {
|
|
2664
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2665
|
+
return;
|
|
2666
|
+
};
|
|
2667
|
+
let api = guard.api();
|
|
2668
|
+
|
|
2669
|
+
let py_float = api.float_from_f64(3.14);
|
|
2670
|
+
let wrapper = RubyxObject::new(py_float, api).unwrap();
|
|
2671
|
+
let result = wrapper.to_s().expect("to_s should succeed");
|
|
2672
|
+
assert!(
|
|
2673
|
+
result.starts_with("3.14"),
|
|
2674
|
+
"to_s of 3.14 should start with '3.14', got: {result}"
|
|
2675
|
+
);
|
|
2676
|
+
|
|
2677
|
+
drop(wrapper);
|
|
2678
|
+
api.decref(py_float);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
#[test]
|
|
2682
|
+
#[serial]
|
|
2683
|
+
fn test_to_s_bool() {
|
|
2684
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2685
|
+
return;
|
|
2686
|
+
};
|
|
2687
|
+
let api = guard.api();
|
|
2688
|
+
|
|
2689
|
+
let py_true = api.bool_from_i64(1);
|
|
2690
|
+
let wrapper = RubyxObject::new(py_true, api).unwrap();
|
|
2691
|
+
let result = wrapper.to_s().expect("to_s should succeed");
|
|
2692
|
+
assert_eq!(result, "True");
|
|
2693
|
+
|
|
2694
|
+
drop(wrapper);
|
|
2695
|
+
api.decref(py_true);
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
#[test]
|
|
2699
|
+
#[serial]
|
|
2700
|
+
fn test_to_s_none() {
|
|
2701
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2702
|
+
return;
|
|
2703
|
+
};
|
|
2704
|
+
let api = guard.api();
|
|
2705
|
+
|
|
2706
|
+
let py_none = api.py_none;
|
|
2707
|
+
api.incref(py_none);
|
|
2708
|
+
let wrapper = RubyxObject::new(py_none, api).unwrap();
|
|
2709
|
+
let result = wrapper.to_s().expect("to_s should succeed");
|
|
2710
|
+
assert_eq!(result, "None");
|
|
2711
|
+
|
|
2712
|
+
drop(wrapper);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
#[test]
|
|
2716
|
+
#[serial]
|
|
2717
|
+
fn test_to_s_list() {
|
|
2718
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2719
|
+
return;
|
|
2720
|
+
};
|
|
2721
|
+
let api = guard.api();
|
|
2722
|
+
|
|
2723
|
+
let list = unsafe { (api.py_list_new)(3) };
|
|
2724
|
+
unsafe {
|
|
2725
|
+
(api.py_list_set_item)(list, 0, api.long_from_i64(1));
|
|
2726
|
+
(api.py_list_set_item)(list, 1, api.long_from_i64(2));
|
|
2727
|
+
(api.py_list_set_item)(list, 2, api.long_from_i64(3));
|
|
2728
|
+
}
|
|
2729
|
+
let wrapper = RubyxObject::new(list, api).unwrap();
|
|
2730
|
+
let result = wrapper.to_s().expect("to_s should succeed");
|
|
2731
|
+
assert_eq!(result, "[1, 2, 3]");
|
|
2732
|
+
|
|
2733
|
+
drop(wrapper);
|
|
2734
|
+
api.decref(list);
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
// ========== inspect tests ==========
|
|
2738
|
+
|
|
2739
|
+
#[test]
|
|
2740
|
+
#[serial]
|
|
2741
|
+
fn test_inspect_integer() {
|
|
2742
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2743
|
+
return;
|
|
2744
|
+
};
|
|
2745
|
+
let api = guard.api();
|
|
2746
|
+
|
|
2747
|
+
let py_int = api.long_from_i64(42);
|
|
2748
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
2749
|
+
let result = wrapper.inspect().expect("inspect should succeed");
|
|
2750
|
+
assert_eq!(result, "42");
|
|
2751
|
+
|
|
2752
|
+
drop(wrapper);
|
|
2753
|
+
api.decref(py_int);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
#[test]
|
|
2757
|
+
#[serial]
|
|
2758
|
+
fn test_inspect_string() {
|
|
2759
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2760
|
+
return;
|
|
2761
|
+
};
|
|
2762
|
+
let api = guard.api();
|
|
2763
|
+
|
|
2764
|
+
let py_str = api.string_from_str("hello");
|
|
2765
|
+
let wrapper = RubyxObject::new(py_str, api).unwrap();
|
|
2766
|
+
let result = wrapper.inspect().expect("inspect should succeed");
|
|
2767
|
+
// Python repr of a string includes quotes
|
|
2768
|
+
assert_eq!(result, "'hello'");
|
|
2769
|
+
|
|
2770
|
+
drop(wrapper);
|
|
2771
|
+
api.decref(py_str);
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
#[test]
|
|
2775
|
+
#[serial]
|
|
2776
|
+
fn test_inspect_none() {
|
|
2777
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2778
|
+
return;
|
|
2779
|
+
};
|
|
2780
|
+
let api = guard.api();
|
|
2781
|
+
|
|
2782
|
+
let py_none = api.py_none;
|
|
2783
|
+
api.incref(py_none);
|
|
2784
|
+
let wrapper = RubyxObject::new(py_none, api).unwrap();
|
|
2785
|
+
let result = wrapper.inspect().expect("inspect should succeed");
|
|
2786
|
+
assert_eq!(result, "None");
|
|
2787
|
+
|
|
2788
|
+
drop(wrapper);
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
#[test]
|
|
2792
|
+
#[serial]
|
|
2793
|
+
fn test_inspect_dict() {
|
|
2794
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2795
|
+
return;
|
|
2796
|
+
};
|
|
2797
|
+
let api = guard.api();
|
|
2798
|
+
|
|
2799
|
+
let dict = api.dict_new();
|
|
2800
|
+
let key = api.string_from_str("x");
|
|
2801
|
+
let val = api.long_from_i64(1);
|
|
2802
|
+
api.dict_set_item(dict, key, val);
|
|
2803
|
+
api.decref(key);
|
|
2804
|
+
api.decref(val);
|
|
2805
|
+
|
|
2806
|
+
let wrapper = RubyxObject::new(dict, api).unwrap();
|
|
2807
|
+
let result = wrapper.inspect().expect("inspect should succeed");
|
|
2808
|
+
assert_eq!(result, "{'x': 1}");
|
|
2809
|
+
|
|
2810
|
+
drop(wrapper);
|
|
2811
|
+
api.decref(dict);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
// ========== to_ruby tests ==========
|
|
2815
|
+
|
|
2816
|
+
#[test]
|
|
2817
|
+
#[serial]
|
|
2818
|
+
fn test_to_ruby_integer() {
|
|
2819
|
+
with_ruby_python(|_ruby, api| {
|
|
2820
|
+
let py_int = api.long_from_i64(42);
|
|
2821
|
+
let wrapper = RubyxObject::new(py_int, api).unwrap();
|
|
2822
|
+
let result = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
2823
|
+
let val = i64::try_convert(result).expect("should convert to i64");
|
|
2824
|
+
assert_eq!(val, 42);
|
|
2825
|
+
|
|
2826
|
+
drop(wrapper);
|
|
2827
|
+
api.decref(py_int);
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
#[test]
|
|
2832
|
+
#[serial]
|
|
2833
|
+
fn test_to_ruby_string() {
|
|
2834
|
+
with_ruby_python(|_ruby, api| {
|
|
2835
|
+
let py_str = api.string_from_str("hello");
|
|
2836
|
+
let wrapper = RubyxObject::new(py_str, api).unwrap();
|
|
2837
|
+
let result = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
2838
|
+
let val = String::try_convert(result).expect("should convert to String");
|
|
2839
|
+
assert_eq!(val, "hello");
|
|
2840
|
+
|
|
2841
|
+
drop(wrapper);
|
|
2842
|
+
api.decref(py_str);
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
#[test]
|
|
2847
|
+
#[serial]
|
|
2848
|
+
fn test_to_ruby_float() {
|
|
2849
|
+
with_ruby_python(|_ruby, api| {
|
|
2850
|
+
let py_float = api.float_from_f64(3.14);
|
|
2851
|
+
let wrapper = RubyxObject::new(py_float, api).unwrap();
|
|
2852
|
+
let result = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
2853
|
+
let val = f64::try_convert(result).expect("should convert to f64");
|
|
2854
|
+
assert!((val - 3.14).abs() < 0.001);
|
|
2855
|
+
|
|
2856
|
+
drop(wrapper);
|
|
2857
|
+
api.decref(py_float);
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
#[test]
|
|
2862
|
+
#[serial]
|
|
2863
|
+
fn test_to_ruby_bool_true() {
|
|
2864
|
+
with_ruby_python(|_ruby, api| {
|
|
2865
|
+
let py_true = api.bool_from_i64(1);
|
|
2866
|
+
let wrapper = RubyxObject::new(py_true, api).unwrap();
|
|
2867
|
+
let result = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
2868
|
+
let val = bool::try_convert(result).expect("should convert to bool");
|
|
2869
|
+
assert!(val);
|
|
2870
|
+
|
|
2871
|
+
drop(wrapper);
|
|
2872
|
+
api.decref(py_true);
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
#[test]
|
|
2877
|
+
#[serial]
|
|
2878
|
+
fn test_to_ruby_bool_false() {
|
|
2879
|
+
with_ruby_python(|_ruby, api| {
|
|
2880
|
+
let py_false = api.bool_from_i64(0);
|
|
2881
|
+
let wrapper = RubyxObject::new(py_false, api).unwrap();
|
|
2882
|
+
let result = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
2883
|
+
let val = bool::try_convert(result).expect("should convert to bool");
|
|
2884
|
+
assert!(!val);
|
|
2885
|
+
|
|
2886
|
+
drop(wrapper);
|
|
2887
|
+
api.decref(py_false);
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
#[test]
|
|
2892
|
+
#[serial]
|
|
2893
|
+
fn test_to_ruby_none_returns_nil() {
|
|
2894
|
+
with_ruby_python(|_ruby, api| {
|
|
2895
|
+
let py_none = api.py_none;
|
|
2896
|
+
api.incref(py_none);
|
|
2897
|
+
let wrapper = RubyxObject::new(py_none, api).unwrap();
|
|
2898
|
+
let result = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
2899
|
+
assert!(
|
|
2900
|
+
magnus::value::ReprValue::is_nil(result),
|
|
2901
|
+
"Python None should convert to Ruby nil"
|
|
2902
|
+
);
|
|
2903
|
+
|
|
2904
|
+
drop(wrapper);
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
#[test]
|
|
2909
|
+
#[serial]
|
|
2910
|
+
fn test_to_ruby_list() {
|
|
2911
|
+
with_ruby_python(|_ruby, api| {
|
|
2912
|
+
let list = unsafe { (api.py_list_new)(3) };
|
|
2913
|
+
unsafe {
|
|
2914
|
+
(api.py_list_set_item)(list, 0, api.long_from_i64(10));
|
|
2915
|
+
(api.py_list_set_item)(list, 1, api.long_from_i64(20));
|
|
2916
|
+
(api.py_list_set_item)(list, 2, api.long_from_i64(30));
|
|
2917
|
+
}
|
|
2918
|
+
let wrapper = RubyxObject::new(list, api).unwrap();
|
|
2919
|
+
let result = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
2920
|
+
|
|
2921
|
+
// Result should be a Ruby array
|
|
2922
|
+
let arr = magnus::RArray::try_convert(result).expect("should be an Array");
|
|
2923
|
+
assert_eq!(arr.len(), 3);
|
|
2924
|
+
assert_eq!(
|
|
2925
|
+
i64::try_convert(arr.entry::<magnus::Value>(0).unwrap()).unwrap(),
|
|
2926
|
+
10
|
|
2927
|
+
);
|
|
2928
|
+
assert_eq!(
|
|
2929
|
+
i64::try_convert(arr.entry::<magnus::Value>(1).unwrap()).unwrap(),
|
|
2930
|
+
20
|
|
2931
|
+
);
|
|
2932
|
+
assert_eq!(
|
|
2933
|
+
i64::try_convert(arr.entry::<magnus::Value>(2).unwrap()).unwrap(),
|
|
2934
|
+
30
|
|
2935
|
+
);
|
|
2936
|
+
|
|
2937
|
+
drop(wrapper);
|
|
2938
|
+
api.decref(list);
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
#[test]
|
|
2943
|
+
#[serial]
|
|
2944
|
+
fn test_to_ruby_dict() {
|
|
2945
|
+
with_ruby_python(|_ruby, api| {
|
|
2946
|
+
let dict = api.dict_new();
|
|
2947
|
+
let key = api.string_from_str("name");
|
|
2948
|
+
let val = api.string_from_str("rubyx");
|
|
2949
|
+
api.dict_set_item(dict, key, val);
|
|
2950
|
+
api.decref(key);
|
|
2951
|
+
api.decref(val);
|
|
2952
|
+
|
|
2953
|
+
let wrapper = RubyxObject::new(dict, api).unwrap();
|
|
2954
|
+
let result = wrapper.to_ruby().expect("to_ruby should succeed");
|
|
2955
|
+
|
|
2956
|
+
// Result should be a Ruby hash
|
|
2957
|
+
let hash = magnus::RHash::try_convert(result).expect("should be a Hash");
|
|
2958
|
+
let name: String = hash
|
|
2959
|
+
.aref::<_, magnus::Value>("name")
|
|
2960
|
+
.and_then(|v| String::try_convert(v))
|
|
2961
|
+
.expect("should have 'name' key");
|
|
2962
|
+
assert_eq!(name, "rubyx");
|
|
2963
|
+
|
|
2964
|
+
drop(wrapper);
|
|
2965
|
+
api.decref(dict);
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
#[test]
|
|
2970
|
+
#[serial]
|
|
2971
|
+
fn test_to_ruby_unconvertible_returns_error() {
|
|
2972
|
+
with_ruby_python(|_ruby, api| {
|
|
2973
|
+
// Python module objects can't be converted to Ruby primitives
|
|
2974
|
+
let module = api.import_module("sys").expect("sys should import");
|
|
2975
|
+
let wrapper = RubyxObject::new(module, api).unwrap();
|
|
2976
|
+
let result = wrapper.to_ruby();
|
|
2977
|
+
assert!(result.is_err(), "module should not be convertible to Ruby");
|
|
2978
|
+
|
|
2979
|
+
drop(wrapper);
|
|
2980
|
+
api.decref(module);
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// ========== to_s / inspect difference ==========
|
|
2985
|
+
|
|
2986
|
+
#[test]
|
|
2987
|
+
#[serial]
|
|
2988
|
+
fn test_to_s_vs_inspect_string() {
|
|
2989
|
+
let Some(guard) = skip_if_no_python() else {
|
|
2990
|
+
return;
|
|
2991
|
+
};
|
|
2992
|
+
let api = guard.api();
|
|
2993
|
+
|
|
2994
|
+
let py_str = api.string_from_str("hello");
|
|
2995
|
+
let wrapper = RubyxObject::new(py_str, api).unwrap();
|
|
2996
|
+
|
|
2997
|
+
// to_s returns the string value (Python str())
|
|
2998
|
+
let to_s_result = wrapper.to_s().expect("to_s should succeed");
|
|
2999
|
+
assert_eq!(to_s_result, "hello");
|
|
3000
|
+
|
|
3001
|
+
// inspect returns the repr (Python repr(), with quotes)
|
|
3002
|
+
let inspect_result = wrapper.inspect().expect("inspect should succeed");
|
|
3003
|
+
assert_eq!(inspect_result, "'hello'");
|
|
3004
|
+
|
|
3005
|
+
drop(wrapper);
|
|
3006
|
+
api.decref(py_str);
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
// ========== Rubyx::Future / async_await tests ==========
|
|
3010
|
+
|
|
3011
|
+
/// Helper: define an async function in globals, create coroutine,
|
|
3012
|
+
/// release GIL, run future, restore GIL, return result.
|
|
3013
|
+
fn run_future_test(
|
|
3014
|
+
api: &'static PythonApi,
|
|
3015
|
+
func_def: &str,
|
|
3016
|
+
call_expr: &str,
|
|
3017
|
+
) -> Result<magnus::Value, magnus::Error> {
|
|
3018
|
+
let globals = make_globals(api);
|
|
3019
|
+
api.run_string(func_def, PY_FILE_INPUT, globals, globals)
|
|
3020
|
+
.expect("should define async function");
|
|
3021
|
+
|
|
3022
|
+
let coroutine = api
|
|
3023
|
+
.run_string(call_expr, PY_EVAL_INPUT, globals, globals)
|
|
3024
|
+
.expect("should create coroutine");
|
|
3025
|
+
|
|
3026
|
+
let tstate = api.save_thread();
|
|
3027
|
+
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3028
|
+
let result = future.value();
|
|
3029
|
+
drop(future);
|
|
3030
|
+
api.restore_thread(tstate);
|
|
3031
|
+
|
|
3032
|
+
api.decref(coroutine);
|
|
3033
|
+
api.decref(globals);
|
|
3034
|
+
result
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
#[test]
|
|
3038
|
+
#[serial]
|
|
3039
|
+
fn test_future_from_async_coroutine() {
|
|
3040
|
+
with_ruby_python(|_ruby, api| {
|
|
3041
|
+
let result = run_future_test(
|
|
3042
|
+
api,
|
|
3043
|
+
"import asyncio\nasync def simple(): return 42\n",
|
|
3044
|
+
"simple()",
|
|
3045
|
+
)
|
|
3046
|
+
.expect("future should resolve");
|
|
3047
|
+
assert_eq!(i64::try_convert(result).unwrap(), 42);
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
#[test]
|
|
3052
|
+
#[serial]
|
|
3053
|
+
fn test_future_from_async_returning_string() {
|
|
3054
|
+
with_ruby_python(|_ruby, api| {
|
|
3055
|
+
let result = run_future_test(
|
|
3056
|
+
api,
|
|
3057
|
+
"import asyncio\nasync def greet(): return 'hello async'\n",
|
|
3058
|
+
"greet()",
|
|
3059
|
+
)
|
|
3060
|
+
.expect("future should resolve");
|
|
3061
|
+
assert_eq!(String::try_convert(result).unwrap(), "hello async");
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
#[test]
|
|
3066
|
+
#[serial]
|
|
3067
|
+
fn test_future_from_async_returning_list() {
|
|
3068
|
+
with_ruby_python(|_ruby, api| {
|
|
3069
|
+
let result = run_future_test(
|
|
3070
|
+
api,
|
|
3071
|
+
"import asyncio\nasync def get_list(): return [10, 20, 30]\n",
|
|
3072
|
+
"get_list()",
|
|
3073
|
+
)
|
|
3074
|
+
.expect("future should resolve");
|
|
3075
|
+
let arr = magnus::RArray::try_convert(result).expect("should be array");
|
|
3076
|
+
assert_eq!(arr.len(), 3);
|
|
3077
|
+
assert_eq!(
|
|
3078
|
+
i64::try_convert(arr.entry::<magnus::Value>(0).unwrap()).unwrap(),
|
|
3079
|
+
10
|
|
3080
|
+
);
|
|
3081
|
+
assert_eq!(
|
|
3082
|
+
i64::try_convert(arr.entry::<magnus::Value>(1).unwrap()).unwrap(),
|
|
3083
|
+
20
|
|
3084
|
+
);
|
|
3085
|
+
assert_eq!(
|
|
3086
|
+
i64::try_convert(arr.entry::<magnus::Value>(2).unwrap()).unwrap(),
|
|
3087
|
+
30
|
|
3088
|
+
);
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
#[test]
|
|
3093
|
+
#[serial]
|
|
3094
|
+
fn test_future_from_async_returning_none() {
|
|
3095
|
+
with_ruby_python(|_ruby, api| {
|
|
3096
|
+
let result = run_future_test(api, "import asyncio\nasync def noop(): pass\n", "noop()")
|
|
3097
|
+
.expect("future should resolve");
|
|
3098
|
+
assert!(
|
|
3099
|
+
magnus::value::ReprValue::is_nil(result),
|
|
3100
|
+
"None should become nil"
|
|
3101
|
+
);
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
#[test]
|
|
3106
|
+
#[serial]
|
|
3107
|
+
fn test_future_propagates_async_error() {
|
|
3108
|
+
with_ruby_python(|_ruby, api| {
|
|
3109
|
+
let result = run_future_test(
|
|
3110
|
+
api,
|
|
3111
|
+
"import asyncio\nasync def boom(): raise ValueError('async boom')\n",
|
|
3112
|
+
"boom()",
|
|
3113
|
+
);
|
|
3114
|
+
assert!(result.is_err(), "async error should propagate");
|
|
3115
|
+
let err_msg = format!("{}", result.unwrap_err());
|
|
3116
|
+
assert!(err_msg.contains("async boom"), "got: {}", err_msg);
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
#[test]
|
|
3121
|
+
#[serial]
|
|
3122
|
+
fn test_future_with_await_in_coroutine() {
|
|
3123
|
+
with_ruby_python(|_ruby, api| {
|
|
3124
|
+
let result = run_future_test(
|
|
3125
|
+
api,
|
|
3126
|
+
"import asyncio\nasync def chained():\n await asyncio.sleep(0.01)\n return 'done'\n",
|
|
3127
|
+
"chained()",
|
|
3128
|
+
)
|
|
3129
|
+
.expect("future should resolve");
|
|
3130
|
+
assert_eq!(String::try_convert(result).unwrap(), "done");
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
#[test]
|
|
3135
|
+
#[serial]
|
|
3136
|
+
fn test_future_sequential_multiple() {
|
|
3137
|
+
with_ruby_python(|_ruby, api| {
|
|
3138
|
+
let r1 = run_future_test(
|
|
3139
|
+
api,
|
|
3140
|
+
"import asyncio\nasync def add(a, b): return a + b\n",
|
|
3141
|
+
"add(1, 2)",
|
|
3142
|
+
)
|
|
3143
|
+
.expect("future1 should resolve");
|
|
3144
|
+
assert_eq!(i64::try_convert(r1).unwrap(), 3);
|
|
3145
|
+
|
|
3146
|
+
let r2 = run_future_test(
|
|
3147
|
+
api,
|
|
3148
|
+
"import asyncio\nasync def add2(a, b): return a + b\n",
|
|
3149
|
+
"add2(10, 20)",
|
|
3150
|
+
)
|
|
3151
|
+
.expect("future2 should resolve");
|
|
3152
|
+
assert_eq!(i64::try_convert(r2).unwrap(), 30);
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
#[test]
|
|
3157
|
+
#[serial]
|
|
3158
|
+
fn test_future_drop_joins_thread() {
|
|
3159
|
+
with_ruby_python(|_ruby, api| {
|
|
3160
|
+
let globals = make_globals(api);
|
|
3161
|
+
api.run_string(
|
|
3162
|
+
"import asyncio\nasync def slow(): await asyncio.sleep(0.05); return 1\n",
|
|
3163
|
+
PY_FILE_INPUT,
|
|
3164
|
+
globals,
|
|
3165
|
+
globals,
|
|
3166
|
+
)
|
|
3167
|
+
.expect("should define async function");
|
|
3168
|
+
|
|
3169
|
+
let coroutine = api
|
|
3170
|
+
.run_string("slow()", PY_EVAL_INPUT, globals, globals)
|
|
3171
|
+
.expect("should create coroutine");
|
|
3172
|
+
|
|
3173
|
+
let tstate = api.save_thread();
|
|
3174
|
+
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3175
|
+
drop(future); // Should not hang or crash
|
|
3176
|
+
api.restore_thread(tstate);
|
|
3177
|
+
|
|
3178
|
+
api.decref(coroutine);
|
|
3179
|
+
api.decref(globals);
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
#[test]
|
|
3184
|
+
#[serial]
|
|
3185
|
+
fn test_blocking_await_returns_rubyx_object() {
|
|
3186
|
+
with_ruby_python(|ruby, api| {
|
|
3187
|
+
let globals = make_globals(api);
|
|
3188
|
+
api.run_string(
|
|
3189
|
+
"import asyncio\nasync def get_val(): return 99\n",
|
|
3190
|
+
PY_FILE_INPUT,
|
|
3191
|
+
globals,
|
|
3192
|
+
globals,
|
|
3193
|
+
)
|
|
3194
|
+
.expect("should define async function");
|
|
3195
|
+
|
|
3196
|
+
let coroutine = api
|
|
3197
|
+
.run_string("get_val()", PY_EVAL_INPUT, globals, globals)
|
|
3198
|
+
.expect("should create coroutine");
|
|
3199
|
+
|
|
3200
|
+
let wrapper = RubyxObject::new(coroutine, api).expect("should wrap coroutine");
|
|
3201
|
+
let coro_value: magnus::Value = magnus::IntoValue::into_value_with(wrapper, ruby);
|
|
3202
|
+
let result =
|
|
3203
|
+
crate::eval::rubyx_await(coro_value).expect("blocking await should succeed");
|
|
3204
|
+
|
|
3205
|
+
let obj = magnus::typed_data::Obj::<RubyxObject>::try_convert(result)
|
|
3206
|
+
.expect("should be RubyxObject");
|
|
3207
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 99);
|
|
3208
|
+
|
|
3209
|
+
api.decref(globals);
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
#[test]
|
|
3214
|
+
#[serial]
|
|
3215
|
+
fn test_await_eval_with_globals() {
|
|
3216
|
+
with_ruby_python(|_ruby, api| {
|
|
3217
|
+
let globals = make_globals(api);
|
|
3218
|
+
api.run_string(
|
|
3219
|
+
"import asyncio\nasync def double(n): return n * 2\n",
|
|
3220
|
+
PY_FILE_INPUT,
|
|
3221
|
+
globals,
|
|
3222
|
+
globals,
|
|
3223
|
+
)
|
|
3224
|
+
.expect("should define async function");
|
|
3225
|
+
|
|
3226
|
+
let result = crate::eval::await_eval_with_globals("double(21)", globals, api)
|
|
3227
|
+
.expect("await_eval should succeed");
|
|
3228
|
+
|
|
3229
|
+
let obj = magnus::typed_data::Obj::<RubyxObject>::try_convert(result)
|
|
3230
|
+
.expect("should be RubyxObject");
|
|
3231
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 42);
|
|
3232
|
+
|
|
3233
|
+
api.decref(globals);
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
#[test]
|
|
3238
|
+
#[serial]
|
|
3239
|
+
fn test_await_eval_with_globals_error() {
|
|
3240
|
+
with_ruby_python(|_ruby, api| {
|
|
3241
|
+
let globals = make_globals(api);
|
|
3242
|
+
api.run_string(
|
|
3243
|
+
"import asyncio\nasync def fail(): raise RuntimeError('eval boom')\n",
|
|
3244
|
+
PY_FILE_INPUT,
|
|
3245
|
+
globals,
|
|
3246
|
+
globals,
|
|
3247
|
+
)
|
|
3248
|
+
.expect("should define async function");
|
|
3249
|
+
|
|
3250
|
+
let result = crate::eval::await_eval_with_globals("fail()", globals, api);
|
|
3251
|
+
assert!(result.is_err(), "should propagate error");
|
|
3252
|
+
|
|
3253
|
+
api.decref(globals);
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
// ========== eval_with_globals tests ==========
|
|
3258
|
+
|
|
3259
|
+
#[test]
|
|
3260
|
+
#[serial]
|
|
3261
|
+
fn test_eval_with_globals_simple_addition() {
|
|
3262
|
+
with_ruby_python(|ruby, api| {
|
|
3263
|
+
let globals = make_globals(api);
|
|
3264
|
+
|
|
3265
|
+
// Inject x=10 and y=20 into globals
|
|
3266
|
+
let py_x = crate::rubyx_object::ruby_to_python(10_i64.into_value_with(ruby), api)
|
|
3267
|
+
.expect("should convert x");
|
|
3268
|
+
let py_y = crate::rubyx_object::ruby_to_python(20_i64.into_value_with(ruby), api)
|
|
3269
|
+
.expect("should convert y");
|
|
3270
|
+
let key_x = api.string_from_str("x");
|
|
3271
|
+
let key_y = api.string_from_str("y");
|
|
3272
|
+
api.dict_set_item(globals, key_x, py_x);
|
|
3273
|
+
api.dict_set_item(globals, key_y, py_y);
|
|
3274
|
+
api.decref(key_x);
|
|
3275
|
+
api.decref(key_y);
|
|
3276
|
+
api.decref(py_x);
|
|
3277
|
+
api.decref(py_y);
|
|
3278
|
+
|
|
3279
|
+
let result =
|
|
3280
|
+
crate::eval::eval_with_globals("x + y", globals, api).expect("eval should succeed");
|
|
3281
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
3282
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 30);
|
|
3283
|
+
|
|
3284
|
+
api.decref(globals);
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
#[test]
|
|
3289
|
+
#[serial]
|
|
3290
|
+
fn test_eval_with_globals_string_interpolation() {
|
|
3291
|
+
with_ruby_python(|ruby, api| {
|
|
3292
|
+
let globals = make_globals(api);
|
|
3293
|
+
|
|
3294
|
+
let py_name = crate::rubyx_object::ruby_to_python("Alice".into_value_with(ruby), api)
|
|
3295
|
+
.expect("should convert name");
|
|
3296
|
+
let key = api.string_from_str("name");
|
|
3297
|
+
api.dict_set_item(globals, key, py_name);
|
|
3298
|
+
api.decref(key);
|
|
3299
|
+
api.decref(py_name);
|
|
3300
|
+
|
|
3301
|
+
let result = crate::eval::eval_with_globals("f'Hello, {name}!'", globals, api)
|
|
3302
|
+
.expect("eval should succeed");
|
|
3303
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
3304
|
+
assert_eq!(
|
|
3305
|
+
api.string_to_string(obj.as_ptr()),
|
|
3306
|
+
Some("Hello, Alice!".to_string())
|
|
3307
|
+
);
|
|
3308
|
+
|
|
3309
|
+
api.decref(globals);
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
#[test]
|
|
3314
|
+
#[serial]
|
|
3315
|
+
fn test_eval_with_globals_list_operations() {
|
|
3316
|
+
with_ruby_python(|ruby, api| {
|
|
3317
|
+
let globals = make_globals(api);
|
|
3318
|
+
|
|
3319
|
+
let arr = magnus::RArray::new();
|
|
3320
|
+
arr.push(1_i64.into_value_with(ruby)).unwrap();
|
|
3321
|
+
arr.push(2_i64.into_value_with(ruby)).unwrap();
|
|
3322
|
+
arr.push(3_i64.into_value_with(ruby)).unwrap();
|
|
3323
|
+
let py_items = crate::rubyx_object::ruby_to_python(arr.into_value_with(ruby), api)
|
|
3324
|
+
.expect("should convert list");
|
|
3325
|
+
let key = api.string_from_str("items");
|
|
3326
|
+
api.dict_set_item(globals, key, py_items);
|
|
3327
|
+
api.decref(key);
|
|
3328
|
+
api.decref(py_items);
|
|
3329
|
+
|
|
3330
|
+
let result = crate::eval::eval_with_globals("sum(items)", globals, api)
|
|
3331
|
+
.expect("eval should succeed");
|
|
3332
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
3333
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 6);
|
|
3334
|
+
|
|
3335
|
+
api.decref(globals);
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
#[test]
|
|
3340
|
+
#[serial]
|
|
3341
|
+
fn test_eval_with_globals_dict_access() {
|
|
3342
|
+
with_ruby_python(|ruby, api| {
|
|
3343
|
+
let globals = make_globals(api);
|
|
3344
|
+
|
|
3345
|
+
let hash = magnus::RHash::new();
|
|
3346
|
+
hash.aset(ruby.sym_new("a"), 100_i64.into_value_with(ruby))
|
|
3347
|
+
.unwrap();
|
|
3348
|
+
hash.aset(ruby.sym_new("b"), 200_i64.into_value_with(ruby))
|
|
3349
|
+
.unwrap();
|
|
3350
|
+
let py_dict = crate::rubyx_object::ruby_to_python(hash.into_value_with(ruby), api)
|
|
3351
|
+
.expect("should convert dict");
|
|
3352
|
+
let key = api.string_from_str("data");
|
|
3353
|
+
api.dict_set_item(globals, key, py_dict);
|
|
3354
|
+
api.decref(key);
|
|
3355
|
+
api.decref(py_dict);
|
|
3356
|
+
|
|
3357
|
+
let result = crate::eval::eval_with_globals("data['a'] + data['b']", globals, api)
|
|
3358
|
+
.expect("eval should succeed");
|
|
3359
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
3360
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 300);
|
|
3361
|
+
|
|
3362
|
+
api.decref(globals);
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
#[test]
|
|
3367
|
+
#[serial]
|
|
3368
|
+
fn test_eval_with_globals_undefined_variable_errors() {
|
|
3369
|
+
with_ruby_python(|_ruby, api| {
|
|
3370
|
+
let globals = make_globals(api);
|
|
3371
|
+
|
|
3372
|
+
let result = crate::eval::eval_with_globals("undefined_var", globals, api);
|
|
3373
|
+
assert!(result.is_err(), "should fail for undefined variable");
|
|
3374
|
+
|
|
3375
|
+
api.decref(globals);
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
#[test]
|
|
3380
|
+
#[serial]
|
|
3381
|
+
fn test_eval_with_globals_multiline_with_globals() {
|
|
3382
|
+
with_ruby_python(|ruby, api| {
|
|
3383
|
+
let globals = make_globals(api);
|
|
3384
|
+
|
|
3385
|
+
let py_n = crate::rubyx_object::ruby_to_python(5_i64.into_value_with(ruby), api)
|
|
3386
|
+
.expect("should convert n");
|
|
3387
|
+
let key = api.string_from_str("n");
|
|
3388
|
+
api.dict_set_item(globals, key, py_n);
|
|
3389
|
+
api.decref(key);
|
|
3390
|
+
api.decref(py_n);
|
|
3391
|
+
|
|
3392
|
+
let code = "result = 0\nfor i in range(n):\n result += i\nresult";
|
|
3393
|
+
let result = crate::eval::eval_with_globals(code, globals, api)
|
|
3394
|
+
.expect("multiline eval should succeed");
|
|
3395
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
3396
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 10); // 0+1+2+3+4 = 10
|
|
3397
|
+
|
|
3398
|
+
api.decref(globals);
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
// ========== await_with_globals tests ==========
|
|
3403
|
+
|
|
3404
|
+
#[test]
|
|
3405
|
+
#[serial]
|
|
3406
|
+
fn test_await_with_globals_simple() {
|
|
3407
|
+
with_ruby_python(|ruby, api| {
|
|
3408
|
+
let globals = make_globals(api);
|
|
3409
|
+
|
|
3410
|
+
// Define async function
|
|
3411
|
+
api.run_string(
|
|
3412
|
+
"import asyncio\nasync def multiply(a, b): return a * b\n",
|
|
3413
|
+
PY_FILE_INPUT,
|
|
3414
|
+
globals,
|
|
3415
|
+
globals,
|
|
3416
|
+
)
|
|
3417
|
+
.expect("should define async function");
|
|
3418
|
+
|
|
3419
|
+
// Inject globals
|
|
3420
|
+
let py_a = crate::rubyx_object::ruby_to_python(7_i64.into_value_with(ruby), api)
|
|
3421
|
+
.expect("should convert a");
|
|
3422
|
+
let py_b = crate::rubyx_object::ruby_to_python(6_i64.into_value_with(ruby), api)
|
|
3423
|
+
.expect("should convert b");
|
|
3424
|
+
let key_a = api.string_from_str("a");
|
|
3425
|
+
let key_b = api.string_from_str("b");
|
|
3426
|
+
api.dict_set_item(globals, key_a, py_a);
|
|
3427
|
+
api.dict_set_item(globals, key_b, py_b);
|
|
3428
|
+
api.decref(key_a);
|
|
3429
|
+
api.decref(key_b);
|
|
3430
|
+
api.decref(py_a);
|
|
3431
|
+
api.decref(py_b);
|
|
3432
|
+
|
|
3433
|
+
let result = crate::eval::await_eval_with_globals("multiply(a, b)", globals, api)
|
|
3434
|
+
.expect("await should succeed");
|
|
3435
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
3436
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 42);
|
|
3437
|
+
|
|
3438
|
+
api.decref(globals);
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
#[test]
|
|
3443
|
+
#[serial]
|
|
3444
|
+
fn test_await_with_globals_string_result() {
|
|
3445
|
+
with_ruby_python(|ruby, api| {
|
|
3446
|
+
let globals = make_globals(api);
|
|
3447
|
+
|
|
3448
|
+
api.run_string(
|
|
3449
|
+
"import asyncio\nasync def greet(who): return f'hi {who}'\n",
|
|
3450
|
+
PY_FILE_INPUT,
|
|
3451
|
+
globals,
|
|
3452
|
+
globals,
|
|
3453
|
+
)
|
|
3454
|
+
.expect("should define async function");
|
|
3455
|
+
|
|
3456
|
+
let py_who = crate::rubyx_object::ruby_to_python("world".into_value_with(ruby), api)
|
|
3457
|
+
.expect("should convert who");
|
|
3458
|
+
let key = api.string_from_str("who");
|
|
3459
|
+
api.dict_set_item(globals, key, py_who);
|
|
3460
|
+
api.decref(key);
|
|
3461
|
+
api.decref(py_who);
|
|
3462
|
+
|
|
3463
|
+
let result = crate::eval::await_eval_with_globals("greet(who)", globals, api)
|
|
3464
|
+
.expect("await should succeed");
|
|
3465
|
+
let obj = Obj::<RubyxObject>::try_convert(result).expect("should be RubyxObject");
|
|
3466
|
+
assert_eq!(
|
|
3467
|
+
api.string_to_string(obj.as_ptr()),
|
|
3468
|
+
Some("hi world".to_string())
|
|
3469
|
+
);
|
|
3470
|
+
|
|
3471
|
+
api.decref(globals);
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
#[test]
|
|
3476
|
+
#[serial]
|
|
3477
|
+
fn test_await_with_globals_error_propagation() {
|
|
3478
|
+
with_ruby_python(|ruby, api| {
|
|
3479
|
+
let globals = make_globals(api);
|
|
3480
|
+
|
|
3481
|
+
api.run_string(
|
|
3482
|
+
"import asyncio\nasync def check(val):\n if val < 0: raise ValueError('negative')\n return val\n",
|
|
3483
|
+
PY_FILE_INPUT,
|
|
3484
|
+
globals,
|
|
3485
|
+
globals,
|
|
3486
|
+
)
|
|
3487
|
+
.expect("should define async function");
|
|
3488
|
+
|
|
3489
|
+
let py_val = crate::rubyx_object::ruby_to_python((-1_i64).into_value_with(ruby), api)
|
|
3490
|
+
.expect("should convert val");
|
|
3491
|
+
let key = api.string_from_str("val");
|
|
3492
|
+
api.dict_set_item(globals, key, py_val);
|
|
3493
|
+
api.decref(key);
|
|
3494
|
+
api.decref(py_val);
|
|
3495
|
+
|
|
3496
|
+
let result = crate::eval::await_eval_with_globals("check(val)", globals, api);
|
|
3497
|
+
assert!(result.is_err(), "should propagate ValueError");
|
|
3498
|
+
|
|
3499
|
+
api.decref(globals);
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
// ========== async_await_with_globals tests ==========
|
|
3504
|
+
|
|
3505
|
+
#[test]
|
|
3506
|
+
#[serial]
|
|
3507
|
+
fn test_async_await_with_globals_future() {
|
|
3508
|
+
with_ruby_python(|ruby, api| {
|
|
3509
|
+
let globals = make_globals(api);
|
|
3510
|
+
|
|
3511
|
+
api.run_string(
|
|
3512
|
+
"import asyncio\nasync def add(x, y): return x + y\n",
|
|
3513
|
+
PY_FILE_INPUT,
|
|
3514
|
+
globals,
|
|
3515
|
+
globals,
|
|
3516
|
+
)
|
|
3517
|
+
.expect("should define async function");
|
|
3518
|
+
|
|
3519
|
+
let py_x = crate::rubyx_object::ruby_to_python(15_i64.into_value_with(ruby), api)
|
|
3520
|
+
.expect("should convert x");
|
|
3521
|
+
let py_y = crate::rubyx_object::ruby_to_python(27_i64.into_value_with(ruby), api)
|
|
3522
|
+
.expect("should convert y");
|
|
3523
|
+
let key_x = api.string_from_str("x");
|
|
3524
|
+
let key_y = api.string_from_str("y");
|
|
3525
|
+
api.dict_set_item(globals, key_x, py_x);
|
|
3526
|
+
api.dict_set_item(globals, key_y, py_y);
|
|
3527
|
+
api.decref(key_x);
|
|
3528
|
+
api.decref(key_y);
|
|
3529
|
+
api.decref(py_x);
|
|
3530
|
+
api.decref(py_y);
|
|
3531
|
+
|
|
3532
|
+
// Create coroutine
|
|
3533
|
+
let coroutine = api
|
|
3534
|
+
.run_string("add(x, y)", PY_EVAL_INPUT, globals, globals)
|
|
3535
|
+
.expect("should create coroutine");
|
|
3536
|
+
|
|
3537
|
+
// Release GIL so the background thread can acquire it
|
|
3538
|
+
let tstate = api.save_thread();
|
|
3539
|
+
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3540
|
+
let result = future.value().expect("future should resolve");
|
|
3541
|
+
drop(future);
|
|
3542
|
+
api.restore_thread(tstate);
|
|
3543
|
+
|
|
3544
|
+
assert_eq!(i64::try_convert(result).unwrap(), 42);
|
|
3545
|
+
|
|
3546
|
+
api.decref(coroutine);
|
|
3547
|
+
api.decref(globals);
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
#[test]
|
|
3552
|
+
#[serial]
|
|
3553
|
+
fn test_async_await_with_globals_future_string() {
|
|
3554
|
+
with_ruby_python(|ruby, api| {
|
|
3555
|
+
let globals = make_globals(api);
|
|
3556
|
+
|
|
3557
|
+
api.run_string(
|
|
3558
|
+
"import asyncio\nasync def fmt(prefix, val): return f'{prefix}: {val}'\n",
|
|
3559
|
+
PY_FILE_INPUT,
|
|
3560
|
+
globals,
|
|
3561
|
+
globals,
|
|
3562
|
+
)
|
|
3563
|
+
.expect("should define async function");
|
|
3564
|
+
|
|
3565
|
+
let py_prefix =
|
|
3566
|
+
crate::rubyx_object::ruby_to_python("result".into_value_with(ruby), api)
|
|
3567
|
+
.expect("should convert prefix");
|
|
3568
|
+
let py_val = crate::rubyx_object::ruby_to_python(99_i64.into_value_with(ruby), api)
|
|
3569
|
+
.expect("should convert val");
|
|
3570
|
+
let key_p = api.string_from_str("prefix");
|
|
3571
|
+
let key_v = api.string_from_str("val");
|
|
3572
|
+
api.dict_set_item(globals, key_p, py_prefix);
|
|
3573
|
+
api.dict_set_item(globals, key_v, py_val);
|
|
3574
|
+
api.decref(key_p);
|
|
3575
|
+
api.decref(key_v);
|
|
3576
|
+
api.decref(py_prefix);
|
|
3577
|
+
api.decref(py_val);
|
|
3578
|
+
|
|
3579
|
+
let coroutine = api
|
|
3580
|
+
.run_string("fmt(prefix, val)", PY_EVAL_INPUT, globals, globals)
|
|
3581
|
+
.expect("should create coroutine");
|
|
3582
|
+
|
|
3583
|
+
let tstate = api.save_thread();
|
|
3584
|
+
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3585
|
+
let result = future.value().expect("future should resolve");
|
|
3586
|
+
drop(future);
|
|
3587
|
+
api.restore_thread(tstate);
|
|
3588
|
+
|
|
3589
|
+
assert_eq!(String::try_convert(result).unwrap(), "result: 99");
|
|
3590
|
+
|
|
3591
|
+
api.decref(coroutine);
|
|
3592
|
+
api.decref(globals);
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
#[test]
|
|
3597
|
+
#[serial]
|
|
3598
|
+
fn test_async_await_with_globals_error() {
|
|
3599
|
+
with_ruby_python(|ruby, api| {
|
|
3600
|
+
let globals = make_globals(api);
|
|
3601
|
+
|
|
3602
|
+
api.run_string(
|
|
3603
|
+
"import asyncio\nasync def div(a, b): return a / b\n",
|
|
3604
|
+
PY_FILE_INPUT,
|
|
3605
|
+
globals,
|
|
3606
|
+
globals,
|
|
3607
|
+
)
|
|
3608
|
+
.expect("should define async function");
|
|
3609
|
+
|
|
3610
|
+
let py_a = crate::rubyx_object::ruby_to_python(10_i64.into_value_with(ruby), api)
|
|
3611
|
+
.expect("should convert a");
|
|
3612
|
+
let py_b = crate::rubyx_object::ruby_to_python(0_i64.into_value_with(ruby), api)
|
|
3613
|
+
.expect("should convert b");
|
|
3614
|
+
let key_a = api.string_from_str("a");
|
|
3615
|
+
let key_b = api.string_from_str("b");
|
|
3616
|
+
api.dict_set_item(globals, key_a, py_a);
|
|
3617
|
+
api.dict_set_item(globals, key_b, py_b);
|
|
3618
|
+
api.decref(key_a);
|
|
3619
|
+
api.decref(key_b);
|
|
3620
|
+
api.decref(py_a);
|
|
3621
|
+
api.decref(py_b);
|
|
3622
|
+
|
|
3623
|
+
let coroutine = api
|
|
3624
|
+
.run_string("div(a, b)", PY_EVAL_INPUT, globals, globals)
|
|
3625
|
+
.expect("should create coroutine");
|
|
3626
|
+
|
|
3627
|
+
let tstate = api.save_thread();
|
|
3628
|
+
let future = crate::future::RubyxFuture::from_coroutine(coroutine, api);
|
|
3629
|
+
let result = future.value();
|
|
3630
|
+
drop(future);
|
|
3631
|
+
api.restore_thread(tstate);
|
|
3632
|
+
|
|
3633
|
+
assert!(result.is_err(), "division by zero should propagate");
|
|
3634
|
+
|
|
3635
|
+
api.decref(coroutine);
|
|
3636
|
+
api.decref(globals);
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
// ========== GIL safety regression tests ==========
|
|
3641
|
+
// These tests ensure GIL is always released on error paths.
|
|
3642
|
+
// A leaked GIL would deadlock subsequent tests (serial execution).
|
|
3643
|
+
|
|
3644
|
+
#[test]
|
|
3645
|
+
#[serial]
|
|
3646
|
+
fn test_eval_with_globals_releases_gil_on_python_error() {
|
|
3647
|
+
with_ruby_python(|ruby, api| {
|
|
3648
|
+
let hash = magnus::RHash::new();
|
|
3649
|
+
hash.aset(ruby.sym_new("x"), 1_i64.into_value_with(ruby))
|
|
3650
|
+
.unwrap();
|
|
3651
|
+
|
|
3652
|
+
// This should fail (NameError: 'undefined_var') but NOT leak the GIL
|
|
3653
|
+
let result =
|
|
3654
|
+
crate::eval::rubyx_eval_with_globals("x + undefined_var".to_string(), hash);
|
|
3655
|
+
assert!(result.is_err(), "should fail for undefined variable");
|
|
3656
|
+
|
|
3657
|
+
// Prove GIL is released: we can acquire it again without deadlocking
|
|
3658
|
+
let gil = api.ensure_gil();
|
|
3659
|
+
let check = api.run_string(
|
|
3660
|
+
"1 + 1",
|
|
3661
|
+
EVAL_INPUT,
|
|
3662
|
+
test_make_globals(api),
|
|
3663
|
+
test_make_globals(api),
|
|
3664
|
+
);
|
|
3665
|
+
assert!(check.is_ok());
|
|
3666
|
+
api.decref(check.unwrap());
|
|
3667
|
+
api.release_gil(gil);
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
#[test]
|
|
3672
|
+
#[serial]
|
|
3673
|
+
fn test_eval_with_globals_releases_gil_on_syntax_error() {
|
|
3674
|
+
with_ruby_python(|ruby, api| {
|
|
3675
|
+
let hash = magnus::RHash::new();
|
|
3676
|
+
hash.aset(ruby.sym_new("x"), 1_i64.into_value_with(ruby))
|
|
3677
|
+
.unwrap();
|
|
3678
|
+
|
|
3679
|
+
let result = crate::eval::rubyx_eval_with_globals("def".to_string(), hash);
|
|
3680
|
+
assert!(result.is_err(), "should fail on syntax error");
|
|
3681
|
+
|
|
3682
|
+
// GIL should be released — acquire again to verify
|
|
3683
|
+
let gil = api.ensure_gil();
|
|
3684
|
+
let check = api.run_string(
|
|
3685
|
+
"2 + 2",
|
|
3686
|
+
EVAL_INPUT,
|
|
3687
|
+
test_make_globals(api),
|
|
3688
|
+
test_make_globals(api),
|
|
3689
|
+
);
|
|
3690
|
+
assert!(check.is_ok());
|
|
3691
|
+
api.decref(check.unwrap());
|
|
3692
|
+
api.release_gil(gil);
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
#[test]
|
|
3697
|
+
#[serial]
|
|
3698
|
+
fn test_await_with_globals_releases_gil_on_error() {
|
|
3699
|
+
with_ruby_python(|ruby, api| {
|
|
3700
|
+
let hash = magnus::RHash::new();
|
|
3701
|
+
hash.aset(ruby.sym_new("x"), 1_i64.into_value_with(ruby))
|
|
3702
|
+
.unwrap();
|
|
3703
|
+
|
|
3704
|
+
// Invalid code — should error but release GIL
|
|
3705
|
+
let result =
|
|
3706
|
+
crate::eval::rubyx_await_with_globals("undefined_coroutine()".to_string(), hash);
|
|
3707
|
+
assert!(result.is_err());
|
|
3708
|
+
|
|
3709
|
+
// Verify GIL is free
|
|
3710
|
+
let gil = api.ensure_gil();
|
|
3711
|
+
let check = api.run_string(
|
|
3712
|
+
"3 + 3",
|
|
3713
|
+
EVAL_INPUT,
|
|
3714
|
+
test_make_globals(api),
|
|
3715
|
+
test_make_globals(api),
|
|
3716
|
+
);
|
|
3717
|
+
assert!(check.is_ok());
|
|
3718
|
+
api.decref(check.unwrap());
|
|
3719
|
+
api.release_gil(gil);
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
#[test]
|
|
3724
|
+
#[serial]
|
|
3725
|
+
fn test_async_await_with_globals_releases_gil_on_error() {
|
|
3726
|
+
with_ruby_python(|ruby, api| {
|
|
3727
|
+
let hash = magnus::RHash::new();
|
|
3728
|
+
hash.aset(ruby.sym_new("x"), 1_i64.into_value_with(ruby))
|
|
3729
|
+
.unwrap();
|
|
3730
|
+
|
|
3731
|
+
// Invalid code — should error but release GIL
|
|
3732
|
+
let result =
|
|
3733
|
+
crate::eval::rubyx_async_await_with_globals("undefined_async()".to_string(), hash);
|
|
3734
|
+
assert!(result.is_err());
|
|
3735
|
+
|
|
3736
|
+
// Verify GIL is free
|
|
3737
|
+
let gil = api.ensure_gil();
|
|
3738
|
+
let check = api.run_string(
|
|
3739
|
+
"4 + 4",
|
|
3740
|
+
EVAL_INPUT,
|
|
3741
|
+
test_make_globals(api),
|
|
3742
|
+
test_make_globals(api),
|
|
3743
|
+
);
|
|
3744
|
+
assert!(check.is_ok());
|
|
3745
|
+
api.decref(check.unwrap());
|
|
3746
|
+
api.release_gil(gil);
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
#[test]
|
|
3751
|
+
#[serial]
|
|
3752
|
+
fn test_eval_with_globals_pyguard_drops_before_gil_release() {
|
|
3753
|
+
// Regression: PyGuard must drop (decref) BEFORE release_gil.
|
|
3754
|
+
// If this deadlocks or segfaults, the ordering is wrong.
|
|
3755
|
+
with_ruby_python(|ruby, _api| {
|
|
3756
|
+
for _ in 0..50 {
|
|
3757
|
+
let hash = magnus::RHash::new();
|
|
3758
|
+
hash.aset(ruby.sym_new("val"), 42_i64.into_value_with(ruby))
|
|
3759
|
+
.unwrap();
|
|
3760
|
+
let result = crate::eval::rubyx_eval_with_globals("val * 2".to_string(), hash);
|
|
3761
|
+
assert!(result.is_ok());
|
|
3762
|
+
}
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
#[test]
|
|
3767
|
+
#[serial]
|
|
3768
|
+
fn test_context_eval_with_globals_releases_gil_on_error() {
|
|
3769
|
+
with_ruby_python(|ruby, api| {
|
|
3770
|
+
let ctx = crate::context::RubyxContext::new().expect("context should create");
|
|
3771
|
+
|
|
3772
|
+
let hash = magnus::RHash::new();
|
|
3773
|
+
hash.aset(ruby.sym_new("x"), 1_i64.into_value_with(ruby))
|
|
3774
|
+
.unwrap();
|
|
3775
|
+
|
|
3776
|
+
let result = ctx.eval_with_globals("x + missing".to_string(), hash);
|
|
3777
|
+
assert!(result.is_err());
|
|
3778
|
+
|
|
3779
|
+
// Verify GIL is free
|
|
3780
|
+
let gil = api.ensure_gil();
|
|
3781
|
+
let check = api.run_string(
|
|
3782
|
+
"5 + 5",
|
|
3783
|
+
EVAL_INPUT,
|
|
3784
|
+
test_make_globals(api),
|
|
3785
|
+
test_make_globals(api),
|
|
3786
|
+
);
|
|
3787
|
+
assert!(check.is_ok());
|
|
3788
|
+
api.decref(check.unwrap());
|
|
3789
|
+
api.release_gil(gil);
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
#[test]
|
|
3794
|
+
#[serial]
|
|
3795
|
+
fn test_context_await_with_globals_releases_gil_on_error() {
|
|
3796
|
+
with_ruby_python(|ruby, api| {
|
|
3797
|
+
let ctx = crate::context::RubyxContext::new().expect("context should create");
|
|
3798
|
+
|
|
3799
|
+
let hash = magnus::RHash::new();
|
|
3800
|
+
hash.aset(ruby.sym_new("n"), 1_i64.into_value_with(ruby))
|
|
3801
|
+
.unwrap();
|
|
3802
|
+
|
|
3803
|
+
let result = ctx.await_eval_with_globals("no_such_coro()".to_string(), hash);
|
|
3804
|
+
assert!(result.is_err());
|
|
3805
|
+
|
|
3806
|
+
// Verify GIL is free
|
|
3807
|
+
let gil = api.ensure_gil();
|
|
3808
|
+
let check = api.run_string(
|
|
3809
|
+
"6 + 6",
|
|
3810
|
+
EVAL_INPUT,
|
|
3811
|
+
test_make_globals(api),
|
|
3812
|
+
test_make_globals(api),
|
|
3813
|
+
);
|
|
3814
|
+
assert!(check.is_ok());
|
|
3815
|
+
api.decref(check.unwrap());
|
|
3816
|
+
api.release_gil(gil);
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
#[test]
|
|
3821
|
+
#[serial]
|
|
3822
|
+
fn test_context_async_await_with_globals_releases_gil_on_error() {
|
|
3823
|
+
with_ruby_python(|ruby, api| {
|
|
3824
|
+
let ctx = crate::context::RubyxContext::new().expect("context should create");
|
|
3825
|
+
|
|
3826
|
+
let hash = magnus::RHash::new();
|
|
3827
|
+
hash.aset(ruby.sym_new("n"), 1_i64.into_value_with(ruby))
|
|
3828
|
+
.unwrap();
|
|
3829
|
+
|
|
3830
|
+
let result = ctx.async_await_eval_with_globals("no_such_coro()".to_string(), hash);
|
|
3831
|
+
assert!(result.is_err());
|
|
3832
|
+
|
|
3833
|
+
// Verify GIL is free
|
|
3834
|
+
let gil = api.ensure_gil();
|
|
3835
|
+
let check = api.run_string(
|
|
3836
|
+
"7 + 7",
|
|
3837
|
+
EVAL_INPUT,
|
|
3838
|
+
test_make_globals(api),
|
|
3839
|
+
test_make_globals(api),
|
|
3840
|
+
);
|
|
3841
|
+
assert!(check.is_ok());
|
|
3842
|
+
api.decref(check.unwrap());
|
|
3843
|
+
api.release_gil(gil);
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
#[test]
|
|
3848
|
+
#[serial]
|
|
3849
|
+
fn test_eval_with_globals_error_maps_to_rubyx_class() {
|
|
3850
|
+
// Regression: extract_exception was consumed on syntax check,
|
|
3851
|
+
// then re-fetched (returning None) → fell back to RuntimeError.
|
|
3852
|
+
with_ruby_python(|ruby, _api| {
|
|
3853
|
+
let hash = magnus::RHash::new();
|
|
3854
|
+
hash.aset(
|
|
3855
|
+
ruby.sym_new("d"),
|
|
3856
|
+
magnus::RHash::new().into_value_with(ruby),
|
|
3857
|
+
)
|
|
3858
|
+
.unwrap();
|
|
3859
|
+
|
|
3860
|
+
// Python KeyError should map to Rubyx::KeyError, not RuntimeError
|
|
3861
|
+
let result = crate::eval::rubyx_eval_with_globals("d['missing']".to_string(), hash);
|
|
3862
|
+
assert!(result.is_err());
|
|
3863
|
+
let err_msg = format!("{}", result.unwrap_err());
|
|
3864
|
+
assert!(
|
|
3865
|
+
err_msg.contains("KeyError"),
|
|
3866
|
+
"Expected KeyError in message, got: {}",
|
|
3867
|
+
err_msg
|
|
3868
|
+
);
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
// ========== error class mapping tests ==========
|
|
3873
|
+
|
|
3874
|
+
#[test]
|
|
3875
|
+
#[serial]
|
|
3876
|
+
fn test_error_mapping_key_error() {
|
|
3877
|
+
let Some(guard) = skip_if_no_python() else {
|
|
3878
|
+
return;
|
|
3879
|
+
};
|
|
3880
|
+
let api = guard.api();
|
|
3881
|
+
let globals = test_make_globals(api);
|
|
3882
|
+
|
|
3883
|
+
let result = api.run_string("{}['missing']", EVAL_INPUT, globals, globals);
|
|
3884
|
+
let py_obj = result.unwrap();
|
|
3885
|
+
assert!(py_obj.is_null());
|
|
3886
|
+
let exc = PythonApi::extract_exception(api);
|
|
3887
|
+
assert!(exc.is_some());
|
|
3888
|
+
if let Some(crate::exception::PythonException::Exception { kind, .. }) = &exc {
|
|
3889
|
+
assert_eq!(kind, "KeyError");
|
|
3890
|
+
} else {
|
|
3891
|
+
panic!("Expected KeyError, got: {:?}", exc);
|
|
3892
|
+
}
|
|
3893
|
+
api.decref(globals);
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
#[test]
|
|
3897
|
+
#[serial]
|
|
3898
|
+
fn test_error_mapping_index_error() {
|
|
3899
|
+
let Some(guard) = skip_if_no_python() else {
|
|
3900
|
+
return;
|
|
3901
|
+
};
|
|
3902
|
+
let api = guard.api();
|
|
3903
|
+
let globals = test_make_globals(api);
|
|
3904
|
+
|
|
3905
|
+
let result = api.run_string("[][5]", EVAL_INPUT, globals, globals);
|
|
3906
|
+
let py_obj = result.unwrap();
|
|
3907
|
+
assert!(py_obj.is_null());
|
|
3908
|
+
let exc = PythonApi::extract_exception(api);
|
|
3909
|
+
if let Some(crate::exception::PythonException::Exception { kind, .. }) = &exc {
|
|
3910
|
+
assert_eq!(kind, "IndexError");
|
|
3911
|
+
} else {
|
|
3912
|
+
panic!("Expected IndexError, got: {:?}", exc);
|
|
3913
|
+
}
|
|
3914
|
+
api.decref(globals);
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
#[test]
|
|
3918
|
+
#[serial]
|
|
3919
|
+
fn test_error_mapping_value_error() {
|
|
3920
|
+
let Some(guard) = skip_if_no_python() else {
|
|
3921
|
+
return;
|
|
3922
|
+
};
|
|
3923
|
+
let api = guard.api();
|
|
3924
|
+
let globals = test_make_globals(api);
|
|
3925
|
+
|
|
3926
|
+
let result = api.run_string("int('bad')", EVAL_INPUT, globals, globals);
|
|
3927
|
+
let py_obj = result.unwrap();
|
|
3928
|
+
assert!(py_obj.is_null());
|
|
3929
|
+
let exc = PythonApi::extract_exception(api);
|
|
3930
|
+
if let Some(crate::exception::PythonException::Exception { kind, .. }) = &exc {
|
|
3931
|
+
assert_eq!(kind, "ValueError");
|
|
3932
|
+
} else {
|
|
3933
|
+
panic!("Expected ValueError, got: {:?}", exc);
|
|
3934
|
+
}
|
|
3935
|
+
api.decref(globals);
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
#[test]
|
|
3939
|
+
#[serial]
|
|
3940
|
+
fn test_error_mapping_type_error() {
|
|
3941
|
+
let Some(guard) = skip_if_no_python() else {
|
|
3942
|
+
return;
|
|
3943
|
+
};
|
|
3944
|
+
let api = guard.api();
|
|
3945
|
+
let globals = test_make_globals(api);
|
|
3946
|
+
|
|
3947
|
+
let result = api.run_string("1 + 'a'", EVAL_INPUT, globals, globals);
|
|
3948
|
+
let py_obj = result.unwrap();
|
|
3949
|
+
assert!(py_obj.is_null());
|
|
3950
|
+
let exc = PythonApi::extract_exception(api);
|
|
3951
|
+
if let Some(crate::exception::PythonException::Exception { kind, .. }) = &exc {
|
|
3952
|
+
assert_eq!(kind, "TypeError");
|
|
3953
|
+
} else {
|
|
3954
|
+
panic!("Expected TypeError, got: {:?}", exc);
|
|
3955
|
+
}
|
|
3956
|
+
api.decref(globals);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
#[test]
|
|
3960
|
+
#[serial]
|
|
3961
|
+
fn test_error_mapping_attribute_error() {
|
|
3962
|
+
let Some(guard) = skip_if_no_python() else {
|
|
3963
|
+
return;
|
|
3964
|
+
};
|
|
3965
|
+
let api = guard.api();
|
|
3966
|
+
let globals = test_make_globals(api);
|
|
3967
|
+
|
|
3968
|
+
let result = api.run_string("(1).nonexistent", EVAL_INPUT, globals, globals);
|
|
3969
|
+
let py_obj = result.unwrap();
|
|
3970
|
+
assert!(py_obj.is_null());
|
|
3971
|
+
let exc = PythonApi::extract_exception(api);
|
|
3972
|
+
if let Some(crate::exception::PythonException::Exception { kind, .. }) = &exc {
|
|
3973
|
+
assert_eq!(kind, "AttributeError");
|
|
3974
|
+
} else {
|
|
3975
|
+
panic!("Expected AttributeError, got: {:?}", exc);
|
|
3976
|
+
}
|
|
3977
|
+
api.decref(globals);
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
#[test]
|
|
3981
|
+
#[serial]
|
|
3982
|
+
fn test_error_mapping_zero_division_falls_to_python_error() {
|
|
3983
|
+
let Some(guard) = skip_if_no_python() else {
|
|
3984
|
+
return;
|
|
3985
|
+
};
|
|
3986
|
+
let api = guard.api();
|
|
3987
|
+
let globals = test_make_globals(api);
|
|
3988
|
+
|
|
3989
|
+
let result = api.run_string("1/0", EVAL_INPUT, globals, globals);
|
|
3990
|
+
let py_obj = result.unwrap();
|
|
3991
|
+
assert!(py_obj.is_null());
|
|
3992
|
+
let exc = PythonApi::extract_exception(api);
|
|
3993
|
+
if let Some(crate::exception::PythonException::Exception { kind, .. }) = &exc {
|
|
3994
|
+
assert_eq!(kind, "ZeroDivisionError");
|
|
3995
|
+
} else {
|
|
3996
|
+
panic!("Expected ZeroDivisionError, got: {:?}", exc);
|
|
3997
|
+
}
|
|
3998
|
+
api.decref(globals);
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
#[test]
|
|
4002
|
+
#[serial]
|
|
4003
|
+
fn test_rubyx_exception_class_maps_known_kinds() {
|
|
4004
|
+
with_ruby_python(|ruby, _api| {
|
|
4005
|
+
use crate::ruby_helpers::rubyx_exception_class;
|
|
4006
|
+
use magnus::{Class, Module};
|
|
4007
|
+
|
|
4008
|
+
// Define Rubyx error classes (normally done by error.rb at gem load time).
|
|
4009
|
+
// define_class needs RClass, so we eval to create exception subclasses.
|
|
4010
|
+
ruby.eval::<magnus::Value>(
|
|
4011
|
+
"
|
|
4012
|
+
module Rubyx
|
|
4013
|
+
class Error < StandardError; end
|
|
4014
|
+
class PythonError < Error; end
|
|
4015
|
+
class KeyError < Error; end
|
|
4016
|
+
class IndexError < Error; end
|
|
4017
|
+
class ValueError < Error; end
|
|
4018
|
+
class TypeError < Error; end
|
|
4019
|
+
class AttributeError < Error; end
|
|
4020
|
+
class ImportError < PythonError; end
|
|
4021
|
+
end
|
|
4022
|
+
",
|
|
4023
|
+
)
|
|
4024
|
+
.expect("should define error classes");
|
|
4025
|
+
|
|
4026
|
+
let key_err = rubyx_exception_class("KeyError");
|
|
4027
|
+
let idx_err = rubyx_exception_class("IndexError");
|
|
4028
|
+
let val_err = rubyx_exception_class("ValueError");
|
|
4029
|
+
let typ_err = rubyx_exception_class("TypeError");
|
|
4030
|
+
let attr_err = rubyx_exception_class("AttributeError");
|
|
4031
|
+
let imp_err = rubyx_exception_class("ImportError");
|
|
4032
|
+
let mnf_err = rubyx_exception_class("ModuleNotFoundError");
|
|
4033
|
+
let unknown = rubyx_exception_class("ZeroDivisionError");
|
|
4034
|
+
|
|
4035
|
+
let class_name =
|
|
4036
|
+
|c: magnus::ExceptionClass| -> String { unsafe { c.name().to_string() } };
|
|
4037
|
+
|
|
4038
|
+
assert_eq!(class_name(key_err), "Rubyx::KeyError");
|
|
4039
|
+
assert_eq!(class_name(idx_err), "Rubyx::IndexError");
|
|
4040
|
+
assert_eq!(class_name(val_err), "Rubyx::ValueError");
|
|
4041
|
+
assert_eq!(class_name(typ_err), "Rubyx::TypeError");
|
|
4042
|
+
assert_eq!(class_name(attr_err), "Rubyx::AttributeError");
|
|
4043
|
+
assert_eq!(class_name(imp_err), "Rubyx::ImportError");
|
|
4044
|
+
assert_eq!(class_name(mnf_err), "Rubyx::ImportError");
|
|
4045
|
+
assert_eq!(class_name(unknown), "Rubyx::PythonError");
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
// ========== respond_to_missing? tests ==========
|
|
4050
|
+
|
|
4051
|
+
#[test]
|
|
4052
|
+
#[serial]
|
|
4053
|
+
fn test_respond_to_missing_via_ruby() {
|
|
4054
|
+
with_ruby_python(|ruby, api| {
|
|
4055
|
+
let os = api.import_module("os").expect("os should import");
|
|
4056
|
+
let wrapper = RubyxObject::new(os, api).unwrap();
|
|
4057
|
+
|
|
4058
|
+
// Test with symbol (Ruby convention)
|
|
4059
|
+
let args = vec!["path".into_value_with(ruby)];
|
|
4060
|
+
assert!(
|
|
4061
|
+
wrapper.respond_to_missing(&args).unwrap(),
|
|
4062
|
+
"os.path should exist"
|
|
4063
|
+
);
|
|
4064
|
+
|
|
4065
|
+
// Test nonexistent
|
|
4066
|
+
let args = vec!["xyz_not_real".into_value_with(ruby)];
|
|
4067
|
+
assert!(
|
|
4068
|
+
!wrapper.respond_to_missing(&args).unwrap(),
|
|
4069
|
+
"nonexistent should be false"
|
|
4070
|
+
);
|
|
4071
|
+
|
|
4072
|
+
drop(wrapper);
|
|
4073
|
+
api.decref(os);
|
|
4074
|
+
});
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
#[test]
|
|
4078
|
+
#[serial]
|
|
4079
|
+
fn test_implicit_conversion_guards_dont_delegate() {
|
|
4080
|
+
with_ruby_python(|ruby, api| {
|
|
4081
|
+
let py_list = unsafe { (api.py_list_new)(0) };
|
|
4082
|
+
let wrapper = RubyxObject::new(py_list, api).unwrap();
|
|
4083
|
+
|
|
4084
|
+
// All of these should raise NoMethodError, not delegate to Python
|
|
4085
|
+
for method in &[
|
|
4086
|
+
"to_ary", "to_str", "to_hash", "to_int", "to_float", "to_io", "to_proc",
|
|
4087
|
+
] {
|
|
4088
|
+
let args = vec![(*method).into_value_with(ruby)];
|
|
4089
|
+
let result = wrapper.method_missing(&args);
|
|
4090
|
+
assert!(
|
|
4091
|
+
result.is_err(),
|
|
4092
|
+
"{} should be guarded, not delegated to Python",
|
|
4093
|
+
method
|
|
4094
|
+
);
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
drop(wrapper);
|
|
4098
|
+
api.decref(py_list);
|
|
4099
|
+
});
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
#[test]
|
|
4103
|
+
#[serial]
|
|
4104
|
+
fn test_respond_to_missing_on_module() {
|
|
4105
|
+
with_ruby_python(|ruby, api| {
|
|
4106
|
+
let json = api.import_module("json").expect("json should import");
|
|
4107
|
+
let wrapper = RubyxObject::new(json, api).unwrap();
|
|
4108
|
+
|
|
4109
|
+
// json.loads and json.dumps should exist
|
|
4110
|
+
assert!(wrapper
|
|
4111
|
+
.respond_to_missing(&["loads".into_value_with(ruby)])
|
|
4112
|
+
.unwrap());
|
|
4113
|
+
assert!(wrapper
|
|
4114
|
+
.respond_to_missing(&["dumps".into_value_with(ruby)])
|
|
4115
|
+
.unwrap());
|
|
4116
|
+
|
|
4117
|
+
// json.nonexistent should not
|
|
4118
|
+
assert!(!wrapper
|
|
4119
|
+
.respond_to_missing(&["nonexistent".into_value_with(ruby)])
|
|
4120
|
+
.unwrap());
|
|
4121
|
+
|
|
4122
|
+
drop(wrapper);
|
|
4123
|
+
api.decref(json);
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
// ========== getitem / setitem / delitem integration ==========
|
|
4128
|
+
|
|
4129
|
+
#[test]
|
|
4130
|
+
#[serial]
|
|
4131
|
+
fn test_getitem_setitem_roundtrip() {
|
|
4132
|
+
with_ruby_python(|ruby, api| {
|
|
4133
|
+
let globals = make_globals(api);
|
|
4134
|
+
let py_dict = api
|
|
4135
|
+
.run_string("{'x': 10}", 258, globals, globals)
|
|
4136
|
+
.expect("should create dict");
|
|
4137
|
+
let wrapper = RubyxObject::new(py_dict, api).unwrap();
|
|
4138
|
+
|
|
4139
|
+
// Read existing
|
|
4140
|
+
let key: magnus::Value = "x".into_value_with(ruby);
|
|
4141
|
+
let result = wrapper.getitem(key).expect("should read 'x'");
|
|
4142
|
+
let obj = magnus::typed_data::Obj::<RubyxObject>::try_convert(result).unwrap();
|
|
4143
|
+
assert_eq!(api.long_to_i64(obj.as_ptr()), 10);
|
|
4144
|
+
|
|
4145
|
+
// Write new
|
|
4146
|
+
let new_key: magnus::Value = "y".into_value_with(ruby);
|
|
4147
|
+
let new_val: magnus::Value = 20_i64.into_value_with(ruby);
|
|
4148
|
+
wrapper.setitem(new_key, new_val).expect("should set 'y'");
|
|
4149
|
+
|
|
4150
|
+
// Read back
|
|
4151
|
+
let check: magnus::Value = "y".into_value_with(ruby);
|
|
4152
|
+
let result2 = wrapper.getitem(check).expect("should read 'y'");
|
|
4153
|
+
let obj2 = magnus::typed_data::Obj::<RubyxObject>::try_convert(result2).unwrap();
|
|
4154
|
+
assert_eq!(api.long_to_i64(obj2.as_ptr()), 20);
|
|
4155
|
+
|
|
4156
|
+
drop(wrapper);
|
|
4157
|
+
api.decref(py_dict);
|
|
4158
|
+
api.decref(globals);
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
#[test]
|
|
4163
|
+
#[serial]
|
|
4164
|
+
fn test_delitem_then_getitem_fails() {
|
|
4165
|
+
with_ruby_python(|ruby, api| {
|
|
4166
|
+
let globals = make_globals(api);
|
|
4167
|
+
let py_dict = api
|
|
4168
|
+
.run_string("{'remove_me': 999}", 258, globals, globals)
|
|
4169
|
+
.expect("should create dict");
|
|
4170
|
+
let wrapper = RubyxObject::new(py_dict, api).unwrap();
|
|
4171
|
+
|
|
4172
|
+
let key: magnus::Value = "remove_me".into_value_with(ruby);
|
|
4173
|
+
wrapper.delitem(key).expect("should delete key");
|
|
4174
|
+
|
|
4175
|
+
let check: magnus::Value = "remove_me".into_value_with(ruby);
|
|
4176
|
+
assert!(
|
|
4177
|
+
wrapper.getitem(check).is_err(),
|
|
4178
|
+
"deleted key should not be found"
|
|
4179
|
+
);
|
|
4180
|
+
|
|
4181
|
+
drop(wrapper);
|
|
4182
|
+
api.decref(py_dict);
|
|
4183
|
+
api.decref(globals);
|
|
4184
|
+
});
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
#[test]
|
|
4188
|
+
#[serial]
|
|
4189
|
+
fn test_getitem_list_integration() {
|
|
4190
|
+
with_ruby_python(|ruby, api| {
|
|
4191
|
+
let globals = make_globals(api);
|
|
4192
|
+
let py_list = api
|
|
4193
|
+
.run_string("['a', 'b', 'c']", 258, globals, globals)
|
|
4194
|
+
.expect("should create list");
|
|
4195
|
+
let wrapper = RubyxObject::new(py_list, api).unwrap();
|
|
4196
|
+
|
|
4197
|
+
for (i, expected) in ["a", "b", "c"].iter().enumerate() {
|
|
4198
|
+
let key: magnus::Value = (i as i64).into_value_with(ruby);
|
|
4199
|
+
let result = wrapper.getitem(key).expect("should read index");
|
|
4200
|
+
let obj = magnus::typed_data::Obj::<RubyxObject>::try_convert(result).unwrap();
|
|
4201
|
+
assert_eq!(
|
|
4202
|
+
api.string_to_string(obj.as_ptr()),
|
|
4203
|
+
Some(expected.to_string())
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
drop(wrapper);
|
|
4208
|
+
api.decref(py_list);
|
|
4209
|
+
api.decref(globals);
|
|
4210
|
+
});
|
|
4211
|
+
}
|
|
4212
|
+
}
|