-
-
Notifications
You must be signed in to change notification settings - Fork 69
Expand file tree
/
Copy pathJSClosureTests.swift
More file actions
153 lines (132 loc) · 5.17 KB
/
JSClosureTests.swift
File metadata and controls
153 lines (132 loc) · 5.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
@_spi(JSObject_id) import JavaScriptKit
import XCTest
class JSClosureTests: XCTestCase {
func testClosureLifetime() {
let evalClosure = JSObject.global.globalObject1.eval_closure.function!
do {
let c1 = JSClosure { arguments in
return arguments[0]
}
XCTAssertEqual(evalClosure(c1, JSValue.number(1.0)), .number(1.0))
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
c1.release()
#endif
}
do {
let array = JSObject.global.Array.function!.new()
let c1 = JSClosure { _ in .number(3) }
_ = array.push!(c1)
XCTAssertEqual(array[0].function!().number, 3.0)
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
c1.release()
#endif
}
do {
let c1 = JSClosure { _ in .undefined }
XCTAssertEqual(c1(), .undefined)
}
do {
let c1 = JSClosure { _ in .number(4) }
XCTAssertEqual(c1(), .number(4))
}
}
func testHostFunctionRegistration() {
// ```js
// global.globalObject1 = {
// ...
// "prop_6": {
// "call_host_1": function() {
// return global.globalObject1.prop_6.host_func_1()
// }
// }
// }
// ```
let globalObject1 = getJSValue(this: .global, name: "globalObject1")
let globalObject1Ref = try! XCTUnwrap(globalObject1.object)
let prop_6 = getJSValue(this: globalObject1Ref, name: "prop_6")
let prop_6Ref = try! XCTUnwrap(prop_6.object)
var isHostFunc1Called = false
let hostFunc1 = JSClosure { (_) -> JSValue in
isHostFunc1Called = true
return .number(1)
}
setJSValue(this: prop_6Ref, name: "host_func_1", value: .object(hostFunc1))
let call_host_1 = getJSValue(this: prop_6Ref, name: "call_host_1")
let call_host_1Func = try! XCTUnwrap(call_host_1.function)
XCTAssertEqual(call_host_1Func(), .number(1))
XCTAssertEqual(isHostFunc1Called, true)
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
hostFunc1.release()
#endif
let evalClosure = JSObject.global.globalObject1.eval_closure.function!
let hostFunc2 = JSClosure { (arguments) -> JSValue in
if let input = arguments[0].number {
return .number(input * 2)
} else {
return .string(String(describing: arguments[0]))
}
}
XCTAssertEqual(evalClosure(hostFunc2, 3), .number(6))
XCTAssertTrue(evalClosure(hostFunc2, true).string != nil)
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
hostFunc2.release()
#endif
}
func testRegressionTestForMisDeallocation() async throws {
// Use Node.js's `--expose-gc` flag to enable manual garbage collection.
guard let gc = JSObject.global.gc.function else {
throw XCTSkip("Missing --expose-gc flag")
}
// Step 1: Create many JSClosure instances
let obj = JSObject()
var closurePointers: Set<UInt32> = []
let numberOfSourceClosures = 10_000
do {
var closures: [JSClosure] = []
for i in 0..<numberOfSourceClosures {
let closure = JSClosure { _ in .undefined }
obj["c\(i)"] = closure.jsValue
closures.append(closure)
// Store
closurePointers.insert(UInt32(UInt(bitPattern: Unmanaged.passUnretained(closure).toOpaque())))
// To avoid all JSClosures having a common address diffs, randomly allocate a new object.
if Bool.random() {
_ = JSObject()
}
}
}
// Step 2: Create many JSObject to make JSObject.id close to Swift heap object address
let minClosurePointer = closurePointers.min() ?? 0
let maxClosurePointer = closurePointers.max() ?? 0
while true {
let obj = JSObject()
if minClosurePointer == obj.id {
break
}
}
// Step 3: Create JSClosure instances and find the one with JSClosure.id == &closurePointers[x]
do {
while true {
let c = JSClosure { _ in .undefined }
if closurePointers.contains(c.id) || c.id > maxClosurePointer {
break
}
// To avoid all JSClosures having a common JSObject.id diffs, randomly allocate a new JS object.
if Bool.random() {
_ = JSObject()
}
}
}
// Step 4: Trigger garbage collection to call the finalizer of the conflicting JSClosure instance
for _ in 0..<100 {
gc()
// Tick the event loop to allow the garbage collector to run finalizers
// registered by FinalizationRegistry.
try await Task.sleep(for: .milliseconds(0))
}
// Step 5: Verify that the JSClosure instances are still alive and can be called
for i in 0..<numberOfSourceClosures {
_ = obj["c\(i)"].function!()
}
}
}