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