1/*
2 * Copyright (c) 2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16#include "ecmascript/builtins/builtins_finalization_registry.h"
17#include "ecmascript/global_env.h"
18#include "ecmascript/linked_hash_table.h"
19#include "ecmascript/jobs/micro_job_queue.h"
20#include "ecmascript/js_finalization_registry.h"
21#include "ecmascript/tests/test_helper.h"
22
23using namespace panda;
24using namespace panda::ecmascript;
25using namespace panda::ecmascript::builtins;
26
27static int testValue = 0;
28
29namespace panda::test {
30class JSFinalizationRegistryTest : public BaseTestWithScope<false> {
31public:
32    class TestClass : public base::BuiltinsBase {
33    public:
34        static JSTaggedValue callbackTest()
35        {
36            testValue++;
37            return JSTaggedValue::Undefined();
38        }
39    };
40};
41
42static JSHandle<JSTaggedValue> CreateFinalizationRegistry(JSThread *thread)
43{
44    auto vm = thread->GetEcmaVM();
45    auto factory = vm->GetFactory();
46    auto env = vm->GetGlobalEnv();
47
48    JSHandle<JSFunction> finaRegFunc(env->GetBuiltinsFinalizationRegistryFunction());
49    JSHandle<JSFunction> callbackFunc = factory->NewJSFunction(env, reinterpret_cast<void *>(
50        JSFinalizationRegistryTest::TestClass::callbackTest));
51    auto ecmaRuntimeCallInfo = TestHelper::CreateEcmaRuntimeCallInfo(thread, JSTaggedValue(*finaRegFunc), 6);
52    ecmaRuntimeCallInfo->SetFunction(finaRegFunc.GetTaggedValue());
53    ecmaRuntimeCallInfo->SetThis(JSTaggedValue::Undefined());
54    ecmaRuntimeCallInfo->SetCallArg(0, callbackFunc.GetTaggedValue());
55
56    auto prev = TestHelper::SetupFrame(thread, ecmaRuntimeCallInfo);
57    JSTaggedValue finalizationRegistry =
58        BuiltinsFinalizationRegistry::FinalizationRegistryConstructor(ecmaRuntimeCallInfo);
59    JSHandle<JSTaggedValue> finalizationRegistryHandle(thread, finalizationRegistry);
60    TestHelper::TearDownFrame(thread, prev);
61    return finalizationRegistryHandle;
62}
63
64/**
65 * @tc.name: Register
66 * @tc.desc: Register the target object to the JSFinalizationRegistry object, and call the callback method after the
67 *           target object is garbage collected. And a unregistration token, which is passed to the unregister method
68 *           when the finalizer is no longer needed. The held value, which is used to represent that object when
69 *           cleaning it up in the finalizer.
70 * @tc.type: FUNC
71 * @tc.require:
72 */
73HWTEST_F_L0(JSFinalizationRegistryTest, Register_001)
74{
75    testValue = 0;
76    auto vm = thread->GetEcmaVM();
77    auto factory = vm->GetFactory();
78    auto env = vm->GetGlobalEnv();
79
80    JSHandle<JSTaggedValue> objectFunc = env->GetObjectFunction();
81    JSHandle<JSTaggedValue> target(factory->NewJSObjectByConstructor(JSHandle<JSFunction>(objectFunc), objectFunc));
82    JSHandle<JSTaggedValue> key(factory->NewFromASCII("key1"));
83    JSHandle<JSTaggedValue> value(thread, JSTaggedValue(1));
84    JSObject::SetProperty(thread, target, key, value);
85    JSHandle<JSTaggedValue> heldValue(thread, JSTaggedValue(100));
86    JSHandle<JSTaggedValue> unregisterToken(thread, JSTaggedValue::Undefined());
87    JSHandle<JSTaggedValue> constructor = CreateFinalizationRegistry(thread);
88    JSHandle<JSFinalizationRegistry> finaRegObj(thread, constructor.GetTaggedValue());
89
90    // If unregisterToken is undefined, use vector to store
91    JSHandle<CellRecord> cellRecord = factory->NewCellRecord();
92    cellRecord->SetToWeakRefTarget(thread, target.GetTaggedValue());
93    cellRecord->SetHeldValue(thread, heldValue);
94    JSHandle<JSTaggedValue> cell(cellRecord);
95    JSHandle<CellRecordVector> expectNoUnregister(thread, finaRegObj->GetNoUnregister());
96    expectNoUnregister = CellRecordVector::Append(thread, expectNoUnregister, cell);
97
98    JSFinalizationRegistry::Register(thread, target, heldValue, unregisterToken, finaRegObj);
99    JSHandle<JSTaggedValue> noUnregister(thread, finaRegObj->GetNoUnregister());
100    JSHandle<JSTaggedValue> finRegLists = env->GetFinRegLists();
101    EXPECT_EQ(finRegLists.GetTaggedValue().GetRawData(),
102        JSHandle<JSTaggedValue>::Cast(finaRegObj).GetTaggedValue().GetRawData());
103    EXPECT_EQ(expectNoUnregister.GetTaggedValue().GetRawData(), noUnregister.GetTaggedValue().GetRawData());
104    EXPECT_EQ(testValue, 0);
105}
106
107HWTEST_F_L0(JSFinalizationRegistryTest, Register_002)
108{
109    testValue = 0;
110    auto vm = thread->GetEcmaVM();
111    auto factory = vm->GetFactory();
112    auto env = vm->GetGlobalEnv();
113
114    JSHandle<JSTaggedValue> objectFunc = env->GetObjectFunction();
115    JSHandle<JSTaggedValue> target(factory->NewJSObjectByConstructor(JSHandle<JSFunction>(objectFunc), objectFunc));
116    JSHandle<JSTaggedValue> key(factory->NewFromASCII("key2"));
117    JSHandle<JSTaggedValue> value(thread, JSTaggedValue(2));
118    JSObject::SetProperty(thread, target, key, value);
119    JSHandle<JSTaggedValue> heldValue(thread, JSTaggedValue(100));
120    JSHandle<JSTaggedValue> unregisterToken = target;
121    JSHandle<JSTaggedValue> constructor = CreateFinalizationRegistry(thread);
122    JSHandle<JSFinalizationRegistry> finaRegObj(thread, constructor.GetTaggedValue());
123
124    // If unregisterToken is not undefined, use hash map to store to facilitate subsequent delete operations
125    JSHandle<CellRecord> cellRecord = factory->NewCellRecord();
126    cellRecord->SetToWeakRefTarget(thread, target.GetTaggedValue());
127    cellRecord->SetHeldValue(thread, heldValue);
128    JSHandle<JSTaggedValue> cell(cellRecord);
129    JSHandle<CellRecordVector> array = JSHandle<CellRecordVector>(CellRecordVector::Create(thread));
130    array = CellRecordVector::Append(thread, array, cell);
131    JSHandle<JSTaggedValue> arrayValue(array);
132    JSHandle<LinkedHashMap> expectMaybeUnregister(thread, finaRegObj->GetMaybeUnregister());
133    expectMaybeUnregister = LinkedHashMap::SetWeakRef(thread, expectMaybeUnregister, unregisterToken, arrayValue);
134
135    JSFinalizationRegistry::Register(thread, target, heldValue, unregisterToken, finaRegObj);
136    JSHandle<JSTaggedValue> maybeUnregister(thread, finaRegObj->GetMaybeUnregister());
137    EXPECT_EQ(expectMaybeUnregister.GetTaggedValue().GetRawData(), maybeUnregister.GetTaggedValue().GetRawData());
138
139    JSHandle<JSTaggedValue> finRegLists = env->GetFinRegLists();
140    EXPECT_EQ(finRegLists.GetTaggedValue().GetRawData(),
141        JSHandle<JSTaggedValue>::Cast(finaRegObj).GetTaggedValue().GetRawData());
142    EXPECT_EQ(testValue, 0);
143}
144
145static void RegisterUnRegisterTestCommon(JSThread *thread, bool testUnRegister = false, bool addFinReg = false)
146{
147    testValue = 0;
148    auto vm = thread->GetEcmaVM();
149    auto factory = vm->GetFactory();
150    auto env = vm->GetGlobalEnv();
151
152    JSHandle<JSTaggedValue> objectFunc = env->GetObjectFunction();
153    vm->SetEnableForceGC(false);
154    JSHandle<JSTaggedValue> target(thread, JSTaggedValue::Undefined());
155    {
156        [[maybe_unused]] EcmaHandleScope handleScope(thread);
157        auto obj = factory->NewJSObjectByConstructor(JSHandle<JSFunction>(objectFunc), objectFunc);
158        target = JSHandle<JSTaggedValue>::Cast(obj);
159        JSHandle<JSTaggedValue> heldValue(thread, JSTaggedValue(100)); // 100: tag value
160        JSHandle<JSTaggedValue> unregisterToken = testUnRegister? target :
161            JSHandle<JSTaggedValue>(thread, JSTaggedValue::Undefined());
162        JSHandle<JSTaggedValue> constructor = CreateFinalizationRegistry(thread);
163        JSHandle<JSFinalizationRegistry> finaRegObj(thread, constructor.GetTaggedValue());
164        if (addFinReg) {
165            JSHandle<CellRecord> cellRecord = factory->NewCellRecord();
166            cellRecord->SetToWeakRefTarget(thread, target.GetTaggedValue());
167            cellRecord->SetHeldValue(thread, heldValue);
168            JSHandle<JSTaggedValue> cell(cellRecord);
169            JSHandle<CellRecordVector> noUnregister(thread, finaRegObj->GetNoUnregister());
170            noUnregister = CellRecordVector::Append(thread, noUnregister, cell);
171            finaRegObj->SetNoUnregister(thread, noUnregister);
172            JSFinalizationRegistry::AddFinRegLists(thread, finaRegObj);
173            JSHandle<JSTaggedValue> finRegLists = env->GetFinRegLists();
174            EXPECT_EQ(finRegLists.GetTaggedValue(), JSHandle<JSTaggedValue>::Cast(finaRegObj).GetTaggedValue());
175        } else {
176            JSFinalizationRegistry::Register(thread, target, heldValue, unregisterToken, finaRegObj);
177            if (testUnRegister) {
178                JSFinalizationRegistry::Unregister(thread, target, finaRegObj);
179            }
180        }
181        EXPECT_EQ(testValue, 0);
182    }
183    vm->CollectGarbage(TriggerGCType::FULL_GC);
184    if (!thread->HasPendingException()) {
185        job::MicroJobQueue::ExecutePendingJob(thread, thread->GetCurrentEcmaContext()->GetMicroJobQueue());
186    }
187    vm->SetEnableForceGC(true);
188}
189
190static void RegisterUnRegisterTestCommonTwoTarget(JSThread *thread, bool testUnRegister = false)
191{
192    testValue = 0;
193    auto vm = thread->GetEcmaVM();
194    auto factory = vm->GetFactory();
195    auto env = vm->GetGlobalEnv();
196
197    JSHandle<JSTaggedValue> objectFunc = env->GetObjectFunction();
198    vm->SetEnableForceGC(false);
199    JSHandle<JSTaggedValue> target1(thread, JSTaggedValue::Undefined());
200    JSHandle<JSTaggedValue> target2(thread, JSTaggedValue::Undefined());
201    {
202        [[maybe_unused]] EcmaHandleScope handleScope(thread);
203        auto obj = factory->NewJSObjectByConstructor(JSHandle<JSFunction>(objectFunc), objectFunc);
204        target1 = JSHandle<JSTaggedValue>::Cast(obj);
205        target2 = JSHandle<JSTaggedValue>::Cast(obj);
206        JSHandle<JSTaggedValue> heldValue(thread, JSTaggedValue(100)); // 100: held value
207        JSHandle<JSTaggedValue> unregisterToken = JSHandle<JSTaggedValue>::Cast(obj);
208        JSHandle<JSTaggedValue> constructor = CreateFinalizationRegistry(thread);
209        JSHandle<JSFinalizationRegistry> finaRegObj1(thread, constructor.GetTaggedValue());
210        JSHandle<JSFinalizationRegistry> finaRegObj2(thread, constructor.GetTaggedValue());
211
212        JSFinalizationRegistry::Register(thread, target1, heldValue, unregisterToken, finaRegObj1);
213        if (testUnRegister) {
214            JSFinalizationRegistry::Unregister(thread, target1, finaRegObj1);
215        }
216        JSFinalizationRegistry::Register(thread, target2, heldValue, unregisterToken, finaRegObj2);
217        EXPECT_EQ(testValue, 0);
218    }
219    vm->CollectGarbage(TriggerGCType::FULL_GC);
220    if (!thread->HasPendingException()) {
221        job::MicroJobQueue::ExecutePendingJob(thread, thread->GetCurrentEcmaContext()->GetMicroJobQueue());
222    }
223    vm->SetEnableForceGC(true);
224}
225
226HWTEST_F_L0(JSFinalizationRegistryTest, Register_003)
227{
228    RegisterUnRegisterTestCommon(thread);
229    EXPECT_EQ(testValue, 1);
230}
231
232HWTEST_F_L0(JSFinalizationRegistryTest, Register_004)
233{
234    RegisterUnRegisterTestCommonTwoTarget(thread);
235    EXPECT_EQ(testValue, 2);
236}
237
238/**
239 * @tc.name: Unregister
240 * @tc.desc: Unregister the JSFinalizationRegistry object from the finalization registry lists.
241 * @tc.type: FUNC
242 * @tc.require:
243 */
244HWTEST_F_L0(JSFinalizationRegistryTest, Unregister_001)
245{
246    RegisterUnRegisterTestCommon(thread, true);
247    EXPECT_EQ(testValue, 0);
248}
249
250HWTEST_F_L0(JSFinalizationRegistryTest, Unregister_002)
251{
252    RegisterUnRegisterTestCommonTwoTarget(thread, true);
253    // only trigger second finalization callback
254    EXPECT_EQ(testValue, 1);
255}
256
257/**
258 * @tc.name: CheckAndCall
259 * @tc.desc: Check whether each JSFinalizationRegistry object in finalization registry lists has been cleared. If so,
260 *           clear the JSFinalizationRegistry object from the lists.
261 * @tc.type: FUNC
262 * @tc.require:
263 */
264HWTEST_F_L0(JSFinalizationRegistryTest, CheckAndCall)
265{
266    testValue = 0;
267    auto vm = thread->GetEcmaVM();
268    auto factory = vm->GetFactory();
269    auto env = vm->GetGlobalEnv();
270
271    JSHandle<JSTaggedValue> objectFunc = env->GetObjectFunction();
272    JSHandle<JSTaggedValue> target(thread, JSTaggedValue::Undefined());
273    JSHandle<JSTaggedValue> constructor = CreateFinalizationRegistry(thread);
274    JSHandle<JSFinalizationRegistry> finaRegObj(thread, constructor.GetTaggedValue());
275    JSHandle<JSTaggedValue> finRegLists(thread, JSTaggedValue::Undefined());
276    vm->SetEnableForceGC(false);
277    {
278        [[maybe_unused]] EcmaHandleScope handleScope(thread);
279        auto obj = factory->NewJSObjectByConstructor(JSHandle<JSFunction>(objectFunc), objectFunc);
280        target = JSHandle<JSTaggedValue>::Cast(obj);
281        JSHandle<JSTaggedValue> heldValue(thread, JSTaggedValue(100));
282        JSHandle<JSTaggedValue> unregisterToken(thread, JSTaggedValue::Undefined());
283        JSFinalizationRegistry::Register(thread, target, heldValue, unregisterToken, finaRegObj);
284        EXPECT_EQ(testValue, 0);
285    }
286    finRegLists = env->GetFinRegLists();
287    EXPECT_EQ(finRegLists.GetTaggedValue(), JSHandle<JSTaggedValue>::Cast(finaRegObj).GetTaggedValue());
288    vm->CollectGarbage(TriggerGCType::FULL_GC);
289    if (!thread->HasPendingException()) {
290        job::MicroJobQueue::ExecutePendingJob(thread, thread->GetCurrentEcmaContext()->GetMicroJobQueue());
291    }
292    vm->SetEnableForceGC(true);
293    EXPECT_EQ(testValue, 1);
294    // If all objects registered in the current JSFinalizationRegistry are garbage collected,
295    // clear the JSFinalizationRegistry from the list
296    JSFinalizationRegistry::CheckAndCall(thread);
297    finRegLists = env->GetFinRegLists();
298    EXPECT_EQ(finRegLists.GetTaggedValue(), JSTaggedValue::Undefined());
299}
300
301/**
302 * @tc.name: CleanupFinalizationRegistry
303 * @tc.desc: Check current JSFinalizationRegistry, While contains any record cell such that cell and WeakRefTarget is
304 *           empty, than remove cell from JSFinalizationRegistry and return whether there are still objects that have
305 *           not been cleaned up.
306 * @tc.type: FUNC
307 * @tc.require:
308 */
309HWTEST_F_L0(JSFinalizationRegistryTest, CleanupFinalizationRegistry)
310{
311    testValue = 0;
312    auto vm = thread->GetEcmaVM();
313    auto factory = vm->GetFactory();
314    auto env = vm->GetGlobalEnv();
315
316    JSHandle<JSTaggedValue> objectFunc = env->GetObjectFunction();
317    JSHandle<JSTaggedValue> target(thread, JSTaggedValue::Undefined());
318    JSHandle<JSTaggedValue> constructor = CreateFinalizationRegistry(thread);
319    JSHandle<JSFinalizationRegistry> finaRegObj(thread, constructor.GetTaggedValue());
320    bool isCleared = false;
321    vm->SetEnableForceGC(false);
322    {
323        [[maybe_unused]] EcmaHandleScope handleScope(thread);
324        auto obj = factory->NewJSObjectByConstructor(JSHandle<JSFunction>(objectFunc), objectFunc);
325        target = JSHandle<JSTaggedValue>::Cast(obj);
326        JSHandle<JSTaggedValue> heldValue(thread, JSTaggedValue(100));
327        JSHandle<JSTaggedValue> unregisterToken(thread, JSTaggedValue::Undefined());
328        JSFinalizationRegistry::Register(thread, target, heldValue, unregisterToken, finaRegObj);
329        EXPECT_EQ(testValue, 0);
330
331        isCleared = JSFinalizationRegistry::CleanupFinalizationRegistry(thread, finaRegObj);
332        EXPECT_FALSE(isCleared);
333    }
334    vm->CollectGarbage(TriggerGCType::FULL_GC);
335    if (!thread->HasPendingException()) {
336        job::MicroJobQueue::ExecutePendingJob(thread, thread->GetCurrentEcmaContext()->GetMicroJobQueue());
337    }
338    vm->SetEnableForceGC(true);
339    EXPECT_EQ(testValue, 1);
340    isCleared = JSFinalizationRegistry::CleanupFinalizationRegistry(thread, finaRegObj);
341    EXPECT_TRUE(isCleared);
342}
343
344/**
345 * @tc.name: CleanFinRegLists
346 * @tc.desc: Clear the JSFinalizationRegistry object from the finalization registry lists.
347 * @tc.type: FUNC
348 * @tc.require:
349 */
350HWTEST_F_L0(JSFinalizationRegistryTest, CleanFinRegLists)
351{
352    testValue = 0;
353    auto vm = thread->GetEcmaVM();
354    auto factory = vm->GetFactory();
355    auto env = vm->GetGlobalEnv();
356
357    JSHandle<JSTaggedValue> objectFunc = env->GetObjectFunction();
358    JSHandle<JSTaggedValue> target(factory->NewJSObjectByConstructor(JSHandle<JSFunction>(objectFunc), objectFunc));
359    JSHandle<JSTaggedValue> key(factory->NewFromASCII("key3"));
360    JSHandle<JSTaggedValue> value(thread, JSTaggedValue(3));
361    JSObject::SetProperty(thread, target, key, value);
362    JSHandle<JSTaggedValue> heldValue(thread, JSTaggedValue(100));
363    JSHandle<JSTaggedValue> unregisterToken(thread, JSTaggedValue::Undefined());
364    JSHandle<JSTaggedValue> constructor = CreateFinalizationRegistry(thread);
365    JSHandle<JSFinalizationRegistry> finaRegObj(thread, constructor.GetTaggedValue());
366
367    JSFinalizationRegistry::Register(thread, target, heldValue, unregisterToken, finaRegObj);
368    JSHandle<JSTaggedValue> finRegLists = env->GetFinRegLists();
369    EXPECT_EQ(finRegLists.GetTaggedValue(), JSHandle<JSTaggedValue>::Cast(finaRegObj).GetTaggedValue());
370    EXPECT_EQ(testValue, 0);
371
372    JSFinalizationRegistry::CleanFinRegLists(thread, finaRegObj);
373    finRegLists = env->GetFinRegLists();
374    EXPECT_EQ(finRegLists.GetTaggedValue(), JSTaggedValue::Undefined());
375}
376
377/**
378 * @tc.name: AddFinRegLists
379 * @tc.desc: Add a JSFinalizationRegistry object to the finalization registry lists.
380 * @tc.type: FUNC
381 * @tc.require:
382 */
383HWTEST_F_L0(JSFinalizationRegistryTest, AddFinRegLists)
384{
385    RegisterUnRegisterTestCommon(thread, false, true);
386    EXPECT_EQ(testValue, 1);
387}
388}  // namespace panda::test