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,1422 @@
|
|
|
1
|
+
use crate::pipe_notify::PipeNotify;
|
|
2
|
+
use crate::ruby_helpers::runtime_error;
|
|
3
|
+
use crate::stream::StreamItem;
|
|
4
|
+
use crossbeam_channel::Receiver;
|
|
5
|
+
use magnus::value::ReprValue;
|
|
6
|
+
use magnus::{Error, Module, Ruby, Value};
|
|
7
|
+
use std::ffi::c_void;
|
|
8
|
+
use std::os::fd::RawFd;
|
|
9
|
+
use std::sync::atomic::AtomicBool;
|
|
10
|
+
use std::sync::Arc;
|
|
11
|
+
|
|
12
|
+
extern "C" {
|
|
13
|
+
/// Release the GVL, call `func(data1)`, then reacquire the GVL.
|
|
14
|
+
///
|
|
15
|
+
/// While `func` runs, other Ruby threads can execute. Inside `func`,
|
|
16
|
+
/// you must NOT call any Ruby C API or access any Ruby VALUE.
|
|
17
|
+
///
|
|
18
|
+
/// If `ubf` is provided, Ruby may call it from another thread to
|
|
19
|
+
/// interrupt `func` (e.g., on Thread#kill or signal delivery).
|
|
20
|
+
///
|
|
21
|
+
/// # Safety
|
|
22
|
+
///
|
|
23
|
+
/// - `func` must not touch Ruby objects (GVL is not held).
|
|
24
|
+
/// - `data1` must remain valid for the duration of `func`.
|
|
25
|
+
/// - `data2` must remain valid for the duration of `ubf` (if provided).
|
|
26
|
+
fn rb_thread_call_without_gvl(
|
|
27
|
+
func: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
|
|
28
|
+
data1: *mut c_void,
|
|
29
|
+
ubf: Option<unsafe extern "C" fn(*mut c_void)>,
|
|
30
|
+
data2: *mut c_void,
|
|
31
|
+
) -> *mut c_void;
|
|
32
|
+
|
|
33
|
+
/// Check for pending Ruby interrupts (Thread#kill, signals, etc.).
|
|
34
|
+
///
|
|
35
|
+
/// Must be called WITH the GVL held. If an interrupt is pending,
|
|
36
|
+
/// this raises a Ruby exception (longjmp). Call this immediately
|
|
37
|
+
/// after `rb_thread_call_without_gvl` returns to deliver any
|
|
38
|
+
/// interrupts that arrived while the GVL was released.
|
|
39
|
+
fn rb_thread_check_ints();
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn create_ruby_io(ruby: &Ruby, fd: RawFd) -> Result<Value, Error> {
|
|
44
|
+
let io_class: Value = ruby.class_object().const_get("IO")?;
|
|
45
|
+
io_class.funcall("for_fd", (fd, magnus::kwargs!("autoclose" => false)))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn has_fiber_scheduler(ruby: &Ruby) -> bool {
|
|
49
|
+
let fiber_class = ruby
|
|
50
|
+
.class_object()
|
|
51
|
+
.const_get("Fiber")
|
|
52
|
+
.unwrap_or_else(|_| ruby.qnil().as_value());
|
|
53
|
+
let scheduler = fiber_class
|
|
54
|
+
.funcall("scheduler", ())
|
|
55
|
+
.unwrap_or_else(|_| ruby.qnil().as_value());
|
|
56
|
+
!scheduler.is_nil()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
unsafe extern "C" fn ubf_cancel(data: *mut c_void) {
|
|
60
|
+
let cancel = &*(data as *const AtomicBool);
|
|
61
|
+
cancel.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
unsafe extern "C" fn recv_without_gvl_cb(args: *mut c_void) -> *mut c_void {
|
|
65
|
+
let args = &mut *(args as *mut RecvArgs);
|
|
66
|
+
loop {
|
|
67
|
+
match args.receiver.try_recv() {
|
|
68
|
+
Ok(item) => {
|
|
69
|
+
args.result = Some(Ok(item));
|
|
70
|
+
return std::ptr::null_mut();
|
|
71
|
+
}
|
|
72
|
+
Err(crossbeam_channel::TryRecvError::Empty) => {
|
|
73
|
+
// Check the cancel flag relaxed already
|
|
74
|
+
if args.cancel.load(std::sync::atomic::Ordering::Relaxed) {
|
|
75
|
+
args.result = None;
|
|
76
|
+
return std::ptr::null_mut();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Wait for timeout
|
|
80
|
+
match args
|
|
81
|
+
.receiver
|
|
82
|
+
.recv_timeout(std::time::Duration::from_millis(50))
|
|
83
|
+
{
|
|
84
|
+
Ok(item) => {
|
|
85
|
+
args.result = Some(Ok(item));
|
|
86
|
+
return std::ptr::null_mut();
|
|
87
|
+
}
|
|
88
|
+
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
|
|
89
|
+
// continue to check cancel
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
|
|
93
|
+
args.result = Some(Err(crossbeam_channel::RecvError));
|
|
94
|
+
return std::ptr::null_mut();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
Err(crossbeam_channel::TryRecvError::Disconnected) => {
|
|
99
|
+
args.result = Some(Err(crossbeam_channel::RecvError));
|
|
100
|
+
return std::ptr::null_mut();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
struct RecvArgs {
|
|
107
|
+
receiver: Receiver<StreamItem>,
|
|
108
|
+
result: Option<Result<StreamItem, crossbeam_channel::RecvError>>,
|
|
109
|
+
cancel: Arc<AtomicBool>,
|
|
110
|
+
}
|
|
111
|
+
#[magnus::wrap(class = "Rubyx::NonBlockingStream", free_immediately)]
|
|
112
|
+
pub(crate) struct NonBlockingStream {
|
|
113
|
+
receiver: Receiver<StreamItem>,
|
|
114
|
+
pipe: Arc<PipeNotify>,
|
|
115
|
+
#[allow(dead_code)]
|
|
116
|
+
cancel: Arc<AtomicBool>,
|
|
117
|
+
}
|
|
118
|
+
impl NonBlockingStream {
|
|
119
|
+
pub(crate) fn new(receiver: Receiver<StreamItem>, pipe: Arc<PipeNotify>) -> Self {
|
|
120
|
+
NonBlockingStream {
|
|
121
|
+
receiver,
|
|
122
|
+
pipe,
|
|
123
|
+
cancel: Arc::new(AtomicBool::new(false)),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn next_gvl_release(&self) -> Option<Result<Value, Error>> {
|
|
128
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
129
|
+
let mut args = RecvArgs {
|
|
130
|
+
receiver: self.receiver.clone(),
|
|
131
|
+
result: None,
|
|
132
|
+
cancel: cancel.clone(),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
unsafe {
|
|
136
|
+
rb_thread_call_without_gvl(
|
|
137
|
+
recv_without_gvl_cb,
|
|
138
|
+
&mut args as *mut RecvArgs as *mut c_void,
|
|
139
|
+
Some(ubf_cancel),
|
|
140
|
+
Arc::as_ptr(&cancel) as *mut c_void,
|
|
141
|
+
);
|
|
142
|
+
rb_thread_check_ints();
|
|
143
|
+
}
|
|
144
|
+
match args.result? {
|
|
145
|
+
Ok(StreamItem::Value(v)) => Some(v.try_into()),
|
|
146
|
+
Ok(StreamItem::Error(e)) => Some(Err(Error::new(runtime_error(), e))),
|
|
147
|
+
Ok(StreamItem::End) | Err(_) => None,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn each_fiber_aware(&self, ruby: &Ruby) -> Result<(), Error> {
|
|
152
|
+
let read_io = create_ruby_io(ruby, self.pipe.read_fd())?;
|
|
153
|
+
let select_arr = ruby.ary_new_from_values(&[read_io]);
|
|
154
|
+
loop {
|
|
155
|
+
// IO.select([read_io])
|
|
156
|
+
let io = ruby
|
|
157
|
+
.class_object()
|
|
158
|
+
.const_get("IO")
|
|
159
|
+
.unwrap_or_else(|_| ruby.qnil().as_value());
|
|
160
|
+
let nil = ruby.qnil().as_value();
|
|
161
|
+
let _: Value = io.funcall("select", (select_arr, nil, nil))?;
|
|
162
|
+
|
|
163
|
+
self.pipe.drain();
|
|
164
|
+
loop {
|
|
165
|
+
match self.receiver.try_recv() {
|
|
166
|
+
Ok(StreamItem::Value(v)) => {
|
|
167
|
+
let val: Value = v.try_into()?;
|
|
168
|
+
let _: Value = ruby.yield_value(val)?;
|
|
169
|
+
}
|
|
170
|
+
Ok(StreamItem::Error(e)) => {
|
|
171
|
+
return Err(Error::new(ruby.exception_runtime_error(), e));
|
|
172
|
+
}
|
|
173
|
+
Ok(StreamItem::End) => return Ok(()),
|
|
174
|
+
Err(crossbeam_channel::TryRecvError::Empty) => break,
|
|
175
|
+
Err(crossbeam_channel::TryRecvError::Disconnected) => return Ok(()),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
pub fn each(&self) -> Result<(), Error> {
|
|
182
|
+
let ruby = Ruby::get().expect("called from Ruby thread");
|
|
183
|
+
if has_fiber_scheduler(&ruby) {
|
|
184
|
+
self.each_fiber_aware(&ruby)
|
|
185
|
+
} else {
|
|
186
|
+
loop {
|
|
187
|
+
match self.next_gvl_release() {
|
|
188
|
+
Some(Ok(value)) => {
|
|
189
|
+
let _: Value = ruby.yield_value(value)?;
|
|
190
|
+
}
|
|
191
|
+
Some(Err(e)) => return Err(e),
|
|
192
|
+
None => return Ok(()),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[cfg(test)]
|
|
200
|
+
mod tests {
|
|
201
|
+
use super::*;
|
|
202
|
+
use crate::stream::SendableValue;
|
|
203
|
+
use crate::test_helpers::with_ruby_python;
|
|
204
|
+
use crossbeam_channel::{bounded, unbounded};
|
|
205
|
+
use magnus::value::ReprValue;
|
|
206
|
+
use magnus::RArray;
|
|
207
|
+
use magnus::TryConvert;
|
|
208
|
+
use serial_test::serial;
|
|
209
|
+
use std::sync::atomic::Ordering;
|
|
210
|
+
use std::time::{Duration, Instant};
|
|
211
|
+
|
|
212
|
+
// ========== Step 3: recv_without_gvl_cb (pure Rust, no Ruby needed) ==========
|
|
213
|
+
|
|
214
|
+
#[test]
|
|
215
|
+
fn test_recv_cb_delivers_value() {
|
|
216
|
+
let (tx, rx) = unbounded();
|
|
217
|
+
tx.send(StreamItem::Value(SendableValue::Integer(42)))
|
|
218
|
+
.unwrap();
|
|
219
|
+
|
|
220
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
221
|
+
let mut args = RecvArgs {
|
|
222
|
+
receiver: rx,
|
|
223
|
+
result: None,
|
|
224
|
+
cancel,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
unsafe {
|
|
228
|
+
recv_without_gvl_cb(&mut args as *mut RecvArgs as *mut c_void);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
match args.result.unwrap().unwrap() {
|
|
232
|
+
StreamItem::Value(SendableValue::Integer(n)) => assert_eq!(n, 42),
|
|
233
|
+
_ => panic!("expected Integer(42)"),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[test]
|
|
238
|
+
fn test_recv_cb_delivers_end() {
|
|
239
|
+
let (tx, rx) = unbounded();
|
|
240
|
+
tx.send(StreamItem::End).unwrap();
|
|
241
|
+
|
|
242
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
243
|
+
let mut args = RecvArgs {
|
|
244
|
+
receiver: rx,
|
|
245
|
+
result: None,
|
|
246
|
+
cancel,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
unsafe {
|
|
250
|
+
recv_without_gvl_cb(&mut args as *mut RecvArgs as *mut c_void);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
assert!(matches!(args.result.unwrap().unwrap(), StreamItem::End));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn test_recv_cb_delivers_error() {
|
|
258
|
+
let (tx, rx) = unbounded();
|
|
259
|
+
tx.send(StreamItem::Error("boom".to_string())).unwrap();
|
|
260
|
+
|
|
261
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
262
|
+
let mut args = RecvArgs {
|
|
263
|
+
receiver: rx,
|
|
264
|
+
result: None,
|
|
265
|
+
cancel,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
unsafe {
|
|
269
|
+
recv_without_gvl_cb(&mut args as *mut RecvArgs as *mut c_void);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
match args.result.unwrap().unwrap() {
|
|
273
|
+
StreamItem::Error(msg) => assert_eq!(msg, "boom"),
|
|
274
|
+
_ => panic!("expected Error"),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[test]
|
|
279
|
+
fn test_recv_cb_handles_disconnect() {
|
|
280
|
+
let (tx, rx) = bounded::<StreamItem>(16);
|
|
281
|
+
drop(tx);
|
|
282
|
+
|
|
283
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
284
|
+
let mut args = RecvArgs {
|
|
285
|
+
receiver: rx,
|
|
286
|
+
result: None,
|
|
287
|
+
cancel,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
unsafe {
|
|
291
|
+
recv_without_gvl_cb(&mut args as *mut RecvArgs as *mut c_void);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
assert!(args.result.unwrap().is_err());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#[test]
|
|
298
|
+
fn test_recv_cb_multiple_values_in_order() {
|
|
299
|
+
let (tx, rx) = unbounded();
|
|
300
|
+
for i in 0..5 {
|
|
301
|
+
tx.send(StreamItem::Value(SendableValue::Integer(i)))
|
|
302
|
+
.unwrap();
|
|
303
|
+
}
|
|
304
|
+
tx.send(StreamItem::End).unwrap();
|
|
305
|
+
|
|
306
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
307
|
+
|
|
308
|
+
for expected in 0..5 {
|
|
309
|
+
let mut args = RecvArgs {
|
|
310
|
+
receiver: rx.clone(),
|
|
311
|
+
result: None,
|
|
312
|
+
cancel: cancel.clone(),
|
|
313
|
+
};
|
|
314
|
+
unsafe {
|
|
315
|
+
recv_without_gvl_cb(&mut args as *mut RecvArgs as *mut c_void);
|
|
316
|
+
}
|
|
317
|
+
match args.result.unwrap().unwrap() {
|
|
318
|
+
StreamItem::Value(SendableValue::Integer(n)) => assert_eq!(n, expected),
|
|
319
|
+
_ => panic!("expected Integer({expected})"),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Next should be End
|
|
324
|
+
let mut args = RecvArgs {
|
|
325
|
+
receiver: rx,
|
|
326
|
+
result: None,
|
|
327
|
+
cancel,
|
|
328
|
+
};
|
|
329
|
+
unsafe {
|
|
330
|
+
recv_without_gvl_cb(&mut args as *mut RecvArgs as *mut c_void);
|
|
331
|
+
}
|
|
332
|
+
assert!(matches!(args.result.unwrap().unwrap(), StreamItem::End));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ========== Step 4: ubf_cancel and cancel flag ==========
|
|
336
|
+
|
|
337
|
+
#[test]
|
|
338
|
+
fn test_ubf_cancel_sets_flag() {
|
|
339
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
340
|
+
assert!(!cancel.load(Ordering::Relaxed));
|
|
341
|
+
|
|
342
|
+
unsafe {
|
|
343
|
+
ubf_cancel(Arc::as_ptr(&cancel) as *mut c_void);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
assert!(cancel.load(Ordering::Relaxed));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[test]
|
|
350
|
+
fn test_recv_cb_respects_cancel_flag() {
|
|
351
|
+
// Empty channel — recv would block forever without cancel
|
|
352
|
+
let (_tx, rx) = bounded::<StreamItem>(16);
|
|
353
|
+
|
|
354
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
355
|
+
let cancel_clone = cancel.clone();
|
|
356
|
+
|
|
357
|
+
let mut args = RecvArgs {
|
|
358
|
+
receiver: rx,
|
|
359
|
+
result: None,
|
|
360
|
+
cancel,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Set cancel from another thread after a short delay
|
|
364
|
+
std::thread::spawn(move || {
|
|
365
|
+
std::thread::sleep(Duration::from_millis(30));
|
|
366
|
+
cancel_clone.store(true, Ordering::Relaxed);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
let start = Instant::now();
|
|
370
|
+
unsafe {
|
|
371
|
+
recv_without_gvl_cb(&mut args as *mut RecvArgs as *mut c_void);
|
|
372
|
+
}
|
|
373
|
+
let elapsed = start.elapsed();
|
|
374
|
+
|
|
375
|
+
// Should return None (cancelled), not block forever
|
|
376
|
+
assert!(args.result.is_none(), "expected None on cancel");
|
|
377
|
+
// Should return within ~100ms (50ms timeout + margin)
|
|
378
|
+
assert!(
|
|
379
|
+
elapsed < Duration::from_millis(200),
|
|
380
|
+
"cancel took {elapsed:?}, expected < 200ms"
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
#[test]
|
|
385
|
+
fn test_recv_cb_cancel_flag_already_set() {
|
|
386
|
+
// Cancel flag set before calling — should return immediately
|
|
387
|
+
let (_tx, rx) = bounded::<StreamItem>(16);
|
|
388
|
+
|
|
389
|
+
let cancel = Arc::new(AtomicBool::new(true)); // pre-set
|
|
390
|
+
let mut args = RecvArgs {
|
|
391
|
+
receiver: rx,
|
|
392
|
+
result: None,
|
|
393
|
+
cancel,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
let start = Instant::now();
|
|
397
|
+
unsafe {
|
|
398
|
+
recv_without_gvl_cb(&mut args as *mut RecvArgs as *mut c_void);
|
|
399
|
+
}
|
|
400
|
+
let elapsed = start.elapsed();
|
|
401
|
+
|
|
402
|
+
assert!(args.result.is_none(), "expected None on pre-set cancel");
|
|
403
|
+
assert!(
|
|
404
|
+
elapsed < Duration::from_millis(10),
|
|
405
|
+
"pre-set cancel took {elapsed:?}, expected near-instant"
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ========== Helper: construct NonBlockingStream for tests ==========
|
|
410
|
+
|
|
411
|
+
fn make_stream(rx: Receiver<StreamItem>) -> NonBlockingStream {
|
|
412
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
413
|
+
NonBlockingStream {
|
|
414
|
+
receiver: rx,
|
|
415
|
+
pipe,
|
|
416
|
+
cancel: Arc::new(AtomicBool::new(false)),
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#[allow(dead_code)]
|
|
421
|
+
fn make_stream_with_pipe(rx: Receiver<StreamItem>, pipe: Arc<PipeNotify>) -> NonBlockingStream {
|
|
422
|
+
NonBlockingStream {
|
|
423
|
+
receiver: rx,
|
|
424
|
+
pipe,
|
|
425
|
+
cancel: Arc::new(AtomicBool::new(false)),
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ========== Step 3+4: next_gvl_release integration (needs Ruby) ==========
|
|
430
|
+
|
|
431
|
+
#[test]
|
|
432
|
+
#[serial]
|
|
433
|
+
fn test_next_gvl_release_delivers_value() {
|
|
434
|
+
with_ruby_python(|_ruby, _api| {
|
|
435
|
+
let (tx, rx) = unbounded();
|
|
436
|
+
tx.send(StreamItem::Value(SendableValue::Integer(42)))
|
|
437
|
+
.unwrap();
|
|
438
|
+
|
|
439
|
+
let stream = make_stream(rx);
|
|
440
|
+
let val = stream.next_gvl_release().unwrap().unwrap();
|
|
441
|
+
assert_eq!(i64::try_convert(val).unwrap(), 42);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[test]
|
|
446
|
+
#[serial]
|
|
447
|
+
fn test_next_gvl_release_multiple_values_in_order() {
|
|
448
|
+
with_ruby_python(|_ruby, _api| {
|
|
449
|
+
let (tx, rx) = unbounded();
|
|
450
|
+
for i in 0..5 {
|
|
451
|
+
tx.send(StreamItem::Value(SendableValue::Integer(i)))
|
|
452
|
+
.unwrap();
|
|
453
|
+
}
|
|
454
|
+
tx.send(StreamItem::End).unwrap();
|
|
455
|
+
|
|
456
|
+
let stream = make_stream(rx);
|
|
457
|
+
for expected in 0..5 {
|
|
458
|
+
let val = stream.next_gvl_release().unwrap().unwrap();
|
|
459
|
+
assert_eq!(i64::try_convert(val).unwrap(), expected);
|
|
460
|
+
}
|
|
461
|
+
assert!(stream.next_gvl_release().is_none());
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#[test]
|
|
466
|
+
#[serial]
|
|
467
|
+
fn test_next_gvl_release_returns_none_on_end() {
|
|
468
|
+
with_ruby_python(|_ruby, _api| {
|
|
469
|
+
let (tx, rx) = unbounded();
|
|
470
|
+
tx.send(StreamItem::End).unwrap();
|
|
471
|
+
|
|
472
|
+
let stream = make_stream(rx);
|
|
473
|
+
assert!(stream.next_gvl_release().is_none());
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
#[test]
|
|
478
|
+
#[serial]
|
|
479
|
+
fn test_next_gvl_release_returns_none_on_disconnect() {
|
|
480
|
+
with_ruby_python(|_ruby, _api| {
|
|
481
|
+
let (tx, rx) = bounded::<StreamItem>(16);
|
|
482
|
+
drop(tx);
|
|
483
|
+
|
|
484
|
+
let stream = make_stream(rx);
|
|
485
|
+
assert!(stream.next_gvl_release().is_none());
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#[test]
|
|
490
|
+
#[serial]
|
|
491
|
+
fn test_next_gvl_release_propagates_error() {
|
|
492
|
+
with_ruby_python(|_ruby, _api| {
|
|
493
|
+
let (tx, rx) = unbounded();
|
|
494
|
+
tx.send(StreamItem::Error("something broke".to_string()))
|
|
495
|
+
.unwrap();
|
|
496
|
+
|
|
497
|
+
let stream = make_stream(rx);
|
|
498
|
+
let err = stream.next_gvl_release().unwrap().unwrap_err();
|
|
499
|
+
assert!(err.to_string().contains("something broke"));
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#[test]
|
|
504
|
+
#[serial]
|
|
505
|
+
fn test_next_gvl_release_all_sendable_types() {
|
|
506
|
+
with_ruby_python(|_ruby, _api| {
|
|
507
|
+
let (tx, rx) = unbounded();
|
|
508
|
+
tx.send(StreamItem::Value(SendableValue::Nil)).unwrap();
|
|
509
|
+
tx.send(StreamItem::Value(SendableValue::Integer(99)))
|
|
510
|
+
.unwrap();
|
|
511
|
+
tx.send(StreamItem::Value(SendableValue::Float(2.5)))
|
|
512
|
+
.unwrap();
|
|
513
|
+
tx.send(StreamItem::Value(SendableValue::Str("hello".to_string())))
|
|
514
|
+
.unwrap();
|
|
515
|
+
tx.send(StreamItem::Value(SendableValue::Bool(true)))
|
|
516
|
+
.unwrap();
|
|
517
|
+
tx.send(StreamItem::End).unwrap();
|
|
518
|
+
|
|
519
|
+
let stream = make_stream(rx);
|
|
520
|
+
|
|
521
|
+
let v = stream.next_gvl_release().unwrap().unwrap();
|
|
522
|
+
assert!(v.is_nil());
|
|
523
|
+
|
|
524
|
+
let v = stream.next_gvl_release().unwrap().unwrap();
|
|
525
|
+
assert_eq!(i64::try_convert(v).unwrap(), 99);
|
|
526
|
+
|
|
527
|
+
let v = stream.next_gvl_release().unwrap().unwrap();
|
|
528
|
+
assert!((f64::try_convert(v).unwrap() - 2.5).abs() < 1e-9);
|
|
529
|
+
|
|
530
|
+
let v = stream.next_gvl_release().unwrap().unwrap();
|
|
531
|
+
assert_eq!(String::try_convert(v).unwrap(), "hello");
|
|
532
|
+
|
|
533
|
+
let v = stream.next_gvl_release().unwrap().unwrap();
|
|
534
|
+
assert!(bool::try_convert(v).unwrap());
|
|
535
|
+
|
|
536
|
+
assert!(stream.next_gvl_release().is_none());
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
#[test]
|
|
541
|
+
#[serial]
|
|
542
|
+
fn test_next_gvl_release_with_delayed_producer() {
|
|
543
|
+
with_ruby_python(|_ruby, _api| {
|
|
544
|
+
let (tx, rx) = unbounded();
|
|
545
|
+
|
|
546
|
+
// Producer sends after a delay — recv must wait without deadlocking
|
|
547
|
+
std::thread::spawn(move || {
|
|
548
|
+
std::thread::sleep(Duration::from_millis(100));
|
|
549
|
+
tx.send(StreamItem::Value(SendableValue::Integer(7)))
|
|
550
|
+
.unwrap();
|
|
551
|
+
tx.send(StreamItem::End).unwrap();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
let stream = make_stream(rx);
|
|
555
|
+
|
|
556
|
+
let start = Instant::now();
|
|
557
|
+
let val = stream.next_gvl_release().unwrap().unwrap();
|
|
558
|
+
let elapsed = start.elapsed();
|
|
559
|
+
|
|
560
|
+
assert_eq!(i64::try_convert(val).unwrap(), 7);
|
|
561
|
+
assert!(
|
|
562
|
+
elapsed >= Duration::from_millis(50),
|
|
563
|
+
"should have waited for producer, took {elapsed:?}"
|
|
564
|
+
);
|
|
565
|
+
assert!(stream.next_gvl_release().is_none());
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ========== Step 6: create_ruby_io ==========
|
|
570
|
+
|
|
571
|
+
#[test]
|
|
572
|
+
#[serial]
|
|
573
|
+
fn test_create_ruby_io_returns_io_object() {
|
|
574
|
+
with_ruby_python(|ruby, _api| {
|
|
575
|
+
let pipe = PipeNotify::new().unwrap();
|
|
576
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
577
|
+
|
|
578
|
+
// Verify it's an instance of IO
|
|
579
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
580
|
+
let is_io: bool = io.funcall("is_a?", (io_class,)).unwrap();
|
|
581
|
+
assert!(is_io, "create_ruby_io should return an IO instance");
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
#[test]
|
|
586
|
+
#[serial]
|
|
587
|
+
fn test_create_ruby_io_fileno_matches_pipe_fd() {
|
|
588
|
+
with_ruby_python(|ruby, _api| {
|
|
589
|
+
let pipe = PipeNotify::new().unwrap();
|
|
590
|
+
let expected_fd = pipe.read_fd();
|
|
591
|
+
let io = create_ruby_io(ruby, expected_fd).unwrap();
|
|
592
|
+
|
|
593
|
+
let fileno: i32 = io.funcall("fileno", ()).unwrap();
|
|
594
|
+
assert_eq!(
|
|
595
|
+
fileno, expected_fd,
|
|
596
|
+
"IO#fileno should match the pipe's read_fd"
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[test]
|
|
602
|
+
#[serial]
|
|
603
|
+
fn test_create_ruby_io_autoclose_is_false() {
|
|
604
|
+
with_ruby_python(|ruby, _api| {
|
|
605
|
+
let pipe = PipeNotify::new().unwrap();
|
|
606
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
607
|
+
|
|
608
|
+
let autoclose: bool = io.funcall("autoclose?", ()).unwrap();
|
|
609
|
+
assert!(
|
|
610
|
+
!autoclose,
|
|
611
|
+
"autoclose must be false to prevent double-close with PipeNotify::drop"
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
#[test]
|
|
617
|
+
#[serial]
|
|
618
|
+
fn test_create_ruby_io_fd_survives_gc() {
|
|
619
|
+
// Verifies that Ruby GC does NOT close the fd because autoclose: false.
|
|
620
|
+
// After GC, the fd should still be valid (PipeNotify owns it).
|
|
621
|
+
with_ruby_python(|ruby, _api| {
|
|
622
|
+
let pipe = PipeNotify::new().unwrap();
|
|
623
|
+
let fd = pipe.read_fd();
|
|
624
|
+
|
|
625
|
+
{
|
|
626
|
+
let _io = create_ruby_io(ruby, fd).unwrap();
|
|
627
|
+
// io goes out of Rust scope here — Ruby may GC it
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Force Ruby GC
|
|
631
|
+
let _: Value = ruby.eval("GC.start").unwrap();
|
|
632
|
+
|
|
633
|
+
// fd should still be valid because autoclose: false
|
|
634
|
+
// fcntl(fd, F_GETFD) returns -1 with EBADF for closed fds
|
|
635
|
+
let ret = unsafe { libc::fcntl(fd, libc::F_GETFD) };
|
|
636
|
+
assert_ne!(
|
|
637
|
+
ret, -1,
|
|
638
|
+
"fd should still be open after Ruby GC (autoclose: false)"
|
|
639
|
+
);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#[test]
|
|
644
|
+
#[serial]
|
|
645
|
+
fn test_create_ruby_io_pipe_drop_closes_fd() {
|
|
646
|
+
// Verifies PipeNotify::drop is the one that closes the fd
|
|
647
|
+
with_ruby_python(|ruby, _api| {
|
|
648
|
+
let pipe = PipeNotify::new().unwrap();
|
|
649
|
+
let fd = pipe.read_fd();
|
|
650
|
+
let _io = create_ruby_io(ruby, fd).unwrap();
|
|
651
|
+
|
|
652
|
+
// Drop the pipe — should close the fd
|
|
653
|
+
drop(pipe);
|
|
654
|
+
|
|
655
|
+
let ret = unsafe { libc::fcntl(fd, libc::F_GETFD) };
|
|
656
|
+
assert_eq!(ret, -1, "fd should be closed after PipeNotify::drop");
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ========== Step 6: IO.select + pipe integration ==========
|
|
661
|
+
|
|
662
|
+
#[test]
|
|
663
|
+
#[serial]
|
|
664
|
+
fn test_io_select_wakes_on_pipe_notify() {
|
|
665
|
+
// IO.select should return when the pipe becomes readable after notify()
|
|
666
|
+
with_ruby_python(|ruby, _api| {
|
|
667
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
668
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
669
|
+
let select_arr = ruby.ary_new_from_values(&[io]);
|
|
670
|
+
let nil = ruby.qnil().as_value();
|
|
671
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
672
|
+
|
|
673
|
+
// Notify from another thread after a short delay
|
|
674
|
+
let pipe_clone = pipe.clone();
|
|
675
|
+
std::thread::spawn(move || {
|
|
676
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
677
|
+
pipe_clone.notify();
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
let start = Instant::now();
|
|
681
|
+
// IO.select([read_io], nil, nil) — blocks until readable
|
|
682
|
+
let result: Value = io_class.funcall("select", (select_arr, nil, nil)).unwrap();
|
|
683
|
+
let elapsed = start.elapsed();
|
|
684
|
+
|
|
685
|
+
assert!(
|
|
686
|
+
!result.is_nil(),
|
|
687
|
+
"IO.select should return non-nil on readable"
|
|
688
|
+
);
|
|
689
|
+
assert!(
|
|
690
|
+
elapsed >= Duration::from_millis(30),
|
|
691
|
+
"IO.select should have waited for notify, took {elapsed:?}"
|
|
692
|
+
);
|
|
693
|
+
assert!(
|
|
694
|
+
elapsed < Duration::from_secs(2),
|
|
695
|
+
"IO.select should not hang — took {elapsed:?}"
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
pipe.drain();
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
#[test]
|
|
703
|
+
#[serial]
|
|
704
|
+
fn test_io_select_returns_nil_on_timeout_without_notify() {
|
|
705
|
+
// IO.select with timeout returns nil if pipe is not notified
|
|
706
|
+
with_ruby_python(|ruby, _api| {
|
|
707
|
+
let pipe = PipeNotify::new().unwrap();
|
|
708
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
709
|
+
let select_arr = ruby.ary_new_from_values(&[io]);
|
|
710
|
+
let nil = ruby.qnil().as_value();
|
|
711
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
712
|
+
|
|
713
|
+
let start = Instant::now();
|
|
714
|
+
// IO.select([read_io], nil, nil, 0.05) — 50ms timeout
|
|
715
|
+
let result: Value = io_class
|
|
716
|
+
.funcall("select", (select_arr, nil, nil, 0.05))
|
|
717
|
+
.unwrap();
|
|
718
|
+
let elapsed = start.elapsed();
|
|
719
|
+
|
|
720
|
+
assert!(result.is_nil(), "IO.select should return nil on timeout");
|
|
721
|
+
assert!(
|
|
722
|
+
elapsed >= Duration::from_millis(30),
|
|
723
|
+
"IO.select should have waited for timeout, took {elapsed:?}"
|
|
724
|
+
);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
#[test]
|
|
729
|
+
#[serial]
|
|
730
|
+
fn test_io_select_immediate_if_already_notified() {
|
|
731
|
+
// If pipe was notified before IO.select, it should return immediately
|
|
732
|
+
with_ruby_python(|ruby, _api| {
|
|
733
|
+
let pipe = PipeNotify::new().unwrap();
|
|
734
|
+
pipe.notify(); // Notify BEFORE select
|
|
735
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
736
|
+
let select_arr = ruby.ary_new_from_values(&[io]);
|
|
737
|
+
let nil = ruby.qnil().as_value();
|
|
738
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
739
|
+
|
|
740
|
+
let start = Instant::now();
|
|
741
|
+
let result: Value = io_class
|
|
742
|
+
.funcall("select", (select_arr, nil, nil, 1.0))
|
|
743
|
+
.unwrap();
|
|
744
|
+
let elapsed = start.elapsed();
|
|
745
|
+
|
|
746
|
+
assert!(
|
|
747
|
+
!result.is_nil(),
|
|
748
|
+
"IO.select should return non-nil when already readable"
|
|
749
|
+
);
|
|
750
|
+
assert!(
|
|
751
|
+
elapsed < Duration::from_millis(50),
|
|
752
|
+
"IO.select should return immediately when pipe is already readable, took {elapsed:?}"
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
pipe.drain();
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#[test]
|
|
760
|
+
#[serial]
|
|
761
|
+
fn test_io_select_multiple_wake_cycles() {
|
|
762
|
+
// Multiple notify → select → drain cycles work correctly
|
|
763
|
+
with_ruby_python(|ruby, _api| {
|
|
764
|
+
let pipe = PipeNotify::new().unwrap();
|
|
765
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
766
|
+
let select_arr = ruby.ary_new_from_values(&[io]);
|
|
767
|
+
let nil = ruby.qnil().as_value();
|
|
768
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
769
|
+
|
|
770
|
+
for _ in 0..5 {
|
|
771
|
+
pipe.notify();
|
|
772
|
+
let result: Value = io_class
|
|
773
|
+
.funcall("select", (select_arr, nil, nil, 1.0))
|
|
774
|
+
.unwrap();
|
|
775
|
+
assert!(!result.is_nil(), "IO.select should wake on each notify");
|
|
776
|
+
pipe.drain();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// After all drains, pipe should be empty — select with timeout returns nil
|
|
780
|
+
let result: Value = io_class
|
|
781
|
+
.funcall("select", (select_arr, nil, nil, 0.01))
|
|
782
|
+
.unwrap();
|
|
783
|
+
assert!(
|
|
784
|
+
result.is_nil(),
|
|
785
|
+
"pipe should be empty after all drain cycles"
|
|
786
|
+
);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
#[test]
|
|
791
|
+
#[serial]
|
|
792
|
+
fn test_io_select_with_channel_drain_pattern() {
|
|
793
|
+
// Test the full pipe + channel coordination pattern used by each_fiber_aware:
|
|
794
|
+
// producer sends item + notifies pipe, consumer does IO.select + drain + try_recv
|
|
795
|
+
with_ruby_python(|ruby, _api| {
|
|
796
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
797
|
+
let (tx, rx) = bounded::<StreamItem>(16);
|
|
798
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
799
|
+
let select_arr = ruby.ary_new_from_values(&[io]);
|
|
800
|
+
let nil = ruby.qnil().as_value();
|
|
801
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
802
|
+
|
|
803
|
+
// Producer: send items + notify for each
|
|
804
|
+
let pipe_clone = pipe.clone();
|
|
805
|
+
std::thread::spawn(move || {
|
|
806
|
+
for i in 0..5 {
|
|
807
|
+
tx.send(StreamItem::Value(SendableValue::Integer(i)))
|
|
808
|
+
.unwrap();
|
|
809
|
+
pipe_clone.notify();
|
|
810
|
+
std::thread::sleep(Duration::from_millis(10));
|
|
811
|
+
}
|
|
812
|
+
tx.send(StreamItem::End).unwrap();
|
|
813
|
+
pipe_clone.notify();
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Consumer: IO.select → drain → try_recv loop
|
|
817
|
+
let mut collected = Vec::new();
|
|
818
|
+
'outer: loop {
|
|
819
|
+
let _: Value = io_class
|
|
820
|
+
.funcall("select", (select_arr, nil, nil, 2.0))
|
|
821
|
+
.unwrap();
|
|
822
|
+
pipe.drain();
|
|
823
|
+
|
|
824
|
+
loop {
|
|
825
|
+
match rx.try_recv() {
|
|
826
|
+
Ok(StreamItem::Value(v)) => {
|
|
827
|
+
if let SendableValue::Integer(n) = v {
|
|
828
|
+
collected.push(n);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
Ok(StreamItem::End) => break 'outer,
|
|
832
|
+
Ok(StreamItem::Error(e)) => panic!("unexpected error: {e}"),
|
|
833
|
+
Err(crossbeam_channel::TryRecvError::Empty) => break,
|
|
834
|
+
Err(crossbeam_channel::TryRecvError::Disconnected) => break 'outer,
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
assert_eq!(
|
|
840
|
+
collected,
|
|
841
|
+
vec![0, 1, 2, 3, 4],
|
|
842
|
+
"all items should arrive in order"
|
|
843
|
+
);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
#[test]
|
|
848
|
+
#[serial]
|
|
849
|
+
fn test_io_select_batch_notify_drains_all() {
|
|
850
|
+
// Producer sends multiple items between consumer wakeups.
|
|
851
|
+
// Consumer must drain ALL bytes from pipe and ALL items from channel.
|
|
852
|
+
with_ruby_python(|ruby, _api| {
|
|
853
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
854
|
+
let (tx, rx) = bounded::<StreamItem>(16);
|
|
855
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
856
|
+
let select_arr = ruby.ary_new_from_values(&[io]);
|
|
857
|
+
let nil = ruby.qnil().as_value();
|
|
858
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
859
|
+
|
|
860
|
+
// Producer: send 10 items rapidly with notifications, then End
|
|
861
|
+
let pipe_clone = pipe.clone();
|
|
862
|
+
std::thread::spawn(move || {
|
|
863
|
+
for i in 0..10 {
|
|
864
|
+
tx.send(StreamItem::Value(SendableValue::Integer(i)))
|
|
865
|
+
.unwrap();
|
|
866
|
+
pipe_clone.notify();
|
|
867
|
+
}
|
|
868
|
+
tx.send(StreamItem::End).unwrap();
|
|
869
|
+
pipe_clone.notify();
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// Give producer time to send everything
|
|
873
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
874
|
+
|
|
875
|
+
// One IO.select + drain should get everything
|
|
876
|
+
let _: Value = io_class
|
|
877
|
+
.funcall("select", (select_arr, nil, nil, 2.0))
|
|
878
|
+
.unwrap();
|
|
879
|
+
pipe.drain();
|
|
880
|
+
|
|
881
|
+
let mut collected = Vec::new();
|
|
882
|
+
loop {
|
|
883
|
+
match rx.try_recv() {
|
|
884
|
+
Ok(StreamItem::Value(v)) => {
|
|
885
|
+
if let SendableValue::Integer(n) = v {
|
|
886
|
+
collected.push(n);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
Ok(StreamItem::End) => break,
|
|
890
|
+
Ok(StreamItem::Error(e)) => panic!("unexpected error: {e}"),
|
|
891
|
+
Err(crossbeam_channel::TryRecvError::Empty) => break,
|
|
892
|
+
Err(crossbeam_channel::TryRecvError::Disconnected) => break,
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
assert_eq!(
|
|
897
|
+
collected,
|
|
898
|
+
(0..10).collect::<Vec<_>>(),
|
|
899
|
+
"draining should collect all items sent in a batch"
|
|
900
|
+
);
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
#[test]
|
|
905
|
+
#[serial]
|
|
906
|
+
fn test_io_select_no_byte_accumulation() {
|
|
907
|
+
// After proper drain cycles, no stale notification bytes remain
|
|
908
|
+
with_ruby_python(|ruby, _api| {
|
|
909
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
910
|
+
let (tx, rx) = bounded::<StreamItem>(64);
|
|
911
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
912
|
+
let select_arr = ruby.ary_new_from_values(&[io]);
|
|
913
|
+
let nil = ruby.qnil().as_value();
|
|
914
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
915
|
+
|
|
916
|
+
// Send 50 items, each with a notification
|
|
917
|
+
for i in 0..50 {
|
|
918
|
+
tx.send(StreamItem::Value(SendableValue::Integer(i)))
|
|
919
|
+
.unwrap();
|
|
920
|
+
pipe.notify();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Consume all items with proper drain pattern
|
|
924
|
+
let mut count = 0;
|
|
925
|
+
loop {
|
|
926
|
+
let result: Value = io_class
|
|
927
|
+
.funcall("select", (select_arr, nil, nil, 0.1))
|
|
928
|
+
.unwrap();
|
|
929
|
+
if result.is_nil() {
|
|
930
|
+
break; // Timeout — pipe is empty
|
|
931
|
+
}
|
|
932
|
+
pipe.drain();
|
|
933
|
+
while let Ok(StreamItem::Value(_)) = rx.try_recv() {
|
|
934
|
+
count += 1;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
assert_eq!(count, 50, "all 50 items should be consumed");
|
|
939
|
+
|
|
940
|
+
// Final check: IO.select with short timeout should return nil (no stale bytes)
|
|
941
|
+
let result: Value = io_class
|
|
942
|
+
.funcall("select", (select_arr, nil, nil, 0.01))
|
|
943
|
+
.unwrap();
|
|
944
|
+
assert!(
|
|
945
|
+
result.is_nil(),
|
|
946
|
+
"no stale notification bytes should remain after proper draining"
|
|
947
|
+
);
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// ========== Step 6: has_fiber_scheduler ==========
|
|
952
|
+
|
|
953
|
+
#[test]
|
|
954
|
+
#[serial]
|
|
955
|
+
fn test_has_fiber_scheduler_returns_false_by_default() {
|
|
956
|
+
with_ruby_python(|ruby, _api| {
|
|
957
|
+
assert!(
|
|
958
|
+
!has_fiber_scheduler(ruby),
|
|
959
|
+
"has_fiber_scheduler should return false when no scheduler is installed"
|
|
960
|
+
);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
#[test]
|
|
965
|
+
#[serial]
|
|
966
|
+
fn test_has_fiber_scheduler_detects_nil_scheduler() {
|
|
967
|
+
// Fiber.scheduler returns nil by default — verify our function handles it
|
|
968
|
+
with_ruby_python(|ruby, _api| {
|
|
969
|
+
let fiber_class: Value = ruby.class_object().const_get("Fiber").unwrap();
|
|
970
|
+
let scheduler: Value = fiber_class.funcall("scheduler", ()).unwrap();
|
|
971
|
+
assert!(
|
|
972
|
+
scheduler.is_nil(),
|
|
973
|
+
"Fiber.scheduler should be nil by default"
|
|
974
|
+
);
|
|
975
|
+
assert!(!has_fiber_scheduler(ruby));
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// ========== Step 6: each_fiber_aware building blocks ==========
|
|
980
|
+
|
|
981
|
+
#[test]
|
|
982
|
+
#[serial]
|
|
983
|
+
fn test_io_select_with_error_item() {
|
|
984
|
+
// Verify the drain pattern correctly surfaces errors from the channel
|
|
985
|
+
with_ruby_python(|ruby, _api| {
|
|
986
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
987
|
+
let (tx, rx) = bounded::<StreamItem>(16);
|
|
988
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
989
|
+
let select_arr = ruby.ary_new_from_values(&[io]);
|
|
990
|
+
let nil = ruby.qnil().as_value();
|
|
991
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
992
|
+
|
|
993
|
+
// Send a value, then an error, with notifications
|
|
994
|
+
tx.send(StreamItem::Value(SendableValue::Integer(1)))
|
|
995
|
+
.unwrap();
|
|
996
|
+
pipe.notify();
|
|
997
|
+
tx.send(StreamItem::Error("stream failed".to_string()))
|
|
998
|
+
.unwrap();
|
|
999
|
+
pipe.notify();
|
|
1000
|
+
|
|
1001
|
+
let _: Value = io_class
|
|
1002
|
+
.funcall("select", (select_arr, nil, nil, 1.0))
|
|
1003
|
+
.unwrap();
|
|
1004
|
+
pipe.drain();
|
|
1005
|
+
|
|
1006
|
+
// First item should be the value
|
|
1007
|
+
match rx.try_recv().unwrap() {
|
|
1008
|
+
StreamItem::Value(SendableValue::Integer(n)) => assert_eq!(n, 1),
|
|
1009
|
+
_ => panic!("expected Integer(1)"),
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Second item should be the error
|
|
1013
|
+
match rx.try_recv().unwrap() {
|
|
1014
|
+
StreamItem::Error(msg) => assert_eq!(msg, "stream failed"),
|
|
1015
|
+
_ => panic!("expected Error"),
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
#[test]
|
|
1021
|
+
#[serial]
|
|
1022
|
+
fn test_io_select_with_disconnect() {
|
|
1023
|
+
// If the producer drops the sender, try_recv returns Disconnected
|
|
1024
|
+
with_ruby_python(|ruby, _api| {
|
|
1025
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1026
|
+
let (tx, rx) = bounded::<StreamItem>(16);
|
|
1027
|
+
let io = create_ruby_io(ruby, pipe.read_fd()).unwrap();
|
|
1028
|
+
let select_arr = ruby.ary_new_from_values(&[io]);
|
|
1029
|
+
let nil = ruby.qnil().as_value();
|
|
1030
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
1031
|
+
|
|
1032
|
+
// Send one value, notify, then drop sender
|
|
1033
|
+
tx.send(StreamItem::Value(SendableValue::Integer(99)))
|
|
1034
|
+
.unwrap();
|
|
1035
|
+
pipe.notify();
|
|
1036
|
+
drop(tx);
|
|
1037
|
+
|
|
1038
|
+
let _: Value = io_class
|
|
1039
|
+
.funcall("select", (select_arr, nil, nil, 1.0))
|
|
1040
|
+
.unwrap();
|
|
1041
|
+
pipe.drain();
|
|
1042
|
+
|
|
1043
|
+
// Get the value
|
|
1044
|
+
match rx.try_recv().unwrap() {
|
|
1045
|
+
StreamItem::Value(SendableValue::Integer(n)) => assert_eq!(n, 99),
|
|
1046
|
+
_ => panic!("expected Integer(99)"),
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Next try_recv should indicate disconnected
|
|
1050
|
+
assert!(matches!(
|
|
1051
|
+
rx.try_recv(),
|
|
1052
|
+
Err(crossbeam_channel::TryRecvError::Disconnected)
|
|
1053
|
+
));
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
#[test]
|
|
1058
|
+
#[serial]
|
|
1059
|
+
fn test_create_ruby_io_multiple_pipes() {
|
|
1060
|
+
// Multiple Ruby IO objects wrapping different pipes should work independently
|
|
1061
|
+
with_ruby_python(|ruby, _api| {
|
|
1062
|
+
let pipe1 = PipeNotify::new().unwrap();
|
|
1063
|
+
let pipe2 = PipeNotify::new().unwrap();
|
|
1064
|
+
|
|
1065
|
+
let io1 = create_ruby_io(ruby, pipe1.read_fd()).unwrap();
|
|
1066
|
+
let io2 = create_ruby_io(ruby, pipe2.read_fd()).unwrap();
|
|
1067
|
+
|
|
1068
|
+
let fileno1: i32 = io1.funcall("fileno", ()).unwrap();
|
|
1069
|
+
let fileno2: i32 = io2.funcall("fileno", ()).unwrap();
|
|
1070
|
+
|
|
1071
|
+
assert_ne!(
|
|
1072
|
+
fileno1, fileno2,
|
|
1073
|
+
"different pipes should have different fds"
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
// Notify pipe1 only — IO.select on io1 should return, io2 should not
|
|
1077
|
+
pipe1.notify();
|
|
1078
|
+
|
|
1079
|
+
let nil = ruby.qnil().as_value();
|
|
1080
|
+
let io_class: Value = ruby.class_object().const_get("IO").unwrap();
|
|
1081
|
+
|
|
1082
|
+
let arr1 = ruby.ary_new_from_values(&[io1]);
|
|
1083
|
+
let result1: Value = io_class.funcall("select", (arr1, nil, nil, 0.1)).unwrap();
|
|
1084
|
+
assert!(
|
|
1085
|
+
!result1.is_nil(),
|
|
1086
|
+
"io1 should be readable after pipe1.notify()"
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
let arr2 = ruby.ary_new_from_values(&[io2]);
|
|
1090
|
+
let result2: Value = io_class.funcall("select", (arr2, nil, nil, 0.01)).unwrap();
|
|
1091
|
+
assert!(
|
|
1092
|
+
result2.is_nil(),
|
|
1093
|
+
"io2 should NOT be readable (pipe2 not notified)"
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
pipe1.drain();
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ========== Step 7: each() auto-dispatch (needs Ruby class registration) ==========
|
|
1101
|
+
|
|
1102
|
+
/// Register NonBlockingStream as a Ruby class so we can test `each` with a block.
|
|
1103
|
+
/// Idempotent — safe to call multiple times.
|
|
1104
|
+
fn ensure_nb_class_registered(ruby: &Ruby) {
|
|
1105
|
+
let rubyx = ruby.define_module("Rubyx").unwrap();
|
|
1106
|
+
let class = rubyx
|
|
1107
|
+
.define_class("NonBlockingStream", ruby.class_object())
|
|
1108
|
+
.unwrap();
|
|
1109
|
+
// define_method is idempotent (replaces if exists)
|
|
1110
|
+
class
|
|
1111
|
+
.define_method("each", magnus::method!(NonBlockingStream::each, 0))
|
|
1112
|
+
.unwrap();
|
|
1113
|
+
class.include_module(ruby.module_enumerable()).unwrap();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/// Wrap a NonBlockingStream as a Ruby object so we can call Ruby methods on it.
|
|
1117
|
+
fn wrap_stream(ruby: &Ruby, stream: NonBlockingStream) -> Value {
|
|
1118
|
+
use magnus::IntoValue;
|
|
1119
|
+
stream.into_value_with(ruby)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
#[test]
|
|
1123
|
+
#[serial]
|
|
1124
|
+
fn test_each_dispatches_gvl_release_without_scheduler() {
|
|
1125
|
+
// Without a Fiber Scheduler, each() should use the GVL-release path
|
|
1126
|
+
// and produce correct results via to_a (which calls each with a block)
|
|
1127
|
+
with_ruby_python(|ruby, _api| {
|
|
1128
|
+
ensure_nb_class_registered(ruby);
|
|
1129
|
+
|
|
1130
|
+
let (tx, rx) = unbounded();
|
|
1131
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1132
|
+
for i in 0..5 {
|
|
1133
|
+
tx.send(StreamItem::Value(SendableValue::Integer(i)))
|
|
1134
|
+
.unwrap();
|
|
1135
|
+
}
|
|
1136
|
+
tx.send(StreamItem::End).unwrap();
|
|
1137
|
+
|
|
1138
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1139
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1140
|
+
|
|
1141
|
+
// to_a calls each internally, which will use GVL-release path
|
|
1142
|
+
// (no Fiber Scheduler installed)
|
|
1143
|
+
let result: Value = ruby_obj.funcall("to_a", ()).unwrap();
|
|
1144
|
+
let arr = RArray::try_convert(result).unwrap();
|
|
1145
|
+
|
|
1146
|
+
assert_eq!(arr.len(), 5);
|
|
1147
|
+
for i in 0..5 {
|
|
1148
|
+
let v = i64::try_convert(arr.entry::<Value>(i).unwrap()).unwrap();
|
|
1149
|
+
assert_eq!(v, i as i64);
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
#[test]
|
|
1155
|
+
#[serial]
|
|
1156
|
+
fn test_each_empty_stream() {
|
|
1157
|
+
with_ruby_python(|ruby, _api| {
|
|
1158
|
+
ensure_nb_class_registered(ruby);
|
|
1159
|
+
|
|
1160
|
+
let (tx, rx) = unbounded();
|
|
1161
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1162
|
+
tx.send(StreamItem::End).unwrap();
|
|
1163
|
+
|
|
1164
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1165
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1166
|
+
|
|
1167
|
+
let result: Value = ruby_obj.funcall("to_a", ()).unwrap();
|
|
1168
|
+
let arr = RArray::try_convert(result).unwrap();
|
|
1169
|
+
assert_eq!(arr.len(), 0, "empty stream should produce empty array");
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
#[test]
|
|
1174
|
+
#[serial]
|
|
1175
|
+
fn test_each_single_value() {
|
|
1176
|
+
with_ruby_python(|ruby, _api| {
|
|
1177
|
+
ensure_nb_class_registered(ruby);
|
|
1178
|
+
|
|
1179
|
+
let (tx, rx) = unbounded();
|
|
1180
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1181
|
+
tx.send(StreamItem::Value(SendableValue::Str("hello".to_string())))
|
|
1182
|
+
.unwrap();
|
|
1183
|
+
tx.send(StreamItem::End).unwrap();
|
|
1184
|
+
|
|
1185
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1186
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1187
|
+
|
|
1188
|
+
let result: Value = ruby_obj.funcall("to_a", ()).unwrap();
|
|
1189
|
+
let arr = RArray::try_convert(result).unwrap();
|
|
1190
|
+
assert_eq!(arr.len(), 1);
|
|
1191
|
+
assert_eq!(
|
|
1192
|
+
String::try_convert(arr.entry::<Value>(0).unwrap()).unwrap(),
|
|
1193
|
+
"hello"
|
|
1194
|
+
);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
#[test]
|
|
1199
|
+
#[serial]
|
|
1200
|
+
fn test_each_mixed_types() {
|
|
1201
|
+
with_ruby_python(|ruby, _api| {
|
|
1202
|
+
ensure_nb_class_registered(ruby);
|
|
1203
|
+
|
|
1204
|
+
let (tx, rx) = unbounded();
|
|
1205
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1206
|
+
tx.send(StreamItem::Value(SendableValue::Integer(42)))
|
|
1207
|
+
.unwrap();
|
|
1208
|
+
tx.send(StreamItem::Value(SendableValue::Float(
|
|
1209
|
+
std::f64::consts::PI,
|
|
1210
|
+
)))
|
|
1211
|
+
.unwrap();
|
|
1212
|
+
tx.send(StreamItem::Value(SendableValue::Str("test".to_string())))
|
|
1213
|
+
.unwrap();
|
|
1214
|
+
tx.send(StreamItem::Value(SendableValue::Bool(true)))
|
|
1215
|
+
.unwrap();
|
|
1216
|
+
tx.send(StreamItem::Value(SendableValue::Nil)).unwrap();
|
|
1217
|
+
tx.send(StreamItem::End).unwrap();
|
|
1218
|
+
|
|
1219
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1220
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1221
|
+
|
|
1222
|
+
let result: Value = ruby_obj.funcall("to_a", ()).unwrap();
|
|
1223
|
+
let arr = RArray::try_convert(result).unwrap();
|
|
1224
|
+
assert_eq!(arr.len(), 5);
|
|
1225
|
+
|
|
1226
|
+
assert_eq!(
|
|
1227
|
+
i64::try_convert(arr.entry::<Value>(0).unwrap()).unwrap(),
|
|
1228
|
+
42
|
|
1229
|
+
);
|
|
1230
|
+
assert!(
|
|
1231
|
+
(f64::try_convert(arr.entry::<Value>(1).unwrap()).unwrap() - std::f64::consts::PI)
|
|
1232
|
+
.abs()
|
|
1233
|
+
< 1e-9
|
|
1234
|
+
);
|
|
1235
|
+
assert_eq!(
|
|
1236
|
+
String::try_convert(arr.entry::<Value>(2).unwrap()).unwrap(),
|
|
1237
|
+
"test"
|
|
1238
|
+
);
|
|
1239
|
+
assert!(bool::try_convert(arr.entry::<Value>(3).unwrap()).unwrap());
|
|
1240
|
+
assert!(arr.entry::<Value>(4).unwrap().is_nil());
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
#[test]
|
|
1245
|
+
#[serial]
|
|
1246
|
+
fn test_each_propagates_error() {
|
|
1247
|
+
with_ruby_python(|ruby, _api| {
|
|
1248
|
+
ensure_nb_class_registered(ruby);
|
|
1249
|
+
|
|
1250
|
+
let (tx, rx) = unbounded();
|
|
1251
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1252
|
+
tx.send(StreamItem::Value(SendableValue::Integer(1)))
|
|
1253
|
+
.unwrap();
|
|
1254
|
+
tx.send(StreamItem::Error("stream exploded".to_string()))
|
|
1255
|
+
.unwrap();
|
|
1256
|
+
|
|
1257
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1258
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1259
|
+
|
|
1260
|
+
let err = ruby_obj.funcall::<_, _, Value>("to_a", ()).unwrap_err();
|
|
1261
|
+
assert!(
|
|
1262
|
+
err.to_string().contains("stream exploded"),
|
|
1263
|
+
"error should propagate: {err}"
|
|
1264
|
+
);
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
#[test]
|
|
1269
|
+
#[serial]
|
|
1270
|
+
fn test_each_with_delayed_producer() {
|
|
1271
|
+
// Producer sends values after a delay — each() should wait via GVL release
|
|
1272
|
+
with_ruby_python(|ruby, _api| {
|
|
1273
|
+
ensure_nb_class_registered(ruby);
|
|
1274
|
+
|
|
1275
|
+
let (tx, rx) = unbounded();
|
|
1276
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1277
|
+
|
|
1278
|
+
std::thread::spawn(move || {
|
|
1279
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
1280
|
+
for i in 0..3 {
|
|
1281
|
+
tx.send(StreamItem::Value(SendableValue::Integer(i)))
|
|
1282
|
+
.unwrap();
|
|
1283
|
+
}
|
|
1284
|
+
tx.send(StreamItem::End).unwrap();
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1288
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1289
|
+
|
|
1290
|
+
let start = Instant::now();
|
|
1291
|
+
let result: Value = ruby_obj.funcall("to_a", ()).unwrap();
|
|
1292
|
+
let elapsed = start.elapsed();
|
|
1293
|
+
|
|
1294
|
+
let arr = RArray::try_convert(result).unwrap();
|
|
1295
|
+
assert_eq!(arr.len(), 3);
|
|
1296
|
+
assert!(
|
|
1297
|
+
elapsed >= Duration::from_millis(30),
|
|
1298
|
+
"should have waited for producer, took {elapsed:?}"
|
|
1299
|
+
);
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
#[test]
|
|
1304
|
+
#[serial]
|
|
1305
|
+
fn test_each_disconnect_returns_empty() {
|
|
1306
|
+
// If producer drops sender immediately, each() should return without error
|
|
1307
|
+
with_ruby_python(|ruby, _api| {
|
|
1308
|
+
ensure_nb_class_registered(ruby);
|
|
1309
|
+
|
|
1310
|
+
let (tx, rx) = bounded::<StreamItem>(16);
|
|
1311
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1312
|
+
drop(tx);
|
|
1313
|
+
|
|
1314
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1315
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1316
|
+
|
|
1317
|
+
let result: Value = ruby_obj.funcall("to_a", ()).unwrap();
|
|
1318
|
+
let arr = RArray::try_convert(result).unwrap();
|
|
1319
|
+
assert_eq!(
|
|
1320
|
+
arr.len(),
|
|
1321
|
+
0,
|
|
1322
|
+
"disconnected channel should produce empty array"
|
|
1323
|
+
);
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
#[test]
|
|
1328
|
+
#[serial]
|
|
1329
|
+
fn test_each_enumerable_first() {
|
|
1330
|
+
// Enumerable#first should work (calls each, takes first N, breaks)
|
|
1331
|
+
with_ruby_python(|ruby, _api| {
|
|
1332
|
+
ensure_nb_class_registered(ruby);
|
|
1333
|
+
|
|
1334
|
+
let (tx, rx) = unbounded();
|
|
1335
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1336
|
+
for i in 0..10 {
|
|
1337
|
+
tx.send(StreamItem::Value(SendableValue::Integer(i)))
|
|
1338
|
+
.unwrap();
|
|
1339
|
+
}
|
|
1340
|
+
tx.send(StreamItem::End).unwrap();
|
|
1341
|
+
|
|
1342
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1343
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1344
|
+
|
|
1345
|
+
let result: Value = ruby_obj.funcall("first", (3,)).unwrap();
|
|
1346
|
+
let arr = RArray::try_convert(result).unwrap();
|
|
1347
|
+
assert_eq!(arr.len(), 3);
|
|
1348
|
+
assert_eq!(i64::try_convert(arr.entry::<Value>(0).unwrap()).unwrap(), 0);
|
|
1349
|
+
assert_eq!(i64::try_convert(arr.entry::<Value>(1).unwrap()).unwrap(), 1);
|
|
1350
|
+
assert_eq!(i64::try_convert(arr.entry::<Value>(2).unwrap()).unwrap(), 2);
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
#[test]
|
|
1355
|
+
#[serial]
|
|
1356
|
+
fn test_each_uses_gvl_release_path_without_scheduler() {
|
|
1357
|
+
// Confirm has_fiber_scheduler returns false AND each() still works.
|
|
1358
|
+
// This proves the GVL-release dispatch path is exercised.
|
|
1359
|
+
with_ruby_python(|ruby, _api| {
|
|
1360
|
+
ensure_nb_class_registered(ruby);
|
|
1361
|
+
|
|
1362
|
+
assert!(
|
|
1363
|
+
!has_fiber_scheduler(ruby),
|
|
1364
|
+
"precondition: no Fiber Scheduler should be installed"
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
let (tx, rx) = unbounded();
|
|
1368
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1369
|
+
tx.send(StreamItem::Value(SendableValue::Integer(99)))
|
|
1370
|
+
.unwrap();
|
|
1371
|
+
tx.send(StreamItem::End).unwrap();
|
|
1372
|
+
|
|
1373
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1374
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1375
|
+
|
|
1376
|
+
let result: Value = ruby_obj.funcall("to_a", ()).unwrap();
|
|
1377
|
+
let arr = RArray::try_convert(result).unwrap();
|
|
1378
|
+
assert_eq!(arr.len(), 1);
|
|
1379
|
+
assert_eq!(
|
|
1380
|
+
i64::try_convert(arr.entry::<Value>(0).unwrap()).unwrap(),
|
|
1381
|
+
99
|
|
1382
|
+
);
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
#[test]
|
|
1387
|
+
#[serial]
|
|
1388
|
+
fn test_each_large_stream() {
|
|
1389
|
+
with_ruby_python(|ruby, _api| {
|
|
1390
|
+
ensure_nb_class_registered(ruby);
|
|
1391
|
+
|
|
1392
|
+
let (tx, rx) = unbounded();
|
|
1393
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
1394
|
+
|
|
1395
|
+
std::thread::spawn(move || {
|
|
1396
|
+
for i in 0..1000 {
|
|
1397
|
+
tx.send(StreamItem::Value(SendableValue::Integer(i)))
|
|
1398
|
+
.unwrap();
|
|
1399
|
+
}
|
|
1400
|
+
tx.send(StreamItem::End).unwrap();
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
let stream = NonBlockingStream::new(rx, pipe);
|
|
1404
|
+
let ruby_obj = wrap_stream(ruby, stream);
|
|
1405
|
+
|
|
1406
|
+
let result: Value = ruby_obj.funcall("to_a", ()).unwrap();
|
|
1407
|
+
let arr = RArray::try_convert(result).unwrap();
|
|
1408
|
+
assert_eq!(arr.len(), 1000);
|
|
1409
|
+
|
|
1410
|
+
// Verify order: check first, middle, last
|
|
1411
|
+
assert_eq!(i64::try_convert(arr.entry::<Value>(0).unwrap()).unwrap(), 0);
|
|
1412
|
+
assert_eq!(
|
|
1413
|
+
i64::try_convert(arr.entry::<Value>(500).unwrap()).unwrap(),
|
|
1414
|
+
500
|
|
1415
|
+
);
|
|
1416
|
+
assert_eq!(
|
|
1417
|
+
i64::try_convert(arr.entry::<Value>(999).unwrap()).unwrap(),
|
|
1418
|
+
999
|
|
1419
|
+
);
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
}
|