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