• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Copyright (c) 2021-2024 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 #ifndef PANDA_PLUGINS_ETS_INTEROP_JS_GTEST_H_
17 #define PANDA_PLUGINS_ETS_INTEROP_JS_GTEST_H_
18 
19 #include <fstream>
20 #include <sstream>
21 #include <regex>
22 #include <gtest/gtest.h>
23 #include <node_api.h>
24 
25 #include "libpandabase/macros.h"
26 
27 namespace ark::ets::interop::js::testing {
28 
29 class EtsInteropTest : public ::testing::Test {
30 public:
SetUpTestSuite()31     static void SetUpTestSuite()
32     {
33         if (std::getenv("ARK_ETS_INTEROP_JS_GTEST_SOURCES") == nullptr) {
34             std::cerr << "ARK_ETS_INTEROP_JS_GTEST_SOURCES not set" << std::endl;
35             std::abort();
36         }
37     }
38 
SetUp()39     void SetUp() override
40     {
41         bool hasPendingExc;
42         [[maybe_unused]] napi_status status = napi_is_exception_pending(GetJsEnv(), &hasPendingExc);
43         ASSERT(status == napi_ok && !hasPendingExc);
44 
45         interopJsTestPath_ = std::getenv("ARK_ETS_INTEROP_JS_GTEST_SOURCES");
46         // This object is used to save global js names
47         if (!SetGtestEnv()) {
48             std::abort();
49         }
50     }
51 
SetGtestEnv()52     bool SetGtestEnv()
53     {
54         napi_value jsGlobalObject = GetGlobalObject(jsEnv_);
55 
56         napi_value jsObject;
57         [[maybe_unused]] napi_status status = napi_create_object(jsEnv_, &jsObject);
58         ASSERT(status == napi_ok);
59 
60         status = napi_set_named_property(jsEnv_, jsGlobalObject, "gtest_env", jsObject);
61 
62         return status == napi_ok;
63     }
64 
65     template <typename R, typename... Args>
CallJsMethod(std::string_view fnName,std::string fileName)66     [[nodiscard]] std::optional<R> CallJsMethod(std::string_view fnName, std::string fileName)
67     {
68         LoadModuleAs(fileName, fileName);
69         return DoCallJsMethod<R>(fnName, {fileName}, jsEnv_);
70     }
71 
RunJsScript(const std::string & script)72     [[nodiscard]] static bool RunJsScript(const std::string &script)
73     {
74         return DoRunJsScript(jsEnv_, script);
75     }
76 
RunJsTestSuite(const std::string & path)77     [[nodiscard]] bool RunJsTestSuite(const std::string &path)
78     {
79         return DoRunJsTestSuite(jsEnv_, path);
80     }
81 
82     template <typename R, typename... Args>
CallEtsMethod(std::string_view fnName,Args &&...args)83     [[nodiscard]] std::optional<R> CallEtsMethod(std::string_view fnName, Args &&...args)
84     {
85         return DoCallEtsMethod<R>(fnName, jsEnv_, std::forward<Args>(args)...);
86     }
87 
GetJsEnv()88     static napi_env GetJsEnv()
89     {
90         return jsEnv_;
91     }
92 
LoadModuleAs(const std::string & moduleName,const std::string & modulePath)93     void LoadModuleAs(const std::string &moduleName, const std::string &modulePath)
94     {
95         auto res = LoadModule(moduleName, modulePath);
96         if (!res) {
97             std::cerr << "Failed to load module: " << modulePath << std::endl;
98             std::abort();
99         }
100     }
101 
102 private:
LoadModule(const std::string & moduleName,const std::string & modulePath)103     bool LoadModule(const std::string &moduleName, const std::string &modulePath)
104     {
105         [[maybe_unused]] napi_status status;
106 
107         napi_value jsUndefined;
108         status = napi_get_undefined(jsEnv_, &jsUndefined);
109         ASSERT(status == napi_ok);
110 
111         napi_value jsGlobalObject = GetGlobalObject(jsEnv_);
112 
113         napi_value jsRequire;
114         status = napi_get_named_property(jsEnv_, jsGlobalObject, "require", &jsRequire);
115         ASSERT(status == napi_ok);
116 
117         napi_value jsPath;
118         auto pathToModule = interopJsTestPath_ + "/" + modulePath;
119         status = napi_create_string_utf8(jsEnv_, pathToModule.data(), pathToModule.length(), &jsPath);
120 
121         napi_value jsGtestEnvObject = GetJsGtestEnvObject(jsEnv_);
122 
123         napi_value jsModule;
124         status = napi_call_function(jsEnv_, jsUndefined, jsRequire, 1, &jsPath, &jsModule);
125         ASSERT(status == napi_ok);
126 
127         status = napi_set_named_property(jsEnv_, jsGtestEnvObject, moduleName.c_str(), jsModule);
128 
129         return status == napi_ok;
130     }
131 
DoRunJsTestSuite(napi_env env,const std::string & path)132     [[nodiscard]] bool DoRunJsTestSuite(napi_env env, const std::string &path)
133     {
134         std::string fullPath = interopJsTestPath_ + "/" + path;
135         std::string testSouceCode = ReadFile(fullPath);
136 
137         // NOTE:
138         //  We wrap the test source code to anonymous lambda function to avoid pollution the global namespace.
139         //  We also set the 'use strict' mode, because right now we are checking interop only in the strict mode.
140         return DoRunJsScript(env, "(() => { \n'use strict'\n" + testSouceCode + "\n})()");
141     }
142 
DoRunJsScript(napi_env env,const std::string & script)143     [[nodiscard]] static bool DoRunJsScript(napi_env env, const std::string &script)
144     {
145         [[maybe_unused]] napi_status status;
146         napi_value jsScript;
147         status = napi_create_string_utf8(env, script.c_str(), script.length(), &jsScript);
148         ASSERT(status == napi_ok);
149 
150         napi_value jsResult;
151         status = napi_run_script(env, jsScript, &jsResult);
152         if (status == napi_generic_failure) {
153             std::cerr << "EtsInteropTest fired an exception" << std::endl;
154             napi_value exc;
155             status = napi_get_and_clear_last_exception(env, &exc);
156             ASSERT(status == napi_ok);
157             napi_value jsStr;
158             status = napi_coerce_to_string(env, exc, &jsStr);
159             ASSERT(status == napi_ok);
160             std::cerr << "Exception string: " << GetString(env, jsStr) << std::endl;
161             return false;
162         }
163         return status == napi_ok;
164     }
165 
ReadFile(const std::string & fullPath)166     static std::string ReadFile(const std::string &fullPath)
167     {
168         std::ifstream jsSourceFile(fullPath);
169         if (!jsSourceFile.is_open()) {
170             std::cerr << __func__ << ": Cannot open file: " << fullPath << std::endl;
171             std::abort();
172         }
173         std::ostringstream stringStream;
174         stringStream << jsSourceFile.rdbuf();
175         return stringStream.str();
176     }
177 
GetString(napi_env env,napi_value jsStr)178     static std::string GetString(napi_env env, napi_value jsStr)
179     {
180         size_t length;
181         [[maybe_unused]] napi_status status = napi_get_value_string_utf8(env, jsStr, nullptr, 0, &length);
182         ASSERT(status == napi_ok);
183         std::string v(length, '\0');
184         size_t copied;
185         status = napi_get_value_string_utf8(env, jsStr, v.data(), length + 1, &copied);
186         ASSERT(status == napi_ok);
187         ASSERT(length == copied);
188         return v;
189     }
190 
191     template <typename T>
GetRetValue(napi_env env,napi_value jsValue)192     static T GetRetValue([[maybe_unused]] napi_env env, [[maybe_unused]] napi_value jsValue)
193     {
194         if constexpr (std::is_same_v<T, double>) {
195             double v;
196             [[maybe_unused]] napi_status status = napi_get_value_double(env, jsValue, &v);
197             ASSERT(status == napi_ok);
198             return v;
199         } else if constexpr (std::is_same_v<T, int32_t>) {
200             int32_t v;
201             [[maybe_unused]] napi_status status = napi_get_value_int32(env, jsValue, &v);
202             ASSERT(status == napi_ok);
203             return v;
204         } else if constexpr (std::is_same_v<T, uint32_t>) {
205             uint32_t v;
206             [[maybe_unused]] napi_status status = napi_get_value_uint32(env, jsValue, &v);
207             ASSERT(status == napi_ok);
208             return v;
209         } else if constexpr (std::is_same_v<T, int64_t>) {
210             int64_t v;
211             [[maybe_unused]] napi_status status = napi_get_value_int64(env, jsValue, &v);
212             ASSERT(status == napi_ok);
213             return v;
214         } else if constexpr (std::is_same_v<T, bool>) {
215             bool v;
216             [[maybe_unused]] napi_status status = napi_get_value_bool(env, jsValue, &v);
217             ASSERT(status == napi_ok);
218             return v;
219         } else if constexpr (std::is_same_v<T, std::string>) {
220             return GetString(env, jsValue);
221         } else if constexpr (std::is_same_v<T, napi_value>) {
222             return jsValue;
223         } else if constexpr (std::is_same_v<T, void>) {
224             // do nothing
225         } else {
226             enum { INCORRECT_TEMPLATE_TYPE = false };
227             static_assert(INCORRECT_TEMPLATE_TYPE, "Incorrect template type");
228         }
229     }
230 
MakeJsArg(napi_env env,double arg)231     static napi_value MakeJsArg(napi_env env, double arg)
232     {
233         napi_value v;
234         [[maybe_unused]] napi_status status = napi_create_double(env, arg, &v);
235         ASSERT(status == napi_ok);
236         return v;
237     }
238 
MakeJsArg(napi_env env,int32_t arg)239     static napi_value MakeJsArg(napi_env env, int32_t arg)
240     {
241         napi_value v;
242         [[maybe_unused]] napi_status status = napi_create_int32(env, arg, &v);
243         ASSERT(status == napi_ok);
244         return v;
245     }
246 
MakeJsArg(napi_env env,uint32_t arg)247     static napi_value MakeJsArg(napi_env env, uint32_t arg)
248     {
249         napi_value v;
250         [[maybe_unused]] napi_status status = napi_create_uint32(env, arg, &v);
251         ASSERT(status == napi_ok);
252         return v;
253     }
254 
MakeJsArg(napi_env env,int64_t arg)255     static napi_value MakeJsArg(napi_env env, int64_t arg)
256     {
257         napi_value v;
258         [[maybe_unused]] napi_status status = napi_create_int64(env, arg, &v);
259         ASSERT(status == napi_ok);
260         return v;
261     }
262 
MakeJsArg(napi_env env,std::string_view arg)263     static napi_value MakeJsArg(napi_env env, std::string_view arg)
264     {
265         napi_value v;
266         [[maybe_unused]] napi_status status = napi_create_string_utf8(env, arg.data(), arg.length(), &v);
267         ASSERT(status == napi_ok);
268         return v;
269     }
270 
MakeJsArg(napi_env env,napi_value arg)271     static napi_value MakeJsArg([[maybe_unused]] napi_env env, napi_value arg)
272     {
273         return arg;
274     }
275 
GetGlobalObject(napi_env env)276     static napi_value GetGlobalObject(napi_env env)
277     {
278         [[maybe_unused]] napi_status status;
279 
280         // Get globalThis
281         napi_value jsGlobalObject;
282         status = napi_get_global(env, &jsGlobalObject);
283         ASSERT(status == napi_ok);
284 
285         return jsGlobalObject;
286     }
287 
GetJsGtestObject(napi_env env)288     static napi_value GetJsGtestObject(napi_env env)
289     {
290         [[maybe_unused]] napi_status status;
291 
292         // Get globalThis
293         napi_value jsGlobalObject;
294         status = napi_get_global(env, &jsGlobalObject);
295         ASSERT(status == napi_ok);
296 
297         // Get globalThis.gtest
298         napi_value jsGtestObject;
299         status = napi_get_named_property(env, jsGlobalObject, "gtest", &jsGtestObject);
300         ASSERT(status == napi_ok);
301 
302         return jsGtestObject;
303     }
304 
GetJsGtestEnvObject(napi_env env)305     static napi_value GetJsGtestEnvObject(napi_env env)
306     {
307         [[maybe_unused]] napi_status status;
308 
309         napi_value jsGlobalObject = GetGlobalObject(jsEnv_);
310 
311         // Get globalThis.gtest_env
312         napi_value jsObject;
313         status = napi_get_named_property(env, jsGlobalObject, "gtest_env", &jsObject);
314         ASSERT(status == napi_ok);
315 
316         return jsObject;
317     }
318 
CallEtsMethodViaJs(napi_env env,napi_value jsFunctionName,const std::initializer_list<napi_value> & napiArgs)319     [[nodiscard]] static bool CallEtsMethodViaJs(napi_env env, napi_value jsFunctionName,
320                                                  const std::initializer_list<napi_value> &napiArgs)
321     {
322         napi_value jsGtestObject = GetJsGtestObject(env);
323 
324         napi_value jsUndefined;
325         [[maybe_unused]] napi_status status = napi_get_undefined(env, &jsUndefined);
326         ASSERT(status == napi_ok);
327 
328         napi_value jsEtsVm;
329         status = napi_get_named_property(env, jsGtestObject, "etsVm", &jsEtsVm);
330         ASSERT(status == napi_ok);
331 
332         napi_value jsEtsVmCall;
333         status = napi_get_named_property(env, jsEtsVm, "call", &jsEtsVmCall);
334         ASSERT(status == napi_ok);
335 
336         auto argc = napiArgs.size() + 1;
337         std::vector<napi_value> argv;
338         argv.reserve(argc);
339         argv.push_back(jsFunctionName);
340         argv.insert(argv.end(), napiArgs.begin(), napiArgs.end());
341 
342         napi_value jsResult;
343         status = napi_call_function(env, jsUndefined, jsEtsVmCall, argc, argv.data(), &jsResult);
344         if (status != napi_ok) {
345             return false;
346         }
347 
348         status = napi_set_named_property(env, jsGtestObject, "ret", jsResult);
349         return status == napi_ok;
350     }
351 
352     template <typename R, typename... Args>
DoCallEtsMethod(std::string_view fnName,napi_env env,Args &&...args)353     [[nodiscard]] static std::optional<R> DoCallEtsMethod(std::string_view fnName, napi_env env, Args &&...args)
354     {
355         [[maybe_unused]] napi_status status;
356 
357         // Get globalThis.gtest
358         napi_value jsGtestObject = GetJsGtestObject(env);
359 
360         // Set globalThis.gtest.functionName
361         napi_value jsFunctionName;
362         status = napi_create_string_utf8(env, fnName.data(), fnName.length(), &jsFunctionName);
363         ASSERT(status == napi_ok);
364         status = napi_set_named_property(env, jsGtestObject, "functionName", jsFunctionName);
365         ASSERT(status == napi_ok);
366 
367         // Set globalThis.gtest.args
368         std::initializer_list<napi_value> napiArgs = {MakeJsArg(env, args)...};
369         napi_value jsArgs;
370         status = napi_create_array_with_length(env, napiArgs.size(), &jsArgs);
371         ASSERT(status == napi_ok);
372         uint32_t i = 0;
373         for (auto arg : napiArgs) {
374             status = napi_set_element(env, jsArgs, i++, arg);
375             ASSERT(status == napi_ok);
376         }
377         status = napi_set_named_property(env, jsGtestObject, "args", jsArgs);
378         ASSERT(status == napi_ok);
379 
380         auto res = CallEtsMethodViaJs(env, jsFunctionName, napiArgs);
381         if (!res) {
382             return std::nullopt;
383         }
384 
385         // Get globalThis.gtest.ret
386         napi_value jsRetValue {};
387         status = napi_get_named_property(env, jsGtestObject, "ret", &jsRetValue);
388         ASSERT(status == napi_ok);
389         return GetRetValue<R>(env, jsRetValue);
390     }
391     template <typename R, typename... Args>
DoCallJsMethod(std::string_view fnName,std::string_view modName,napi_env env)392     [[nodiscard]] static std::optional<R> DoCallJsMethod(std::string_view fnName, std::string_view modName,
393                                                          napi_env env)
394     {
395         napi_value calledFn;
396         napi_value arg;
397         napi_value jsUndefined;
398         napi_value jsGtestEnvObject = GetJsGtestEnvObject(jsEnv_);
399 
400         napi_value jsModule;
401         [[maybe_unused]] napi_status status = napi_get_named_property(env, jsGtestEnvObject, modName.data(), &jsModule);
402         ASSERT(status == napi_ok);
403         status = napi_get_named_property(env, jsModule, fnName.data(), &calledFn);
404         ASSERT(status == napi_ok);
405 
406         status = napi_get_undefined(env, &jsUndefined);
407         ASSERT(status == napi_ok);
408         napi_value *argv = &arg;
409         size_t argc = 0;
410 
411         napi_value returnVal;
412         status = napi_call_function(env, jsUndefined, calledFn, argc, argv, &returnVal);
413         ASSERT(status == napi_ok);
414         return GetRetValue<R>(env, returnVal);
415     }
416     friend class JsEnvAccessor;
417     static napi_env jsEnv_;
418 
419 protected:
420     // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes)
421     std::string interopJsTestPath_;
422 };
423 
424 }  // namespace ark::ets::interop::js::testing
425 
426 #endif  // !PANDA_PLUGINS_ETS_INTEROP_JS_GTEST_H_
427