Skip to content

Commit 016fa5b

Browse files
authored
test: Add GC lifecycle test for identity-cached wrappers (#731)
1 parent 8d279a0 commit 016fa5b

3 files changed

Lines changed: 75 additions & 0 deletions

File tree

Tests/BridgeJSIdentityTests/Generated/BridgeJS.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,25 @@ fileprivate func _bjs_ArrayIdentityElement_wrap_extern(_ pointer: UnsafeMutableR
333333
return _bjs_ArrayIdentityElement_wrap_extern(pointer)
334334
}
335335

336+
#if arch(wasm32)
337+
@_extern(wasm, module: "BridgeJSIdentityTests", name: "bjs_gc")
338+
fileprivate func bjs_gc_extern() -> Void
339+
#else
340+
fileprivate func bjs_gc_extern() -> Void {
341+
fatalError("Only available on WebAssembly")
342+
}
343+
#endif
344+
@inline(never) fileprivate func bjs_gc() -> Void {
345+
return bjs_gc_extern()
346+
}
347+
348+
func _$gc() throws(JSException) -> Void {
349+
bjs_gc()
350+
if let error = _swift_js_take_exception() {
351+
throw error
352+
}
353+
}
354+
336355
#if arch(wasm32)
337356
@_extern(wasm, module: "BridgeJSIdentityTests", name: "bjs_IdentityModeTestImports_runJsIdentityModeTests_static")
338357
fileprivate func bjs_IdentityModeTestImports_runJsIdentityModeTests_static_extern() -> Void

Tests/BridgeJSIdentityTests/Generated/JavaScript/BridgeJS.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,23 @@
406406
"children" : [
407407
{
408408
"functions" : [
409+
{
410+
"effects" : {
411+
"isAsync" : false,
412+
"isStatic" : false,
413+
"isThrows" : true
414+
},
415+
"from" : "global",
416+
"name" : "gc",
417+
"parameters" : [
418+
419+
],
420+
"returnType" : {
421+
"void" : {
409422

423+
}
424+
}
425+
}
410426
],
411427
"types" : [
412428
{

Tests/BridgeJSIdentityTests/IdentityModeTests.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import XCTest
22
import JavaScriptKit
33

4+
@JSFunction(from: .global) func gc() throws(JSException) -> Void
5+
46
@JSClass struct IdentityModeTestImports {
57
@JSFunction static func runJsIdentityModeTests() throws(JSException)
68
}
@@ -9,6 +11,44 @@ final class IdentityModeTests: XCTestCase {
911
func testRunJsIdentityModeTests() throws {
1012
try IdentityModeTestImports.runJsIdentityModeTests()
1113
}
14+
15+
/// Verifies that identity-cached wrappers are properly reclaimed by GC.
16+
///
17+
/// Creates an identity-mode object, crosses it multiple times (filling the
18+
/// identity cache), drops all references, triggers GC + event loop ticks,
19+
/// and verifies the Swift object is deallocated. This proves that the
20+
/// WeakRef-based identity cache does not prevent garbage collection.
21+
func testIdentityCachedWrapperIsReclaimedByGC() async throws {
22+
RetainLeakSubject.deinits = 0
23+
24+
// Create object and cross it multiple times to fill identity cache
25+
_retainLeakSubject = RetainLeakSubject(tag: 99)
26+
weak var weakSubject = _retainLeakSubject
27+
28+
// Cross to JS 5 times (populates identity cache with WeakRef)
29+
for _ in 0..<5 {
30+
_ = getRetainLeakSubject()
31+
}
32+
33+
// Drop Swift-side strong reference
34+
_retainLeakSubject = nil
35+
36+
// JS wrapper should still be alive via the identity cache's WeakRef,
37+
// but WeakRef doesn't prevent GC. Trigger GC + event loop ticks to
38+
// let FinalizationRegistry fire and call deinit.
39+
for _ in 0..<100 {
40+
try gc()
41+
try await Task.sleep(for: .milliseconds(0))
42+
if weakSubject == nil {
43+
break
44+
}
45+
}
46+
47+
// The identity-cached wrapper should have been collected,
48+
// FinalizationRegistry should have fired, deinit should have run.
49+
XCTAssertNil(weakSubject, "Identity-cached object should be deallocated after GC")
50+
XCTAssertEqual(RetainLeakSubject.deinits, 1, "Deinit should fire exactly once")
51+
}
1252
}
1353

1454
@JS class IdentityTestSubject {

0 commit comments

Comments
 (0)