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,950 @@
|
|
|
1
|
+
use crate::async_gen::AsyncGeneratorStream;
|
|
2
|
+
use crate::ruby_helpers::runtime_error;
|
|
3
|
+
use magnus::value::ReprValue;
|
|
4
|
+
use magnus::{Error, Ruby, Value};
|
|
5
|
+
|
|
6
|
+
#[magnus::wrap(class = "Rubyx::Stream", free_immediately)]
|
|
7
|
+
pub(crate) struct RubyxStream {
|
|
8
|
+
inner: std::cell::RefCell<Option<AsyncGeneratorStream>>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl RubyxStream {
|
|
12
|
+
pub fn each(&self) -> Result<Value, magnus::Error> {
|
|
13
|
+
let ruby = Ruby::get()
|
|
14
|
+
.map_err(|e| magnus::Error::new(runtime_error(), format!("Error getting Ruby: {e}")))?;
|
|
15
|
+
|
|
16
|
+
// Enumerator
|
|
17
|
+
if !ruby.block_given() {
|
|
18
|
+
let receiver: Value = ruby.current_receiver()?;
|
|
19
|
+
return Ok(receiver.enumeratorize("each", ()).as_value());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Stream
|
|
23
|
+
// Take ownership of the stream so it gets dropped when `each` returns.
|
|
24
|
+
// This is critical: when Ruby's `first`/`take` break out of `each` early,
|
|
25
|
+
// the stream must be cleaned up immediately (cancel + join worker thread)
|
|
26
|
+
// rather than waiting for Ruby's GC. Otherwise the worker thread holds
|
|
27
|
+
// the Python GIL and subsequent Python calls deadlock.
|
|
28
|
+
let mut stream = self
|
|
29
|
+
.inner
|
|
30
|
+
.borrow_mut()
|
|
31
|
+
.take()
|
|
32
|
+
.ok_or_else(|| magnus::Error::new(runtime_error(), "Stream already consumed"))?;
|
|
33
|
+
for result in &mut stream {
|
|
34
|
+
match result {
|
|
35
|
+
Ok(val) => {
|
|
36
|
+
let _: Value = ruby.yield_value(val)?;
|
|
37
|
+
}
|
|
38
|
+
Err(err) => return Err(err),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
Ok(ruby.qnil().as_value())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn next_item(&self) -> Result<Value, Error> {
|
|
45
|
+
let ruby = Ruby::get()
|
|
46
|
+
.map_err(|e| magnus::Error::new(runtime_error(), format!("Error getting Ruby: {e}")))?;
|
|
47
|
+
let mut inner = self.inner.borrow_mut();
|
|
48
|
+
let stream = inner
|
|
49
|
+
.as_mut()
|
|
50
|
+
.ok_or_else(|| magnus::Error::new(runtime_error(), "Stream already consumed"))?;
|
|
51
|
+
match stream.next() {
|
|
52
|
+
Some(Ok(val)) => Ok(val),
|
|
53
|
+
Some(Err(err)) => Err(err),
|
|
54
|
+
None => {
|
|
55
|
+
// Stream exhausted — take it out so it gets dropped and cleaned up
|
|
56
|
+
inner.take();
|
|
57
|
+
Err(magnus::Error::new(
|
|
58
|
+
ruby.exception_stop_iteration(),
|
|
59
|
+
"iteration reached an end",
|
|
60
|
+
))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
pub fn from_stream(stream: AsyncGeneratorStream) -> Self {
|
|
65
|
+
Self {
|
|
66
|
+
inner: std::cell::RefCell::new(Some(stream)),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[cfg(test)]
|
|
72
|
+
mod tests {
|
|
73
|
+
use crate::stream::SendableValue;
|
|
74
|
+
use crate::test_helpers::{skip_if_no_python, with_ruby_python};
|
|
75
|
+
use crossbeam_channel::{bounded, unbounded};
|
|
76
|
+
use magnus::value::ReprValue;
|
|
77
|
+
use magnus::TryConvert;
|
|
78
|
+
use serial_test::serial;
|
|
79
|
+
use std::thread;
|
|
80
|
+
|
|
81
|
+
// ========== SendableValue → magnus::Value conversion ==========
|
|
82
|
+
|
|
83
|
+
#[test]
|
|
84
|
+
#[serial]
|
|
85
|
+
fn test_sendable_nil_converts_to_ruby_nil() {
|
|
86
|
+
with_ruby_python(|_ruby, _api| {
|
|
87
|
+
let val: magnus::Value = SendableValue::Nil
|
|
88
|
+
.try_into()
|
|
89
|
+
.expect("Nil conversion should succeed");
|
|
90
|
+
assert!(
|
|
91
|
+
val.is_nil(),
|
|
92
|
+
"SendableValue::Nil should convert to Ruby nil"
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
#[serial]
|
|
99
|
+
fn test_sendable_integer_converts_to_ruby_integer() {
|
|
100
|
+
with_ruby_python(|_ruby, _api| {
|
|
101
|
+
let val: magnus::Value = SendableValue::Integer(42)
|
|
102
|
+
.try_into()
|
|
103
|
+
.expect("Integer conversion should succeed");
|
|
104
|
+
let n = i64::try_convert(val).expect("should be convertible to i64");
|
|
105
|
+
assert_eq!(n, 42);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[test]
|
|
110
|
+
#[serial]
|
|
111
|
+
fn test_sendable_integer_negative() {
|
|
112
|
+
with_ruby_python(|_ruby, _api| {
|
|
113
|
+
let val: magnus::Value = SendableValue::Integer(-99)
|
|
114
|
+
.try_into()
|
|
115
|
+
.expect("negative integer conversion should succeed");
|
|
116
|
+
let n = i64::try_convert(val).expect("should be convertible to i64");
|
|
117
|
+
assert_eq!(n, -99);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[test]
|
|
122
|
+
#[serial]
|
|
123
|
+
fn test_sendable_integer_zero() {
|
|
124
|
+
with_ruby_python(|_ruby, _api| {
|
|
125
|
+
let val: magnus::Value = SendableValue::Integer(0)
|
|
126
|
+
.try_into()
|
|
127
|
+
.expect("zero conversion should succeed");
|
|
128
|
+
let n = i64::try_convert(val).expect("should be convertible to i64");
|
|
129
|
+
assert_eq!(n, 0);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
#[serial]
|
|
135
|
+
fn test_sendable_float_converts_to_ruby_float() {
|
|
136
|
+
with_ruby_python(|_ruby, _api| {
|
|
137
|
+
let val: magnus::Value = SendableValue::Float(std::f64::consts::PI)
|
|
138
|
+
.try_into()
|
|
139
|
+
.expect("Float conversion should succeed");
|
|
140
|
+
let f = f64::try_convert(val).expect("should be convertible to f64");
|
|
141
|
+
assert!((f - std::f64::consts::PI).abs() < 1e-9);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[test]
|
|
146
|
+
#[serial]
|
|
147
|
+
fn test_sendable_float_negative() {
|
|
148
|
+
with_ruby_python(|_ruby, _api| {
|
|
149
|
+
let val: magnus::Value = SendableValue::Float(-0.5)
|
|
150
|
+
.try_into()
|
|
151
|
+
.expect("negative float conversion should succeed");
|
|
152
|
+
let f = f64::try_convert(val).expect("should be convertible to f64");
|
|
153
|
+
assert!((f - (-0.5)).abs() < 1e-9);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
#[serial]
|
|
159
|
+
fn test_sendable_string_converts_to_ruby_string() {
|
|
160
|
+
with_ruby_python(|_ruby, _api| {
|
|
161
|
+
let val: magnus::Value = SendableValue::Str("hello world".to_string())
|
|
162
|
+
.try_into()
|
|
163
|
+
.expect("String conversion should succeed");
|
|
164
|
+
let s = String::try_convert(val).expect("should be convertible to String");
|
|
165
|
+
assert_eq!(s, "hello world");
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[test]
|
|
170
|
+
#[serial]
|
|
171
|
+
fn test_sendable_string_empty() {
|
|
172
|
+
with_ruby_python(|_ruby, _api| {
|
|
173
|
+
let val: magnus::Value = SendableValue::Str(String::new())
|
|
174
|
+
.try_into()
|
|
175
|
+
.expect("empty string conversion should succeed");
|
|
176
|
+
let s = String::try_convert(val).expect("should be convertible to String");
|
|
177
|
+
assert_eq!(s, "");
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[test]
|
|
182
|
+
#[serial]
|
|
183
|
+
fn test_sendable_string_unicode() {
|
|
184
|
+
with_ruby_python(|_ruby, _api| {
|
|
185
|
+
let val: magnus::Value = SendableValue::Str("こんにちは🌍".to_string())
|
|
186
|
+
.try_into()
|
|
187
|
+
.expect("unicode string conversion should succeed");
|
|
188
|
+
let s = String::try_convert(val).expect("should be convertible to String");
|
|
189
|
+
assert_eq!(s, "こんにちは🌍");
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[test]
|
|
194
|
+
#[serial]
|
|
195
|
+
fn test_sendable_bool_true() {
|
|
196
|
+
with_ruby_python(|_ruby, _api| {
|
|
197
|
+
let val: magnus::Value = SendableValue::Bool(true)
|
|
198
|
+
.try_into()
|
|
199
|
+
.expect("Bool(true) conversion should succeed");
|
|
200
|
+
let b = bool::try_convert(val).expect("should be convertible to bool");
|
|
201
|
+
assert!(b);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#[test]
|
|
206
|
+
#[serial]
|
|
207
|
+
fn test_sendable_bool_false() {
|
|
208
|
+
with_ruby_python(|_ruby, _api| {
|
|
209
|
+
let val: magnus::Value = SendableValue::Bool(false)
|
|
210
|
+
.try_into()
|
|
211
|
+
.expect("Bool(false) conversion should succeed");
|
|
212
|
+
let b = bool::try_convert(val).expect("should be convertible to bool");
|
|
213
|
+
assert!(!b);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ========== SendableValue::List → Ruby Array ==========
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
#[serial]
|
|
221
|
+
fn test_sendable_list_converts_to_ruby_array() {
|
|
222
|
+
with_ruby_python(|_ruby, _api| {
|
|
223
|
+
let list = SendableValue::List(vec![
|
|
224
|
+
SendableValue::Integer(1),
|
|
225
|
+
SendableValue::Integer(2),
|
|
226
|
+
SendableValue::Integer(3),
|
|
227
|
+
]);
|
|
228
|
+
let val: magnus::Value = list.try_into().expect("List conversion should succeed");
|
|
229
|
+
|
|
230
|
+
assert!(val.is_kind_of(_ruby.class_array()));
|
|
231
|
+
let arr = magnus::RArray::try_convert(val).expect("should be an Array");
|
|
232
|
+
assert_eq!(arr.len(), 3);
|
|
233
|
+
|
|
234
|
+
let items: Vec<i64> = (0..3)
|
|
235
|
+
.map(|i| i64::try_convert(arr.entry::<magnus::Value>(i).unwrap()).unwrap())
|
|
236
|
+
.collect();
|
|
237
|
+
assert_eq!(items, vec![1, 2, 3]);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
#[serial]
|
|
243
|
+
fn test_sendable_list_empty() {
|
|
244
|
+
with_ruby_python(|_ruby, _api| {
|
|
245
|
+
let list = SendableValue::List(vec![]);
|
|
246
|
+
let val: magnus::Value = list
|
|
247
|
+
.try_into()
|
|
248
|
+
.expect("empty list conversion should succeed");
|
|
249
|
+
let arr = magnus::RArray::try_convert(val).expect("should be an Array");
|
|
250
|
+
assert_eq!(arr.len(), 0);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
#[serial]
|
|
256
|
+
fn test_sendable_list_mixed_types() {
|
|
257
|
+
with_ruby_python(|_ruby, _api| {
|
|
258
|
+
let list = SendableValue::List(vec![
|
|
259
|
+
SendableValue::Integer(42),
|
|
260
|
+
SendableValue::Str("hello".to_string()),
|
|
261
|
+
SendableValue::Float(2.5),
|
|
262
|
+
SendableValue::Bool(true),
|
|
263
|
+
SendableValue::Nil,
|
|
264
|
+
]);
|
|
265
|
+
let val: magnus::Value = list
|
|
266
|
+
.try_into()
|
|
267
|
+
.expect("mixed list conversion should succeed");
|
|
268
|
+
|
|
269
|
+
let arr = magnus::RArray::try_convert(val).expect("should be an Array");
|
|
270
|
+
assert_eq!(arr.len(), 5);
|
|
271
|
+
|
|
272
|
+
let v0 = arr.entry::<magnus::Value>(0).unwrap();
|
|
273
|
+
let v1 = arr.entry::<magnus::Value>(1).unwrap();
|
|
274
|
+
let v2 = arr.entry::<magnus::Value>(2).unwrap();
|
|
275
|
+
let v3 = arr.entry::<magnus::Value>(3).unwrap();
|
|
276
|
+
let v4 = arr.entry::<magnus::Value>(4).unwrap();
|
|
277
|
+
|
|
278
|
+
assert_eq!(i64::try_convert(v0).unwrap(), 42);
|
|
279
|
+
assert_eq!(String::try_convert(v1).unwrap(), "hello");
|
|
280
|
+
assert!((f64::try_convert(v2).unwrap() - 2.5).abs() < 1e-9);
|
|
281
|
+
assert!(bool::try_convert(v3).unwrap());
|
|
282
|
+
assert!(v4.is_nil());
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[test]
|
|
287
|
+
#[serial]
|
|
288
|
+
fn test_sendable_list_nested() {
|
|
289
|
+
with_ruby_python(|_ruby, _api| {
|
|
290
|
+
let nested = SendableValue::List(vec![
|
|
291
|
+
SendableValue::List(vec![SendableValue::Integer(1), SendableValue::Integer(2)]),
|
|
292
|
+
SendableValue::List(vec![SendableValue::Integer(3), SendableValue::Integer(4)]),
|
|
293
|
+
]);
|
|
294
|
+
let val: magnus::Value = nested
|
|
295
|
+
.try_into()
|
|
296
|
+
.expect("nested list conversion should succeed");
|
|
297
|
+
|
|
298
|
+
let outer = magnus::RArray::try_convert(val).expect("should be an Array");
|
|
299
|
+
assert_eq!(outer.len(), 2);
|
|
300
|
+
|
|
301
|
+
let inner0 = magnus::RArray::try_convert(outer.entry::<magnus::Value>(0).unwrap())
|
|
302
|
+
.expect("inner should be an Array");
|
|
303
|
+
assert_eq!(inner0.len(), 2);
|
|
304
|
+
assert_eq!(
|
|
305
|
+
i64::try_convert(inner0.entry::<magnus::Value>(0).unwrap()).unwrap(),
|
|
306
|
+
1
|
|
307
|
+
);
|
|
308
|
+
assert_eq!(
|
|
309
|
+
i64::try_convert(inner0.entry::<magnus::Value>(1).unwrap()).unwrap(),
|
|
310
|
+
2
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
let inner1 = magnus::RArray::try_convert(outer.entry::<magnus::Value>(1).unwrap())
|
|
314
|
+
.expect("inner should be an Array");
|
|
315
|
+
assert_eq!(
|
|
316
|
+
i64::try_convert(inner1.entry::<magnus::Value>(0).unwrap()).unwrap(),
|
|
317
|
+
3
|
|
318
|
+
);
|
|
319
|
+
assert_eq!(
|
|
320
|
+
i64::try_convert(inner1.entry::<magnus::Value>(1).unwrap()).unwrap(),
|
|
321
|
+
4
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ========== SendableValue::Dict → Ruby Hash ==========
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
#[serial]
|
|
330
|
+
fn test_sendable_dict_converts_to_ruby_hash() {
|
|
331
|
+
with_ruby_python(|_ruby, _api| {
|
|
332
|
+
let dict = SendableValue::Dict(vec![
|
|
333
|
+
(
|
|
334
|
+
SendableValue::Str("name".to_string()),
|
|
335
|
+
SendableValue::Str("Alice".to_string()),
|
|
336
|
+
),
|
|
337
|
+
(
|
|
338
|
+
SendableValue::Str("age".to_string()),
|
|
339
|
+
SendableValue::Integer(30),
|
|
340
|
+
),
|
|
341
|
+
]);
|
|
342
|
+
let val: magnus::Value = dict.try_into().expect("Dict conversion should succeed");
|
|
343
|
+
|
|
344
|
+
assert!(val.is_kind_of(_ruby.class_hash()));
|
|
345
|
+
let hash = magnus::RHash::try_convert(val).expect("should be a Hash");
|
|
346
|
+
|
|
347
|
+
let name: String = hash.fetch("name").expect("should have 'name' key");
|
|
348
|
+
assert_eq!(name, "Alice");
|
|
349
|
+
let age: i64 = hash.fetch("age").expect("should have 'age' key");
|
|
350
|
+
assert_eq!(age, 30);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
#[serial]
|
|
356
|
+
fn test_sendable_dict_empty() {
|
|
357
|
+
with_ruby_python(|_ruby, _api| {
|
|
358
|
+
let dict = SendableValue::Dict(vec![]);
|
|
359
|
+
let val: magnus::Value = dict
|
|
360
|
+
.try_into()
|
|
361
|
+
.expect("empty dict conversion should succeed");
|
|
362
|
+
let hash = magnus::RHash::try_convert(val).expect("should be a Hash");
|
|
363
|
+
assert_eq!(hash.len(), 0);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[test]
|
|
368
|
+
#[serial]
|
|
369
|
+
fn test_sendable_dict_with_integer_keys() {
|
|
370
|
+
with_ruby_python(|_ruby, _api| {
|
|
371
|
+
let dict = SendableValue::Dict(vec![
|
|
372
|
+
(
|
|
373
|
+
SendableValue::Integer(1),
|
|
374
|
+
SendableValue::Str("one".to_string()),
|
|
375
|
+
),
|
|
376
|
+
(
|
|
377
|
+
SendableValue::Integer(2),
|
|
378
|
+
SendableValue::Str("two".to_string()),
|
|
379
|
+
),
|
|
380
|
+
]);
|
|
381
|
+
let val: magnus::Value = dict
|
|
382
|
+
.try_into()
|
|
383
|
+
.expect("Dict with int keys conversion should succeed");
|
|
384
|
+
|
|
385
|
+
let hash = magnus::RHash::try_convert(val).expect("should be a Hash");
|
|
386
|
+
let v1: String = hash.fetch(1_i64).expect("should have key 1");
|
|
387
|
+
assert_eq!(v1, "one");
|
|
388
|
+
let v2: String = hash.fetch(2_i64).expect("should have key 2");
|
|
389
|
+
assert_eq!(v2, "two");
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#[test]
|
|
394
|
+
#[serial]
|
|
395
|
+
fn test_sendable_dict_nested_list_value() {
|
|
396
|
+
with_ruby_python(|_ruby, _api| {
|
|
397
|
+
let dict = SendableValue::Dict(vec![(
|
|
398
|
+
SendableValue::Str("data".to_string()),
|
|
399
|
+
SendableValue::List(vec![SendableValue::Integer(10), SendableValue::Integer(20)]),
|
|
400
|
+
)]);
|
|
401
|
+
let val: magnus::Value = dict
|
|
402
|
+
.try_into()
|
|
403
|
+
.expect("Dict with list value conversion should succeed");
|
|
404
|
+
|
|
405
|
+
let hash = magnus::RHash::try_convert(val).expect("should be a Hash");
|
|
406
|
+
let data: magnus::Value = hash.fetch("data").expect("should have 'data' key");
|
|
407
|
+
let arr = magnus::RArray::try_convert(data).expect("value should be an Array");
|
|
408
|
+
assert_eq!(arr.len(), 2);
|
|
409
|
+
assert_eq!(
|
|
410
|
+
i64::try_convert(arr.entry::<magnus::Value>(0).unwrap()).unwrap(),
|
|
411
|
+
10
|
|
412
|
+
);
|
|
413
|
+
assert_eq!(
|
|
414
|
+
i64::try_convert(arr.entry::<magnus::Value>(1).unwrap()).unwrap(),
|
|
415
|
+
20
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ========== AsyncStream as Iterator (via channel) ==========
|
|
421
|
+
|
|
422
|
+
#[test]
|
|
423
|
+
#[serial]
|
|
424
|
+
fn test_async_stream_iterates_values() {
|
|
425
|
+
with_ruby_python(|_ruby, _api| {
|
|
426
|
+
let (value_tx, value_rx) = unbounded();
|
|
427
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
428
|
+
|
|
429
|
+
thread::spawn(move || {
|
|
430
|
+
value_tx.send(Some(SendableValue::Integer(10))).ok();
|
|
431
|
+
value_tx.send(Some(SendableValue::Integer(20))).ok();
|
|
432
|
+
value_tx.send(Some(SendableValue::Integer(30))).ok();
|
|
433
|
+
value_tx.send(None).ok(); // End signal
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
let mut stream = crate::stream::AsyncStream::from_channel(value_rx, cancel_tx);
|
|
437
|
+
|
|
438
|
+
let v1 = stream.next().unwrap().unwrap();
|
|
439
|
+
let v2 = stream.next().unwrap().unwrap();
|
|
440
|
+
let v3 = stream.next().unwrap().unwrap();
|
|
441
|
+
assert!(stream.next().is_none());
|
|
442
|
+
|
|
443
|
+
assert_eq!(i64::try_convert(v1).unwrap(), 10);
|
|
444
|
+
assert_eq!(i64::try_convert(v2).unwrap(), 20);
|
|
445
|
+
assert_eq!(i64::try_convert(v3).unwrap(), 30);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
#[serial]
|
|
451
|
+
fn test_async_stream_empty() {
|
|
452
|
+
with_ruby_python(|_ruby, _api| {
|
|
453
|
+
let (value_tx, value_rx) = unbounded();
|
|
454
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
455
|
+
|
|
456
|
+
thread::spawn(move || {
|
|
457
|
+
value_tx.send(None).ok(); // Immediate end
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
let mut stream = crate::stream::AsyncStream::from_channel(value_rx, cancel_tx);
|
|
461
|
+
assert!(
|
|
462
|
+
stream.next().is_none(),
|
|
463
|
+
"Empty stream should return None immediately"
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#[test]
|
|
469
|
+
#[serial]
|
|
470
|
+
fn test_async_stream_mixed_types() {
|
|
471
|
+
with_ruby_python(|_ruby, _api| {
|
|
472
|
+
let (value_tx, value_rx) = unbounded();
|
|
473
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
474
|
+
|
|
475
|
+
thread::spawn(move || {
|
|
476
|
+
value_tx.send(Some(SendableValue::Integer(1))).ok();
|
|
477
|
+
value_tx
|
|
478
|
+
.send(Some(SendableValue::Str("hello".to_string())))
|
|
479
|
+
.ok();
|
|
480
|
+
value_tx.send(Some(SendableValue::Float(2.5))).ok();
|
|
481
|
+
value_tx.send(Some(SendableValue::Bool(true))).ok();
|
|
482
|
+
value_tx.send(Some(SendableValue::Nil)).ok();
|
|
483
|
+
value_tx.send(None).ok();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
let mut stream = crate::stream::AsyncStream::from_channel(value_rx, cancel_tx);
|
|
487
|
+
let mut results = Vec::new();
|
|
488
|
+
while let Some(Ok(val)) = stream.next() {
|
|
489
|
+
results.push(val);
|
|
490
|
+
}
|
|
491
|
+
assert_eq!(results.len(), 5);
|
|
492
|
+
|
|
493
|
+
assert_eq!(i64::try_convert(results[0]).unwrap(), 1);
|
|
494
|
+
assert_eq!(String::try_convert(results[1]).unwrap(), "hello");
|
|
495
|
+
assert!((f64::try_convert(results[2]).unwrap() - 2.5).abs() < 1e-9);
|
|
496
|
+
assert!(bool::try_convert(results[3]).unwrap());
|
|
497
|
+
assert!(results[4].is_nil());
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
#[test]
|
|
502
|
+
#[serial]
|
|
503
|
+
fn test_async_stream_with_collections() {
|
|
504
|
+
with_ruby_python(|_ruby, _api| {
|
|
505
|
+
let (value_tx, value_rx) = unbounded();
|
|
506
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
507
|
+
|
|
508
|
+
thread::spawn(move || {
|
|
509
|
+
value_tx
|
|
510
|
+
.send(Some(SendableValue::List(vec![
|
|
511
|
+
SendableValue::Integer(1),
|
|
512
|
+
SendableValue::Integer(2),
|
|
513
|
+
])))
|
|
514
|
+
.ok();
|
|
515
|
+
value_tx
|
|
516
|
+
.send(Some(SendableValue::Dict(vec![(
|
|
517
|
+
SendableValue::Str("key".to_string()),
|
|
518
|
+
SendableValue::Str("val".to_string()),
|
|
519
|
+
)])))
|
|
520
|
+
.ok();
|
|
521
|
+
value_tx.send(None).ok();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
let mut stream = crate::stream::AsyncStream::from_channel(value_rx, cancel_tx);
|
|
525
|
+
|
|
526
|
+
let arr_val = stream.next().unwrap().unwrap();
|
|
527
|
+
let arr = magnus::RArray::try_convert(arr_val).expect("should be Array");
|
|
528
|
+
assert_eq!(arr.len(), 2);
|
|
529
|
+
|
|
530
|
+
let hash_val = stream.next().unwrap().unwrap();
|
|
531
|
+
let hash = magnus::RHash::try_convert(hash_val).expect("should be Hash");
|
|
532
|
+
let v: String = hash.fetch("key").unwrap();
|
|
533
|
+
assert_eq!(v, "val");
|
|
534
|
+
|
|
535
|
+
assert!(stream.next().is_none());
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
#[test]
|
|
540
|
+
#[serial]
|
|
541
|
+
fn test_async_stream_cancellation_via_drop() {
|
|
542
|
+
// Verify that dropping the stream doesn't hang even with a blocked producer
|
|
543
|
+
let Some(_guard) = skip_if_no_python() else {
|
|
544
|
+
return;
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
let (value_tx, value_rx) = bounded(1); // small buffer
|
|
548
|
+
let (cancel_tx, cancel_rx) = bounded(1);
|
|
549
|
+
|
|
550
|
+
let producer = thread::spawn(move || {
|
|
551
|
+
// Fill the buffer
|
|
552
|
+
let _ = value_tx.send(Some(SendableValue::Integer(1)));
|
|
553
|
+
// This send will block because buffer is full and consumer won't read
|
|
554
|
+
// The cancel signal or channel close should unblock us
|
|
555
|
+
loop {
|
|
556
|
+
crossbeam_channel::select! {
|
|
557
|
+
send(value_tx, Some(SendableValue::Integer(2))) -> res => {
|
|
558
|
+
if res.is_err() { break; } // channel closed
|
|
559
|
+
}
|
|
560
|
+
recv(cancel_rx) -> _ => {
|
|
561
|
+
break; // cancelled
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Drop the stream (sends cancel, drops receiver)
|
|
568
|
+
let stream = crate::stream::AsyncStream::from_channel(value_rx, cancel_tx);
|
|
569
|
+
drop(stream);
|
|
570
|
+
|
|
571
|
+
// Producer thread should finish without hanging
|
|
572
|
+
producer
|
|
573
|
+
.join()
|
|
574
|
+
.expect("producer should not hang after cancellation");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#[test]
|
|
578
|
+
#[serial]
|
|
579
|
+
fn test_async_stream_channel_closed_returns_none() {
|
|
580
|
+
with_ruby_python(|_ruby, _api| {
|
|
581
|
+
let (value_tx, value_rx) = unbounded();
|
|
582
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
583
|
+
|
|
584
|
+
// Drop sender immediately — simulates producer crash
|
|
585
|
+
drop(value_tx);
|
|
586
|
+
|
|
587
|
+
let mut stream = crate::stream::AsyncStream::from_channel(value_rx, cancel_tx);
|
|
588
|
+
assert!(stream.next().is_none(), "Closed channel should return None");
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ========== python_to_sendable via Python objects ==========
|
|
593
|
+
|
|
594
|
+
#[test]
|
|
595
|
+
#[serial]
|
|
596
|
+
fn test_python_to_sendable_primitives() {
|
|
597
|
+
let Some(guard) = skip_if_no_python() else {
|
|
598
|
+
return;
|
|
599
|
+
};
|
|
600
|
+
let api = guard.api();
|
|
601
|
+
|
|
602
|
+
// None
|
|
603
|
+
let result = crate::rubyx_object::python_to_sendable(api.py_none, api);
|
|
604
|
+
assert!(matches!(result, Ok(SendableValue::Nil)));
|
|
605
|
+
|
|
606
|
+
// Integer
|
|
607
|
+
let py_int = api.long_from_i64(42);
|
|
608
|
+
let result = crate::rubyx_object::python_to_sendable(py_int, api);
|
|
609
|
+
assert!(matches!(result, Ok(SendableValue::Integer(42))));
|
|
610
|
+
api.decref(py_int);
|
|
611
|
+
|
|
612
|
+
// Float
|
|
613
|
+
let py_float = api.float_from_f64(std::f64::consts::PI);
|
|
614
|
+
let result = crate::rubyx_object::python_to_sendable(py_float, api);
|
|
615
|
+
match result {
|
|
616
|
+
Ok(SendableValue::Float(f)) => assert!((f - std::f64::consts::PI).abs() < 1e-9),
|
|
617
|
+
other => panic!("Expected Float, got {:?}", other),
|
|
618
|
+
}
|
|
619
|
+
api.decref(py_float);
|
|
620
|
+
|
|
621
|
+
// String
|
|
622
|
+
let py_str = api.string_from_str("test");
|
|
623
|
+
let result = crate::rubyx_object::python_to_sendable(py_str, api);
|
|
624
|
+
assert!(matches!(result, Ok(SendableValue::Str(s)) if s == "test"));
|
|
625
|
+
api.decref(py_str);
|
|
626
|
+
|
|
627
|
+
// Bool true
|
|
628
|
+
let result = crate::rubyx_object::python_to_sendable(api.py_true, api);
|
|
629
|
+
assert!(matches!(result, Ok(SendableValue::Bool(true))));
|
|
630
|
+
|
|
631
|
+
// Bool false
|
|
632
|
+
let result = crate::rubyx_object::python_to_sendable(api.py_false, api);
|
|
633
|
+
assert!(matches!(result, Ok(SendableValue::Bool(false))));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#[test]
|
|
637
|
+
#[serial]
|
|
638
|
+
fn test_python_to_sendable_list() {
|
|
639
|
+
let Some(guard) = skip_if_no_python() else {
|
|
640
|
+
return;
|
|
641
|
+
};
|
|
642
|
+
let api = guard.api();
|
|
643
|
+
|
|
644
|
+
let list = api.list_new(3);
|
|
645
|
+
api.list_set_item(list, 0, api.long_from_i64(10));
|
|
646
|
+
api.list_set_item(list, 1, api.long_from_i64(20));
|
|
647
|
+
api.list_set_item(list, 2, api.long_from_i64(30));
|
|
648
|
+
|
|
649
|
+
let result = crate::rubyx_object::python_to_sendable(list, api);
|
|
650
|
+
match result {
|
|
651
|
+
Ok(SendableValue::List(items)) => {
|
|
652
|
+
assert_eq!(items.len(), 3);
|
|
653
|
+
assert!(matches!(items[0], SendableValue::Integer(10)));
|
|
654
|
+
assert!(matches!(items[1], SendableValue::Integer(20)));
|
|
655
|
+
assert!(matches!(items[2], SendableValue::Integer(30)));
|
|
656
|
+
}
|
|
657
|
+
other => panic!("Expected List, got {:?}", other),
|
|
658
|
+
}
|
|
659
|
+
api.decref(list);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
#[test]
|
|
663
|
+
#[serial]
|
|
664
|
+
fn test_python_to_sendable_tuple() {
|
|
665
|
+
let Some(guard) = skip_if_no_python() else {
|
|
666
|
+
return;
|
|
667
|
+
};
|
|
668
|
+
let api = guard.api();
|
|
669
|
+
|
|
670
|
+
let tuple = api.tuple_new(2);
|
|
671
|
+
api.tuple_set_item(tuple, 0, api.string_from_str("a"));
|
|
672
|
+
api.tuple_set_item(tuple, 1, api.long_from_i64(1));
|
|
673
|
+
|
|
674
|
+
let result = crate::rubyx_object::python_to_sendable(tuple, api);
|
|
675
|
+
match result {
|
|
676
|
+
Ok(SendableValue::List(items)) => {
|
|
677
|
+
assert_eq!(items.len(), 2);
|
|
678
|
+
assert!(matches!(&items[0], SendableValue::Str(s) if s == "a"));
|
|
679
|
+
assert!(matches!(items[1], SendableValue::Integer(1)));
|
|
680
|
+
}
|
|
681
|
+
other => panic!("Expected List (from tuple), got {:?}", other),
|
|
682
|
+
}
|
|
683
|
+
api.decref(tuple);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
#[test]
|
|
687
|
+
#[serial]
|
|
688
|
+
fn test_python_to_sendable_dict() {
|
|
689
|
+
let Some(guard) = skip_if_no_python() else {
|
|
690
|
+
return;
|
|
691
|
+
};
|
|
692
|
+
let api = guard.api();
|
|
693
|
+
|
|
694
|
+
let dict = api.dict_new();
|
|
695
|
+
let key = api.string_from_str("x");
|
|
696
|
+
let val = api.long_from_i64(99);
|
|
697
|
+
api.dict_set_item(dict, key, val);
|
|
698
|
+
api.decref(key);
|
|
699
|
+
api.decref(val);
|
|
700
|
+
|
|
701
|
+
let result = crate::rubyx_object::python_to_sendable(dict, api);
|
|
702
|
+
match result {
|
|
703
|
+
Ok(SendableValue::Dict(entries)) => {
|
|
704
|
+
assert_eq!(entries.len(), 1);
|
|
705
|
+
assert!(matches!(&entries[0].0, SendableValue::Str(s) if s == "x"));
|
|
706
|
+
assert!(matches!(entries[0].1, SendableValue::Integer(99)));
|
|
707
|
+
}
|
|
708
|
+
other => panic!("Expected Dict, got {:?}", other),
|
|
709
|
+
}
|
|
710
|
+
api.decref(dict);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
#[test]
|
|
714
|
+
#[serial]
|
|
715
|
+
fn test_python_to_sendable_nested_structure() {
|
|
716
|
+
let Some(guard) = skip_if_no_python() else {
|
|
717
|
+
return;
|
|
718
|
+
};
|
|
719
|
+
let api = guard.api();
|
|
720
|
+
|
|
721
|
+
// Build: {"items": [1, 2], "flag": True}
|
|
722
|
+
let inner_list = api.list_new(2);
|
|
723
|
+
api.list_set_item(inner_list, 0, api.long_from_i64(1));
|
|
724
|
+
api.list_set_item(inner_list, 1, api.long_from_i64(2));
|
|
725
|
+
|
|
726
|
+
let dict = api.dict_new();
|
|
727
|
+
let key1 = api.string_from_str("items");
|
|
728
|
+
api.dict_set_item(dict, key1, inner_list);
|
|
729
|
+
api.decref(key1);
|
|
730
|
+
api.decref(inner_list);
|
|
731
|
+
|
|
732
|
+
let key2 = api.string_from_str("flag");
|
|
733
|
+
api.incref(api.py_true);
|
|
734
|
+
api.dict_set_item(dict, key2, api.py_true);
|
|
735
|
+
api.decref(key2);
|
|
736
|
+
|
|
737
|
+
let result = crate::rubyx_object::python_to_sendable(dict, api);
|
|
738
|
+
match result {
|
|
739
|
+
Ok(SendableValue::Dict(entries)) => {
|
|
740
|
+
assert_eq!(entries.len(), 2);
|
|
741
|
+
// Dict order is not guaranteed; find each key
|
|
742
|
+
let items_entry = entries
|
|
743
|
+
.iter()
|
|
744
|
+
.find(|(k, _)| matches!(k, SendableValue::Str(s) if s == "items"))
|
|
745
|
+
.expect("should have 'items' key");
|
|
746
|
+
assert!(matches!(&items_entry.1, SendableValue::List(l) if l.len() == 2));
|
|
747
|
+
|
|
748
|
+
let flag_entry = entries
|
|
749
|
+
.iter()
|
|
750
|
+
.find(|(k, _)| matches!(k, SendableValue::Str(s) if s == "flag"))
|
|
751
|
+
.expect("should have 'flag' key");
|
|
752
|
+
assert!(matches!(flag_entry.1, SendableValue::Bool(true)));
|
|
753
|
+
}
|
|
754
|
+
other => panic!("Expected Dict, got {:?}", other),
|
|
755
|
+
}
|
|
756
|
+
api.decref(dict);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ========== RubyxStream::next_item ==========
|
|
760
|
+
|
|
761
|
+
#[test]
|
|
762
|
+
#[serial]
|
|
763
|
+
fn test_next_item_returns_values() {
|
|
764
|
+
with_ruby_python(|_ruby, _api| {
|
|
765
|
+
let (tx, rx) = unbounded();
|
|
766
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
767
|
+
|
|
768
|
+
thread::spawn(move || {
|
|
769
|
+
tx.send(Some(SendableValue::Integer(1))).ok();
|
|
770
|
+
tx.send(Some(SendableValue::Integer(2))).ok();
|
|
771
|
+
tx.send(Some(SendableValue::Integer(3))).ok();
|
|
772
|
+
tx.send(None).ok();
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
let stream = crate::async_gen::AsyncGeneratorStream::from_channel(rx, cancel_tx);
|
|
776
|
+
let rubyx_stream = crate::rubyx_stream::RubyxStream::from_stream(stream);
|
|
777
|
+
|
|
778
|
+
let v1 = rubyx_stream
|
|
779
|
+
.next_item()
|
|
780
|
+
.expect("first next_item should succeed");
|
|
781
|
+
let v2 = rubyx_stream
|
|
782
|
+
.next_item()
|
|
783
|
+
.expect("second next_item should succeed");
|
|
784
|
+
let v3 = rubyx_stream
|
|
785
|
+
.next_item()
|
|
786
|
+
.expect("third next_item should succeed");
|
|
787
|
+
|
|
788
|
+
assert_eq!(i64::try_convert(v1).unwrap(), 1);
|
|
789
|
+
assert_eq!(i64::try_convert(v2).unwrap(), 2);
|
|
790
|
+
assert_eq!(i64::try_convert(v3).unwrap(), 3);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
#[test]
|
|
795
|
+
#[serial]
|
|
796
|
+
fn test_next_item_raises_stop_iteration_at_end() {
|
|
797
|
+
with_ruby_python(|ruby, _api| {
|
|
798
|
+
let (tx, rx) = unbounded();
|
|
799
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
800
|
+
|
|
801
|
+
thread::spawn(move || {
|
|
802
|
+
tx.send(Some(SendableValue::Integer(42))).ok();
|
|
803
|
+
tx.send(None).ok();
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
let stream = crate::async_gen::AsyncGeneratorStream::from_channel(rx, cancel_tx);
|
|
807
|
+
let rubyx_stream = crate::rubyx_stream::RubyxStream::from_stream(stream);
|
|
808
|
+
|
|
809
|
+
// First call succeeds
|
|
810
|
+
let val = rubyx_stream.next_item().expect("should return value");
|
|
811
|
+
assert_eq!(i64::try_convert(val).unwrap(), 42);
|
|
812
|
+
|
|
813
|
+
// Second call should raise StopIteration
|
|
814
|
+
let err = rubyx_stream
|
|
815
|
+
.next_item()
|
|
816
|
+
.expect_err("should raise StopIteration");
|
|
817
|
+
assert!(
|
|
818
|
+
err.is_kind_of(ruby.exception_stop_iteration()),
|
|
819
|
+
"error should be StopIteration, got: {}",
|
|
820
|
+
err
|
|
821
|
+
);
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
#[test]
|
|
826
|
+
#[serial]
|
|
827
|
+
fn test_next_item_empty_stream_raises_stop_iteration() {
|
|
828
|
+
with_ruby_python(|ruby, _api| {
|
|
829
|
+
let (tx, rx) = unbounded();
|
|
830
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
831
|
+
|
|
832
|
+
thread::spawn(move || {
|
|
833
|
+
tx.send(None).ok(); // immediate end
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
let stream = crate::async_gen::AsyncGeneratorStream::from_channel(rx, cancel_tx);
|
|
837
|
+
let rubyx_stream = crate::rubyx_stream::RubyxStream::from_stream(stream);
|
|
838
|
+
|
|
839
|
+
let err = rubyx_stream
|
|
840
|
+
.next_item()
|
|
841
|
+
.expect_err("should raise StopIteration");
|
|
842
|
+
assert!(
|
|
843
|
+
err.is_kind_of(ruby.exception_stop_iteration()),
|
|
844
|
+
"empty stream should raise StopIteration"
|
|
845
|
+
);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
#[test]
|
|
850
|
+
#[serial]
|
|
851
|
+
fn test_next_item_repeated_after_exhaustion() {
|
|
852
|
+
with_ruby_python(|ruby, _api| {
|
|
853
|
+
let (tx, rx) = unbounded();
|
|
854
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
855
|
+
|
|
856
|
+
thread::spawn(move || {
|
|
857
|
+
tx.send(None).ok();
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
let stream = crate::async_gen::AsyncGeneratorStream::from_channel(rx, cancel_tx);
|
|
861
|
+
let rubyx_stream = crate::rubyx_stream::RubyxStream::from_stream(stream);
|
|
862
|
+
|
|
863
|
+
// First call raises StopIteration
|
|
864
|
+
let err1 = rubyx_stream
|
|
865
|
+
.next_item()
|
|
866
|
+
.expect_err("should raise StopIteration");
|
|
867
|
+
assert!(err1.is_kind_of(ruby.exception_stop_iteration()));
|
|
868
|
+
|
|
869
|
+
// Second call should also raise (StopIteration or stream consumed error)
|
|
870
|
+
let err2 = rubyx_stream
|
|
871
|
+
.next_item()
|
|
872
|
+
.expect_err("should still raise after exhaustion");
|
|
873
|
+
// Could be StopIteration again or RuntimeError("Stream already consumed")
|
|
874
|
+
// Both are acceptable — the key is it doesn't return nil or panic
|
|
875
|
+
assert!(
|
|
876
|
+
err2.is_kind_of(ruby.exception_stop_iteration())
|
|
877
|
+
|| err2.is_kind_of(ruby.exception_runtime_error()),
|
|
878
|
+
"should raise StopIteration or RuntimeError"
|
|
879
|
+
);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
#[test]
|
|
884
|
+
#[serial]
|
|
885
|
+
fn test_next_item_with_mixed_types() {
|
|
886
|
+
with_ruby_python(|_ruby, _api| {
|
|
887
|
+
let (tx, rx) = unbounded();
|
|
888
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
889
|
+
|
|
890
|
+
thread::spawn(move || {
|
|
891
|
+
tx.send(Some(SendableValue::Str("hello".to_string()))).ok();
|
|
892
|
+
tx.send(Some(SendableValue::Float(std::f64::consts::PI)))
|
|
893
|
+
.ok();
|
|
894
|
+
tx.send(Some(SendableValue::Bool(true))).ok();
|
|
895
|
+
tx.send(Some(SendableValue::Nil)).ok();
|
|
896
|
+
tx.send(None).ok();
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
let stream = crate::async_gen::AsyncGeneratorStream::from_channel(rx, cancel_tx);
|
|
900
|
+
let rubyx_stream = crate::rubyx_stream::RubyxStream::from_stream(stream);
|
|
901
|
+
|
|
902
|
+
let v1 = rubyx_stream.next_item().unwrap();
|
|
903
|
+
assert_eq!(String::try_convert(v1).unwrap(), "hello");
|
|
904
|
+
|
|
905
|
+
let v2 = rubyx_stream.next_item().unwrap();
|
|
906
|
+
assert!((f64::try_convert(v2).unwrap() - std::f64::consts::PI).abs() < 1e-9);
|
|
907
|
+
|
|
908
|
+
let v3 = rubyx_stream.next_item().unwrap();
|
|
909
|
+
assert!(bool::try_convert(v3).unwrap());
|
|
910
|
+
|
|
911
|
+
let v4 = rubyx_stream.next_item().unwrap();
|
|
912
|
+
assert!(v4.is_nil());
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
#[test]
|
|
917
|
+
#[serial]
|
|
918
|
+
fn test_next_item_with_collections() {
|
|
919
|
+
with_ruby_python(|_ruby, _api| {
|
|
920
|
+
let (tx, rx) = unbounded();
|
|
921
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
922
|
+
|
|
923
|
+
thread::spawn(move || {
|
|
924
|
+
tx.send(Some(SendableValue::List(vec![
|
|
925
|
+
SendableValue::Integer(1),
|
|
926
|
+
SendableValue::Integer(2),
|
|
927
|
+
])))
|
|
928
|
+
.ok();
|
|
929
|
+
tx.send(Some(SendableValue::Dict(vec![(
|
|
930
|
+
SendableValue::Str("k".to_string()),
|
|
931
|
+
SendableValue::Integer(99),
|
|
932
|
+
)])))
|
|
933
|
+
.ok();
|
|
934
|
+
tx.send(None).ok();
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
let stream = crate::async_gen::AsyncGeneratorStream::from_channel(rx, cancel_tx);
|
|
938
|
+
let rubyx_stream = crate::rubyx_stream::RubyxStream::from_stream(stream);
|
|
939
|
+
|
|
940
|
+
let arr_val = rubyx_stream.next_item().unwrap();
|
|
941
|
+
let arr = magnus::RArray::try_convert(arr_val).expect("should be Array");
|
|
942
|
+
assert_eq!(arr.len(), 2);
|
|
943
|
+
|
|
944
|
+
let hash_val = rubyx_stream.next_item().unwrap();
|
|
945
|
+
let hash = magnus::RHash::try_convert(hash_val).expect("should be Hash");
|
|
946
|
+
let v: i64 = hash.fetch("k").unwrap();
|
|
947
|
+
assert_eq!(v, 99);
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|