Skip to content
File

Blob: src/rust/jsg-test/tests/gc.rs

rust1425 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5//! GC (garbage collection) tests for Rust resources.
6//!
7//! These tests verify that Rust resources are properly cleaned up when:
8//! 1. All Rust `Ref` handles are dropped and no JavaScript wrapper exists (immediate cleanup)
9//! 2. All Rust `Ref` handles are dropped and V8 garbage collects the JS wrapper
10//!
11//! Note: Circular references through `Ref<T>` are NOT collected, matching the behavior
12//! of C++ `jsg::Rc<T>` which uses `kj::Own<T>` cross-references.
13 
14use std::cell::Cell;
15use std::sync::atomic::AtomicUsize;
16use std::sync::atomic::Ordering;
17 
18use jsg::ToJS;
19use jsg_macros::jsg_method;
20use jsg_macros::jsg_resource;
21 
22/// Counter to track how many `SimpleResource` instances have been dropped.
23static SIMPLE_RESOURCE_DROPS: AtomicUsize = AtomicUsize::new(0);
24 
25#[jsg_resource]
26struct SimpleResource {
27 pub name: String,
28}
29 
30impl Drop for SimpleResource {
31 fn drop(&mut self) {
32 SIMPLE_RESOURCE_DROPS.fetch_add(1, Ordering::SeqCst);
33 }
34}
35 
36#[jsg_resource]
37#[expect(clippy::unnecessary_wraps)]
38impl SimpleResource {
39 #[jsg_method]
40 fn get_name(&self) -> Result<String, jsg::Error> {
41 Ok(self.name.clone())
42 }
43}
44 
45/// Counter to track how many `ParentResource` instances have been dropped.
46static PARENT_RESOURCE_DROPS: AtomicUsize = AtomicUsize::new(0);
47 
48#[jsg_resource]
49struct ParentResource {
50 pub child: jsg::Rc<SimpleResource>,
51 pub optional_child: Option<jsg::Rc<SimpleResource>>,
52}
53 
54impl Drop for ParentResource {
55 fn drop(&mut self) {
56 PARENT_RESOURCE_DROPS.fetch_add(1, Ordering::SeqCst);
57 }
58}
59 
60#[jsg_resource]
61impl ParentResource {}
62 
63/// Tests that resources are dropped immediately when all Rust Refs are dropped
64/// and no JS wrapper exists.
65///
66/// In the Wrappable model, dropping the last Ref decrements the kj refcount to 0,
67/// which immediately destroys the Wrappable (and thus the Rust resource).
68/// No GC is needed.
69#[test]
70fn supports_gc_via_ref_drop() {
71 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
72 
73 let harness = crate::Harness::new();
74 harness.run_in_context(|_lock, _ctx| {
75 let resource = jsg::Rc::new(SimpleResource {
76 name: "test".to_owned(),
77 });
78 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
79 std::mem::drop(resource);
80 // In the Wrappable model, no wrapper means immediate cleanup when refcount hits 0
81 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
82 Ok(())
83 });
84}
85 
86/// Tests that resources are dropped via V8 GC when JS wrapper is collected.
87///
88/// When a resource is wrapped for JavaScript:
89/// 1. Dropping all Rust `Ref` handles calls `removeStrongRef()` but the `CppgcShim`
90/// still holds a `kj::Own` keeping the object alive
91/// 2. V8 GC collects the wrapper, `CppgcShim` is destroyed, `kj::Own` is dropped
92/// 3. kj refcount reaches 0, Wrappable is destroyed
93#[test]
94fn supports_gc_via_weak_callback() {
95 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
96 
97 let harness = crate::Harness::new();
98 harness.run_in_context(|lock, _ctx| {
99 let resource = jsg::Rc::new(SimpleResource {
100 name: "test".to_owned(),
101 });
102 let _wrapped = resource.clone().to_js(lock);
103 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
104 std::mem::drop(resource);
105 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
106 Ok(())
107 });
108 
109 harness.run_in_context(|lock, _ctx| {
110 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
111 crate::Harness::request_gc(lock);
112 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
113 Ok(())
114 });
115}
116 
117#[test]
118fn resource_with_traced_ref_field() {
119 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
120 PARENT_RESOURCE_DROPS.store(0, Ordering::SeqCst);
121 
122 let harness = crate::Harness::new();
123 harness.run_in_context(|lock, _ctx| {
124 let child = jsg::Rc::new(SimpleResource {
125 name: "child".to_owned(),
126 });
127 let optional_child = jsg::Rc::new(SimpleResource {
128 name: "optional_child".to_owned(),
129 });
130 
131 let parent = jsg::Rc::new(ParentResource {
132 child: child.clone(),
133 optional_child: Some(optional_child.clone()),
134 });
135 
136 let _wrapped = parent.clone().to_js(lock);
137 
138 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
139 assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
140 
141 std::mem::drop(child);
142 std::mem::drop(optional_child);
143 std::mem::drop(parent);
144 Ok(())
145 });
146 
147 harness.run_in_context(|lock, _ctx| {
148 crate::Harness::request_gc(lock);
149 assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
150 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 2);
151 Ok(())
152 });
153}
154 
155#[test]
156fn child_traced_ref_kept_alive_by_parent() {
157 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
158 PARENT_RESOURCE_DROPS.store(0, Ordering::SeqCst);
159 
160 let harness = crate::Harness::new();
161 harness.run_in_context(|lock, _ctx| {
162 let child = jsg::Rc::new(SimpleResource {
163 name: "child".to_owned(),
164 });
165 
166 let parent = jsg::Rc::new(ParentResource {
167 child: child.clone(),
168 optional_child: None,
169 });
170 
171 // Wrap the parent so it has a JS object, then let the Local go out of scope
172 // by not storing it. The wrapper is now only held weakly by cppgc.
173 let _ = parent.clone().to_js(lock);
174 
175 // Child not collected because parent still holds a Ref (which holds a kj::Own)
176 std::mem::drop(child);
177 crate::Harness::request_gc(lock);
178 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
179 
180 // Drop the Rust strong ref. The parent still has a wrapper, but with no
181 // strong refs and no JS references, the wrapper is eligible for GC.
182 std::mem::drop(parent);
183 Ok(())
184 });
185 
186 // GC in a separate context so the Local handle from wrap() is gone
187 harness.run_in_context(|lock, _ctx| {
188 crate::Harness::request_gc(lock);
189 assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
190 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
191 Ok(())
192 });
193}
194 
195#[test]
196fn weak_ref_upgrade() {
197 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
198 
199 let harness = crate::Harness::new();
200 harness.run_in_context(|_lock, _ctx| {
201 let strong = jsg::Rc::new(SimpleResource {
202 name: "test".to_owned(),
203 });
204 let weak = strong.downgrade();
205 
206 assert!(weak.is_alive());
207 let upgraded = weak.upgrade();
208 assert!(upgraded.is_some());
209 assert!(weak.is_alive());
210 
211 std::mem::drop(upgraded);
212 assert!(weak.is_alive());
213 
214 std::mem::drop(strong);
215 // No wrapper, so resource is destroyed immediately when last strong ref drops
216 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
217 assert!(!weak.is_alive());
218 assert!(weak.upgrade().is_none());
219 Ok(())
220 });
221}
222 
223/// Tests that a wrapped resource stays alive as long as JS wrapper exists.
224#[test]
225fn wrapped_resource_kept_alive_by_js() {
226 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
227 
228 let harness = crate::Harness::new();
229 harness.run_in_context(|lock, _ctx| {
230 let strong = jsg::Rc::new(SimpleResource {
231 name: "test".to_owned(),
232 });
233 let _wrapped = strong.clone().to_js(lock);
234 std::mem::drop(strong);
235 // CppgcShim still holds kj::Own, keeping it alive
236 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
237 Ok(())
238 });
239 
240 harness.run_in_context(|lock, _ctx| {
241 crate::Harness::request_gc(lock);
242 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
243 Ok(())
244 });
245}
246 
247/// Tests weak ref behavior with wrapped resources.
248#[test]
249fn weak_ref_with_wrapped_resource() {
250 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
251 
252 let harness = crate::Harness::new();
253 harness.run_in_context(|lock, _ctx| {
254 let strong = jsg::Rc::new(SimpleResource {
255 name: "test".to_owned(),
256 });
257 let _wrapped = strong.clone().to_js(lock);
258 let weak = strong.downgrade();
259 
260 assert!(weak.upgrade().is_some());
261 std::mem::drop(strong);
262 // CppgcShim keeps it alive even after dropping the strong ref
263 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
264 Ok(())
265 });
266 
267 harness.run_in_context(|lock, _ctx| {
268 crate::Harness::request_gc(lock);
269 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
270 Ok(())
271 });
272}
273 
274/// Tests parent-child GC with traced refs.
275#[test]
276fn traced_ref_in_gc() {
277 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
278 PARENT_RESOURCE_DROPS.store(0, Ordering::SeqCst);
279 
280 let harness = crate::Harness::new();
281 harness.run_in_context(|lock, _ctx| {
282 let child = jsg::Rc::new(SimpleResource {
283 name: "child".to_owned(),
284 });
285 let _child_wrapped = child.clone().to_js(lock);
286 
287 let parent = jsg::Rc::new(ParentResource {
288 child: child.clone(),
289 optional_child: None,
290 });
291 let _parent_wrapped = parent.clone().to_js(lock);
292 
293 std::mem::drop(child);
294 std::mem::drop(parent);
295 
296 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
297 assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
298 Ok(())
299 });
300 
301 harness.run_in_context(|lock, _ctx| {
302 crate::Harness::request_gc(lock);
303 assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
304 crate::Harness::request_gc(lock);
305 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
306 Ok(())
307 });
308}
309 
310// =============================================================================
311// WeakRef tests
312// =============================================================================
313 
314/// Tests that `WeakRef::get()` returns the resource data while the resource is alive.
315#[test]
316fn weak_ref_get_returns_resource_data() {
317 let harness = crate::Harness::new();
318 harness.run_in_context(|_lock, _ctx| {
319 let strong = jsg::Rc::new(SimpleResource {
320 name: "hello".to_owned(),
321 });
322 let weak = strong.downgrade();
323 
324 // get() should return a reference to the resource
325 let resource = weak.upgrade().expect("weak ref should be alive");
326 assert_eq!(resource.name, "hello");
327 Ok(())
328 });
329}
330 
331/// Tests that `WeakRef::get()` returns `None` after the resource is dropped.
332#[test]
333fn weak_ref_get_returns_none_after_drop() {
334 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
335 
336 let harness = crate::Harness::new();
337 harness.run_in_context(|_lock, _ctx| {
338 let strong = jsg::Rc::new(SimpleResource {
339 name: "ephemeral".to_owned(),
340 });
341 let weak = strong.downgrade();
342 assert!(weak.upgrade().is_some());
343 
344 std::mem::drop(strong);
345 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
346 
347 // get() must return None without touching freed memory
348 assert!(weak.upgrade().is_none());
349 Ok(())
350 });
351}
352 
353/// Tests that `WeakRef::default()` creates a dead weak reference.
354///
355/// A default `WeakRef` has `wrappable: None` and an expired `Weak<R>`.
356/// All operations must be safe: `get()` -> `None`, `upgrade()` -> `None`, `is_alive()` -> `false`.
357#[test]
358fn weak_ref_default_is_dead() {
359 let harness = crate::Harness::new();
360 harness.run_in_context(|_lock, _ctx| {
361 let weak: jsg::Weak<SimpleResource> = jsg::Weak::default();
362 
363 assert!(!weak.is_alive());
364 assert!(weak.upgrade().is_none());
365 assert!(weak.upgrade().is_none());
366 Ok(())
367 });
368}
369 
370/// Tests that cloning a `WeakRef` shares the alive marker.
371///
372/// When the resource dies, ALL clones must see `is_alive() == false` simultaneously.
373#[test]
374fn weak_ref_clone_shares_alive_marker() {
375 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
376 
377 let harness = crate::Harness::new();
378 harness.run_in_context(|_lock, _ctx| {
379 let strong = jsg::Rc::new(SimpleResource {
380 name: "shared".to_owned(),
381 });
382 
383 let weak1 = strong.downgrade();
384 let weak2 = weak1.clone();
385 let weak3 = weak2.clone();
386 
387 assert!(weak1.is_alive());
388 assert!(weak2.is_alive());
389 assert!(weak3.is_alive());
390 
391 std::mem::drop(strong);
392 
393 // All clones see death simultaneously
394 assert!(!weak1.is_alive());
395 assert!(!weak2.is_alive());
396 assert!(!weak3.is_alive());
397 assert!(weak1.upgrade().is_none());
398 assert!(weak2.upgrade().is_none());
399 Ok(())
400 });
401}
402 
403/// Tests that `WeakRef::upgrade()` with a wrapped resource creates a strong ref
404/// that prevents GC collection.
405#[test]
406fn weak_ref_upgrade_with_wrapped_resource_prevents_gc() {
407 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
408 
409 let harness = crate::Harness::new();
410 harness.run_in_context(|lock, _ctx| {
411 let strong = jsg::Rc::new(SimpleResource {
412 name: "persistent".to_owned(),
413 });
414 let _wrapped = strong.clone().to_js(lock);
415 let weak = strong.downgrade();
416 
417 // Drop the original strong ref, but upgrade from weak creates a new one
418 std::mem::drop(strong);
419 let upgraded = weak.upgrade().expect("should be alive via wrapper");
420 assert_eq!(upgraded.name, "persistent");
421 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
422 
423 // The upgraded Ref keeps the resource alive
424 std::mem::drop(upgraded);
425 Ok(())
426 });
427 
428 harness.run_in_context(|lock, _ctx| {
429 crate::Harness::request_gc(lock);
430 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
431 Ok(())
432 });
433}
434 
435/// Tests that `WeakRef::trace()` is a no-op and doesn't prevent GC collection.
436///
437/// A resource holding a `WeakRef` to another resource should not keep it alive through tracing.
438#[test]
439fn weak_ref_trace_does_not_prevent_gc() {
440 static HOLDER_DROPS: AtomicUsize = AtomicUsize::new(0);
441 
442 #[jsg_resource]
443 struct WeakRefHolder {
444 weak: jsg::Weak<SimpleResource>,
445 }
446 
447 impl Drop for WeakRefHolder {
448 fn drop(&mut self) {
449 HOLDER_DROPS.fetch_add(1, Ordering::SeqCst);
450 }
451 }
452 
453 #[jsg_resource]
454 impl WeakRefHolder {}
455 
456 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
457 HOLDER_DROPS.store(0, Ordering::SeqCst);
458 
459 let harness = crate::Harness::new();
460 harness.run_in_context(|lock, _ctx| {
461 let target = jsg::Rc::new(SimpleResource {
462 name: "target".to_owned(),
463 });
464 let _target_wrapped = target.clone().to_js(lock);
465 
466 let holder = jsg::Rc::new(WeakRefHolder {
467 weak: target.downgrade(),
468 });
469 let _holder_wrapped = holder.clone().to_js(lock);
470 
471 // WeakRef should be upgradable while the target is alive
472 assert!(holder.weak.upgrade().is_some());
473 
474 // Drop all Rust refs — both are only held by JS wrappers
475 std::mem::drop(target);
476 std::mem::drop(holder);
477 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
478 Ok(())
479 });
480 
481 // GC should collect the target — the WeakRef in holder doesn't keep it alive
482 harness.run_in_context(|lock, _ctx| {
483 crate::Harness::request_gc(lock);
484 // Both should be collected (holder's WeakRef doesn't prevent target's collection)
485 assert_eq!(HOLDER_DROPS.load(Ordering::SeqCst), 1);
486 crate::Harness::request_gc(lock);
487 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
488 Ok(())
489 });
490}
491 
492// =============================================================================
493// Ref tests
494// =============================================================================
495 
496/// Tests that `Ref::clone()` keeps the resource alive after the original is dropped.
497#[test]
498fn ref_clone_keeps_resource_alive() {
499 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
500 
501 let harness = crate::Harness::new();
502 harness.run_in_context(|_lock, _ctx| {
503 let original = jsg::Rc::new(SimpleResource {
504 name: "cloned".to_owned(),
505 });
506 let clone1 = original.clone();
507 let clone2 = original.clone();
508 
509 std::mem::drop(original);
510 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
511 assert_eq!(clone1.name, "cloned");
512 
513 std::mem::drop(clone1);
514 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
515 assert_eq!(clone2.name, "cloned");
516 
517 std::mem::drop(clone2);
518 // Only now all refs are gone — resource is destroyed
519 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
520 Ok(())
521 });
522}
523 
524/// Tests that dropping multiple Refs only triggers destruction on the last one.
525#[test]
526fn multiple_ref_drops_only_last_triggers_destruction() {
527 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
528 
529 let harness = crate::Harness::new();
530 harness.run_in_context(|_lock, _ctx| {
531 let original = jsg::Rc::new(SimpleResource {
532 name: "multi".to_owned(),
533 });
534 
535 // Create several clones
536 let clones: Vec<_> = (0..5).map(|_| original.clone()).collect();
537 std::mem::drop(original);
538 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
539 
540 // Drop one by one
541 for (i, c) in clones.into_iter().enumerate() {
542 std::mem::drop(c);
543 if i < 4 {
544 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
545 }
546 }
547 // Last clone dropped — resource is destroyed
548 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
549 Ok(())
550 });
551}
552 
553/// Tests that `Ref::deref()` returns the correct resource data.
554#[test]
555fn ref_deref_returns_correct_resource_data() {
556 let harness = crate::Harness::new();
557 harness.run_in_context(|_lock, _ctx| {
558 let r = jsg::Rc::new(SimpleResource {
559 name: "deref-test".to_owned(),
560 });
561 
562 // Deref should return the resource with correct data
563 assert_eq!(r.name, "deref-test");
564 
565 // Clone should also deref to the same data
566 #[expect(clippy::redundant_clone)]
567 let clone = r.clone();
568 assert_eq!(clone.name, "deref-test");
569 Ok(())
570 });
571}
572 
573// =============================================================================
574// unwrap / FromJS tests
575// =============================================================================
576 
577/// Tests that `FromJS for Ref<R>` creates a strong reference from a JS wrapper.
578///
579/// The returned `Ref<R>` must keep the resource alive even after dropping the original.
580#[test]
581fn from_js_creates_strong_reference_from_js_wrapper() {
582 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
583 
584 let harness = crate::Harness::new();
585 harness.run_in_context(|lock, ctx| {
586 let resource = jsg::Rc::new(SimpleResource {
587 name: "unwrap-me".to_owned(),
588 });
589 let wrapped = resource.clone().to_js(lock);
590 ctx.set_global("obj", wrapped);
591 
592 // Get the JS value back and use FromJS to create a new strong Ref
593 let js_val = ctx.eval_raw("obj").unwrap();
594 let new_ref: jsg::Rc<SimpleResource> =
595 <jsg::Rc<SimpleResource> as jsg::FromJS>::from_js(lock, js_val)
596 .expect("FromJS should succeed for a wrapped resource");
597 assert_eq!(new_ref.name, "unwrap-me");
598 
599 // Drop the original Ref — the unwrapped ref should keep it alive
600 std::mem::drop(resource);
601 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
602 
603 // The unwrapped ref is still valid
604 assert_eq!(new_ref.name, "unwrap-me");
605 
606 // Dropping the unwrapped ref (with wrapper still alive) should not destroy
607 std::mem::drop(new_ref);
608 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
609 Ok(())
610 });
611 
612 // GC the JS wrapper — now the resource can be collected
613 harness.run_in_context(|lock, _ctx| {
614 crate::Harness::request_gc(lock);
615 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
616 Ok(())
617 });
618}
619 
620/// Tests that `FromJS` returns `Err` for a plain JS object.
621///
622/// A plain `{}` object has no internal fields and no wrappable tag.
623/// `FromJS::from_js` must return `Err` without crashing.
624#[test]
625fn from_js_returns_err_for_plain_js_object() {
626 let harness = crate::Harness::new();
627 harness.run_in_context(|lock, ctx| {
628 let plain_obj = ctx.eval_raw("({})").unwrap();
629 let result = <jsg::Rc<SimpleResource> as jsg::FromJS>::from_js(lock, plain_obj);
630 assert!(
631 result.is_err(),
632 "FromJS should return Err for a plain JS object"
633 );
634 Ok(())
635 });
636}
637 
638// =============================================================================
639// Wrap/template caching tests
640// =============================================================================
641 
642/// Tests that wrapping multiple instances of the same type uses the same template.
643///
644/// Verifies that `Realm::get_constructor()` caches the template on first use
645/// and returns the same one on subsequent calls.
646#[test]
647fn wrap_multiple_instances_of_same_type() {
648 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
649 
650 let harness = crate::Harness::new();
651 harness.run_in_context(|lock, ctx| {
652 let r1 = jsg::Rc::new(SimpleResource {
653 name: "first".to_owned(),
654 });
655 let r2 = jsg::Rc::new(SimpleResource {
656 name: "second".to_owned(),
657 });
658 
659 let w1 = r1.to_js(lock);
660 let w2 = r2.to_js(lock);
661 ctx.set_global("r1", w1);
662 ctx.set_global("r2", w2);
663 
664 // Both should be instances of the same constructor (same prototype chain)
665 let same_proto: bool = ctx
666 .eval(
667 lock,
668 "Object.getPrototypeOf(r1) === Object.getPrototypeOf(r2)",
669 )
670 .unwrap();
671 assert!(same_proto);
672 
673 // Both should work independently
674 let n1: String = ctx.eval(lock, "r1.getName()").unwrap();
675 let n2: String = ctx.eval(lock, "r2.getName()").unwrap();
676 assert_eq!(n1, "first");
677 assert_eq!(n2, "second");
678 Ok(())
679 });
680}
681 
682/// Tests that wrapping the same resource twice returns the same JS object.
683///
684/// `Wrappable::tryGetHandle()` should detect an existing wrapper and return it.
685#[test]
686fn wrap_same_resource_twice_returns_same_object() {
687 let harness = crate::Harness::new();
688 harness.run_in_context(|lock, ctx| {
689 let resource = jsg::Rc::new(SimpleResource {
690 name: "singleton".to_owned(),
691 });
692 
693 let w1 = resource.clone().to_js(lock);
694 ctx.set_global("w1", w1);
695 let w2 = resource.to_js(lock);
696 ctx.set_global("w2", w2);
697 
698 // Both wraps should return the exact same JS object
699 let same: bool = ctx.eval(lock, "w1 === w2").unwrap();
700 assert!(same);
701 Ok(())
702 });
703}
704 
705// =============================================================================
706// Resource data integrity tests
707// =============================================================================
708 
709/// Tests that resource data survives a GC cycle when held by a JS global.
710#[test]
711fn resource_data_survives_gc_via_js_global() {
712 let harness = crate::Harness::new();
713 harness.run_in_context(|lock, ctx| {
714 let resource = jsg::Rc::new(SimpleResource {
715 name: "survivor".to_owned(),
716 });
717 let wrapped = resource.to_js(lock);
718 ctx.set_global("obj", wrapped);
719 
720 // Force GC — the global reference should keep it alive
721 crate::Harness::request_gc(lock);
722 
723 // Data should still be correct
724 let name: String = ctx.eval(lock, "obj.getName()").unwrap();
725 assert_eq!(name, "survivor");
726 Ok(())
727 });
728}
729 
730/// Tests parent-child relationships where parent is only held by JS.
731///
732/// When the parent is wrapped and held by a JS global, its `Ref<SimpleResource>` child
733/// should be traced during GC and kept alive even without any Rust strong refs.
734#[test]
735fn parent_ref_keeps_child_alive_through_gc() {
736 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
737 PARENT_RESOURCE_DROPS.store(0, Ordering::SeqCst);
738 
739 let harness = crate::Harness::new();
740 harness.run_in_context(|lock, ctx| {
741 let child = jsg::Rc::new(SimpleResource {
742 name: "child".to_owned(),
743 });
744 let parent = jsg::Rc::new(ParentResource {
745 child: child.clone(),
746 optional_child: None,
747 });
748 let wrapped = parent.clone().to_js(lock);
749 ctx.set_global("parent", wrapped);
750 
751 // Drop all Rust refs
752 std::mem::drop(child);
753 std::mem::drop(parent);
754 
755 // GC should NOT collect the parent (held by global) or child (held by parent's Ref)
756 crate::Harness::request_gc(lock);
757 assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
758 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
759 Ok(())
760 });
761 
762 // New context — global is gone
763 harness.run_in_context(|lock, _ctx| {
764 crate::Harness::request_gc(lock);
765 assert_eq!(PARENT_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
766 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
767 Ok(())
768 });
769}
770 
771/// Tests that dropping all strong refs correctly invalidates ALL weak refs.
772///
773/// Creates multiple `WeakRef`s from different `Ref`s, verifies they all see death at the same time.
774#[test]
775fn instance_drop_invalidates_all_weak_refs() {
776 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
777 
778 let harness = crate::Harness::new();
779 harness.run_in_context(|_lock, _ctx| {
780 let original = jsg::Rc::new(SimpleResource {
781 name: "shared-target".to_owned(),
782 });
783 let clone1 = original.clone();
784 let clone2 = original.clone();
785 
786 // Create weak refs from different strong refs
787 let weak_from_original = original.downgrade();
788 let weak_from_clone1 = clone1.downgrade();
789 let weak_from_clone2 = clone2.downgrade();
790 
791 // All alive
792 assert!(weak_from_original.is_alive());
793 assert!(weak_from_clone1.is_alive());
794 assert!(weak_from_clone2.is_alive());
795 
796 // Drop all strong refs — resource is destroyed
797 std::mem::drop(original);
798 std::mem::drop(clone1);
799 std::mem::drop(clone2);
800 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
801 
802 // ALL weak refs see death
803 assert!(!weak_from_original.is_alive());
804 assert!(!weak_from_clone1.is_alive());
805 assert!(!weak_from_clone2.is_alive());
806 assert!(weak_from_original.upgrade().is_none());
807 assert!(weak_from_clone1.upgrade().is_none());
808 assert!(weak_from_clone2.upgrade().is_none());
809 Ok(())
810 });
811}
812 
813// =============================================================================
814// Nullable<Ref<T>> tracing tests
815// =============================================================================
816 
817/// Counter to track how many `NullableParent` instances have been dropped.
818static NULLABLE_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
819 
820#[jsg_resource]
821struct NullableParent {
822 pub child: jsg::Nullable<jsg::Rc<SimpleResource>>,
823}
824 
825impl Drop for NullableParent {
826 fn drop(&mut self) {
827 NULLABLE_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
828 }
829}
830 
831#[jsg_resource]
832impl NullableParent {}
833 
834/// Tests that `Nullable<Ref<T>>` with `Nullable::Some` keeps the child alive through GC tracing.
835#[test]
836fn nullable_ref_some_keeps_child_alive_through_gc() {
837 SIMPLE_RESOURCE_DROPS.store(0, Ordering::SeqCst);
838 NULLABLE_PARENT_DROPS.store(0, Ordering::SeqCst);
839 
840 let harness = crate::Harness::new();
841 harness.run_in_context(|lock, ctx| {
842 let child = jsg::Rc::new(SimpleResource {
843 name: "nullable_child".to_owned(),
844 });
845 let parent = jsg::Rc::new(NullableParent {
846 child: jsg::Nullable::Some(child.clone()),
847 });
848 let wrapped = parent.clone().to_js(lock);
849 ctx.set_global("parent", wrapped);
850 
851 // Drop all Rust refs
852 std::mem::drop(child);
853 std::mem::drop(parent);
854 
855 // GC should NOT collect the parent (held by global) or child (traced via Nullable::Some)
856 crate::Harness::request_gc(lock);
857 assert_eq!(NULLABLE_PARENT_DROPS.load(Ordering::SeqCst), 0);
858 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
859 Ok(())
860 });
861 
862 // New context — global is gone
863 harness.run_in_context(|lock, _ctx| {
864 crate::Harness::request_gc(lock);
865 assert_eq!(NULLABLE_PARENT_DROPS.load(Ordering::SeqCst), 1);
866 assert_eq!(SIMPLE_RESOURCE_DROPS.load(Ordering::SeqCst), 1);
867 Ok(())
868 });
869}
870 
871/// Tests that `Nullable<Ref<T>>` with `Nullable::Null` doesn't cause issues during GC.
872#[test]
873fn nullable_ref_null_does_not_crash_during_gc() {
874 NULLABLE_PARENT_DROPS.store(0, Ordering::SeqCst);
875 
876 let harness = crate::Harness::new();
877 harness.run_in_context(|lock, ctx| {
878 let parent = jsg::Rc::new(NullableParent {
879 child: jsg::Nullable::Null,
880 });
881 let wrapped = parent.clone().to_js(lock);
882 ctx.set_global("parent", wrapped);
883 
884 std::mem::drop(parent);
885 
886 // GC with Nullable::Null should not crash
887 crate::Harness::request_gc(lock);
888 assert_eq!(NULLABLE_PARENT_DROPS.load(Ordering::SeqCst), 0);
889 Ok(())
890 });
891 
892 harness.run_in_context(|lock, _ctx| {
893 crate::Harness::request_gc(lock);
894 assert_eq!(NULLABLE_PARENT_DROPS.load(Ordering::SeqCst), 1);
895 Ok(())
896 });
897}
898 
899/// Tests that `Nullable<Ref<T>>` with `Nullable::Undefined` doesn't cause issues during GC.
900#[test]
901fn nullable_ref_undefined_does_not_crash_during_gc() {
902 NULLABLE_PARENT_DROPS.store(0, Ordering::SeqCst);
903 
904 let harness = crate::Harness::new();
905 harness.run_in_context(|lock, ctx| {
906 let parent = jsg::Rc::new(NullableParent {
907 child: jsg::Nullable::Undefined,
908 });
909 let wrapped = parent.clone().to_js(lock);
910 ctx.set_global("parent", wrapped);
911 
912 std::mem::drop(parent);
913 
914 // GC with Nullable::Undefined should not crash
915 crate::Harness::request_gc(lock);
916 assert_eq!(NULLABLE_PARENT_DROPS.load(Ordering::SeqCst), 0);
917 Ok(())
918 });
919 
920 harness.run_in_context(|lock, _ctx| {
921 crate::Harness::request_gc(lock);
922 assert_eq!(NULLABLE_PARENT_DROPS.load(Ordering::SeqCst), 1);
923 Ok(())
924 });
925}
926 
927// ---------------------------------------------------------------------------
928// Rc<NativeState> shared ownership — native object outlives Ref but is
929// dropped when V8 GC collects the JS wrapper.
930// ---------------------------------------------------------------------------
931 
932static NATIVE_STATE_DROPS: AtomicUsize = AtomicUsize::new(0);
933 
934struct NativeState {
935 value: u64,
936}
937 
938impl jsg::Traced for NativeState {}
939 
940impl Drop for NativeState {
941 fn drop(&mut self) {
942 NATIVE_STATE_DROPS.fetch_add(1, Ordering::SeqCst);
943 }
944}
945 
946#[jsg_resource]
947struct NativeOwnerResource {
948 state: std::rc::Rc<NativeState>,
949}
950 
951#[jsg_resource]
952impl NativeOwnerResource {
953 #[jsg_method]
954 fn get_value(&self) -> jsg::Number {
955 #[expect(clippy::cast_precision_loss)]
956 jsg::Number::new(self.state.value as f64)
957 }
958}
959 
960/// Regression test for the `strong` flag bug in `wrappable_remove_strong_ref`.
961///
962/// When a parent resource (with a JS wrapper) holds a `Ref<Child>`, GC tracing
963/// transitions the child's ref from strong→weak via `visitRef`, which calls
964/// `removeStrongRef()` once.
965///
966/// **Bug**: `wrappable_remove_strong_ref` always passed `strong=true` to
967/// `maybeDeferDestruction`, so when the parent was later collected and the
968/// child `Ref` dropped, `~RefToDelete` called `removeStrongRef()` a second
969/// time — underflowing the strong refcount.
970///
971/// This test keeps a direct Rust ref to the child alive so we can observe
972/// the strong refcount after the parent's traced ref is dropped by GC.
973/// With the bug: `strong_refcount()` returns 0 instead of 1.
974#[test]
975#[cfg(debug_assertions)]
976fn traced_ref_drop_respects_strong_flag() {
977 let harness = crate::Harness::new();
978 let mut child_holder: Option<jsg::Rc<SimpleResource>> = None;
979 
980 harness.run_in_context(|lock, _ctx| {
981 let child = jsg::Rc::new(SimpleResource {
982 name: "traced_child".to_owned(),
983 });
984 
985 // strongRefcount = 1 (our ref).
986 assert_eq!(child.strong_refcount(), 1);
987 
988 let parent = jsg::Rc::new(ParentResource {
989 child: child.clone(),
990 optional_child: None,
991 });
992 
993 // strongRefcount = 2 (our ref + parent's ref).
994 assert_eq!(child.strong_refcount(), 2);
995 
996 // Wrap the parent — child is reachable only through the parent's Ref.
997 let _wrapped = parent.clone().to_js(lock);
998 
999 // GC traces parent wrapper → visitRef on child → parent's ref
1000 // transitions strong→weak, removeStrongRef() called once.
1001 // strongRefcount = 1 (only our direct ref remains strong).
1002 crate::Harness::request_gc(lock);
1003 assert_eq!(child.strong_refcount(), 1);
1004 
1005 // Drop parent Rust ref. JS wrapper keeps parent alive.
1006 std::mem::drop(parent);
1007 
1008 // Move child out so we can observe it after GC collects the parent.
1009 child_holder = Some(child);
1010 Ok(())
1011 });
1012 
1013 let child = child_holder.expect("child not set");
1014 
1015 // Second GC: parent's JS wrapper is unreachable → parent collected →
1016 // parent's child Ref drops → wrappable_remove_strong_ref called.
1017 //
1018 // With the bug: parent's ref has strong=false but remove_strong_ref
1019 // always passes true → removeStrongRef() called → strongRefcount
1020 // goes from 1 to 0, but our ref is still strong → should be 1!
1021 //
1022 // With the fix: strong=false is threaded through → no removeStrongRef
1023 // call → strongRefcount stays at 1 (correct).
1024 harness.run_in_context(|lock, _ctx| {
1025 crate::Harness::request_gc(lock);
1026 assert_eq!(
1027 child.strong_refcount(),
1028 1,
1029 "strong refcount underflowed — \
1030 wrappable_remove_strong_ref passed strong=true for a traced (weak) ref"
1031 );
1032 Ok(())
1033 });
1034}
1035 
1036/// A Rust resource holds an `Rc<NativeState>`. After wrapping for JS and
1037/// dropping the Rust `Ref`, the native state is kept alive by the `CppgcShim`.
1038/// Minor (young-generation) GC must collect the wrapper and drop the native
1039/// state — proving the `ResetRoot` / `detachLater` path works for Rust resources.
1040#[test]
1041fn rc_native_object_dropped_on_minor_gc() {
1042 NATIVE_STATE_DROPS.store(0, Ordering::SeqCst);
1043 
1044 let harness = crate::Harness::new();
1045 let state = std::rc::Rc::new(NativeState { value: 99 });
1046 let state_clone = state.clone();
1047 
1048 harness.run_in_context(|lock, _ctx| {
1049 let resource = jsg::Rc::new(NativeOwnerResource { state: state_clone });
1050 
1051 // Wrap for JS so CppgcShim takes ownership of the Wrappable.
1052 let _wrapped = resource.clone().to_js(lock);
1053 
1054 // Drop the Rust Ref. CppgcShim still keeps the resource alive.
1055 std::mem::drop(resource);
1056 assert_eq!(NATIVE_STATE_DROPS.load(Ordering::SeqCst), 0);
1057 Ok(())
1058 });
1059 
1060 // Drop the external Rc clone. The resource's Rc inside the Wrappable
1061 // is the last one, but the Wrappable is alive (held by CppgcShim).
1062 std::mem::drop(state);
1063 assert_eq!(NATIVE_STATE_DROPS.load(Ordering::SeqCst), 0);
1064 
1065 // Minor GC collects the young-generation wrapper via the ResetRoot path,
1066 // which detaches the CppgcShim → drops kj::Own<Wrappable> → drops the
1067 // resource → drops Rc<NativeState> → NativeState is dropped.
1068 harness.run_in_context(|lock, _ctx| {
1069 crate::Harness::request_minor_gc(lock);
1070 assert_eq!(NATIVE_STATE_DROPS.load(Ordering::SeqCst), 1);
1071 Ok(())
1072 });
1073}
1074 
1075// =============================================================================
1076// Global<Value> back-reference cycle — collected via visit_global tracing
1077// =============================================================================
1078//
1079// `jsg::v8::Global<T>` fields on Rust resources participate in GC tracing via
1080// `GcVisitor::visit_global`, which implements the same strong↔traced dual-mode
1081// switching as `jsg::Data` / `jsg::V8Ref<T>` in C++.
1082//
1083// When the parent Wrappable has strong Rust refs the handle stays strong.
1084// Once all Rust refs are dropped and only the JS wrapper keeps it alive,
1085// `visit_global` downgrades the handle to a `v8::TracedReference` that cppgc
1086// can follow — allowing the GC to detect and collect the cycle.
1087 
1088static CYCLIC_RESOURCE_DROPS: AtomicUsize = AtomicUsize::new(0);
1089 
1090/// A resource that stores a `Global<Value>` via `Cell` so the back-reference
1091/// to the resource's own JS wrapper can be installed after wrapping.
1092///
1093/// `Cell<Option<…>>` provides interior mutability through `&self`, matching
1094/// the access pattern of `Traced::trace(&self)`.
1095#[jsg_resource]
1096struct CyclicResource {
1097 /// The stored "callback". Uses `Cell` so it can be set after `to_js()`
1098 /// returns the wrapper `Local`, without requiring `&mut self`.
1099 callback: std::cell::Cell<Option<jsg::v8::Global<jsg::v8::Value>>>,
1100}
1101 
1102impl Drop for CyclicResource {
1103 fn drop(&mut self) {
1104 CYCLIC_RESOURCE_DROPS.fetch_add(1, Ordering::SeqCst);
1105 }
1106}
1107 
1108#[jsg_resource]
1109impl CyclicResource {}
1110 
1111/// Verifies that a `Global<Value>` back-reference cycle is collected by GC.
1112///
1113/// The cycle:
1114/// CyclicResource.callback (Global<Value>) → JS wrapper
1115/// JS wrapper → `CppgcShim` → `Wrappable` → `CyclicResource` (same object)
1116///
1117/// `visit_global` downgrades the `Global` to a `v8::TracedReference` once all
1118/// strong Rust refs are dropped, making the cycle visible to cppgc so it can
1119/// be collected on the next full GC.
1120#[test]
1121fn global_value_back_ref_is_collected_by_gc() {
1122 CYCLIC_RESOURCE_DROPS.store(0, Ordering::SeqCst);
1123 
1124 let harness = crate::Harness::new();
1125 harness.run_in_context(|lock, _ctx| {
1126 let resource = jsg::Rc::new(CyclicResource {
1127 callback: std::cell::Cell::new(None),
1128 });
1129 
1130 // Wrap to produce the JS object, then immediately promote to a
1131 // Global so we can store it back into the resource's own field.
1132 // `to_js` consumes the Rc clone but the original `resource` still
1133 // holds a strong ref, so the resource stays alive.
1134 let wrapper_local = resource.clone().to_js(lock);
1135 let wrapper_global = wrapper_local.to_global(lock);
1136 
1137 // Install the back-reference: resource now holds a Global pointing
1138 // to its own JS wrapper, closing the cycle.
1139 resource.callback.set(Some(wrapper_global));
1140 
1141 // Drop the only Rust Rc. The cycle is now closed but the Global
1142 // will be downgraded to a TracedReference by visit_global, making
1143 // it visible to cppgc.
1144 std::mem::drop(resource);
1145 assert_eq!(CYCLIC_RESOURCE_DROPS.load(Ordering::SeqCst), 0);
1146 Ok(())
1147 });
1148 
1149 // Full GC can now detect and collect the cycle because visit_global
1150 // downgrades the Global to a TracedReference during tracing.
1151 harness.run_in_context(|lock, _ctx| {
1152 crate::Harness::request_gc(lock);
1153 assert_eq!(
1154 CYCLIC_RESOURCE_DROPS.load(Ordering::SeqCst),
1155 1,
1156 "full GC should collect the cyclic resource via visit_global tracing"
1157 );
1158 Ok(())
1159 });
1160}
1161 
1162// =============================================================================
1163// Cell<T> tracing tests
1164// =============================================================================
1165 
1166static CELL_CHILD_DROPS: AtomicUsize = AtomicUsize::new(0);
1167 
1168#[jsg_resource]
1169struct CellChild;
1170 
1171impl Drop for CellChild {
1172 fn drop(&mut self) {
1173 CELL_CHILD_DROPS.fetch_add(1, Ordering::SeqCst);
1174 }
1175}
1176 
1177#[jsg_resource]
1178impl CellChild {}
1179 
1180static CELL_PARENT_DROPS: AtomicUsize = AtomicUsize::new(0);
1181 
1182/// Resource with a `Cell<jsg::Rc<T>>` field.
1183#[jsg_resource]
1184struct CellParent {
1185 pub child: Cell<jsg::Rc<CellChild>>,
1186}
1187 
1188impl Drop for CellParent {
1189 fn drop(&mut self) {
1190 CELL_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
1191 }
1192}
1193 
1194#[jsg_resource]
1195impl CellParent {}
1196 
1197/// Resource with a `Cell<Option<jsg::Rc<T>>>` field.
1198#[jsg_resource]
1199struct CellOptionParent {
1200 pub child: Cell<Option<jsg::Rc<CellChild>>>,
1201}
1202 
1203impl Drop for CellOptionParent {
1204 fn drop(&mut self) {
1205 CELL_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
1206 }
1207}
1208 
1209#[jsg_resource]
1210impl CellOptionParent {}
1211 
1212/// Resource with a `Cell<jsg::Nullable<jsg::Rc<T>>>` field.
1213#[jsg_resource]
1214struct CellNullableParent {
1215 pub child: Cell<jsg::Nullable<jsg::Rc<CellChild>>>,
1216}
1217 
1218impl Drop for CellNullableParent {
1219 fn drop(&mut self) {
1220 CELL_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
1221 }
1222}
1223 
1224#[jsg_resource]
1225impl CellNullableParent {}
1226 
1227/// Resource using the fully-qualified `std::cell::Cell<jsg::Rc<T>>` syntax.
1228#[jsg_resource]
1229struct StdCellParent {
1230 pub child: std::cell::Cell<jsg::Rc<CellChild>>,
1231}
1232 
1233impl Drop for StdCellParent {
1234 fn drop(&mut self) {
1235 CELL_PARENT_DROPS.fetch_add(1, Ordering::SeqCst);
1236 }
1237}
1238 
1239#[jsg_resource]
1240impl StdCellParent {}
1241 
1242/// `Cell<jsg::Rc<T>>` keeps the child alive through GC.
1243#[test]
1244fn cell_ref_keeps_child_alive_through_gc() {
1245 CELL_CHILD_DROPS.store(0, Ordering::SeqCst);
1246 CELL_PARENT_DROPS.store(0, Ordering::SeqCst);
1247 
1248 let harness = crate::Harness::new();
1249 harness.run_in_context(|lock, ctx| {
1250 let child = jsg::Rc::new(CellChild);
1251 let parent = jsg::Rc::new(CellParent {
1252 child: Cell::new(child.clone()),
1253 });
1254 let wrapped = parent.clone().to_js(lock);
1255 ctx.set_global("parent", wrapped);
1256 
1257 // Drop all Rust refs.
1258 std::mem::drop(child);
1259 std::mem::drop(parent);
1260 
1261 // Parent is JS-global-held; child is kept alive via Cell<Rc<T>> tracing.
1262 crate::Harness::request_gc(lock);
1263 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 0);
1264 assert_eq!(CELL_CHILD_DROPS.load(Ordering::SeqCst), 0);
1265 Ok(())
1266 });
1267 
1268 harness.run_in_context(|lock, _ctx| {
1269 crate::Harness::request_gc(lock);
1270 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 1);
1271 assert_eq!(CELL_CHILD_DROPS.load(Ordering::SeqCst), 1);
1272 Ok(())
1273 });
1274}
1275 
1276/// `Cell<Option<jsg::Rc<T>>>` with `Some` keeps the child alive through GC.
1277#[test]
1278fn cell_option_ref_some_keeps_child_alive_through_gc() {
1279 CELL_CHILD_DROPS.store(0, Ordering::SeqCst);
1280 CELL_PARENT_DROPS.store(0, Ordering::SeqCst);
1281 
1282 let harness = crate::Harness::new();
1283 harness.run_in_context(|lock, ctx| {
1284 let child = jsg::Rc::new(CellChild);
1285 let parent = jsg::Rc::new(CellOptionParent {
1286 child: Cell::new(Some(child.clone())),
1287 });
1288 let wrapped = parent.clone().to_js(lock);
1289 ctx.set_global("parent", wrapped);
1290 
1291 std::mem::drop(child);
1292 std::mem::drop(parent);
1293 
1294 crate::Harness::request_gc(lock);
1295 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 0);
1296 assert_eq!(CELL_CHILD_DROPS.load(Ordering::SeqCst), 0);
1297 Ok(())
1298 });
1299 
1300 harness.run_in_context(|lock, _ctx| {
1301 crate::Harness::request_gc(lock);
1302 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 1);
1303 assert_eq!(CELL_CHILD_DROPS.load(Ordering::SeqCst), 1);
1304 Ok(())
1305 });
1306}
1307 
1308/// `Cell<Option<jsg::Rc<T>>>` with `None` does not crash during GC.
1309#[test]
1310fn cell_option_ref_none_does_not_crash_during_gc() {
1311 CELL_PARENT_DROPS.store(0, Ordering::SeqCst);
1312 
1313 let harness = crate::Harness::new();
1314 harness.run_in_context(|lock, ctx| {
1315 let parent = jsg::Rc::new(CellOptionParent {
1316 child: Cell::new(None),
1317 });
1318 let wrapped = parent.clone().to_js(lock);
1319 ctx.set_global("parent", wrapped);
1320 
1321 std::mem::drop(parent);
1322 
1323 crate::Harness::request_gc(lock);
1324 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 0);
1325 Ok(())
1326 });
1327 
1328 harness.run_in_context(|lock, _ctx| {
1329 crate::Harness::request_gc(lock);
1330 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 1);
1331 Ok(())
1332 });
1333}
1334 
1335/// `Cell<jsg::Nullable<jsg::Rc<T>>>` with `Nullable::Some` keeps child alive.
1336#[test]
1337fn cell_nullable_ref_some_keeps_child_alive_through_gc() {
1338 CELL_CHILD_DROPS.store(0, Ordering::SeqCst);
1339 CELL_PARENT_DROPS.store(0, Ordering::SeqCst);
1340 
1341 let harness = crate::Harness::new();
1342 harness.run_in_context(|lock, ctx| {
1343 let child = jsg::Rc::new(CellChild);
1344 let parent = jsg::Rc::new(CellNullableParent {
1345 child: Cell::new(jsg::Nullable::Some(child.clone())),
1346 });
1347 let wrapped = parent.clone().to_js(lock);
1348 ctx.set_global("parent", wrapped);
1349 
1350 std::mem::drop(child);
1351 std::mem::drop(parent);
1352 
1353 crate::Harness::request_gc(lock);
1354 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 0);
1355 assert_eq!(CELL_CHILD_DROPS.load(Ordering::SeqCst), 0);
1356 Ok(())
1357 });
1358 
1359 harness.run_in_context(|lock, _ctx| {
1360 crate::Harness::request_gc(lock);
1361 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 1);
1362 assert_eq!(CELL_CHILD_DROPS.load(Ordering::SeqCst), 1);
1363 Ok(())
1364 });
1365}
1366 
1367/// `Cell<jsg::Nullable<jsg::Rc<T>>>` with `Nullable::Null` does not crash during GC.
1368#[test]
1369fn cell_nullable_ref_null_does_not_crash_during_gc() {
1370 CELL_PARENT_DROPS.store(0, Ordering::SeqCst);
1371 
1372 let harness = crate::Harness::new();
1373 harness.run_in_context(|lock, ctx| {
1374 let parent = jsg::Rc::new(CellNullableParent {
1375 child: Cell::new(jsg::Nullable::Null),
1376 });
1377 let wrapped = parent.clone().to_js(lock);
1378 ctx.set_global("parent", wrapped);
1379 
1380 std::mem::drop(parent);
1381 
1382 crate::Harness::request_gc(lock);
1383 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 0);
1384 Ok(())
1385 });
1386 
1387 harness.run_in_context(|lock, _ctx| {
1388 crate::Harness::request_gc(lock);
1389 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 1);
1390 Ok(())
1391 });
1392}
1393 
1394/// `std::cell::Cell<jsg::Rc<T>>` (fully-qualified path) keeps child alive through GC.
1395#[test]
1396fn std_cell_ref_keeps_child_alive_through_gc() {
1397 CELL_CHILD_DROPS.store(0, Ordering::SeqCst);
1398 CELL_PARENT_DROPS.store(0, Ordering::SeqCst);
1399 
1400 let harness = crate::Harness::new();
1401 harness.run_in_context(|lock, ctx| {
1402 let child = jsg::Rc::new(CellChild);
1403 let parent = jsg::Rc::new(StdCellParent {
1404 child: std::cell::Cell::new(child.clone()),
1405 });
1406 let wrapped = parent.clone().to_js(lock);
1407 ctx.set_global("parent", wrapped);
1408 
1409 std::mem::drop(child);
1410 std::mem::drop(parent);
1411 
1412 crate::Harness::request_gc(lock);
1413 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 0);
1414 assert_eq!(CELL_CHILD_DROPS.load(Ordering::SeqCst), 0);
1415 Ok(())
1416 });
1417 
1418 harness.run_in_context(|lock, _ctx| {
1419 crate::Harness::request_gc(lock);
1420 assert_eq!(CELL_PARENT_DROPS.load(Ordering::SeqCst), 1);
1421 assert_eq!(CELL_CHILD_DROPS.load(Ordering::SeqCst), 1);
1422 Ok(())
1423 });
1424}