1 /** 2 * Copyright (c) 2021-2023 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 <gtest/gtest.h> 22 #include <node_api.h> 23 24 #include "libpandabase/macros.h" 25 26 namespace panda::ets::interop::js::testing { 27 28 class EtsInteropTest : public ::testing::Test { 29 public: SetUpTestSuite()30 static void SetUpTestSuite() 31 { 32 if (std::getenv("ARK_ETS_INTEROP_JS_GTEST_SOURCES") == nullptr) { 33 std::cerr << "ARK_ETS_INTEROP_JS_GTEST_SOURCES not set" << std::endl; 34 std::abort(); 35 } 36 } 37 SetUp()38 void SetUp() override 39 { 40 bool hasPendingExc; 41 [[maybe_unused]] napi_status status = napi_is_exception_pending(GetJsEnv(), &hasPendingExc); 42 ASSERT(status == napi_ok && !hasPendingExc); 43 44 interopJsTestPath_ = std::getenv("ARK_ETS_INTEROP_JS_GTEST_SOURCES"); 45 // This object is used to save global js names 46 if (!RunJsScript("var gtest_env = {};\n")) { 47 std::abort(); 48 } 49 } 50 RunJsScript(const std::string & script)51 [[nodiscard]] static bool RunJsScript(const std::string &script) 52 { 53 return DoRunJsScript(jsEnv_, script); 54 } 55 56 template <typename R> RunJsScriptByPath(const std::string & path)57 [[nodiscard]] std::optional<R> RunJsScriptByPath(const std::string &path) 58 { 59 return DoRunJsScriptByPath<R>(jsEnv_, path); 60 } 61 RunJsTestSuite(const std::string & path)62 [[nodiscard]] bool RunJsTestSuite(const std::string &path) 63 { 64 return DoRunJsTestSuite(jsEnv_, path); 65 } 66 67 template <typename R, typename... Args> CallEtsMethod(std::string_view fnName,Args &&...args)68 [[nodiscard]] std::optional<R> CallEtsMethod(std::string_view fnName, Args &&...args) 69 { 70 return DoCallEtsMethod<R>(fnName, jsEnv_, std::forward<Args>(args)...); 71 } 72 GetJsEnv()73 static napi_env GetJsEnv() 74 { 75 return jsEnv_; 76 } 77 LoadModuleAs(const std::string & moduleName,const std::string & modulePath)78 void LoadModuleAs(const std::string &moduleName, const std::string &modulePath) 79 { 80 auto res = 81 RunJsScript("gtest_env." + moduleName + " = require(\"" + interopJsTestPath_ + "/" + modulePath + "\");\n"); 82 if (!res) { 83 std::cerr << "Failed to load module: " << modulePath << std::endl; 84 std::abort(); 85 } 86 } 87 88 private: DoRunJsTestSuite(napi_env env,const std::string & path)89 [[nodiscard]] bool DoRunJsTestSuite(napi_env env, const std::string &path) 90 { 91 std::string fullPath = interopJsTestPath_ + "/" + path; 92 std::string testSouceCode = ReadFile(fullPath); 93 94 // NOTE: 95 // We wrap the test source code to anonymous lambda function to avoid pollution the global namespace. 96 // We also set the 'use strict' mode, because right now we are checking interop only in the strict mode. 97 return DoRunJsScript(env, "(() => { \n'use strict'\n" + testSouceCode + "\n})()"); 98 } 99 DoRunJsScript(napi_env env,const std::string & script)100 [[nodiscard]] static bool DoRunJsScript(napi_env env, const std::string &script) 101 { 102 [[maybe_unused]] napi_status status; 103 napi_value jsScript; 104 status = napi_create_string_utf8(env, script.c_str(), script.length(), &jsScript); 105 ASSERT(status == napi_ok); 106 107 napi_value jsResult; 108 status = napi_run_script(env, jsScript, &jsResult); 109 if (status == napi_generic_failure) { 110 std::cerr << "EtsInteropTest fired an exception" << std::endl; 111 napi_value exc; 112 status = napi_get_and_clear_last_exception(env, &exc); 113 ASSERT(status == napi_ok); 114 napi_value jsStr; 115 status = napi_coerce_to_string(env, exc, &jsStr); 116 ASSERT(status == napi_ok); 117 std::cerr << "Exception string: " << GetString(env, jsStr) << std::endl; 118 return false; 119 } 120 return status == napi_ok; 121 } 122 ReadFile(const std::string & fullPath)123 static std::string ReadFile(const std::string &fullPath) 124 { 125 std::ifstream jsSourceFile(fullPath); 126 if (!jsSourceFile.is_open()) { 127 std::cerr << __func__ << ": Cannot open file: " << fullPath << std::endl; 128 std::abort(); 129 } 130 std::ostringstream stringStream; 131 stringStream << jsSourceFile.rdbuf(); 132 return stringStream.str(); 133 } 134 135 template <typename T> DoRunJsScriptByPath(napi_env env,const std::string & path)136 [[nodiscard]] std::optional<T> DoRunJsScriptByPath(napi_env env, const std::string &path) 137 { 138 std::string fullPath = interopJsTestPath_ + "/" + path; 139 140 if (!DoRunJsScript(env, ReadFile(fullPath))) { 141 return std::nullopt; 142 } 143 144 [[maybe_unused]] napi_status status; 145 napi_value jsRetValue {}; 146 status = napi_get_named_property(env, GetJsGtestObject(env), "ret", &jsRetValue); 147 ASSERT(status == napi_ok); 148 149 // Get globalThis.gtest.ret 150 return GetRetValue<T>(env, jsRetValue); 151 } 152 GetString(napi_env env,napi_value jsStr)153 static std::string GetString(napi_env env, napi_value jsStr) 154 { 155 size_t length; 156 [[maybe_unused]] napi_status status = napi_get_value_string_utf8(env, jsStr, nullptr, 0, &length); 157 ASSERT(status == napi_ok); 158 std::string v(length, '\0'); 159 size_t copied; 160 status = napi_get_value_string_utf8(env, jsStr, v.data(), length + 1, &copied); 161 ASSERT(status == napi_ok); 162 ASSERT(length == copied); 163 return v; 164 } 165 166 template <typename T> GetRetValue(napi_env env,napi_value jsValue)167 static T GetRetValue([[maybe_unused]] napi_env env, [[maybe_unused]] napi_value jsValue) 168 { 169 if constexpr (std::is_same_v<T, double>) { 170 double v; 171 [[maybe_unused]] napi_status status = napi_get_value_double(env, jsValue, &v); 172 ASSERT(status == napi_ok); 173 return v; 174 } else if constexpr (std::is_same_v<T, int32_t>) { 175 int32_t v; 176 [[maybe_unused]] napi_status status = napi_get_value_int32(env, jsValue, &v); 177 ASSERT(status == napi_ok); 178 return v; 179 } else if constexpr (std::is_same_v<T, uint32_t>) { 180 uint32_t v; 181 [[maybe_unused]] napi_status status = napi_get_value_uint32(env, jsValue, &v); 182 ASSERT(status == napi_ok); 183 return v; 184 } else if constexpr (std::is_same_v<T, int64_t>) { 185 int64_t v; 186 [[maybe_unused]] napi_status status = napi_get_value_int64(env, jsValue, &v); 187 ASSERT(status == napi_ok); 188 return v; 189 } else if constexpr (std::is_same_v<T, bool>) { 190 bool v; 191 [[maybe_unused]] napi_status status = napi_get_value_bool(env, jsValue, &v); 192 ASSERT(status == napi_ok); 193 return v; 194 } else if constexpr (std::is_same_v<T, std::string>) { 195 return GetString(env, jsValue); 196 } else if constexpr (std::is_same_v<T, napi_value>) { 197 return jsValue; 198 } else if constexpr (std::is_same_v<T, void>) { 199 // do nothing 200 } else { 201 enum { INCORRECT_TEMPLATE_TYPE = false }; 202 static_assert(INCORRECT_TEMPLATE_TYPE, "Incorrect template type"); 203 } 204 } 205 MakeJsArg(napi_env env,double arg)206 static napi_value MakeJsArg(napi_env env, double arg) 207 { 208 napi_value v; 209 [[maybe_unused]] napi_status status = napi_create_double(env, arg, &v); 210 ASSERT(status == napi_ok); 211 return v; 212 } 213 MakeJsArg(napi_env env,int32_t arg)214 static napi_value MakeJsArg(napi_env env, int32_t arg) 215 { 216 napi_value v; 217 [[maybe_unused]] napi_status status = napi_create_int32(env, arg, &v); 218 ASSERT(status == napi_ok); 219 return v; 220 } 221 MakeJsArg(napi_env env,uint32_t arg)222 static napi_value MakeJsArg(napi_env env, uint32_t arg) 223 { 224 napi_value v; 225 [[maybe_unused]] napi_status status = napi_create_uint32(env, arg, &v); 226 ASSERT(status == napi_ok); 227 return v; 228 } 229 MakeJsArg(napi_env env,int64_t arg)230 static napi_value MakeJsArg(napi_env env, int64_t arg) 231 { 232 napi_value v; 233 [[maybe_unused]] napi_status status = napi_create_int64(env, arg, &v); 234 ASSERT(status == napi_ok); 235 return v; 236 } 237 MakeJsArg(napi_env env,std::string_view arg)238 static napi_value MakeJsArg(napi_env env, std::string_view arg) 239 { 240 napi_value v; 241 [[maybe_unused]] napi_status status = napi_create_string_utf8(env, arg.data(), arg.length(), &v); 242 ASSERT(status == napi_ok); 243 return v; 244 } 245 MakeJsArg(napi_env env,napi_value arg)246 static napi_value MakeJsArg([[maybe_unused]] napi_env env, napi_value arg) 247 { 248 return arg; 249 } 250 GetJsGtestObject(napi_env env)251 static napi_value GetJsGtestObject(napi_env env) 252 { 253 [[maybe_unused]] napi_status status; 254 255 // Get globalThis 256 napi_value jsGlobalObject; 257 status = napi_get_global(env, &jsGlobalObject); 258 ASSERT(status == napi_ok); 259 260 // Get globalThis.gtest 261 napi_value jsGtestObject; 262 status = napi_get_named_property(env, jsGlobalObject, "gtest", &jsGtestObject); 263 ASSERT(status == napi_ok); 264 265 return jsGtestObject; 266 } 267 268 template <typename R, typename... Args> DoCallEtsMethod(std::string_view fnName,napi_env env,Args &&...args)269 [[nodiscard]] static std::optional<R> DoCallEtsMethod(std::string_view fnName, napi_env env, Args &&...args) 270 { 271 [[maybe_unused]] napi_status status; 272 273 // Get globalThis.gtest 274 napi_value jsGtestObject = GetJsGtestObject(env); 275 276 // Set globalThis.gtest.functionName 277 napi_value jsFunctionName; 278 status = napi_create_string_utf8(env, fnName.data(), fnName.length(), &jsFunctionName); 279 ASSERT(status == napi_ok); 280 status = napi_set_named_property(env, jsGtestObject, "functionName", jsFunctionName); 281 ASSERT(status == napi_ok); 282 283 // Set globalThis.gtest.args 284 std::initializer_list<napi_value> napiArgs = {MakeJsArg(env, args)...}; 285 napi_value jsArgs; 286 status = napi_create_array_with_length(env, napiArgs.size(), &jsArgs); 287 ASSERT(status == napi_ok); 288 uint32_t i = 0; 289 for (auto arg : napiArgs) { 290 status = napi_set_element(env, jsArgs, i++, arg); 291 ASSERT(status == napi_ok); 292 } 293 status = napi_set_named_property(env, jsGtestObject, "args", jsArgs); 294 ASSERT(status == napi_ok); 295 296 // Call ETS method via JS 297 auto res = RunJsScript(R"( 298 gtest.ret = gtest.etsVm.call(gtest.functionName, ...gtest.args); 299 )"); 300 if (!res) { 301 return std::nullopt; 302 } 303 304 // Get globalThis.gtest.ret 305 napi_value jsRetValue {}; 306 status = napi_get_named_property(env, jsGtestObject, "ret", &jsRetValue); 307 ASSERT(status == napi_ok); 308 return GetRetValue<R>(env, jsRetValue); 309 } 310 311 friend class JsEnvAccessor; 312 static napi_env jsEnv_; 313 314 protected: 315 // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes) 316 std::string interopJsTestPath_; 317 }; 318 319 } // namespace panda::ets::interop::js::testing 320 321 #endif // !PANDA_PLUGINS_ETS_INTEROP_JS_GTEST_H 322