From f9e82e9c3828edef89ef9914ef9f6774626d836a Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 11 Oct 2025 21:44:14 -0700 Subject: [PATCH 1/6] JavaScriptCore wrapper previously passed nullptr to JSObjectCallAsFunction when the receiver was undefined, so the VM forcibly substituted the global object even in strict mode. The new implementation always routes through Function.prototype.call, preserving the exact thisArg. This only affected JSC: Chakra already pushes recv onto the argv array before invoking JsCallFunction, and V8 hands the raw recv value to Function::Call. Neither engine coerces in strict mode, so no additional fixes were required. --- .../Source/js_native_api_javascriptcore.cc | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.cc b/Core/Node-API/Source/js_native_api_javascriptcore.cc index ef8127f6..c0606e2a 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.cc +++ b/Core/Node-API/Source/js_native_api_javascriptcore.cc @@ -1563,14 +1563,28 @@ napi_status napi_call_function(napi_env env, CHECK_ARG(env, argv); } + JSObjectRef function_object = ToJSObject(env, func); + + std::vector call_args(argc + 1); + call_args[0] = ToJSValue(recv); + for (size_t i = 0; i < argc; ++i) { + call_args[i + 1] = ToJSValue(argv[i]); + } + JSValueRef exception{}; - JSValueRef return_value{JSObjectCallAsFunction( - env->context, - ToJSObject(env, func), - JSValueIsUndefined(env->context, ToJSValue(recv)) ? nullptr : ToJSObject(env, recv), - argc, - ToJSValues(argv), - &exception)}; + JSValueRef call_value{JSObjectGetProperty( + env->context, function_object, JSString("call"), &exception)}; + CHECK_JSC(env, exception); + + JSObjectRef call_object = JSValueToObject(env->context, call_value, &exception); + CHECK_JSC(env, exception); + + JSValueRef return_value{JSObjectCallAsFunction(env->context, + call_object, + function_object, + call_args.size(), + call_args.data(), + &exception)}; CHECK_JSC(env, exception); if (result != nullptr) { From c365db22d3f4255d06d8916677b89cc78a6725fe Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 11 Oct 2025 21:49:32 -0700 Subject: [PATCH 2/6] Add the node-lite test suite Vlad added into hermes-windows. The JSC bug fix in strict mode was actually found by the suite! The failing behavior was exercised by Tests/NodeApi/test/js-native-api/3_callbacks/test.js. New cmake targets emit a node-lite binary and a NodeApiTests binary, all currently enabled tests for currently supported NAPI v5 pass on Mac. Next step is to enable them for running in Android simulator. --- Tests/CMakeLists.txt | 1 + Tests/NodeApi/.clang-format | 111 ++ Tests/NodeApi/CMakeLists.txt | 175 +++ Tests/NodeApi/child_process.cpp | 192 +++ Tests/NodeApi/child_process.h | 27 + Tests/NodeApi/child_process_mac.cpp | 166 +++ Tests/NodeApi/compat.h | 51 + Tests/NodeApi/include/node_api.h | 67 + Tests/NodeApi/include/node_api_types.h | 24 + Tests/NodeApi/js_runtime_api.cpp | 112 ++ Tests/NodeApi/js_runtime_api.h | 219 +++ Tests/NodeApi/node_lite.cpp | 1268 ++++++++++++++++ Tests/NodeApi/node_lite.h | 365 +++++ Tests/NodeApi/node_lite_jsruntimehost.cpp | 90 ++ Tests/NodeApi/node_lite_mac.cpp | 39 + Tests/NodeApi/node_lite_windows.cpp | 25 + Tests/NodeApi/string_utils.cpp | 38 + Tests/NodeApi/string_utils.h | 21 + Tests/NodeApi/test/.clang-format | 111 ++ Tests/NodeApi/test/CMakeLists.txt | 91 ++ Tests/NodeApi/test/babel.config.js | 6 + Tests/NodeApi/test/basics/async_rejected.js | 19 + Tests/NodeApi/test/basics/async_resolved.js | 15 + Tests/NodeApi/test/basics/hello.js | 1 + Tests/NodeApi/test/basics/mustcall_failure.js | 3 + Tests/NodeApi/test/basics/mustcall_success.js | 4 + .../test/basics/mustnotcall_failure.js | 4 + .../test/basics/mustnotcall_success.js | 3 + Tests/NodeApi/test/basics/throw_string.js | 1 + Tests/NodeApi/test/common/assert.js | 400 +++++ Tests/NodeApi/test/common/gc.js | 34 + Tests/NodeApi/test/common/index.js | 57 + Tests/NodeApi/test/js-native-api/.gitignore | 7 + .../2_function_arguments.c | 39 + .../2_function_arguments/CMakeLists.txt | 4 + .../2_function_arguments/binding.gyp | 10 + .../2_function_arguments/test.js | 6 + .../js-native-api/3_callbacks/3_callbacks.c | 58 + .../js-native-api/3_callbacks/CMakeLists.txt | 4 + .../js-native-api/3_callbacks/binding.gyp | 10 + .../test/js-native-api/3_callbacks/test.js | 22 + .../4_object_factory/4_object_factory.c | 24 + .../4_object_factory/CMakeLists.txt | 4 + .../4_object_factory/binding.gyp | 10 + .../js-native-api/4_object_factory/test.js | 8 + .../5_function_factory/5_function_factory.c | 24 + .../5_function_factory/CMakeLists.txt | 4 + .../5_function_factory/binding.gyp | 10 + .../js-native-api/5_function_factory/test.js | 7 + .../6_object_wrap/CMakeLists.txt | 21 + .../js-native-api/6_object_wrap/binding.gyp | 28 + .../js-native-api/6_object_wrap/myobject.cc | 270 ++++ .../js-native-api/6_object_wrap/myobject.h | 28 + .../6_object_wrap/nested_wrap.cc | 99 ++ .../js-native-api/6_object_wrap/nested_wrap.h | 33 + .../6_object_wrap/nested_wrap.js | 20 + .../6_object_wrap/test-basic-finalizer.js | 24 + .../6_object_wrap/test-object-wrap-ref.js | 14 + .../test/js-native-api/6_object_wrap/test.js | 48 + .../7_factory_wrap/7_factory_wrap.cc | 32 + .../7_factory_wrap/CMakeLists.txt | 5 + .../js-native-api/7_factory_wrap/binding.gyp | 11 + .../js-native-api/7_factory_wrap/myobject.cc | 101 ++ .../js-native-api/7_factory_wrap/myobject.h | 27 + .../test/js-native-api/7_factory_wrap/test.js | 27 + .../8_passing_wrapped/8_passing_wrapped.cc | 61 + .../8_passing_wrapped/CMakeLists.txt | 5 + .../8_passing_wrapped/binding.gyp | 11 + .../8_passing_wrapped/myobject.cc | 91 ++ .../8_passing_wrapped/myobject.h | 28 + .../js-native-api/8_passing_wrapped/test.js | 21 + .../NodeApi/test/js-native-api/CMakeLists.txt | 7 + Tests/NodeApi/test/js-native-api/common-inl.h | 71 + Tests/NodeApi/test/js-native-api/common.h | 132 ++ .../NodeApi/test/js-native-api/entry_point.h | 12 + .../js-native-api/test_array/CMakeLists.txt | 4 + .../test/js-native-api/test_array/binding.gyp | 10 + .../test/js-native-api/test_array/test.js | 61 + .../js-native-api/test_array/test_array.c | 188 +++ .../js-native-api/test_bigint/CMakeLists.txt | 4 + .../js-native-api/test_bigint/binding.gyp | 10 + .../test/js-native-api/test_bigint/test.js | 52 + .../js-native-api/test_bigint/test_bigint.c | 159 ++ .../test_cannot_run_js/CMakeLists.txt | 13 + .../test_cannot_run_js/binding.gyp | 18 + .../js-native-api/test_cannot_run_js/test.js | 24 + .../test_cannot_run_js/test_cannot_run_js.c | 66 + .../test_constructor/CMakeLists.txt | 5 + .../test_constructor/binding.gyp | 11 + .../js-native-api/test_constructor/test.js | 62 + .../js-native-api/test_constructor/test2.js | 8 + .../test_constructor/test_constructor.c | 200 +++ .../test_constructor/test_null.c | 111 ++ .../test_constructor/test_null.h | 8 + .../test_constructor/test_null.js | 18 + .../test_conversions/CMakeLists.txt | 5 + .../test_conversions/binding.gyp | 11 + .../js-native-api/test_conversions/test.js | 218 +++ .../test_conversions/test_conversions.c | 158 ++ .../test_conversions/test_null.c | 102 ++ .../test_conversions/test_null.h | 8 + .../test_dataview/CMakeLists.txt | 4 + .../js-native-api/test_dataview/binding.gyp | 10 + .../test/js-native-api/test_dataview/test.js | 24 + .../test_dataview/test_dataview.c | 102 ++ .../js-native-api/test_date/CMakeLists.txt | 4 + .../test/js-native-api/test_date/binding.gyp | 10 + .../test/js-native-api/test_date/test.js | 21 + .../test/js-native-api/test_date/test_date.c | 64 + .../js-native-api/test_error/CMakeLists.txt | 6 + .../test/js-native-api/test_error/binding.gyp | 10 + .../test/js-native-api/test_error/test.js | 148 ++ .../js-native-api/test_error/test_error.c | 197 +++ .../test_exception/CMakeLists.txt | 4 + .../js-native-api/test_exception/binding.gyp | 10 + .../test/js-native-api/test_exception/test.js | 115 ++ .../test_exception/testFinalizerException.js | 31 + .../test_exception/test_exception.c | 116 ++ .../test_finalizer/CMakeLists.txt | 7 + .../js-native-api/test_finalizer/binding.gyp | 11 + .../test/js-native-api/test_finalizer/test.js | 45 + .../test_finalizer/test_fatal_finalize.js | 35 + .../test_finalizer/test_finalizer.c | 148 ++ .../test_function/CMakeLists.txt | 4 + .../js-native-api/test_function/binding.gyp | 10 + .../test/js-native-api/test_function/test.js | 52 + .../test_function/test_function.c | 204 +++ .../js-native-api/test_general/CMakeLists.txt | 4 + .../js-native-api/test_general/binding.gyp | 10 + .../test/js-native-api/test_general/test.js | 97 ++ .../test_general/testEnvCleanup.js | 57 + .../test_general/testFinalizer.js | 38 + .../js-native-api/test_general/testGlobals.js | 8 + .../test_general/testInstanceOf.js | 46 + .../js-native-api/test_general/testNapiRun.js | 14 + .../test_general/testNapiStatus.js | 8 + .../test_general/testV8Instanceof.js | 121 ++ .../test_general/testV8Instanceof2.js | 341 +++++ .../js-native-api/test_general/test_general.c | 315 ++++ .../test_handle_scope/CMakeLists.txt | 4 + .../test_handle_scope/binding.gyp | 10 + .../js-native-api/test_handle_scope/test.js | 19 + .../test_handle_scope/test_handle_scope.c | 86 ++ .../test_instance_data/CMakeLists.txt | 4 + .../test_instance_data/binding.gyp | 10 + .../js-native-api/test_instance_data/test.js | 41 + .../test_instance_data/test_instance_data.c | 96 ++ .../test_new_target/CMakeLists.txt | 4 + .../js-native-api/test_new_target/binding.gyp | 11 + .../js-native-api/test_new_target/test.js | 21 + .../test_new_target/test_new_target.c | 92 ++ .../js-native-api/test_number/CMakeLists.txt | 5 + .../js-native-api/test_number/binding.gyp | 11 + .../test/js-native-api/test_number/test.js | 134 ++ .../js-native-api/test_number/test_null.c | 77 + .../js-native-api/test_number/test_null.h | 8 + .../js-native-api/test_number/test_null.js | 18 + .../js-native-api/test_number/test_number.c | 110 ++ .../js-native-api/test_object/CMakeLists.txt | 10 + .../js-native-api/test_object/binding.gyp | 17 + .../test/js-native-api/test_object/test.js | 393 +++++ .../test_object/test_exceptions.c | 82 + .../test_object/test_exceptions.js | 18 + .../js-native-api/test_object/test_null.c | 400 +++++ .../js-native-api/test_object/test_null.h | 8 + .../js-native-api/test_object/test_null.js | 53 + .../js-native-api/test_object/test_object.c | 755 ++++++++++ .../js-native-api/test_promise/CMakeLists.txt | 4 + .../js-native-api/test_promise/binding.gyp | 10 + .../test/js-native-api/test_promise/test.js | 61 + .../js-native-api/test_promise/test_promise.c | 64 + .../test_properties/CMakeLists.txt | 6 + .../js-native-api/test_properties/binding.gyp | 10 + .../js-native-api/test_properties/test.js | 69 + .../test_properties/test_properties.c | 113 ++ .../test_reference/CMakeLists.txt | 11 + .../js-native-api/test_reference/binding.gyp | 16 + .../test/js-native-api/test_reference/test.js | 158 ++ .../test_reference/test_finalizer.c | 79 + .../test_reference/test_finalizer.js | 24 + .../test_reference/test_reference.c | 252 ++++ .../test_reference_double_free/CMakeLists.txt | 4 + .../test_reference_double_free/binding.gyp | 10 + .../test_reference_double_free/test.js | 11 + .../test_reference_double_free.c | 90 ++ .../test_reference_double_free/test_wrap.js | 10 + .../js-native-api/test_string/CMakeLists.txt | 7 + .../js-native-api/test_string/binding.gyp | 14 + .../test/js-native-api/test_string/test.js | 91 ++ .../js-native-api/test_string/test_null.c | 71 + .../js-native-api/test_string/test_null.h | 8 + .../js-native-api/test_string/test_null.js | 17 + .../js-native-api/test_string/test_string.c | 498 +++++++ .../js-native-api/test_symbol/CMakeLists.txt | 4 + .../js-native-api/test_symbol/binding.gyp | 10 + .../test/js-native-api/test_symbol/test1.js | 19 + .../test/js-native-api/test_symbol/test2.js | 17 + .../test/js-native-api/test_symbol/test3.js | 19 + .../js-native-api/test_symbol/test_symbol.c | 38 + .../test_typedarray/CMakeLists.txt | 4 + .../js-native-api/test_typedarray/binding.gyp | 10 + .../js-native-api/test_typedarray/test.js | 109 ++ .../test_typedarray/test_typedarray.c | 249 ++++ Tests/NodeApi/test/package.json | 14 + Tests/NodeApi/test/yarn.lock | 1326 +++++++++++++++++ Tests/NodeApi/test_basics.cpp | 80 + Tests/NodeApi/test_main.cpp | 201 +++ Tests/NodeApi/test_main.h | 23 + 208 files changed, 15531 insertions(+) create mode 100644 Tests/NodeApi/.clang-format create mode 100644 Tests/NodeApi/CMakeLists.txt create mode 100644 Tests/NodeApi/child_process.cpp create mode 100644 Tests/NodeApi/child_process.h create mode 100644 Tests/NodeApi/child_process_mac.cpp create mode 100644 Tests/NodeApi/compat.h create mode 100644 Tests/NodeApi/include/node_api.h create mode 100644 Tests/NodeApi/include/node_api_types.h create mode 100644 Tests/NodeApi/js_runtime_api.cpp create mode 100644 Tests/NodeApi/js_runtime_api.h create mode 100644 Tests/NodeApi/node_lite.cpp create mode 100644 Tests/NodeApi/node_lite.h create mode 100644 Tests/NodeApi/node_lite_jsruntimehost.cpp create mode 100644 Tests/NodeApi/node_lite_mac.cpp create mode 100644 Tests/NodeApi/node_lite_windows.cpp create mode 100644 Tests/NodeApi/string_utils.cpp create mode 100644 Tests/NodeApi/string_utils.h create mode 100644 Tests/NodeApi/test/.clang-format create mode 100644 Tests/NodeApi/test/CMakeLists.txt create mode 100644 Tests/NodeApi/test/babel.config.js create mode 100644 Tests/NodeApi/test/basics/async_rejected.js create mode 100644 Tests/NodeApi/test/basics/async_resolved.js create mode 100644 Tests/NodeApi/test/basics/hello.js create mode 100644 Tests/NodeApi/test/basics/mustcall_failure.js create mode 100644 Tests/NodeApi/test/basics/mustcall_success.js create mode 100644 Tests/NodeApi/test/basics/mustnotcall_failure.js create mode 100644 Tests/NodeApi/test/basics/mustnotcall_success.js create mode 100644 Tests/NodeApi/test/basics/throw_string.js create mode 100644 Tests/NodeApi/test/common/assert.js create mode 100644 Tests/NodeApi/test/common/gc.js create mode 100644 Tests/NodeApi/test/common/index.js create mode 100644 Tests/NodeApi/test/js-native-api/.gitignore create mode 100644 Tests/NodeApi/test/js-native-api/2_function_arguments/2_function_arguments.c create mode 100644 Tests/NodeApi/test/js-native-api/2_function_arguments/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/2_function_arguments/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/2_function_arguments/test.js create mode 100644 Tests/NodeApi/test/js-native-api/3_callbacks/3_callbacks.c create mode 100644 Tests/NodeApi/test/js-native-api/3_callbacks/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/3_callbacks/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/3_callbacks/test.js create mode 100644 Tests/NodeApi/test/js-native-api/4_object_factory/4_object_factory.c create mode 100644 Tests/NodeApi/test/js-native-api/4_object_factory/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/4_object_factory/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/4_object_factory/test.js create mode 100644 Tests/NodeApi/test/js-native-api/5_function_factory/5_function_factory.c create mode 100644 Tests/NodeApi/test/js-native-api/5_function_factory/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/5_function_factory/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/5_function_factory/test.js create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.cc create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.h create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.cc create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.h create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.js create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/test-basic-finalizer.js create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/test-object-wrap-ref.js create mode 100644 Tests/NodeApi/test/js-native-api/6_object_wrap/test.js create mode 100644 Tests/NodeApi/test/js-native-api/7_factory_wrap/7_factory_wrap.cc create mode 100644 Tests/NodeApi/test/js-native-api/7_factory_wrap/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/7_factory_wrap/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.cc create mode 100644 Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.h create mode 100644 Tests/NodeApi/test/js-native-api/7_factory_wrap/test.js create mode 100644 Tests/NodeApi/test/js-native-api/8_passing_wrapped/8_passing_wrapped.cc create mode 100644 Tests/NodeApi/test/js-native-api/8_passing_wrapped/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/8_passing_wrapped/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.cc create mode 100644 Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.h create mode 100644 Tests/NodeApi/test/js-native-api/8_passing_wrapped/test.js create mode 100644 Tests/NodeApi/test/js-native-api/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/common-inl.h create mode 100644 Tests/NodeApi/test/js-native-api/common.h create mode 100644 Tests/NodeApi/test/js-native-api/entry_point.h create mode 100644 Tests/NodeApi/test/js-native-api/test_array/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_array/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_array/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_array/test_array.c create mode 100644 Tests/NodeApi/test/js-native-api/test_bigint/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_bigint/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_bigint/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_bigint/test_bigint.c create mode 100644 Tests/NodeApi/test/js-native-api/test_cannot_run_js/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_cannot_run_js/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_cannot_run_js/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_cannot_run_js/test_cannot_run_js.c create mode 100644 Tests/NodeApi/test/js-native-api/test_constructor/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_constructor/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_constructor/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_constructor/test2.js create mode 100644 Tests/NodeApi/test/js-native-api/test_constructor/test_constructor.c create mode 100644 Tests/NodeApi/test/js-native-api/test_constructor/test_null.c create mode 100644 Tests/NodeApi/test/js-native-api/test_constructor/test_null.h create mode 100644 Tests/NodeApi/test/js-native-api/test_constructor/test_null.js create mode 100644 Tests/NodeApi/test/js-native-api/test_conversions/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_conversions/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_conversions/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_conversions/test_conversions.c create mode 100644 Tests/NodeApi/test/js-native-api/test_conversions/test_null.c create mode 100644 Tests/NodeApi/test/js-native-api/test_conversions/test_null.h create mode 100644 Tests/NodeApi/test/js-native-api/test_dataview/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_dataview/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_dataview/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_dataview/test_dataview.c create mode 100644 Tests/NodeApi/test/js-native-api/test_date/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_date/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_date/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_date/test_date.c create mode 100644 Tests/NodeApi/test/js-native-api/test_error/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_error/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_error/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_error/test_error.c create mode 100644 Tests/NodeApi/test/js-native-api/test_exception/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_exception/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_exception/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_exception/testFinalizerException.js create mode 100644 Tests/NodeApi/test/js-native-api/test_exception/test_exception.c create mode 100644 Tests/NodeApi/test/js-native-api/test_finalizer/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_finalizer/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_finalizer/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_finalizer/test_fatal_finalize.js create mode 100644 Tests/NodeApi/test/js-native-api/test_finalizer/test_finalizer.c create mode 100644 Tests/NodeApi/test/js-native-api/test_function/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_function/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_function/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_function/test_function.c create mode 100644 Tests/NodeApi/test/js-native-api/test_general/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_general/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_general/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_general/testEnvCleanup.js create mode 100644 Tests/NodeApi/test/js-native-api/test_general/testFinalizer.js create mode 100644 Tests/NodeApi/test/js-native-api/test_general/testGlobals.js create mode 100644 Tests/NodeApi/test/js-native-api/test_general/testInstanceOf.js create mode 100644 Tests/NodeApi/test/js-native-api/test_general/testNapiRun.js create mode 100644 Tests/NodeApi/test/js-native-api/test_general/testNapiStatus.js create mode 100644 Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof.js create mode 100644 Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof2.js create mode 100644 Tests/NodeApi/test/js-native-api/test_general/test_general.c create mode 100644 Tests/NodeApi/test/js-native-api/test_handle_scope/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_handle_scope/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_handle_scope/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_handle_scope/test_handle_scope.c create mode 100644 Tests/NodeApi/test/js-native-api/test_instance_data/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_instance_data/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_instance_data/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_instance_data/test_instance_data.c create mode 100644 Tests/NodeApi/test/js-native-api/test_new_target/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_new_target/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_new_target/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_new_target/test_new_target.c create mode 100644 Tests/NodeApi/test/js-native-api/test_number/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_number/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_number/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_number/test_null.c create mode 100644 Tests/NodeApi/test/js-native-api/test_number/test_null.h create mode 100644 Tests/NodeApi/test/js-native-api/test_number/test_null.js create mode 100644 Tests/NodeApi/test/js-native-api/test_number/test_number.c create mode 100644 Tests/NodeApi/test/js-native-api/test_object/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_object/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_object/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_object/test_exceptions.c create mode 100644 Tests/NodeApi/test/js-native-api/test_object/test_exceptions.js create mode 100644 Tests/NodeApi/test/js-native-api/test_object/test_null.c create mode 100644 Tests/NodeApi/test/js-native-api/test_object/test_null.h create mode 100644 Tests/NodeApi/test/js-native-api/test_object/test_null.js create mode 100644 Tests/NodeApi/test/js-native-api/test_object/test_object.c create mode 100644 Tests/NodeApi/test/js-native-api/test_promise/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_promise/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_promise/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_promise/test_promise.c create mode 100644 Tests/NodeApi/test/js-native-api/test_properties/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_properties/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_properties/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_properties/test_properties.c create mode 100644 Tests/NodeApi/test/js-native-api/test_reference/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_reference/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_reference/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.c create mode 100644 Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.js create mode 100644 Tests/NodeApi/test/js-native-api/test_reference/test_reference.c create mode 100644 Tests/NodeApi/test/js-native-api/test_reference_double_free/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_reference_double_free/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_reference_double_free/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_reference_double_free/test_reference_double_free.c create mode 100644 Tests/NodeApi/test/js-native-api/test_reference_double_free/test_wrap.js create mode 100644 Tests/NodeApi/test/js-native-api/test_string/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_string/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_string/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_string/test_null.c create mode 100644 Tests/NodeApi/test/js-native-api/test_string/test_null.h create mode 100644 Tests/NodeApi/test/js-native-api/test_string/test_null.js create mode 100644 Tests/NodeApi/test/js-native-api/test_string/test_string.c create mode 100644 Tests/NodeApi/test/js-native-api/test_symbol/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_symbol/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_symbol/test1.js create mode 100644 Tests/NodeApi/test/js-native-api/test_symbol/test2.js create mode 100644 Tests/NodeApi/test/js-native-api/test_symbol/test3.js create mode 100644 Tests/NodeApi/test/js-native-api/test_symbol/test_symbol.c create mode 100644 Tests/NodeApi/test/js-native-api/test_typedarray/CMakeLists.txt create mode 100644 Tests/NodeApi/test/js-native-api/test_typedarray/binding.gyp create mode 100644 Tests/NodeApi/test/js-native-api/test_typedarray/test.js create mode 100644 Tests/NodeApi/test/js-native-api/test_typedarray/test_typedarray.c create mode 100644 Tests/NodeApi/test/package.json create mode 100644 Tests/NodeApi/test/yarn.lock create mode 100644 Tests/NodeApi/test_basics.cpp create mode 100644 Tests/NodeApi/test_main.cpp create mode 100644 Tests/NodeApi/test_main.h diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 2cb5d26c..a7efe8f4 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(UnitTests) +add_subdirectory(NodeApi) npm(install --silent) diff --git a/Tests/NodeApi/.clang-format b/Tests/NodeApi/.clang-format new file mode 100644 index 00000000..b3fd9613 --- /dev/null +++ b/Tests/NodeApi/.clang-format @@ -0,0 +1,111 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 8 +UseTab: Never diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt new file mode 100644 index 00000000..48c5b5a8 --- /dev/null +++ b/Tests/NodeApi/CMakeLists.txt @@ -0,0 +1,175 @@ +set(NODE_API_TEST_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) + +option(JSR_NODE_API_BUILD_NATIVE_TESTS "Build Node-API native addon test modules" OFF) + +if(JSR_NODE_API_BUILD_NATIVE_TESTS) + set(JSR_NODE_API_NATIVE_TEST_DIRS + 2_function_arguments + 3_callbacks + 4_object_factory + 5_function_factory + ) +endif() + +function(node_api_copy_test_sources TARGET_NAME) + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${NODE_API_TEST_ROOT}/test + $/test + COMMENT "Copying Node-API test assets for ${TARGET_NAME}" + ) +endfunction() + +if(APPLE) + set(NODE_LITE_PLATFORM_SRC node_lite_mac.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process_mac.cpp) +elseif(WIN32) + set(NODE_LITE_PLATFORM_SRC node_lite_windows.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process.cpp) +else() + set(NODE_LITE_PLATFORM_SRC node_lite_mac.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process_mac.cpp) + message(WARNING "Node-API node_lite platform not yet customized for ${CMAKE_SYSTEM_NAME}; using POSIX defaults.") +endif() + +add_executable(node_lite + ${NODE_LITE_CHILD_PROCESS_SRC} + child_process.h + compat.h + js_runtime_api.cpp + js_runtime_api.h + node_lite.cpp + node_lite.h + node_lite_jsruntimehost.cpp + ${NODE_LITE_PLATFORM_SRC} + string_utils.cpp + string_utils.h +) + +target_include_directories(node_lite + PRIVATE + ${NODE_API_TEST_ROOT} + ${NODE_API_TEST_ROOT}/include + ${CMAKE_SOURCE_DIR}/Core/Node-API/Include/Shared + ${CMAKE_SOURCE_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE} + ${CMAKE_SOURCE_DIR}/Core/Node-API/Source +) + +target_compile_definitions(node_lite + PRIVATE + NODE_API_EXPERIMENTAL_NO_WARNING +) + +target_link_libraries(node_lite + PRIVATE + napi +) + +node_api_copy_test_sources(node_lite) + +add_executable(NodeApiTests + ${NODE_LITE_CHILD_PROCESS_SRC} + child_process.h + string_utils.cpp + string_utils.h + test_basics.cpp + test_main.cpp + test_main.h +) + +target_include_directories(NodeApiTests + PRIVATE + ${NODE_API_TEST_ROOT} + ${NODE_API_TEST_ROOT}/include +) + +target_link_libraries(NodeApiTests + PRIVATE + gtest_main +) + +node_api_copy_test_sources(NodeApiTests) + +add_dependencies(NodeApiTests node_lite) + +add_custom_target(NodeApiModules) + +if(JSR_NODE_API_BUILD_NATIVE_TESTS) + list(JOIN JSR_NODE_API_NATIVE_TEST_DIRS "," NODE_API_NATIVE_TESTS_STRING) + target_compile_definitions(node_lite + PRIVATE + NODE_API_TESTS_HAVE_NATIVE_MODULES=1 + NODE_API_AVAILABLE_NATIVE_TESTS=\"${NODE_API_NATIVE_TESTS_STRING}\" + ) + target_compile_definitions(NodeApiTests + PRIVATE + NODE_API_TESTS_HAVE_NATIVE_MODULES=1 + NODE_API_AVAILABLE_NATIVE_TESTS=\"${NODE_API_NATIVE_TESTS_STRING}\" + ) +endif() + +function(add_node_api_module MODULE_TARGET) + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "" "SOURCES;DEFINES") + + get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) + + if(NOT "${MODULE_TARGET}" STREQUAL "${FOLDER_NAME}") + set(MODULE_TARGET "${FOLDER_NAME}_${MODULE_TARGET}") + endif() + + add_library(${MODULE_TARGET} MODULE) + target_sources(${MODULE_TARGET} PRIVATE ${ARG_SOURCES}) + target_include_directories(${MODULE_TARGET} + PRIVATE + ${NODE_API_TEST_ROOT}/include + ${CMAKE_SOURCE_DIR}/Core/Node-API/Include/Shared + ${CMAKE_SOURCE_DIR}/Core/Node-API/Include/Shared/napi + ${CMAKE_SOURCE_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE} + ${CMAKE_SOURCE_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE}/napi + ) + + target_compile_definitions(${MODULE_TARGET} + PRIVATE + NODE_API_EXPERIMENTAL_NO_WARNING + NODE_GYP_MODULE_NAME=\"${FOLDER_NAME}\" + ${ARG_DEFINES} + ) + + if(APPLE) + target_link_options(${MODULE_TARGET} + PRIVATE + "-undefined" "dynamic_lookup" + ) + endif() + + set(MODULE_OUTPUT_DIR + ${CMAKE_CURRENT_BINARY_DIR}/build/$,Debug,Release>) + set_target_properties(${MODULE_TARGET} + PROPERTIES + PREFIX "" + SUFFIX ".node" + ARCHIVE_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR} + LIBRARY_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR} + RUNTIME_OUTPUT_DIRECTORY ${MODULE_OUTPUT_DIR} + ) + + add_dependencies(NodeApiModules ${MODULE_TARGET}) + + add_custom_command(TARGET ${MODULE_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + $/test/js-native-api/${FOLDER_NAME}/build + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_BINARY_DIR}/build + $/test/js-native-api/${FOLDER_NAME}/build + COMMAND ${CMAKE_COMMAND} -E make_directory + $/test/js-native-api/${FOLDER_NAME}/build + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_BINARY_DIR}/build + $/test/js-native-api/${FOLDER_NAME}/build + COMMENT "Copying Node-API module ${MODULE_TARGET} outputs" + ) +endfunction() + +add_dependencies(NodeApiTests NodeApiModules) + +add_subdirectory(test) diff --git a/Tests/NodeApi/child_process.cpp b/Tests/NodeApi/child_process.cpp new file mode 100644 index 00000000..2fd22cd8 --- /dev/null +++ b/Tests/NodeApi/child_process.cpp @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// Windows-specific implementation of the `spawnSync` function for creating +// child processes and capturing their output. This code is designed to work +// with the Windows API and is not portable to other platforms. It uses pipes +// to redirect the standard output and error streams of the child process back +// to the parent process, allowing the parent to read the output and error +// messages generated by the child process. +// +// The `spawnSync` function takes a command and a list of arguments, creates a +// child process to execute the command, and returns a `ProcessResult` structure +// containing the exit status and the captured output and error messages. +// + +#include "child_process.h" + +#include +#include +#include +#include +#include "string_utils.h" + +#ifndef VerifyElseExit +#define VerifyElseExit(condition) \ + do { \ + if (!(condition)) { \ + ExitOnError(#condition); \ + } \ + } while (false) +#endif + +namespace node_api_tests { + +namespace { + +std::string ReadFromPipe(HANDLE pipeHandle); +void ExitOnError(const char* message); + +struct AutoHandle { + HANDLE handle{NULL}; + + AutoHandle() = default; + AutoHandle(HANDLE handle) : handle(handle) {} + ~AutoHandle() { ::CloseHandle(handle); } + + AutoHandle(const AutoHandle&) = delete; + AutoHandle& operator=(const AutoHandle&) = delete; + + void Close() { + ::CloseHandle(handle); + handle = NULL; + } +}; +} // namespace + +// Create a child process that uses the previously created pipes for STDIN and +// STDOUT. +ProcessResult SpawnSync(std::string_view command, + std::vector args) { + ProcessResult result{}; + + // Set the bInheritHandle flag so pipe handles are inherited. + + SECURITY_ATTRIBUTES handles_are_inheritable = { + sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + + AutoHandle out_read_handle, out_write_handle; + VerifyElseExit(CreatePipe(&out_read_handle.handle, + &out_write_handle.handle, + &handles_are_inheritable, + 0)); + // Ensure the read handle to the pipe for STDOUT is not inherited. + VerifyElseExit( + SetHandleInformation(out_read_handle.handle, HANDLE_FLAG_INHERIT, 0)); + + AutoHandle err_read_handle, err_write_handle; + VerifyElseExit(CreatePipe(&err_read_handle.handle, + &err_write_handle.handle, + &handles_are_inheritable, + 0)); + // Ensure the read handle to the pipe for STDERR is not inherited. + VerifyElseExit( + SetHandleInformation(err_read_handle.handle, HANDLE_FLAG_INHERIT, 0)); + + // Set up members of the STARTUPINFO structure. + // This structure specifies the STDIN and STDOUT handles for redirection. + STARTUPINFOA startup_info{}; + startup_info.cb = sizeof(STARTUPINFOA); + startup_info.dwFlags |= STARTF_USESTDHANDLES; + startup_info.hStdInput = ::GetStdHandle(STD_INPUT_HANDLE); + startup_info.hStdOutput = out_write_handle.handle; + startup_info.hStdError = err_write_handle.handle; + + // Create the child process. + + std::string commandLine = std::string(command); + for (std::string& arg : args) { + commandLine += " " + arg; + } + PROCESS_INFORMATION process_info{}; + VerifyElseExit( + CreateProcessA(nullptr, + const_cast(commandLine.c_str()), // command line + nullptr, // process security attributes + nullptr, // primary thread security attributes + TRUE, // handles are inherited + CREATE_DEFAULT_ERROR_MODE, // creation flags + nullptr, // use parent's environment + nullptr, // use parent's current directory + &startup_info, // STARTUPINFO pointer + &process_info)); // receives PROCESS_INFORMATION + + VerifyElseExit(WAIT_OBJECT_0 == + ::WaitForSingleObject(process_info.hProcess, INFINITE)); + + DWORD exit_code; + VerifyElseExit(::GetExitCodeProcess(process_info.hProcess, &exit_code)); + + // Close handles to the child process and its primary thread. + // Some applications might keep these handles to monitor the status + // of the child process, for example. + ::CloseHandle(process_info.hProcess); + ::CloseHandle(process_info.hThread); + + // Close handles to the stdin and stdout pipes no longer needed by the child + // process. If they are not explicitly closed, there is no way to recognize + // that the child process has ended. + + out_write_handle.Close(); + err_write_handle.Close(); + + result.status = exit_code; + result.std_output = + ReplaceAll(ReadFromPipe(out_read_handle.handle), "\r\n", "\n"); + result.std_error = + ReplaceAll(ReadFromPipe(err_read_handle.handle), "\r\n", "\n"); + + return result; +} + +namespace { +std::string ReadFromPipe(HANDLE pipeHandle) { + std::string result; + constexpr size_t bufferSize = 4096; + char buffer[bufferSize]; + + for (;;) { + DWORD bytesRead; + BOOL isSuccess = + ::ReadFile(pipeHandle, buffer, bufferSize, &bytesRead, nullptr); + if (!isSuccess || bytesRead == 0) break; + + result.append(buffer, bytesRead); + } + + return result; +} + +// Format a readable error message, display a message box, +// and exit from the application. +void ExitOnError(const char* message) { + LPVOID lpMsgBuf; + LPVOID lpDisplayBuf; + DWORD dw = GetLastError(); + + ::FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + dw, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&lpMsgBuf, + 0, + nullptr); + + lpDisplayBuf = (LPVOID)LocalAlloc( + LMEM_ZEROINIT, (lstrlenA((LPCSTR)lpMsgBuf) + lstrlenA(message) + 40)); + ::StringCchPrintfA((LPSTR)lpDisplayBuf, + LocalSize(lpDisplayBuf), + "%s failed with error %d: %s", + message, + dw, + lpMsgBuf); + fprintf(stderr, "%s\n", (const char*)lpDisplayBuf); + + ::LocalFree(lpMsgBuf); + ::LocalFree(lpDisplayBuf); + ::ExitProcess(1); +} + +} // namespace +} // namespace node_api_tests \ No newline at end of file diff --git a/Tests/NodeApi/child_process.h b/Tests/NodeApi/child_process.h new file mode 100644 index 00000000..aed58908 --- /dev/null +++ b/Tests/NodeApi/child_process.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef NODE_API_TEST_CHILD_PROCESS_H +#define NODE_API_TEST_CHILD_PROCESS_H + +#include +#include +#include + +namespace node_api_tests { + +// Struct to hold the result of a child process execution. +struct ProcessResult { + uint32_t status; // Exit status of the child process. + std::string std_output; // Standard output from the child process. + std::string std_error; // Standard error from the child process. +}; + +// Creates a child process to run the given command with the specified +// arguments. +ProcessResult SpawnSync(std::string_view command, + std::vector args); + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_CHILD_PROCESS_H \ No newline at end of file diff --git a/Tests/NodeApi/child_process_mac.cpp b/Tests/NodeApi/child_process_mac.cpp new file mode 100644 index 00000000..8e5f0d18 --- /dev/null +++ b/Tests/NodeApi/child_process_mac.cpp @@ -0,0 +1,166 @@ +#include "child_process.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Verify the condition. +// - If true, resume execution. +// - If false, print a message to stderr and exit the app with exit code 1. +#ifndef VerifyElseExit +#define VerifyElseExit(condition) \ + do { \ + if (!(condition)) { \ + ExitOnError(#condition, nullptr); \ + } \ + } while (false) +#endif + +// Verify the condition. +// - If true, resume execution. +// - If false, destroy the passed `posix_spawn_file_actions_t* actions`, then +// print a message to stderr and exit the app with exit code 1. +#ifndef VerifyElseExitWithCleanup +#define VerifyElseExitWithCleanup(condition, actions_ptr) \ + do { \ + if (!(condition)) { \ + ExitOnError(#condition, actions_ptr); \ + } \ + } while (false) +#endif + +extern char** environ; + +namespace node_api_tests { + +namespace { + +std::string ReadFromFd(int fd); +void ExitOnError(const char* message, posix_spawn_file_actions_t* actions); + +} // namespace + +ProcessResult SpawnSync(std::string_view command, + std::vector args) { + ProcessResult result{}; + + // These int arrays each comprise two file descriptors: { readEnd, writeEnd }. + int stdout_pipe[2], stderr_pipe[2]; + VerifyElseExit(pipe(stdout_pipe) == 0); + VerifyElseExit(pipe(stderr_pipe) == 0); + + posix_spawn_file_actions_t actions; + VerifyElseExit(posix_spawn_file_actions_init(&actions) == 0); + + VerifyElseExitWithCleanup(posix_spawn_file_actions_adddup2( + &actions, stdout_pipe[1], STDOUT_FILENO) == 0, + &actions); + VerifyElseExitWithCleanup(posix_spawn_file_actions_adddup2( + &actions, stderr_pipe[1], STDERR_FILENO) == 0, + &actions); + + VerifyElseExitWithCleanup( + posix_spawn_file_actions_addclose(&actions, stdout_pipe[0]) == 0, + &actions); + VerifyElseExitWithCleanup( + posix_spawn_file_actions_addclose(&actions, stderr_pipe[0]) == 0, + &actions); + + std::vector argv; + argv.push_back(strdup(std::string(command).c_str())); + for (const std::string& arg : args) { + argv.push_back(strdup(arg.c_str())); + } + argv.push_back(nullptr); + + pid_t pid; + VerifyElseExitWithCleanup( + posix_spawnp(&pid, argv[0], &actions, nullptr, argv.data(), environ) == 0, + &actions); + + posix_spawn_file_actions_destroy(&actions); + + // Close the write ends of the pipes. + close(stdout_pipe[1]); + close(stderr_pipe[1]); + + int wait_status; + pid_t waited_pid; + do { + waited_pid = waitpid(pid, &wait_status, 0); + } while (waited_pid == -1 && errno == EINTR); + + VerifyElseExit(waited_pid == pid); + + if (WIFEXITED(wait_status)) { + result.status = WEXITSTATUS(wait_status); + } else if (WIFSIGNALED(wait_status)) { + result.status = 128 + WTERMSIG(wait_status); + } else { + result.status = 1; + } + result.std_output = ReadFromFd(stdout_pipe[0]); + result.std_error = ReadFromFd(stderr_pipe[0]); + + // Close the read ends of the pipes. + close(stdout_pipe[0]); + close(stderr_pipe[0]); + + for (char* arg : argv) { + free(arg); + } + + return result; +} + +namespace { + +std::string ReadFromFd(int fd) { + std::string result; + constexpr size_t bufferSize = 4096; + char buffer[bufferSize]; + ssize_t bytesRead; + while (true) { + bytesRead = read(fd, buffer, bufferSize); + if (bytesRead > 0) { + result.append(buffer, bytesRead); + continue; + } + + if (bytesRead == 0) { + break; + } + + if (errno == EINTR) { + continue; + } + + ExitOnError("read", nullptr); + } + return result; +} + +// Format a readable error message, print it to console, and exit from the +// application. +void ExitOnError(const char* message, posix_spawn_file_actions_t* actions) { + int err = errno; + const char* err_msg = strerror(err); + + fprintf(stderr, "%s failed with error %d: %s\n", message, err, err_msg); + + if (actions != nullptr) { + posix_spawn_file_actions_destroy(actions); + } + + exit(1); +} + +} // namespace +} // namespace node_api_tests diff --git a/Tests/NodeApi/compat.h b/Tests/NodeApi/compat.h new file mode 100644 index 00000000..7974b34c --- /dev/null +++ b/Tests/NodeApi/compat.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#ifndef SRC_PUBLIC_COMPAT_H_ +#define SRC_PUBLIC_COMPAT_H_ + +// This file contains some useful datatypes recently introduced in C++17 and +// C++20. They must be removed after we switch the toolset to the newer C++ +// language version. + +#include +#ifdef __cpp_lib_span +#include +#endif + +namespace node_api_tests { + +#ifdef __cpp_lib_span +using std::span; +#else +/** + * @brief A span of values that can be used to pass arguments to function. + * + * For C++20 we should consider to replace it with std::span. + */ +template +struct span { + constexpr span(std::initializer_list il) noexcept + : data_{const_cast(il.begin())}, size_{il.size()} {} + constexpr span(T* data, size_t size) noexcept : data_{data}, size_{size} {} + + [[nodiscard]] constexpr T* data() const noexcept { return data_; } + + [[nodiscard]] constexpr size_t size() const noexcept { return size_; } + + [[nodiscard]] constexpr T* begin() const noexcept { return data_; } + + [[nodiscard]] constexpr T* end() const noexcept { return data_ + size_; } + + const T& operator[](size_t index) const noexcept { return *(data_ + index); } + + private: + T* data_; + size_t size_; +}; +#endif // __cpp_lib_span + +} // namespace node_api_tests + +#endif // SRC_PUBLIC_COMPAT_H_ diff --git a/Tests/NodeApi/include/node_api.h b/Tests/NodeApi/include/node_api.h new file mode 100644 index 00000000..0ba4bc6e --- /dev/null +++ b/Tests/NodeApi/include/node_api.h @@ -0,0 +1,67 @@ +#ifndef NODE_API_H_ +#define NODE_API_H_ + +#include +#include "node_api_types.h" + +#ifdef __cplusplus +#define NODE_API_EXTERN_C_START extern "C" { +#define NODE_API_EXTERN_C_END } +#else +#define NODE_API_EXTERN_C_START +#define NODE_API_EXTERN_C_END +#endif + +#ifdef _WIN32 +#define NAPI_MODULE_EXPORT __declspec(dllexport) +#else +#define NAPI_MODULE_EXPORT __attribute__((visibility("default"))) +#endif + +#ifndef NAPI_MODULE_VERSION +#define NAPI_MODULE_VERSION 1 +#endif + +typedef napi_value(NAPI_CDECL* napi_addon_register_func)(napi_env env, + napi_value exports); + +typedef struct napi_module_s { + int nm_version; + unsigned int nm_flags; + const char* nm_filename; + napi_addon_register_func nm_register_func; + const char* nm_modname; + void* nm_priv; + void* reserved[4]; +} napi_module; + +#define NODE_API_MODULE_GET_API_VERSION_FUNCTION node_api_module_get_api_version_v1 +#define NODE_API_MODULE_REGISTER_FUNCTION napi_register_module_v1 + +#define NAPI_MODULE_INIT() \ + NODE_API_EXTERN_C_START \ + NAPI_MODULE_EXPORT int32_t NODE_API_MODULE_GET_API_VERSION_FUNCTION(void) {\ + return NAPI_VERSION; \ + } \ + NAPI_MODULE_EXPORT napi_value NODE_API_MODULE_REGISTER_FUNCTION( \ + napi_env env, napi_value exports); \ + NODE_API_EXTERN_C_END \ + static napi_value napi_module_init_impl(napi_env env, napi_value exports); \ + NODE_API_EXTERN_C_START \ + NAPI_MODULE_EXPORT napi_value NODE_API_MODULE_REGISTER_FUNCTION( \ + napi_env env, napi_value exports) { \ + return napi_module_init_impl(env, exports); \ + } \ + NODE_API_EXTERN_C_END \ + static napi_value napi_module_init_impl(napi_env env, napi_value exports) + +#define NAPI_MODULE(modname, regfunc) \ + NAPI_MODULE_INIT() { \ + (void)(modname); \ + return regfunc(env, exports); \ + } + +#define NAPI_MODULE_X(modname, regfunc, priv, flags) \ + NAPI_MODULE(modname, regfunc) + +#endif // NODE_API_H_ diff --git a/Tests/NodeApi/include/node_api_types.h b/Tests/NodeApi/include/node_api_types.h new file mode 100644 index 00000000..97a2fb22 --- /dev/null +++ b/Tests/NodeApi/include/node_api_types.h @@ -0,0 +1,24 @@ +#ifndef NODE_API_TYPES_H_ +#define NODE_API_TYPES_H_ + +#include + +typedef struct napi_callback_scope__* napi_callback_scope; +typedef struct napi_async_context__* napi_async_context; +typedef struct napi_async_work__* napi_async_work; + +typedef void(NAPI_CDECL* napi_async_execute_callback)(napi_env env, + void* data); +typedef void(NAPI_CDECL* napi_async_complete_callback)(napi_env env, + napi_status status, + void* data); + +typedef struct { + uint32_t major; + uint32_t minor; + uint32_t patch; + const char* release; +} napi_node_version; + +#endif // NODE_API_TYPES_H_ + diff --git a/Tests/NodeApi/js_runtime_api.cpp b/Tests/NodeApi/js_runtime_api.cpp new file mode 100644 index 00000000..c03740ac --- /dev/null +++ b/Tests/NodeApi/js_runtime_api.cpp @@ -0,0 +1,112 @@ +#include "js_runtime_api.h" + +#include +#include + +#if defined(__APPLE__) +#include +#include "js_native_api_javascriptcore.h" +#elif defined(__ANDROID__) +#include +#include "js_native_api_v8.h" +#endif + +struct jsr_napi_env_scope_s { + napi_env env{nullptr}; +}; + +napi_status jsr_open_napi_env_scope(napi_env env, + jsr_napi_env_scope* scope) { + if (scope == nullptr) { + return napi_invalid_arg; + } + + auto* scope_impl = new jsr_napi_env_scope_s{}; + scope_impl->env = env; + *scope = scope_impl; + return napi_ok; +} + +napi_status jsr_close_napi_env_scope(napi_env /*env*/, + jsr_napi_env_scope scope) { + if (scope == nullptr) { + return napi_invalid_arg; + } + + delete scope; + return napi_ok; +} + +napi_status jsr_run_script(napi_env env, + napi_value source, + const char* source_url, + napi_value* result) { + return napi_run_script(env, source, source_url, result); +} + +napi_status jsr_collect_garbage(napi_env env) { +#if defined(__APPLE__) + if (env == nullptr) { + return napi_invalid_arg; + } + + JSGlobalContextRef context = env->context; + if (context == nullptr) { + return napi_invalid_arg; + } + + JSGarbageCollect(context); + return napi_ok; +#elif defined(__ANDROID__) + if (env == nullptr) { + return napi_invalid_arg; + } + + v8::Isolate* isolate = env->isolate; + if (isolate == nullptr) { + return napi_invalid_arg; + } + + isolate->RequestGarbageCollectionForTesting( + v8::Isolate::kFullGarbageCollection); + return napi_ok; +#else + (void)env; + return napi_generic_failure; +#endif +} + +napi_status jsr_initialize_native_module( + napi_env env, + napi_addon_register_func register_module, + int32_t /*api_version*/, + napi_value* exports) { + if (env == nullptr || register_module == nullptr || exports == nullptr) { + return napi_invalid_arg; + } + + napi_value module_exports{}; + napi_status status = napi_create_object(env, &module_exports); + if (status != napi_ok) { + return status; + } + + napi_value returned_exports = register_module(env, module_exports); + + bool has_exception = false; + status = napi_is_exception_pending(env, &has_exception); + if (status != napi_ok) { + return status; + } + + if (has_exception) { + return napi_pending_exception; + } + + if (returned_exports != nullptr && returned_exports != module_exports) { + module_exports = returned_exports; + } + + *exports = module_exports; + return napi_ok; +} diff --git a/Tests/NodeApi/js_runtime_api.h b/Tests/NodeApi/js_runtime_api.h new file mode 100644 index 00000000..27b39461 --- /dev/null +++ b/Tests/NodeApi/js_runtime_api.h @@ -0,0 +1,219 @@ +#ifndef HERMES_JS_RUNTIME_API_H +#define HERMES_JS_RUNTIME_API_H + +#include "node_api.h" + +// +// Node-API extensions required for JavaScript engine hosting. +// +// It is a very early version of the APIs which we consider to be experimental. +// These APIs are not stable yet and are subject to change while we continue +// their development. After some time we will stabilize the APIs and make them +// "officially stable". +// + +#define JSR_API NAPI_EXTERN napi_status NAPI_CDECL + +EXTERN_C_START + +typedef struct jsr_runtime_s *jsr_runtime; +typedef struct jsr_config_s *jsr_config; +typedef struct jsr_prepared_script_s *jsr_prepared_script; +typedef struct jsr_napi_env_scope_s *jsr_napi_env_scope; + +typedef void(NAPI_CDECL *jsr_data_delete_cb)(void *data, void *deleter_data); + +//============================================================================= +// jsr_runtime +//============================================================================= + +JSR_API jsr_create_runtime(jsr_config config, jsr_runtime *runtime); +JSR_API jsr_delete_runtime(jsr_runtime runtime); +JSR_API jsr_runtime_get_node_api_env(jsr_runtime runtime, napi_env *env); + +//============================================================================= +// jsr_config +//============================================================================= + +JSR_API jsr_create_config(jsr_config *config); +JSR_API jsr_delete_config(jsr_config config); + +JSR_API jsr_config_enable_inspector(jsr_config config, bool value); +JSR_API jsr_config_set_inspector_runtime_name( + jsr_config config, + const char *name); +JSR_API jsr_config_set_inspector_port(jsr_config config, uint16_t port); +JSR_API jsr_config_set_inspector_break_on_start(jsr_config config, bool value); + +JSR_API jsr_config_enable_gc_api(jsr_config config, bool value); + +JSR_API jsr_config_set_explicit_microtasks(jsr_config config, bool value); + +// A callback to process unhandled JS error +typedef void(NAPI_CDECL *jsr_unhandled_error_cb)( + void *cb_data, + napi_env env, + napi_value error); + +JSR_API jsr_config_on_unhandled_error( + jsr_config config, + void *cb_data, + jsr_unhandled_error_cb unhandled_error_cb); + +//============================================================================= +// jsr_config task runner +//============================================================================= + +// A callback to run task +typedef void(NAPI_CDECL *jsr_task_run_cb)(void *task_data); + +// A callback to post task to the task runner +typedef void(NAPI_CDECL *jsr_task_runner_post_task_cb)( + void *task_runner_data, + void *task_data, + jsr_task_run_cb task_run_cb, + jsr_data_delete_cb task_data_delete_cb, + void *deleter_data); + +JSR_API jsr_config_set_task_runner( + jsr_config config, + void *task_runner_data, + jsr_task_runner_post_task_cb task_runner_post_task_cb, + jsr_data_delete_cb task_runner_data_delete_cb, + void *deleter_data); + +//============================================================================= +// jsr_config script cache +//============================================================================= + +typedef void(NAPI_CDECL *jsr_script_cache_load_cb)( + void *script_cache_data, + const char *source_url, + uint64_t source_hash, + const char *runtime_name, + uint64_t runtime_version, + const char *cache_tag, + const uint8_t **buffer, + size_t *buffer_size, + jsr_data_delete_cb *buffer_delete_cb, + void **deleter_data); + +typedef void(NAPI_CDECL *jsr_script_cache_store_cb)( + void *script_cache_data, + const char *source_url, + uint64_t source_hash, + const char *runtime_name, + uint64_t runtime_version, + const char *cache_tag, + const uint8_t *buffer, + size_t buffer_size, + jsr_data_delete_cb buffer_delete_cb, + void *deleter_data); + +JSR_API jsr_config_set_script_cache( + jsr_config config, + void *script_cache_data, + jsr_script_cache_load_cb script_cache_load_cb, + jsr_script_cache_store_cb script_cache_store_cb, + jsr_data_delete_cb script_cache_data_delete_cb, + void *deleter_data); + +//============================================================================= +// napi_env scope +//============================================================================= + +// Opens the napi_env scope in the current thread. +// Calling Node-API functions without the opened scope may cause a failure. +// The scope must be closed by the jsr_close_napi_env_scope call. +JSR_API jsr_open_napi_env_scope(napi_env env, jsr_napi_env_scope *scope); + +// Closes the napi_env scope in the current thread. It must match to the +// jsr_open_napi_env_scope call. +JSR_API jsr_close_napi_env_scope(napi_env env, jsr_napi_env_scope scope); + +//============================================================================= +// Additional functions to implement JSI +//============================================================================= + +// To implement JSI description() +JSR_API jsr_get_description(napi_env env, const char **result); + +// To implement JSI queueMicrotask() +JSR_API jsr_queue_microtask(napi_env env, napi_value callback); + +// To implement JSI drainMicrotasks() +JSR_API +jsr_drain_microtasks(napi_env env, int32_t max_count_hint, bool *result); + +// To implement JSI isInspectable() +JSR_API jsr_is_inspectable(napi_env env, bool *result); + +//============================================================================= +// Script preparing and running. +// +// Script is usually converted to byte code, or in other words - prepared - for +// execution. Then, we can run the prepared script. +//============================================================================= + +// Run script with source URL. +JSR_API jsr_run_script( + napi_env env, + napi_value source, + const char *source_url, + napi_value *result); + +// Prepare the script for running. +JSR_API jsr_create_prepared_script( + napi_env env, + const uint8_t *script_data, + size_t script_length, + jsr_data_delete_cb script_delete_cb, + void *deleter_data, + const char *source_url, + jsr_prepared_script *result); + +// Delete the prepared script. +JSR_API jsr_delete_prepared_script( + napi_env env, + jsr_prepared_script prepared_script); + +// Run the prepared script. +JSR_API jsr_prepared_script_run( + napi_env env, + jsr_prepared_script prepared_script, + napi_value *result); + +//============================================================================= +// Functions to support unit tests. +//============================================================================= + +// Provides a hint to run garbage collection. +// It is typically used for unit tests. +// It requires enabling GC by calling jsr_config_enable_gc_api. +JSR_API jsr_collect_garbage(napi_env env); + +// Checks if the environment has an unhandled promise rejection. +JSR_API jsr_has_unhandled_promise_rejection(napi_env env, bool *result); + +// Gets and clears the last unhandled promise rejection. +JSR_API jsr_get_and_clear_last_unhandled_promise_rejection( + napi_env env, + napi_value *result); + +// Create new napi_env for the runtime. +JSR_API +jsr_create_node_api_env(napi_env root_env, int32_t api_version, napi_env *env); + +// Run task in the environment context. +JSR_API jsr_run_task(napi_env env, jsr_task_run_cb task_cb, void *data); + +// Initializes native module. +JSR_API jsr_initialize_native_module( + napi_env env, + napi_addon_register_func register_module, + int32_t api_version, + napi_value *exports); + +EXTERN_C_END + +#endif // HERMES_JS_RUNTIME_API_H \ No newline at end of file diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp new file mode 100644 index 00000000..5401d39c --- /dev/null +++ b/Tests/NodeApi/node_lite.cpp @@ -0,0 +1,1268 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "node_lite.h" +#include "js_runtime_api.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "child_process.h" + +namespace fs = std::filesystem; + +namespace node_api_tests { + +namespace { + +NodeApiRef MakeNodeApiRef(napi_env env, napi_value value) { + napi_ref ref{}; + NODE_LITE_CALL(napi_create_reference(env, value, 1, &ref)); + return NodeApiRef(ref, NodeApiRefDeleter(env)); +} + +template +void ThrowJSErrorOnException(napi_env env, TCallback&& callback) noexcept { + try { + callback(); + } catch (const NodeLiteException& e) { + if (e.error_status() == napi_pending_exception) { + napi_value error = NodeApi::GetAndClearLastException(env); + NodeApi::ThrowError(env, error); + } else { + NodeApi::ThrowError(env, e.what()); + } + } catch (const std::exception& e) { + NodeApi::ThrowError(env, e.what()); + } +} + +template +void ExitOnException(napi_env env, TCallback&& callback) noexcept { + try { + callback(); + } catch (const NodeLiteException& e) { + if (e.error_status() == napi_pending_exception) { + napi_value error = NodeApi::GetAndClearLastException(env); + NodeLiteErrorHandler::ExitWithJSError(env, error); + } else { + NodeLiteErrorHandler::ExitWithMessage(e.what()); + } + } catch (const std::exception& e) { + NodeLiteErrorHandler::ExitWithMessage(e.what()); + } +} + +std::string ReadFileText(napi_env env, fs::path file_path) { + std::ifstream file_stream(file_path.string()); + NODE_LITE_ASSERT(file_stream.is_open(), + "Failed to open file: %s. Error: %s", + file_path.c_str(), + std::strerror(errno)); + std::ostringstream ss; + ss << file_stream.rdbuf(); + return ss.str(); +} + +class NodeApiCallbackInfo { + public: + NodeApiCallbackInfo(napi_env env, napi_callback_info info) { + size_t argc{inline_args_.size()}; + napi_value* argv = inline_args_.data(); + NODE_LITE_CALL( + napi_get_cb_info(env, info, &argc, argv, &this_arg_, &data_)); + if (argc > inline_args_.size()) { + dynamic_args_ = std::make_unique(argc); + argv = dynamic_args_.get(); + NODE_LITE_CALL( + napi_get_cb_info(env, info, &argc, argv, &this_arg_, &data_)); + } + args_ = span(argv, argc); + } + + span args() const { return args_; } + napi_value this_arg() const { return this_arg_; } + void* data() const { return data_; } + + private: + std::array inline_args_{}; + std::unique_ptr dynamic_args_{}; + span args_{}; + napi_value this_arg_{}; + void* data_{}; +}; + +} // namespace + +//============================================================================= +// NodeApiTest implementation +//============================================================================= + +std::unique_ptr CreateEnvHolder( + std::shared_ptr taskRunner, + std::function onUnhandledError); + +//============================================================================= +// NodeLiteModule implementation +//============================================================================= + +using ModuleRegisterFuncCallback = napi_value(NAPI_CDECL*)(napi_env env, + napi_value exports); +using ModuleApiVersionCallback = int32_t(NAPI_CDECL*)(); + +NodeLiteModule::NodeLiteModule(std::filesystem::path module_path) noexcept + : module_path_(std::move(module_path)) {} + +NodeLiteModule::NodeLiteModule(std::filesystem::path module_path, + InitModuleCallback init_module) noexcept + : module_path_(std::move(module_path)), + init_module_(std::move(init_module)) {} + +napi_value NodeLiteModule::LoadModule(napi_env env) { + if (state_ == State::kLoaded) { + return NodeApi::GetReferenceValue(env, exports_.get()); + } + if (state_ == State::kLoading) { + return NodeApi::GetUndefined(env); + } + NODE_LITE_ASSERT(state_ == State::kNotLoaded, + "Unexpected module '%s' state: %d", + module_path_.string().c_str(), + static_cast(state_)); + state_ = State::kLoading; + struct ResetStateIfFailed { + NodeLiteModule* module_; + ~ResetStateIfFailed() { + if (module_->state_ == State::kLoading) { + module_->state_ = State::kNotLoaded; + } + } + } reset_state_if_failed{this}; + + if (init_module_) { + napi_value exports = NodeApi::CreateObject(env); + napi_value init_exports = init_module_(env, exports); + if (init_exports != nullptr && + NodeApi::TypeOf(env, init_exports) != napi_undefined) { + exports = init_exports; + } + exports_ = MakeNodeApiRef(env, exports); + } else if (module_path_.extension() == ".js") { + exports_ = MakeNodeApiRef(env, LoadScriptModule(env)); + } else if (module_path_.extension() == ".node") { + exports_ = MakeNodeApiRef(env, LoadNativeModule(env)); + } else { + NODE_LITE_ASSERT( + false, "Unsupported module type: %s", module_path_.string().c_str()); + } + state_ = State::kLoaded; + return NodeApi::GetReferenceValue(env, exports_.get()); +} + +napi_value NodeLiteModule::LoadScriptModule(napi_env env) { + std::string module_func_wrapper = + "(function(module, exports, require, __filename, __dirname) {"; + module_func_wrapper += ReadModuleFileText(env); + + size_t source_map_index = module_func_wrapper.find("//# sourceMappingURL"); + constexpr const char* module_suffix = "\nreturn module.exports; })\n"; + if (source_map_index != std::string::npos) { + module_func_wrapper.insert(source_map_index, module_suffix); + } else { + module_func_wrapper += module_suffix; + } + + napi_value module_func = NodeApi::RunScript( + env, module_func_wrapper, module_path_.string().c_str()); + + NODE_LITE_ASSERT(NodeApi::TypeOf(env, module_func) == napi_function); + + napi_value exports = NodeApi::CreateObject(env); + napi_value file_name = NodeApi::CreateString(env, module_path_.string()); + napi_value dir_name = + NodeApi::CreateString(env, module_path_.parent_path().string()); + + napi_value module_obj = NodeApi::CreateObject(env); + NodeApi::SetProperty(env, module_obj, "exports", exports); + NodeApi::SetProperty(env, module_obj, "__filename", file_name); + NodeApi::SetProperty(env, module_obj, "__dirname", dir_name); + + napi_value require = NodeApi::CreateFunction( + env, "require", [this](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least one argument"); + std::string module_path = NodeApi::ToStdString(env, args[0]); + NodeLiteRuntime* runtime = NodeLiteRuntime::GetRuntime(env); + return runtime + ->ResolveModule(module_path_.parent_path().string(), module_path) + .LoadModule(env); + }); + + return NodeApi::CallFunction( + env, module_func, {module_obj, exports, require, file_name, dir_name}); +} + +napi_value NodeLiteModule::LoadNativeModule(napi_env env) { + ModuleApiVersionCallback getModuleApiVersion = + reinterpret_cast(NodeLitePlatform::LoadFunction( + env, module_path_.c_str(), "node_api_module_get_api_version_v1")); + int32_t moduleApiVersion = getModuleApiVersion ? getModuleApiVersion() : 8; + + ModuleRegisterFuncCallback moduleRegisterFunc = + reinterpret_cast( + NodeLitePlatform::LoadFunction( + env, module_path_.c_str(), "napi_register_module_v1")); + NODE_LITE_ASSERT(moduleRegisterFunc != nullptr, + "Failed to find 'napi_register_module_v1' in module: %s", + module_path_.c_str()); + + napi_value exports{}; + NODE_LITE_CALL(jsr_initialize_native_module( + env, moduleRegisterFunc, moduleApiVersion, &exports)); + return exports; +} + +std::string NodeLiteModule::ReadModuleFileText(napi_env env) { + return ReadFileText(env, module_path_); +} + +//============================================================================= +// NodeLiteRuntime implementation +//============================================================================= + +/*static*/ void NodeLiteRuntime::Run(std::vector argv) { + // Convert arguments to vector of strings and skip all options before the JS + // file name. + std::vector args; + args.reserve(argv.size()); + bool skipOptions = true; + if (argv.size() < 2) { + NodeLiteErrorHandler::ExitWithMessage("", [&](std::ostream& os) { + os << "Usage: " << argv[0] << " "; + }); + } + args.push_back(argv[0]); + for (int i = 1; i < argv.size(); i++) { + if (skipOptions && std::string_view(argv[i]).find("--") == 0) { + continue; + } + skipOptions = false; + args.push_back(argv[i]); + } + + std::shared_ptr taskRunner = + std::make_shared(); + + fs::path exe_path = fs::canonical(argv[0]); + + fs::path test_root_path = exe_path.parent_path(); + fs::path js_root = test_root_path / "test"; + if (!fs::exists(js_root)) { + test_root_path = test_root_path.parent_path(); + js_root = test_root_path / "test"; + } + if (!fs::exists(js_root)) { + NodeLiteErrorHandler::ExitWithMessage("Error: Cannot find test directory."); + } + + std::string jsFilePath = args[1]; + std::unique_ptr runtime = NodeLiteRuntime::Create( + std::move(taskRunner), js_root.string(), std::move(args)); + runtime->RunTestScript(jsFilePath); +} + +/*static*/ std::unique_ptr NodeLiteRuntime::Create( + std::shared_ptr task_runner, + std::string js_root, + std::vector args) { + std::unique_ptr runtime = + std::make_unique(PrivateTag{}, + std::move(task_runner), + std::move(js_root), + std::move(args)); + runtime->Initialize(); + return runtime; +} + +NodeLiteRuntime::NodeLiteRuntime( + PrivateTag, + std::shared_ptr task_runner, + std::string js_root, + std::vector args) + : task_runner_(std::move(task_runner)), + js_root_(std::move(js_root)), + args_(std::move(args)) {} + +void NodeLiteRuntime::Initialize() { + env_holder_ = + CreateEnvHolder(task_runner_, [this](napi_env env, napi_value error) { + NODE_LITE_ASSERT(env == env_, + "Unhandled error in different napi_env: %p != %p", + env, + env_); + OnUncaughtException(error); + }); + env_ = env_holder_->getEnv(); + NodeApiEnvScope env_scope{env_}; + NodeApiHandleScope handle_scope{env_}; + DefineBuiltInModules(); + DefineGlobalFunctions(); +} + +NodeLiteModule& NodeLiteRuntime::ResolveModule( + const std::string& parent_module_path, const std::string& module_path) { + napi_env env = env_; + fs::path fs_module_path = ResolveModulePath(parent_module_path, module_path); + if (auto it = registered_modules_.find(fs_module_path.string()); + it != registered_modules_.end()) { + return *it->second; + } + + if (auto [it, succeeded] = registered_modules_.try_emplace( + fs_module_path.string(), + std::make_unique(fs_module_path.string())); + succeeded) { + return *it->second; + } + + NODE_LITE_ASSERT( + false, "Failed to register module: %s", fs_module_path.string().c_str()); +} + +fs::path NodeLiteRuntime::ResolveModulePath( + const std::string& parent_module_path, const std::string& module_path) { + napi_env env = env_; + // 1. See if it is an embedded module such as "assert". + auto it = node_js_modules_.find(module_path); + if (it != node_js_modules_.end()) { + return fs::path(it->second); + } + + // 2. Check if it is a relative or an absolute path to a module. + { + fs::path fs_module_path = fs::path(module_path); + if (!fs_module_path.is_absolute()) { + fs::path fs_parent_module_path = fs::path(parent_module_path); + NODE_LITE_ASSERT(fs_parent_module_path.is_absolute(), + "Parent module path '%s' is not absolute", + parent_module_path.c_str()); + fs_module_path = fs_parent_module_path / fs_module_path; + } + fs_module_path = fs::weakly_canonical(fs_module_path); + + if (fs::exists(fs_module_path) && fs::is_regular_file(fs_module_path)) { + return fs_module_path; + } + if (fs::path result = fs::path(fs_module_path).replace_extension(".js"); + fs::exists(result)) { + return result; + } + if (fs::path result = fs_module_path / "index.js"; fs::exists(result)) { + return result; + } + // See if it is a native module. + fs::path node_module_path = + fs::path(fs_module_path).replace_extension(".node"); + if (fs::exists(node_module_path)) { + return node_module_path; + } + // See if the module was prefixed with the parent folder to disambiguate C++ + // project name. + fs::path fs_parent_folder = fs::path(parent_module_path).filename(); + node_module_path.replace_filename(fs_parent_folder.string() + "_" + + node_module_path.filename().string()); + if (fs::exists(node_module_path)) { + return node_module_path; + } + } + + // 3. Check if it is in the node_modules folder. + { + fs::path fs_module_path = fs::weakly_canonical( + fs::path(js_root_) / "node_modules" / fs::path(module_path)); + + if (fs::exists(fs_module_path) && fs::is_regular_file(fs_module_path)) { + return fs_module_path; + } + if (fs::path result = fs::path(fs_module_path).replace_extension(".js"); + fs::exists(result)) { + return result; + } + if (fs::path result = fs_module_path / "index.js"; fs::exists(result)) { + return result; + } + } + + NODE_LITE_ASSERT( + false, "Cannot resolve module path '%s'", module_path.c_str()); +} + +void NodeLiteRuntime::AddNativeModule( + const std::string& module_name, + std::function initModule) { + napi_env env = env_; + auto [_, succeeded] = registered_modules_.try_emplace( + module_name, + std::make_unique(module_name, std::move(initModule))); + NODE_LITE_ASSERT( + succeeded, "Failed to register module: %s", module_name.c_str()); +} + +void NodeLiteRuntime::RunTestScript(const std::string& script_path) { + NodeApiEnvScope env_scope{env_}; + NodeApiHandleScope handle_scope{env_}; + { + ExitOnException(env_, [this, &script_path]() { + NodeApiHandleScope scope{env_}; + NodeLiteModule& main_module = ResolveModule(js_root_, script_path); + main_module.LoadModule(env_); + }); + ExitOnException(env_, [this]() { + task_runner_->DrainTaskQueue(); + OnExit(); + on_exit_callbacks_.clear(); + on_uncaughtException_callbacks_.clear(); + }); + } +} + +void NodeLiteRuntime::OnExit() { + for (NodeApiRef& callback_ref : on_exit_callbacks_) { + napi_value callback = NodeApi::GetReferenceValue(env_, callback_ref.get()); + NodeApi::CallFunction(env_, callback, {NodeApi::CreateUInt32(env_, 0)}); + } +} + +void NodeLiteRuntime::OnUncaughtException(napi_value error) { + bool shouldExit = true; + for (NodeApiRef& callback_ref : on_uncaughtException_callbacks_) { + napi_value callback = NodeApi::GetReferenceValue(env_, callback_ref.get()); + napi_value result = NodeApi::CallFunction( + env_, + callback, + {error, NodeApi::CreateString(env_, "uncaughtException")}); + // If at least one callback returns false, we do not exit. + // TODO: (vmoroz) Investigate the Node.js behavior in that case + // if (shouldExit && NodeApi::TypeOf(env_, result) == napi_boolean) { + // shouldExit = NodeApi::GetBoolean(env_, result); + //} + shouldExit = false; + } + + if (shouldExit) { + NodeLiteErrorHandler::ExitWithJSError(env_, error); + } +} + +/*static*/ NodeLiteRuntime* NodeLiteRuntime::GetRuntime(napi_env env) { + napi_value global = NodeApi::GetGlobal(env); + return static_cast(NodeApi::GetValueExternal( + env, NodeApi::GetProperty(env, global, "__NodeLiteRuntime__"))); +} + +void NodeLiteRuntime::DefineBuiltInModules() { + napi_env env = env_; + // Define "assert" module + { + fs::path assert_path = + fs::weakly_canonical(fs::path(js_root_) / "common" / "assert.js"); + std::string assert_path_str = assert_path.string(); + NODE_LITE_ASSERT(fs::exists(assert_path), + "Failed to find assert.js file: %s", + assert_path_str.c_str()); + node_js_modules_.try_emplace(assert_path_str, assert_path_str); + node_js_modules_.try_emplace(assert_path.replace_extension().string(), + assert_path_str); + node_js_modules_.try_emplace("assert", assert_path_str); + node_js_modules_.try_emplace("node:assert", assert_path_str); + } + + // Define "child_process" module + { + node_js_modules_.try_emplace("child_process", "child_process"); + node_js_modules_.try_emplace("node:child_process", "child_process"); + AddNativeModule("child_process", [this](napi_env env, napi_value exports) { + NodeApi::SetMethod( + env_, exports, "spawnSync", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 2, + "Expected at least 2 arguments, but got: %zu", + args.size()); + std::string command = NodeApi::ToStdString(env, args[0]); + std::vector command_args = + NodeApi::ToStdStringArray(env, args[1]); + ProcessResult call_result = SpawnSync(command, command_args); + napi_value result = NodeApi::CreateObject(env); + NodeApi::SetPropertyUInt32( + env, result, "status", call_result.status); + NodeApi::SetPropertyString( + env, result, "stderr", call_result.std_error); + NodeApi::SetPropertyString( + env, result, "stdout", call_result.std_output); + NodeApi::SetPropertyNull(env, result, "signal"); + return result; + }); + return exports; + }); + } + + // Define "fs" module + { + node_js_modules_.try_emplace("fs", "fs"); + node_js_modules_.try_emplace("node:fs", "fs"); + AddNativeModule("fs", [this](napi_env env, napi_value exports) { + NodeApi::SetMethod( + env_, exports, "existsSync", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + fs::path path = fs::path{NodeApi::ToStdString(env, args[0])}; + return NodeApi::GetBoolean(env, fs::exists(path)); + }); + NodeApi::SetMethod( + env_, + exports, + "readFileSync", + [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + fs::path path = fs::path{NodeApi::ToStdString(env, args[0])}; + return NodeApi::CreateString(env, ReadFileText(env, path)); + }); + return exports; + }); + } + + // Define "path" module + { + node_js_modules_.try_emplace("path", "path"); + node_js_modules_.try_emplace("node:path", "path"); + AddNativeModule("path", [this](napi_env env, napi_value exports) { + NodeApi::SetMethod( + env_, exports, "join", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 2, + "Expected at least 2 arguments, but got: %zu", + args.size()); + fs::path path = fs::path{NodeApi::ToStdString(env, args[0])}; + for (size_t i = 1; i < args.size(); ++i) { + path /= NodeApi::ToStdString(env, args[i]); + } + return NodeApi::CreateString(env, path.string()); + }); + return exports; + }); + } +} + +void NodeLiteRuntime::DefineGlobalFunctions() { + NodeApiHandleScope scope{env_}; + napi_value global = NodeApi::GetGlobal(env_); + + // Add global.global + NodeApi::SetProperty(env_, global, "global", global); + + // Add global.__NodeLiteRuntime__ + NodeApi::SetProperty( + env_, global, "__NodeLiteRuntime__", NodeApi::CreateExternal(env_, this)); + + // Remove the global.require defined by Hermes + NodeApi::DeleteProperty(env_, global, "require"); + + // global.gc() + NodeApi::SetMethod( + env_, global, "gc", [](napi_env env, span /*args*/) { + NODE_LITE_CALL(jsr_collect_garbage(env)); + return nullptr; + }); + + auto set_immediate_cb = [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, + "Expected at least 1 argument, but got: %zu", + args.size()); + std::shared_ptr callback_ref = + std::make_shared(MakeNodeApiRef(env, args[0])); + uint32_t task_id = GetRuntime(env)->task_runner_->PostTask( + [env, callback_ref = std::move(callback_ref)]() { + ExitOnException(env, [env, &callback_ref]() { + NodeApiHandleScope scope{env}; + napi_value callback = + NodeApi::GetReferenceValue(env, callback_ref->get()); + NodeApi::CallFunction(env, callback, {}); + }); + }); + return NodeApi::CreateUInt32(env, task_id); + }; + + // global.setImmediate() + NodeApi::SetMethod(env_, global, "setImmediate", set_immediate_cb); + + // global.setTimeout() + NodeApi::SetMethod(env_, global, "setTimeout", set_immediate_cb); + + // global.clearTimeout() + NodeApi::SetMethod( + env_, global, "clearTimeout", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, + "Expected at least 1 argument, but got: %zu", + args.size()); + uint32_t task_id = NodeApi::GetValueUInt32(env, args[0]); + GetRuntime(env)->task_runner_->RemoveTask(task_id); + return nullptr; + }); + + // global.process + { + napi_value process_obj = NodeApi::CreateObject(env_); + NodeApi::SetProperty(env_, global, "process", process_obj); + + // process.argv + NodeApi::SetPropertyStringArray(env_, process_obj, "argv", args_); + + // process.execPath + NodeApi::SetPropertyString(env_, process_obj, "execPath", args_[0]); + +// process.target_config +#ifdef NDEBUG + NodeApi::SetPropertyString(env_, process_obj, "target_config", "Release"); +#else + NodeApi::SetPropertyString(env_, process_obj, "target_config", "Debug"); +#endif + +// process.platform +#ifdef WIN32 + NodeApi::SetPropertyString(env_, process_obj, "platform", "win32"); +#else + // TODO: (vmoroz) Add support for other platforms. + NodeApi::SetPropertyString(env_, process_obj, "platform", "other"); +#endif + + // process.exit(exit_code) + NodeApi::SetMethod( + env_, process_obj, "exit", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, + "Expected at least 1 argument, but got: " + "%zu", + args.size()); + int32_t exit_code = NodeApi::GetValueInt32(env, args[0]); + exit(exit_code); + return nullptr; + }); + + // process.on('event_name', callback) + NodeApi::SetMethod( + env_, process_obj, "on", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 2, + "Expected at least 2 arguments, but got: %zu", + args.size()); + std::string event_name = NodeApi::ToStdString(env, args[0]); + if (event_name == "exit") { + NODE_LITE_ASSERT(NodeApi::TypeOf(env, args[1]) == napi_function, + "Expected function as second argument"); + GetRuntime(env)->on_exit_callbacks_.push_back( + MakeNodeApiRef(env, args[1])); + } else if (event_name == "uncaughtException") { + NODE_LITE_ASSERT(NodeApi::TypeOf(env, args[1]) == napi_function, + "Expected function as second argument"); + GetRuntime(env)->on_uncaughtException_callbacks_.push_back( + MakeNodeApiRef(env, args[1])); + } else { + NODE_LITE_ASSERT(false, + "Unsupported process event name: %s", + event_name.c_str()); + } + return nullptr; + }); + } + + // global.console + { + napi_value console_obj = NodeApi::CreateObject(env_); + NodeApi::SetProperty(env_, global, "console", console_obj); + + // console.log() + NodeApi::SetMethod( + env_, console_obj, "log", [](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + std::string message = NodeApi::ToStdString(env, args[0]); + std::cout << message << std::endl; + return nullptr; + }); + + // console.error() + NodeApi::SetMethod( + env_, + console_obj, + "error", + [](napi_env env, span args) -> napi_value { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + std::string message = NodeApi::ToStdString(env, args[0]); + std::cerr << message << std::endl; + return nullptr; + }); + } +} + +std::string NodeLiteRuntime::ProcessStack(std::string const& stack, + std::string const& assertMethod) { + // Split up the stack string into an array of stack frames + auto stackStream = std::istringstream(stack); + std::string stackFrame; + std::vector stackFrames; + while (std::getline(stackStream, stackFrame, '\n')) { + stackFrames.push_back(std::move(stackFrame)); + } + + // Remove first and last stack frames: one is the error message + // and another is the module root call. + if (!stackFrames.empty()) { + stackFrames.pop_back(); + } + if (!stackFrames.empty()) { + stackFrames.erase(stackFrames.begin()); + } + + std::string processedStack; + bool assertFuncFound = false; + std::string assertFuncPattern = assertMethod + " ("; + const std::regex locationRE("(\\w+):(\\d+)"); + std::smatch locationMatch; + // for (auto const& frame : stackFrames) { + // if (assertFuncFound) { + // std::string processedFrame; + // if (std::regex_search(frame, locationMatch, locationRE)) { + // if (auto const* scriptInfo = + // GetTestScriptInfo(locationMatch[1].str())) { + // int32_t cppLine = + // scriptInfo->line + std::stoi(locationMatch[2].str()) - 1; + // processedFrame = locationMatch.prefix().str() + + // UseSrcFilePath(scriptInfo->filePath.string()) + + // ':' + std::to_string(cppLine) + + // locationMatch.suffix().str(); + // } + // } + // processedStack += + // (!processedFrame.empty() ? processedFrame : frame) + '\n'; + // } else { + // auto pos = frame.find(assertFuncPattern); + // if (pos != std::string::npos) { + // if (frame[pos - 1] == '.' || frame[pos - 1] == ' ') { + // assertFuncFound = true; + // } + // } + // } + // } + + return processedStack; +} + +//============================================================================= +// NodeApiRefDeleter implementation +//============================================================================= + +NodeApiRefDeleter::NodeApiRefDeleter() noexcept = default; + +NodeApiRefDeleter::NodeApiRefDeleter(napi_env env) noexcept : env_(env) {} + +void NodeApiRefDeleter::operator()(napi_ref ref) noexcept { + if (ref == nullptr || env_ == nullptr) { + return; + } + napi_env env = env_; + NODE_LITE_CALL(napi_delete_reference(env, ref)); +} + +//============================================================================= +// NodeLiteTaskRunner implementation +//============================================================================= + +uint32_t NodeLiteTaskRunner::PostTask(std::function&& task) noexcept { + uint32_t task_id = next_task_id_++; + task_queue_.emplace_back(task_id, std::move(task)); + return task_id; +} + +void NodeLiteTaskRunner::RemoveTask(uint32_t task_id) noexcept { + task_queue_.remove_if( + [task_id](const std::pair>& entry) { + return entry.first == task_id; + }); +} + +void NodeLiteTaskRunner::DrainTaskQueue() noexcept { + while (!task_queue_.empty()) { + std::pair> task = + std::move(task_queue_.front()); + task_queue_.pop_front(); + task.second(); + } +} + +/*static*/ void NodeLiteTaskRunner::PostTaskCallback( + void* task_runner_data, + void* task_data, + jsr_task_run_cb task_run_cb, + jsr_data_delete_cb task_data_delete_cb, + void* deleter_data) { + NodeLiteTaskRunner* taskRunnerPtr = + static_cast*>(task_runner_data) + ->get(); + taskRunnerPtr->PostTask( + [task_run_cb, task_data, task_data_delete_cb, deleter_data]() { + if (task_run_cb != nullptr) { + task_run_cb(task_data); + } + if (task_data_delete_cb != nullptr) { + task_data_delete_cb(task_data, deleter_data); + } + }); +} + +/*static*/ void NodeLiteTaskRunner::DeleteCallback(void* data, + void* /*deleter_data*/) { + delete static_cast*>(data); +} + +//============================================================================= +// NodeApiHandleScope implementation +//============================================================================= + +NodeApiHandleScope::NodeApiHandleScope(napi_env env) noexcept : env_{env} { + NODE_LITE_CALL(napi_open_handle_scope(env, &scope_)); +} + +NodeApiHandleScope::~NodeApiHandleScope() noexcept { + napi_env env = env_; + NODE_LITE_CALL(napi_close_handle_scope(env, scope_)); +} + +//============================================================================= +// NodeApiEnvScope implementation +//============================================================================= + +NodeApiEnvScope::NodeApiEnvScope(napi_env env) noexcept : env_{env} { + NODE_LITE_CALL(jsr_open_napi_env_scope(env, &scope_)); +} + +NodeApiEnvScope ::~NodeApiEnvScope() noexcept { + if (env_ != nullptr) { + napi_env env = env_; + NODE_LITE_CALL(jsr_close_napi_env_scope(env, scope_)); + } +} + +NodeApiEnvScope::NodeApiEnvScope(NodeApiEnvScope&& other) noexcept + : env_{std::exchange(other.env_, nullptr)}, + scope_{std::exchange(other.scope_, nullptr)} {} + +NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { + if (this != &other) { + NodeApiEnvScope temp(std::move(*this)); + env_ = std::exchange(other.env_, nullptr); + scope_ = std::exchange(other.scope_, nullptr); + } + return *this; +} + +//============================================================================= +// NodeLiteErrorHandler implementation +//============================================================================= + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::OnNodeApiFailed( + napi_env env, napi_status error_code) { + const char* errorMessage = "An exception is pending"; + if (NodeApi::IsExceptionPending(env)) { + error_code = napi_pending_exception; + } else { + const napi_extended_error_info* error_info{}; + napi_status status = napi_get_last_error_info(env, &error_info); + if (status != napi_ok) { + NodeLiteErrorHandler::ExitWithMessage("", [&](std::ostream& os) { + os << "Failed to get last error info: " << status; + }); + } + errorMessage = error_info->error_message; + } + throw NodeLiteException(error_code, errorMessage); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::OnAssertFailed( + napi_env env, char const* expr, char const* message) { + std::string error_message = FormatString("Assert failed: %s.", expr); + if (message != nullptr) { + std::string message_str{message}; + if (!message_str.empty()) { + error_message += " " + message_str; + } + } + napi_status error_code = NodeApi::IsExceptionPending(env) + ? napi_pending_exception + : napi_generic_failure; + + throw NodeLiteException(error_code, error_message.c_str()); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithJSError( + napi_env env, napi_value error) noexcept { + // TODO: protect from stack overflow + napi_valuetype error_value_type = NodeApi::TypeOf(env, error); + if (error_value_type == napi_object) { + std::string name = NodeApi::GetPropertyString(env, error, "name"); + if (name == "AssertionError") { + ExitWithJSAssertError(env, error); + } + std::string message = NodeApi::GetPropertyString(env, error, "message"); + std::string stack = NodeApi::GetPropertyString(env, error, "stack"); + ExitWithMessage("JavaScript error", [&](std::ostream& os) { + os << "Exception: " << name << '\n' + << " Message: " << message << '\n' + << "Callstack: " << '\n' + << stack; + }); + } else { + std::string message = NodeApi::CoerceToString(env, error); + ExitWithMessage("JavaScript error", + [&](std::ostream& os) { os << " Message: " << message; }); + } +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithJSAssertError( + napi_env env, napi_value error) noexcept { + std::string message = NodeApi::GetPropertyString(env, error, "message"); + std::string method = NodeApi::GetPropertyString(env, error, "method"); + std::string expected = NodeApi::GetPropertyString(env, error, "expected"); + std::string actual = NodeApi::GetPropertyString(env, error, "actual"); + std::string source_file = + NodeApi::GetPropertyString(env, error, "sourceFile"); + int32_t source_line = NodeApi::GetPropertyInt32(env, error, "sourceLine"); + std::string error_stack = + NodeApi::GetPropertyString(env, error, "errorStack"); + if (error_stack.empty()) { + error_stack = NodeApi::GetPropertyString(env, error, "stack"); + } + std::string method_name = "assert." + method; + std::stringstream error_details; + if (method_name != "assert.fail") { + error_details << " Expected: " << expected << '\n' + << " Actual: " << actual << '\n'; + } + + ExitWithMessage("JavaScript assertion error", [&](std::ostream& os) { + os << "Exception: " << "AssertionError" << '\n' + << " Method: " << method_name << '\n' + << " Message: " << message << '\n' + << error_details.str(/*a filler for formatting*/) + << "Callstack: " << '\n' + << error_stack; + }); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithMessage( + const std::string& message, + std::function get_error_details) noexcept { + std::ostringstream details_stream; + get_error_details(details_stream); + std::string details = details_stream.str(); + if (!message.empty()) { + std::cerr << message; + } + if (!details.empty()) { + if (!message.empty()) { + std::cerr << "\n"; + } + std::cerr << details; + } + std::cerr << std::endl; + exit(1); +} + +//============================================================================= +// NodeApi implementation +//============================================================================= + +/*static*/ bool NodeApi::IsExceptionPending(napi_env env) { + bool result{}; + NODE_LITE_CALL(napi_is_exception_pending(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetAndClearLastException(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_get_and_clear_last_exception(env, &result)); + return result; +} + +/*static*/ void NodeApi::ThrowError(napi_env env, napi_value error) { + NODE_LITE_CALL(napi_throw(env, error)); +} + +/*static*/ void NodeApi::ThrowError(napi_env env, const char* error_message) { + NODE_LITE_CALL(napi_throw_error(env, "", error_message)); +} + +/*static*/ napi_value NodeApi::GetNull(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_get_null(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetUndefined(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_get_undefined(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetGlobal(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_get_global(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetBoolean(napi_env env, bool value) { + napi_value result{}; + NODE_LITE_CALL(napi_get_boolean(env, value, &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetReferenceValue(napi_env env, napi_ref ref) { + napi_value result{}; + NODE_LITE_CALL(napi_get_reference_value(env, ref, &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateUInt32(napi_env env, std::uint32_t value) { + napi_value result{}; + NODE_LITE_CALL(napi_create_uint32(env, value, &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateString(napi_env env, + std::string_view value) { + napi_value result{}; + NODE_LITE_CALL( + napi_create_string_utf8(env, value.data(), value.size(), &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateStringArray( + napi_env env, std::vector const& value) { + napi_value result{}; + NODE_LITE_CALL(napi_create_array(env, &result)); + + uint32_t index = 0; + for (const std::string& item : value) { + NODE_LITE_CALL( + napi_set_element(env, result, index++, CreateString(env, item))); + } + return result; +} + +/*static*/ napi_value NodeApi::CreateObject(napi_env env) { + napi_value result{}; + NODE_LITE_CALL(napi_create_object(env, &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateExternal(napi_env env, void* data) { + napi_value result{}; + NODE_LITE_CALL(napi_create_external(env, data, nullptr, nullptr, &result)); + return result; +} + +/*static*/ int32_t NodeApi::GetValueInt32(napi_env env, napi_value value) { + int32_t result{}; + NODE_LITE_CALL(napi_get_value_int32(env, value, &result)); + return result; +} + +/*static*/ uint32_t NodeApi::GetValueUInt32(napi_env env, napi_value value) { + uint32_t result{}; + NODE_LITE_CALL(napi_get_value_uint32(env, value, &result)); + return result; +} + +/*static*/ void* NodeApi::GetValueExternal(napi_env env, napi_value value) { + void* result{}; + NODE_LITE_CALL(napi_get_value_external(env, value, &result)); + return result; +} + +/*static*/ bool NodeApi::HasProperty(napi_env env, + napi_value obj, + std::string_view utf8_name) { + bool result{}; + NODE_LITE_CALL(napi_has_named_property(env, obj, utf8_name.data(), &result)); + return result; +} + +/*static*/ napi_value NodeApi::GetProperty(napi_env env, + napi_value obj, + std::string_view utf8_name) { + napi_value result{}; + NODE_LITE_CALL(napi_get_named_property(env, obj, utf8_name.data(), &result)); + return result; +} + +/*static*/ std::string NodeApi::GetPropertyString(napi_env env, + napi_value obj, + std::string_view utf8_name) { + if (HasProperty(env, obj, utf8_name)) { + return ToStdString(env, GetProperty(env, obj, utf8_name)); + } else { + return ""; + } +} + +/*static*/ int32_t NodeApi::GetPropertyInt32(napi_env env, + napi_value obj, + std::string_view utf8_name) { + return GetValueInt32(env, GetProperty(env, obj, utf8_name)); +} + +/*static*/ std::string NodeApi::CoerceToString(napi_env env, napi_value value) { + napi_value str_value; + NODE_LITE_CALL(napi_coerce_to_string(env, value, &str_value)); + return ToStdString(env, str_value); +} + +/*static*/ void NodeApi::SetProperty(napi_env env, + napi_value obj, + std::string_view utf8_name, + napi_value value) { + NODE_LITE_CALL(napi_set_named_property(env, obj, utf8_name.data(), value)); +} + +/*static*/ void NodeApi::SetPropertyUInt32(napi_env env, + napi_value obj, + std::string_view utf8_name, + uint32_t value) { + SetProperty(env, obj, utf8_name, CreateUInt32(env, value)); +} + +/*static*/ void NodeApi::SetPropertyString(napi_env env, + napi_value obj, + std::string_view utf8_name, + std::string_view value) { + SetProperty(env, obj, utf8_name, CreateString(env, value)); +} + +/*static*/ void NodeApi::SetPropertyStringArray( + napi_env env, + napi_value obj, + std::string_view utf8_name, + std::vector const& value) { + SetProperty(env, obj, utf8_name, CreateStringArray(env, value)); +} + +/*static*/ void NodeApi::SetPropertyNull(napi_env env, + napi_value obj, + std::string_view utf8_name) { + SetProperty(env, obj, utf8_name, GetNull(env)); +} + +/*static*/ void NodeApi::SetMethod(napi_env env, + napi_value obj, + std::string_view utf8_name, + NodeApiCallback cb) { + NodeApi::SetProperty(env, obj, utf8_name, CreateFunction(env, utf8_name, cb)); +} + +/*static*/ bool NodeApi::DeleteProperty(napi_env env, + napi_value obj, + std::string_view utf8_name) { + bool result{}; + NODE_LITE_CALL( + napi_delete_property(env, obj, CreateString(env, utf8_name), &result)); + return result; +} + +/*static*/ std::string NodeApi::ToStdString(napi_env env, napi_value value) { + size_t str_size{}; + NODE_LITE_CALL(napi_get_value_string_utf8(env, value, nullptr, 0, &str_size)); + std::string result(str_size, '\0'); + NODE_LITE_CALL(napi_get_value_string_utf8( + env, value, &result[0], str_size + 1, nullptr)); + return result; +} + +/*static*/ std::vector NodeApi::ToStdStringArray( + napi_env env, napi_value value) { + std::vector result; + bool is_array; + NODE_LITE_CALL(napi_is_array(env, value, &is_array)); + if (is_array) { + uint32_t length; + NODE_LITE_CALL(napi_get_array_length(env, value, &length)); + result.reserve(length); + for (uint32_t i = 0; i < length; i++) { + napi_value element; + NODE_LITE_CALL(napi_get_element(env, value, i, &element)); + result.push_back(CoerceToString(env, element)); + } + } + return result; +} + +/*static*/ napi_value NodeApi::RunScript(napi_env env, napi_value script) { + napi_value result{}; + NODE_LITE_CALL(napi_run_script(env, script, nullptr, &result)); + return result; +} + +/*static*/ napi_value NodeApi::RunScript(napi_env env, + const std::string& code, + char const* source_url) { + napi_value script = NodeApi::CreateString(env, code); + + if (source_url != nullptr) { + napi_value result{}; + NODE_LITE_CALL(jsr_run_script(env, script, source_url, &result)); + return result; + } + return RunScript(env, script); +} + +/*static*/ napi_valuetype NodeApi::TypeOf(napi_env env, napi_value value) { + napi_valuetype result{}; + NODE_LITE_CALL(napi_typeof(env, value, &result)); + return result; +} + +/*static*/ napi_value NodeApi::CallFunction(napi_env env, + napi_value func, + span args) { + napi_value result{}; + NODE_LITE_CALL(napi_call_function( + env, GetUndefined(env), func, args.size(), args.data(), &result)); + return result; +} + +/*static*/ napi_value NodeApi::CreateFunction(napi_env env, + std::string_view name, + NodeApiCallback cb) { + napi_value result{}; + NODE_LITE_CALL(napi_create_function( + env, + name.data(), + name.size(), + [](napi_env env, napi_callback_info info) { + napi_value result{}; + ThrowJSErrorOnException(env, [env, info, &result]() { + NodeApiCallbackInfo callback_info{env, info}; + NodeApiCallback* cb = + static_cast(callback_info.data()); + result = (*cb)(env, callback_info.args()); + }); + return result; + }, + // TODO: (vmoroz) Find a way to delete it on close. + new NodeApiCallback(std::move(cb)), + &result)); + return result; +} + +} // namespace node_api_tests + +int main(int argc, char* argv[]) { + node_api_tests::NodeLiteRuntime::Run( + std::vector(argv, argv + argc)); +} diff --git a/Tests/NodeApi/node_lite.h b/Tests/NodeApi/node_lite.h new file mode 100644 index 00000000..5b9044dd --- /dev/null +++ b/Tests/NodeApi/node_lite.h @@ -0,0 +1,365 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// A simple Node.js-like runtime that runs Node-API test scripts. + +#ifndef NODE_API_TEST_NODE_LITE_H +#define NODE_API_TEST_NODE_LITE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "compat.h" +#include "string_utils.h" + +#define NAPI_EXPERIMENTAL +#include "js_runtime_api.h" + +#define NODE_LITE_CALL(expr) \ + do { \ + napi_status temp_status__ = (expr); \ + if (temp_status__ != napi_status::napi_ok) { \ + NodeLiteErrorHandler::OnNodeApiFailed(env, temp_status__); \ + } \ + } while (false) + +#define NODE_LITE_ASSERT(expr, ...) \ + do { \ + if (!(expr)) { \ + NodeLiteErrorHandler::OnAssertFailed( \ + env, #expr, FormatString("" __VA_ARGS__).c_str()); \ + } \ + } while (false) + +namespace node_api_tests { + +// Forward declarations +class NodeLiteModule; +class NodeLiteRuntime; +class NodeLiteTaskRunner; +class NodeApiRefDeleter; +class NodeApiHandleScope; +class NodeApiEnvScope; +class NodeLiteErrorHandler; + +struct IEnvHolder { + virtual ~IEnvHolder() {} + virtual napi_env getEnv() = 0; +}; + +class NodeLiteTaskRunner { + public: + using QueueEntry = std::pair>; + + uint32_t PostTask(std::function&& task) noexcept; + void RemoveTask(uint32_t task_id) noexcept; + void DrainTaskQueue() noexcept; + + static void PostTaskCallback(void* task_runner_data, + void* task_data, + jsr_task_run_cb task_run_cb, + jsr_data_delete_cb task_data_delete_cb, + void* deleter_data); + + static void DeleteCallback(void* data, void* /*deleter_data*/); + + private: + std::list task_queue_; + uint32_t next_task_id_{1}; +}; + +class NodeLiteException : public std::runtime_error { + public: + explicit NodeLiteException(napi_status error_status, + const char* message) noexcept + : runtime_error{message}, error_status_{error_status} {} + + napi_status error_status() const noexcept { return error_status_; } + + private: + napi_status error_status_; +}; + +class NodeLiteErrorHandler { + public: + [[noreturn]] static void OnNodeApiFailed(napi_env env, + napi_status error_status); + + [[noreturn]] static void OnAssertFailed(napi_env env, + char const* expr, + char const* message); + + [[noreturn]] static void ExitWithJSError(napi_env env, + napi_value error) noexcept; + + [[noreturn]] static void ExitWithJSAssertError(napi_env env, + napi_value error) noexcept; + + [[noreturn]] static void ExitWithMessage( + const std::string& message, + std::function get_error_details = nullptr) noexcept; +}; + +// Define NodeApiRef "smart pointer" for napi_ref as unique_ptr with a custom +// deleter. +class NodeApiRefDeleter { + public: + NodeApiRefDeleter() noexcept; + explicit NodeApiRefDeleter(napi_env env) noexcept; + + void operator()(napi_ref ref) noexcept; + + private: + napi_env env_{}; +}; + +using NodeApiRef = std::unique_ptr; + +class NodeApiHandleScope { + public: + explicit NodeApiHandleScope(napi_env env) noexcept; + ~NodeApiHandleScope() noexcept; + + private: + napi_env env_{}; + napi_handle_scope scope_{}; +}; + +class NodeApiEnvScope { + public: + explicit NodeApiEnvScope(napi_env env) noexcept; + + ~NodeApiEnvScope() noexcept; + + NodeApiEnvScope(NodeApiEnvScope&& other) noexcept; + NodeApiEnvScope& operator=(NodeApiEnvScope&& other) noexcept; + + NodeApiEnvScope(const NodeApiEnvScope&) = delete; + NodeApiEnvScope& operator=(const NodeApiEnvScope&) = delete; + + private: + napi_env env_{}; + jsr_napi_env_scope scope_{}; +}; + +class NodeLiteModule { + public: + using InitModuleCallback = + std::function; + + explicit NodeLiteModule(std::filesystem::path module_path) noexcept; + explicit NodeLiteModule(std::filesystem::path module_path, + InitModuleCallback init_module) noexcept; + + napi_value LoadModule(napi_env env); + + NodeLiteModule(const NodeLiteModule&) = delete; + NodeLiteModule& operator=(const NodeLiteModule&) = delete; + + private: + napi_value LoadScriptModule(napi_env env); + napi_value LoadNativeModule(napi_env env); + std::string ReadModuleFileText(napi_env env); + + private: + enum class State { + kNotLoaded, + kLoading, + kLoaded, + }; + + private: + State state_{State::kNotLoaded}; + std::filesystem::path module_path_; + InitModuleCallback init_module_; + NodeApiRef exports_; +}; + +// The Node.js-like runtime that is enough to run Node-API tests. +class NodeLiteRuntime { + struct PrivateTag {}; + + public: + static std::unique_ptr Create( + std::shared_ptr task_runner, + std::string js_root, + std::vector args); + + explicit NodeLiteRuntime(PrivateTag tag, + std::shared_ptr task_runner, + std::string js_root, + std::vector args); + + static void Run(std::vector args); + + NodeLiteModule& ResolveModule(const std::string& parent_module_path, + const std::string& module_path); + + std::filesystem::path ResolveModulePath(const std::string& parent_module_path, + const std::string& module_path); + + void RunTestScript(const std::string& script_path); + + void AddNativeModule( + const std::string& module_name, + std::function initModule); + + void HandleUnhandledPromiseRejections(); + void OnExit(); + void OnUncaughtException(napi_value error); + + std::string ProcessStack(std::string const& stack, + std::string const& assertMethod); + + static NodeLiteRuntime* GetRuntime(napi_env env); + + private: + void Initialize(); + void DefineGlobalFunctions(); + void DefineBuiltInModules(); + + private: + std::shared_ptr task_runner_; + std::string js_root_; + std::vector args_; + std::unique_ptr env_holder_; + napi_env env_{}; + std::unordered_map> + registered_modules_; + std::unordered_map node_js_modules_; + std::vector on_exit_callbacks_; + std::vector on_uncaughtException_callbacks_; +}; + +class NodeLitePlatform { + public: + static void* LoadFunction(napi_env env, + const std::filesystem::path& lib_path, + const std::string& function_name) noexcept; +}; + +using NodeApiCallback = + std::function args)>; + +// Wraps up Node-API function calls. +// To simplify usage patterns it throws NodeApiException on errors. +class NodeApi { + public: + static bool IsExceptionPending(napi_env env); + + static napi_value GetAndClearLastException(napi_env env); + + static void ThrowError(napi_env env, napi_value error); + + static void ThrowError(napi_env env, const char* error_message); + + static napi_value GetNull(napi_env env); + + static napi_value GetUndefined(napi_env env); + + static napi_value GetGlobal(napi_env env); + + static napi_value GetBoolean(napi_env env, bool value); + + static napi_value GetReferenceValue(napi_env env, napi_ref ref); + + static napi_value CreateUInt32(napi_env env, std::uint32_t value); + + static napi_value CreateString(napi_env env, std::string_view value); + + static napi_value CreateStringArray(napi_env env, + std::vector const& value); + + static napi_value CreateObject(napi_env env); + + static napi_value CreateExternal(napi_env env, void* data); + + static int32_t GetValueInt32(napi_env env, napi_value value); + + static uint32_t GetValueUInt32(napi_env env, napi_value value); + + static void* GetValueExternal(napi_env env, napi_value value); + + static bool HasProperty(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static napi_value GetProperty(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static std::string GetPropertyString(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static int32_t GetPropertyInt32(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static void SetProperty(napi_env env, + napi_value obj, + std::string_view utf8_name, + napi_value value); + + static void SetPropertyUInt32(napi_env env, + napi_value obj, + std::string_view utf8_name, + uint32_t value); + + static void SetPropertyString(napi_env env, + napi_value obj, + std::string_view utf8_name, + std::string_view value); + + static void SetPropertyStringArray(napi_env env, + napi_value obj, + std::string_view utf8_name, + std::vector const& value); + + static void SetPropertyNull(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static void SetMethod(napi_env env, + napi_value obj, + std::string_view utf8_name, + NodeApiCallback cb); + + static bool DeleteProperty(napi_env env, + napi_value obj, + std::string_view utf8_name); + + static std::string CoerceToString(napi_env env, napi_value value); + + static std::string ToStdString(napi_env env, napi_value value); + + static std::vector ToStdStringArray(napi_env env, + napi_value value); + + static napi_value RunScript(napi_env env, napi_value script); + + static napi_value RunScript(napi_env env, + const std::string& code, + char const* source_url); + + static napi_valuetype TypeOf(napi_env env, napi_value value); + + static napi_value CallFunction(napi_env env, + napi_value func, + span args); + + static napi_value CreateFunction(napi_env env, + std::string_view name, + NodeApiCallback cb); +}; + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_NODE_LITE_H \ No newline at end of file diff --git a/Tests/NodeApi/node_lite_jsruntimehost.cpp b/Tests/NodeApi/node_lite_jsruntimehost.cpp new file mode 100644 index 00000000..6833672e --- /dev/null +++ b/Tests/NodeApi/node_lite_jsruntimehost.cpp @@ -0,0 +1,90 @@ +#include "node_lite.h" + +#include +#include + +#include +#include + +#if defined(__APPLE__) +#include +#include "js_native_api_javascriptcore.h" +#elif defined(__ANDROID__) +#include +#include "js_native_api_v8.h" +#endif + +namespace node_api_tests { + +namespace { + +class JsRuntimeHostEnvHolder : public IEnvHolder { + public: + JsRuntimeHostEnvHolder( + std::shared_ptr /*taskRunner*/, + std::function onUnhandledError) + : onUnhandledError_(std::move(onUnhandledError)) { +#if defined(__APPLE__) + context_ = JSGlobalContextCreateInGroup(nullptr, nullptr); + env_ = Napi::Attach(context_); +#elif defined(__ANDROID__) + // TODO: Implement a dedicated V8 environment for Android Node-API tests. + // For now we surface a clear failure so we remember to provide a proper + // implementation before enabling Android execution. + (void)onUnhandledError_; + throw std::runtime_error( + "Android Node-API tests are not yet implemented for node_lite."); +#else + (void)onUnhandledError_; + throw std::runtime_error( + "node_lite is only implemented for Apple platforms in this port."); +#endif + } + + ~JsRuntimeHostEnvHolder() override { +#if defined(__APPLE__) + if (env_ != nullptr) { + Napi::Env napiEnv{env_}; + + if (onUnhandledError_) { + bool hasPending = false; + if (napi_is_exception_pending(env_, &hasPending) == napi_ok && + hasPending) { + napi_value error{}; + if (napi_get_and_clear_last_exception(env_, &error) == napi_ok) { + onUnhandledError_(env_, error); + } + } + } + + Napi::Detach(napiEnv); + env_ = nullptr; + } + + if (context_ != nullptr) { + JSGlobalContextRelease(context_); + context_ = nullptr; + } +#endif + } + + napi_env getEnv() override { return env_; } + + private: +#if defined(__APPLE__) + JSGlobalContextRef context_{}; +#endif + napi_env env_{}; + std::function onUnhandledError_{}; +}; + +} // namespace + +std::unique_ptr CreateEnvHolder( + std::shared_ptr taskRunner, + std::function onUnhandledError) { + return std::make_unique( + std::move(taskRunner), std::move(onUnhandledError)); +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/node_lite_mac.cpp b/Tests/NodeApi/node_lite_mac.cpp new file mode 100644 index 00000000..ce8ffb17 --- /dev/null +++ b/Tests/NodeApi/node_lite_mac.cpp @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include "node_lite.h" +#include "string_utils.h" + +namespace node_api_tests { + +//============================================================================= +// NodeLitePlatform implementation +//============================================================================= + +/*static*/ void* NodeLitePlatform::LoadFunction( + napi_env env, + const std::filesystem::path& lib_path, + const std::string& function_name) noexcept { + void* library_handle = dlopen(lib_path.string().c_str(), RTLD_NOW | RTLD_LOCAL); + if (library_handle == nullptr) { + const char* error_message = dlerror(); + NODE_LITE_ASSERT(false, + "Failed to load dynamic library: %s. Error: %s", + lib_path.c_str(), + error_message != nullptr ? error_message : "Unknown error"); + return nullptr; + } + + dlerror(); // Clear any existing error state before dlsym. + void* symbol = dlsym(library_handle, function_name.c_str()); + const char* error_message = dlerror(); + NODE_LITE_ASSERT(error_message == nullptr, + "Failed to resolve symbol: %s in %s. Error: %s", + function_name.c_str(), + lib_path.c_str(), + error_message != nullptr ? error_message : "Unknown error"); + return symbol; +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/node_lite_windows.cpp b/Tests/NodeApi/node_lite_windows.cpp new file mode 100644 index 00000000..6b41d83a --- /dev/null +++ b/Tests/NodeApi/node_lite_windows.cpp @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include "node_lite.h" +#include "string_utils.h" + +namespace node_api_tests { + +//============================================================================= +// NodeLitePlatform implementation +//============================================================================= + +/*static*/ void* NodeLitePlatform::LoadFunction( + napi_env env, + const std::filesystem::path& lib_path, + const std::string& function_name) noexcept { + HMODULE dll_module = ::LoadLibraryA(lib_path.string().c_str()); + NODE_LITE_ASSERT(dll_module != NULL, + "Failed to load DLL: %s. Error: %s", + lib_path.c_str(), + std::strerror(errno)); + return ::GetProcAddress(dll_module, function_name.c_str()); +} +} // namespace node_api_tests diff --git a/Tests/NodeApi/string_utils.cpp b/Tests/NodeApi/string_utils.cpp new file mode 100644 index 00000000..583f3e15 --- /dev/null +++ b/Tests/NodeApi/string_utils.cpp @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "string_utils.h" +#include + +namespace node_api_tests { + +std::string FormatString(const char *format, ...) noexcept { + va_list args1; + va_start(args1, format); + va_list args2; + va_copy(args2, args1); + std::string result = + std::string(std::vsnprintf(nullptr, 0, format, args1), '\0'); + va_end(args1); + std::vsnprintf(&result[0], result.size() + 1, format, args2); + va_end(args2); + return result; +} + +std::string ReplaceAll( + std::string str, + std::string_view from, + std::string_view to) noexcept { + std::string result = std::move(str); + if (from.empty()) + return result; + size_t start_pos = 0; + while ((start_pos = result.find(from, start_pos)) != std::string::npos) { + result.replace(start_pos, from.length(), to); + start_pos += to.length(); // In case if 'to' contains 'from', like + // replacing 'x' with 'yx' + } + return result; +} + +} // namespace node_api_tests \ No newline at end of file diff --git a/Tests/NodeApi/string_utils.h b/Tests/NodeApi/string_utils.h new file mode 100644 index 00000000..0d290208 --- /dev/null +++ b/Tests/NodeApi/string_utils.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef NODE_API_TEST_STRING_UTILS_H +#define NODE_API_TEST_STRING_UTILS_H + +#include +#include + +namespace node_api_tests { + +std::string FormatString(const char *format, ...) noexcept; + +std::string ReplaceAll( + std::string str, + std::string_view from, + std::string_view to) noexcept; + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_STRING_UTILS_H \ No newline at end of file diff --git a/Tests/NodeApi/test/.clang-format b/Tests/NodeApi/test/.clang-format new file mode 100644 index 00000000..b3fd9613 --- /dev/null +++ b/Tests/NodeApi/test/.clang-format @@ -0,0 +1,111 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 8 +UseTab: Never diff --git a/Tests/NodeApi/test/CMakeLists.txt b/Tests/NodeApi/test/CMakeLists.txt new file mode 100644 index 00000000..01521826 --- /dev/null +++ b/Tests/NodeApi/test/CMakeLists.txt @@ -0,0 +1,91 @@ +if(WIN32) + # "npx" interrupts current shell script execution without the "call" + set(npx cmd /c npx) + set(yarn cmd /c yarn) +else() + set(npx "npx") + set(yarn "yarn") +endif() + +# copy JS Tools package files +add_custom_command( + OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/babel.config.js + ${CMAKE_CURRENT_BINARY_DIR}/package.json + ${CMAKE_CURRENT_BINARY_DIR}/yarn.lock + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/babel.config.js + ${CMAKE_CURRENT_SOURCE_DIR}/package.json + ${CMAKE_CURRENT_SOURCE_DIR}/yarn.lock + ${CMAKE_CURRENT_BINARY_DIR} +) +add_custom_target(copyNodeApiJSToolsFiles) + +# run "yarn install" +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/node_modules.sha1 + DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/package.json + ${CMAKE_CURRENT_BINARY_DIR}/yarn.lock + COMMAND ${yarn} install --frozen-lockfile + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) +add_custom_target(installNodeApiTestJsTools + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/node_modules.sha1 +) +add_dependencies(installNodeApiTestJsTools copyNodeApiJSToolsFiles) + +# add the Babel transform commands for each test JS file +set(testJSRootDir ${CMAKE_CURRENT_SOURCE_DIR}) + +# Collect all .js files recursively +file(GLOB_RECURSE basicsTestJSFiles "basics/*.js") +file(GLOB_RECURSE commonTestJSFiles "common/*.js") +file(GLOB_RECURSE jsNativeApiTestJSFiles "js-native-api/*.js") +set(testJSFiles + ${basicsTestJSFiles} + ${commonTestJSFiles} + ${jsNativeApiTestJSFiles}) + +foreach(absoluteTestJSFile ${testJSFiles}) + # create target directory + file(RELATIVE_PATH testJSFile ${testJSRootDir} ${absoluteTestJSFile}) + get_filename_component(testJSDir ${testJSFile} DIRECTORY) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${testJSDir}) + + # generate Hermes-compatible JavaScript code + add_custom_command( + OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/${testJSFile} + ${CMAKE_CURRENT_BINARY_DIR}/${testJSFile}.map + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${testJSFile} + COMMAND + ${npx} + "babel" + "--retain-lines" + "--source-maps" + "true" + "--out-file" + "${CMAKE_CURRENT_BINARY_DIR}/${testJSFile}" + "--source-map-target" + "${CMAKE_CURRENT_BINARY_DIR}/${testJSFile}.map" + "${CMAKE_CURRENT_SOURCE_DIR}/${testJSFile}" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + + # build a list of all outputs + list(APPEND transformedJSFiles + ${CMAKE_CURRENT_BINARY_DIR}/${testJSFile} + ${CMAKE_CURRENT_BINARY_DIR}/${testJSFile}.map + ) +endforeach() + +# run the Babel transforms for all required output files +add_custom_target(transformJSFiles + DEPENDS ${transformedJSFiles} +) +add_dependencies(transformJSFiles installNodeApiTestJsTools) + +if(JSR_NODE_API_BUILD_NATIVE_TESTS) + add_subdirectory(js-native-api) +endif() diff --git a/Tests/NodeApi/test/babel.config.js b/Tests/NodeApi/test/babel.config.js new file mode 100644 index 00000000..c8cda4fe --- /dev/null +++ b/Tests/NodeApi/test/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['module:@react-native/babel-preset', + { "unstable_transformProfile": "hermes-canary"}] + ], +}; \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/async_rejected.js b/Tests/NodeApi/test/basics/async_rejected.js new file mode 100644 index 00000000..3c2d956a --- /dev/null +++ b/Tests/NodeApi/test/basics/async_rejected.js @@ -0,0 +1,19 @@ +function resolveAfterTimeout() { + return new Promise((_, reject) => { + setTimeout(() => { + reject("test async rejected"); + }, 0); + }); +} + +async function asyncCall() { + console.log("test async calling"); + try { + const result = await resolveAfterTimeout(); + console.log(`Unexpected: ${result}`); + } catch (error) { + console.error(`Expected: ${error}`); + } +} + +asyncCall(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/async_resolved.js b/Tests/NodeApi/test/basics/async_resolved.js new file mode 100644 index 00000000..d9b44145 --- /dev/null +++ b/Tests/NodeApi/test/basics/async_resolved.js @@ -0,0 +1,15 @@ +function resolveAfterTimeout() { + return new Promise((resolve) => { + setTimeout(() => { + resolve("test async resolved"); + }, 0); + }); +} + +async function asyncCall() { + console.log("test async calling"); + const result = await resolveAfterTimeout(); + console.log(`Expected: ${result}`); +} + +asyncCall(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/hello.js b/Tests/NodeApi/test/basics/hello.js new file mode 100644 index 00000000..7a2bb74e --- /dev/null +++ b/Tests/NodeApi/test/basics/hello.js @@ -0,0 +1 @@ +console.log("Hello"); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/mustcall_failure.js b/Tests/NodeApi/test/basics/mustcall_failure.js new file mode 100644 index 00000000..105650f8 --- /dev/null +++ b/Tests/NodeApi/test/basics/mustcall_failure.js @@ -0,0 +1,3 @@ +const common = require('../common'); + +common.mustCall(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/mustcall_success.js b/Tests/NodeApi/test/basics/mustcall_success.js new file mode 100644 index 00000000..cec9ac6a --- /dev/null +++ b/Tests/NodeApi/test/basics/mustcall_success.js @@ -0,0 +1,4 @@ +const common = require('../common'); + +var fn = common.mustCall(); +fn(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/mustnotcall_failure.js b/Tests/NodeApi/test/basics/mustnotcall_failure.js new file mode 100644 index 00000000..49db06cc --- /dev/null +++ b/Tests/NodeApi/test/basics/mustnotcall_failure.js @@ -0,0 +1,4 @@ +const common = require('../common'); + +var fn = common.mustNotCall(); +fn(); \ No newline at end of file diff --git a/Tests/NodeApi/test/basics/mustnotcall_success.js b/Tests/NodeApi/test/basics/mustnotcall_success.js new file mode 100644 index 00000000..b933a30f --- /dev/null +++ b/Tests/NodeApi/test/basics/mustnotcall_success.js @@ -0,0 +1,3 @@ +const common = require('../common'); + +common.mustNotCall(); diff --git a/Tests/NodeApi/test/basics/throw_string.js b/Tests/NodeApi/test/basics/throw_string.js new file mode 100644 index 00000000..f6cc7b60 --- /dev/null +++ b/Tests/NodeApi/test/basics/throw_string.js @@ -0,0 +1 @@ +throw "Script failed"; \ No newline at end of file diff --git a/Tests/NodeApi/test/common/assert.js b/Tests/NodeApi/test/common/assert.js new file mode 100644 index 00000000..12bb4dac --- /dev/null +++ b/Tests/NodeApi/test/common/assert.js @@ -0,0 +1,400 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// The JavaScript code in this file is adopted from the Node.js project. +// See the src\napi\Readme.md about the Node.js copyright notice. + +"use strict"; + +class AssertionError extends Error { + constructor(options) { + const { message, actual, expected, method, errorStack } = options; + + super(String(message)); + + this.name = "AssertionError"; + this.method = String(method); + this.actual = String(actual); + this.expected = String(expected); + this.errorStack = errorStack || ""; + setAssertionSource(this, method); + } +} + +function setAssertionSource(error, method) { + let result = { sourceFile: "", sourceLine: 0 }; + const stackArray = (error.errorStack || error.stack).split("\n"); + const methodNamePattern = `${method} (`; + let methodNameFound = false; + for (const stackFrame of stackArray) { + if (methodNameFound) { + const stackFrameParts = stackFrame.split(":"); + if (stackFrameParts.length >= 2) { + let sourceFile = stackFrameParts[0]; + if (sourceFile.startsWith(" at ")) { + sourceFile = sourceFile.substr(7); + } + result = { sourceFile, sourceLine: Number(stackFrameParts[1]) }; + } + break; + } else { + methodNameFound = stackFrame.indexOf(methodNamePattern) >= 0; + } + } + Object.assign(error, result); +} + +const assert = (module.exports = ok); + +assert.fail = function fail(message) { + message = message || "Failed"; + let errorInfo = message; + if (typeof message !== "object") { + errorInfo = { message, method: fail.name }; + } + throw new AssertionError(errorInfo); +}; + +function innerOk(fn, argLen, value, message) { + if (!value) { + if (argLen === 0) { + message = "No value argument passed to `assert.ok()`"; + } else if (message == null) { + message = "The expression evaluated to a falsy value"; + } + + assert.fail({ + message, + actual: formatValue(value), + expected: formatValue(true), + method: fn.name, + }); + } +} + +// Pure assertion tests whether a value is truthy, as determined by !!value. +function ok(...args) { + innerOk(ok, args.length, ...args); +} +assert.ok = ok; + +let compareErrorMessage = undefined; +function innerComparison( + method, + compare, + defaultMessage, + argLen, + actual, + expected, + message +) { + if (!compare(actual, expected)) { + if (argLen < 2) { + message = `'assert.${method.name}' expects two or more arguments.`; + } else if (message == null) { + message = defaultMessage; + } + if (typeof compareErrorMessage === "string") { + message += "; " + compareErrorMessage; + compareErrorMessage = undefined; + } + assert.fail({ + message, + actual: formatValue(actual), + expected: formatValue(expected), + method: method.name, + }); + } +} + +assert.strictEqual = function strictEqual(...args) { + innerComparison( + strictEqual, + Object.is, + "Values are not strict equal", + args.length, + ...args + ); +}; + +assert.notStrictEqual = function notStrictEqual(...args) { + innerComparison( + notStrictEqual, + negate(Object.is), + "Values must not be strict equal", + args.length, + ...args + ); +}; + +assert.deepStrictEqual = function deepStrictEqual(...args) { + innerComparison( + deepStrictEqual, + isDeepStrictEqual, + "Values are not deep strict equal", + args.length, + ...args + ); +}; + +assert.notDeepStrictEqual = function notDeepStrictEqual(...args) { + innerComparison( + notDeepStrictEqual, + negate(isDeepStrictEqual), + "Values must not be deep strict equal", + args.length, + ...args + ); +}; + +function innerThrows(method, argLen, fn, expected, message) { + let actual = "Did not throw"; + function succeeds() { + try { + fn(); + return false; + } catch (error) { + if (typeof expected === "function") { + if (expected.prototype !== undefined && error instanceof expected) { + return true; + } else { + return expected(error); + } + } else if (expected instanceof RegExp) { + actual = `${error.name}: ${error.message}`; + return expected.test(actual); + } else if (expected) { + actual = `${error.name}: ${error.message}`; + if (expected.name && expected.name != error.name) { + return false; + } else if (expected.message && expected.message != error.message) { + return false; + } else if (expected.code && expected.code != error.code) { + return false; + } + } + return true; + } + } + + if (argLen < 1 || typeof fn !== "function") { + message = `'assert.${method.name}' expects a function parameter.`; + } else if (message == null) { + if (expected) { + message = `'assert.${method.name}' failed to throw an exception that matches '${expected}'.`; + } else { + message = `'assert.${method.name}' failed to throw an exception.`; + } + } + + if (!succeeds()) { + throw new AssertionError({ + message, + actual, + expected, + method: method.name, + }); + } +} + +assert.throws = function throws(...args) { + innerThrows(throws, args.length, ...args); +}; + +function innerMatch(method, argLen, value, expected, message) { + let succeeds = false; + if (argLen < 1 || typeof value !== "string") { + message = `'assert.${method.name}' expects a string parameter.`; + } else if (!(expected instanceof RegExp)) { + message = `'assert.${method.name}' expects a RegExp as a second parameter.`; + } else { + succeeds = expected.test(value); + if (!succeeds && message == null) { + message = `'assert.${method.name}' failed to match '${expected}'.`; + } + } + + if (!succeeds) { + throw new AssertionError({ + message, + actual: value, + expected, + method: method.name, + }); + } +} + +assert.match = function match(...args) { + innerMatch(match, args.length, ...args); +}; + +function negate(compare) { + return (...args) => !compare(...args); +} + +function isDeepStrictEqual(left, right) { + function check(left, right) { + if (left === right) { + return true; + } + if (typeof left !== typeof right) { + compareErrorMessage = `Different types: ${typeof left} vs ${typeof right}`; + return false; + } + if (Array.isArray(left)) { + return Array.isArray(right) && checkArray(left, right); + } + if (typeof left === "number") { + return isNaN(left) && isNaN(right); + } + if (typeof left === "object") { + return typeof right === "object" && checkObject(left, right); + } + return false; + } + + function checkArray(left, right) { + if (left.length !== right.length) { + compareErrorMessage = `Different array lengths: ${left.length} vs ${right.length}`; + return false; + } + for (let i = 0; i < left.length; ++i) { + if (!check(left[i], right[i])) { + compareErrorMessage = `Different values at index ${i}: ${left[i]} vs ${right[i]}`; + return false; + } + } + return true; + } + + function checkObject(left, right) { + const leftNames = Object.getOwnPropertyNames(left); + const rightNames = Object.getOwnPropertyNames(right); + if (leftNames.length !== rightNames.length) { + compareErrorMessage = `Different set of property names: ${leftNames.length} vs ${rightNames.length}`; + return false; + } + for (let i = 0; i < leftNames.length; ++i) { + if (!check(left[leftNames[i]], right[leftNames[i]])) { + compareErrorMessage = `Different values for property '${leftNames[i]}': ${left[leftNames[i]]} vs ${right[leftNames[i]]}`; + return false; + } + } + const leftSymbols = Object.getOwnPropertySymbols(left); + const rightSymbols = Object.getOwnPropertySymbols(right); + if (leftSymbols.length !== rightSymbols.length) { + compareErrorMessage = `Different set of symbol names: ${leftSymbols.length} vs ${rightSymbols.length}`; + return false; + } + for (let i = 0; i < leftSymbols.length; ++i) { + if (!check(left[leftSymbols[i]], right[leftSymbols[i]])) { + compareErrorMessage = `${leftSymbols[i].toString()}: different value`; + return false; + } + } + return check(Object.getPrototypeOf(left), Object.getPrototypeOf(right)); + } + + return check(left, right); +} + +const mustCallChecks = []; + +function runCallChecks() { + const failed = mustCallChecks.filter((context) => { + if ("minimum" in context) { + context.messageSegment = `at least ${context.minimum}`; + return context.actual < context.minimum; + } + context.messageSegment = `exactly ${context.exact}`; + return context.actual !== context.exact; + }); + + mustCallChecks.length = 0; + + failed.forEach((context) => { + assert.fail({ + message: `Mismatched ${context.name} function calls`, + actual: `${context.actual} calls`, + expected: `${context.messageSegment} calls`, + method: context.method.name, + errorStack: context.stack, + }); + }); +}; +assert.runCallChecks = runCallChecks; + +function getCallSite() { + try { + throw new Error(""); + } catch (err) { + return err.stack; + } +} + +assert.mustNotCall = function mustNotCall(msg) { + return function mustNotCall(...args) { + assert.fail({ + message: String(msg || "Function should not have been called"), + actual: + args.length > 0 + ? `Called with arguments: ${args.map(String).join(", ")}` + : "Called without arguments", + expected: "Not to be called", + method: mustNotCall.name, + }); + }; +}; + +assert.mustCall = function mustCall(fn, exact) { + return _mustCallInner(fn, exact, "exact", mustCall); +}; + +assert.mustCallAtLeast = function mustCallAtLeast(fn, minimum) { + return _mustCallInner(fn, minimum, "minimum", mustCallAtLeast); +}; + +const noop = () => {}; + +function _mustCallInner(fn, criteria = 1, field, method) { + if (typeof fn === "number") { + criteria = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + + if (typeof criteria !== "number") { + throw new TypeError(`Invalid ${field} value: ${criteria}`); + } + + const context = { + [field]: criteria, + actual: 0, + stack: getCallSite(), + name: fn.name || "", + method, + }; + + // Add the exit listener only once to avoid listener leak warnings + if (mustCallChecks.length === 0) process.on('exit', runCallChecks); + + mustCallChecks.push(context); + + return function () { + context.actual++; + return fn.apply(this, arguments); + }; +} + +function formatValue(value) { + let type = typeof value; + if (type === "object") { + if (Array.isArray(value)) { + return " []"; + } else { + return " {}"; + } + } + return `<${type}> ${value}`; +} diff --git a/Tests/NodeApi/test/common/gc.js b/Tests/NodeApi/test/common/gc.js new file mode 100644 index 00000000..66f77055 --- /dev/null +++ b/Tests/NodeApi/test/common/gc.js @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// The JavaScript code in this file is adopted from the Node.js project. +// See the src\napi\Readme.md about the Node.js copyright notice. +"use strict"; + +function gcUntil(name, condition) { + if (typeof name === "function") { + condition = name; + name = undefined; + } + return new Promise((resolve, reject) => { + let count = 0; + function gcAndCheck() { + setImmediate(() => { + count++; + global.gc(); + if (condition()) { + resolve(); + } else if (count < 10) { + gcAndCheck(); + } else { + reject(name === undefined ? undefined : "Test " + name + " failed"); + } + }); + } + gcAndCheck(); + }); +} + +Object.assign(module.exports, { + gcUntil, +}); diff --git a/Tests/NodeApi/test/common/index.js b/Tests/NodeApi/test/common/index.js new file mode 100644 index 00000000..e4ec6151 --- /dev/null +++ b/Tests/NodeApi/test/common/index.js @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// The JavaScript code in this file is adopted from the Node.js project. +// See the src\napi\Readme.md about the Node.js copyright notice. +"use strict"; + +const { mustCall, mustCallAtLeast, mustNotCall } = require("assert"); +const { gcUntil } = require("gc"); + +const buildType = process.target_config; +const isWindows = process.platform === 'win32'; + +// Returns true if the exit code "exitCode" and/or signal name "signal" +// represent the exit code and/or signal name of a node process that aborted, +// false otherwise. +function nodeProcessAborted(exitCode, signal) { + // Depending on the compiler used, node will exit with either + // exit code 132 (SIGILL), 133 (SIGTRAP) or 134 (SIGABRT). + let expectedExitCodes = [132, 133, 134]; + + // On platforms using KSH as the default shell (like SmartOS), + // when a process aborts, KSH exits with an exit code that is + // greater than 256, and thus the exit code emitted with the 'exit' + // event is null and the signal is set to either SIGILL, SIGTRAP, + // or SIGABRT (depending on the compiler). + const expectedSignals = ['SIGILL', 'SIGTRAP', 'SIGABRT']; + + // On Windows, 'aborts' are of 2 types, depending on the context: + // (i) Exception breakpoint, if --abort-on-uncaught-exception is on + // which corresponds to exit code 2147483651 (0x80000003) + // (ii) Otherwise, _exit(134) which is called in place of abort() due to + // raising SIGABRT exiting with ambiguous exit code '3' by default + if (isWindows) + expectedExitCodes = [0x80000003, 134]; + + // When using --abort-on-uncaught-exception, V8 will use + // base::OS::Abort to terminate the process. + // Depending on the compiler used, the shell or other aspects of + // the platform used to build the node binary, this will actually + // make V8 exit by aborting or by raising a signal. In any case, + // one of them (exit code or signal) needs to be set to one of + // the expected exit codes or signals. + if (signal !== null) { + return expectedSignals.includes(signal); + } + return expectedExitCodes.includes(exitCode); +} + +Object.assign(module.exports, { + buildType, + gcUntil, + mustCall, + mustCallAtLeast, + mustNotCall, + nodeProcessAborted, +}); \ No newline at end of file diff --git a/Tests/NodeApi/test/js-native-api/.gitignore b/Tests/NodeApi/test/js-native-api/.gitignore new file mode 100644 index 00000000..6e29bde8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/.gitignore @@ -0,0 +1,7 @@ +.buildstamp +.docbuildstamp +Makefile +*.Makefile +*.mk +gyp-mac-tool +/*/build diff --git a/Tests/NodeApi/test/js-native-api/2_function_arguments/2_function_arguments.c b/Tests/NodeApi/test/js-native-api/2_function_arguments/2_function_arguments.c new file mode 100644 index 00000000..c03085db --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/2_function_arguments/2_function_arguments.c @@ -0,0 +1,39 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value Add(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype0 == napi_number && valuetype1 == napi_number, + "Wrong argument type. Numbers expected."); + + double value0; + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value0)); + + double value1; + NODE_API_CALL(env, napi_get_value_double(env, args[1], &value1)); + + napi_value sum; + NODE_API_CALL(env, napi_create_double(env, value0 + value1, &sum)); + + return sum; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor desc = DECLARE_NODE_API_PROPERTY("add", Add); + NODE_API_CALL(env, napi_define_properties(env, exports, 1, &desc)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/2_function_arguments/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/2_function_arguments/CMakeLists.txt new file mode 100644 index 00000000..258ed16e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/2_function_arguments/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(2_function_arguments + SOURCES + 2_function_arguments.c +) diff --git a/Tests/NodeApi/test/js-native-api/2_function_arguments/binding.gyp b/Tests/NodeApi/test/js-native-api/2_function_arguments/binding.gyp new file mode 100644 index 00000000..8f89a61e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/2_function_arguments/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "2_function_arguments", + "sources": [ + "2_function_arguments.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/2_function_arguments/test.js b/Tests/NodeApi/test/js-native-api/2_function_arguments/test.js new file mode 100644 index 00000000..2966cc0b --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/2_function_arguments/test.js @@ -0,0 +1,6 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/2_function_arguments`); + +assert.strictEqual(addon.add(3, 5), 8); diff --git a/Tests/NodeApi/test/js-native-api/3_callbacks/3_callbacks.c b/Tests/NodeApi/test/js-native-api/3_callbacks/3_callbacks.c new file mode 100644 index 00000000..fd7b6618 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/3_callbacks/3_callbacks.c @@ -0,0 +1,58 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value RunCallback(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, + "Wrong number of arguments. Expects a single argument."); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + NODE_API_ASSERT(env, valuetype0 == napi_function, + "Wrong type of arguments. Expects a function as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + NODE_API_ASSERT(env, valuetype1 == napi_undefined, + "Additional arguments should be undefined."); + + napi_value argv[1]; + const char* str = "hello world"; + size_t str_len = strlen(str); + NODE_API_CALL(env, napi_create_string_utf8(env, str, str_len, argv)); + + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + + napi_value cb = args[0]; + NODE_API_CALL(env, napi_call_function(env, global, cb, 1, argv, NULL)); + + return NULL; +} + +static napi_value RunCallbackWithRecv(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value cb = args[0]; + napi_value recv = args[1]; + NODE_API_CALL(env, napi_call_function(env, recv, cb, 0, NULL, NULL)); + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor desc[2] = { + DECLARE_NODE_API_PROPERTY("RunCallback", RunCallback), + DECLARE_NODE_API_PROPERTY("RunCallbackWithRecv", RunCallbackWithRecv), + }; + NODE_API_CALL(env, napi_define_properties(env, exports, 2, desc)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/3_callbacks/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/3_callbacks/CMakeLists.txt new file mode 100644 index 00000000..02134621 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/3_callbacks/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(3_callbacks + SOURCES + 3_callbacks.c +) diff --git a/Tests/NodeApi/test/js-native-api/3_callbacks/binding.gyp b/Tests/NodeApi/test/js-native-api/3_callbacks/binding.gyp new file mode 100644 index 00000000..d64b5e48 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/3_callbacks/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "3_callbacks", + "sources": [ + "3_callbacks.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/3_callbacks/test.js b/Tests/NodeApi/test/js-native-api/3_callbacks/test.js new file mode 100644 index 00000000..ace0f2a7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/3_callbacks/test.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/3_callbacks`); + +addon.RunCallback(function(msg) { + assert.strictEqual(msg, 'hello world'); +}); + +function testRecv(desiredRecv) { + addon.RunCallbackWithRecv(function() { + assert.strictEqual(this, desiredRecv); + }, desiredRecv); +} + +testRecv(undefined); +testRecv(null); +testRecv(5); +testRecv(true); +testRecv('Hello'); +testRecv([]); +testRecv({}); diff --git a/Tests/NodeApi/test/js-native-api/4_object_factory/4_object_factory.c b/Tests/NodeApi/test/js-native-api/4_object_factory/4_object_factory.c new file mode 100644 index 00000000..38169b0f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/4_object_factory/4_object_factory.c @@ -0,0 +1,24 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value CreateObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value obj; + NODE_API_CALL(env, napi_create_object(env, &obj)); + + NODE_API_CALL(env, napi_set_named_property(env, obj, "msg", args[0])); + + return obj; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + NODE_API_CALL(env, + napi_create_function(env, "exports", -1, CreateObject, NULL, &exports)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/4_object_factory/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/4_object_factory/CMakeLists.txt new file mode 100644 index 00000000..b4a9917c --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/4_object_factory/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(4_object_factory + SOURCES + 4_object_factory.c +) diff --git a/Tests/NodeApi/test/js-native-api/4_object_factory/binding.gyp b/Tests/NodeApi/test/js-native-api/4_object_factory/binding.gyp new file mode 100644 index 00000000..86f8b1f0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/4_object_factory/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "4_object_factory", + "sources": [ + "4_object_factory.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/4_object_factory/test.js b/Tests/NodeApi/test/js-native-api/4_object_factory/test.js new file mode 100644 index 00000000..fbfbd67f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/4_object_factory/test.js @@ -0,0 +1,8 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/4_object_factory`); + +const obj1 = addon('hello'); +const obj2 = addon('world'); +assert.strictEqual(`${obj1.msg} ${obj2.msg}`, 'hello world'); diff --git a/Tests/NodeApi/test/js-native-api/5_function_factory/5_function_factory.c b/Tests/NodeApi/test/js-native-api/5_function_factory/5_function_factory.c new file mode 100644 index 00000000..744a8c72 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/5_function_factory/5_function_factory.c @@ -0,0 +1,24 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value MyFunction(napi_env env, napi_callback_info info) { + napi_value str; + NODE_API_CALL(env, napi_create_string_utf8(env, "hello world", -1, &str)); + return str; +} + +static napi_value CreateFunction(napi_env env, napi_callback_info info) { + napi_value fn; + NODE_API_CALL(env, + napi_create_function(env, "theFunction", -1, MyFunction, NULL, &fn)); + return fn; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + NODE_API_CALL(env, + napi_create_function(env, "exports", -1, CreateFunction, NULL, &exports)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/5_function_factory/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/5_function_factory/CMakeLists.txt new file mode 100644 index 00000000..4a7e51a7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/5_function_factory/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(5_function_factory + SOURCES + 5_function_factory.c +) diff --git a/Tests/NodeApi/test/js-native-api/5_function_factory/binding.gyp b/Tests/NodeApi/test/js-native-api/5_function_factory/binding.gyp new file mode 100644 index 00000000..06bd385e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/5_function_factory/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "5_function_factory", + "sources": [ + "5_function_factory.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/5_function_factory/test.js b/Tests/NodeApi/test/js-native-api/5_function_factory/test.js new file mode 100644 index 00000000..bacb22ce --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/5_function_factory/test.js @@ -0,0 +1,7 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/5_function_factory`); + +const fn = addon(); +assert.strictEqual(fn(), 'hello world'); // 'hello world' diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/6_object_wrap/CMakeLists.txt new file mode 100644 index 00000000..27486730 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/CMakeLists.txt @@ -0,0 +1,21 @@ +add_node_api_module(myobject + SOURCES + myobject.cc + myobject.h +) + +add_node_api_module(myobject_basic_finalizer + SOURCES + myobject.cc + myobject.h + DEFINES + NAPI_EXPERIMENTAL +) + +add_node_api_module(nested_wrap + SOURCES + nested_wrap.cc + nested_wrap.h + DEFINES + "NAPI_VERSION=10" +) diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/binding.gyp b/Tests/NodeApi/test/js-native-api/6_object_wrap/binding.gyp new file mode 100644 index 00000000..e7a9d7ba --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/binding.gyp @@ -0,0 +1,28 @@ +{ + "targets": [ + { + "target_name": "myobject", + "sources": [ + "myobject.cc", + "myobject.h", + ] + }, + { + "target_name": "myobject_basic_finalizer", + "defines": [ "NAPI_EXPERIMENTAL" ], + "sources": [ + "myobject.cc", + "myobject.h", + ] + }, + { + "target_name": "nested_wrap", + # Test without basic finalizers as it schedules differently. + "defines": [ "NAPI_VERSION=10" ], + "sources": [ + "nested_wrap.cc", + "nested_wrap.h", + ], + }, + ] +} diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.cc b/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.cc new file mode 100644 index 00000000..5633d929 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.cc @@ -0,0 +1,270 @@ +#include "myobject.h" +#include "../common.h" +#include "../entry_point.h" +#include "assert.h" + +typedef int32_t FinalizerData; + +napi_ref MyObject::constructor; + +MyObject::MyObject(double value) + : value_(value), env_(nullptr), wrapper_(nullptr) {} + +MyObject::~MyObject() { + napi_delete_reference(env_, wrapper_); +} + +void MyObject::Destructor(node_api_basic_env env, + void* nativeObject, + void* /*finalize_hint*/) { + MyObject* obj = static_cast(nativeObject); + delete obj; + + FinalizerData* data; + NODE_API_BASIC_CALL_RETURN_VOID( + env, napi_get_instance_data(env, reinterpret_cast(&data))); + *data += 1; +} + +void MyObject::Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + {"value", nullptr, nullptr, GetValue, SetValue, 0, napi_default, 0}, + {"valueReadonly", + nullptr, + nullptr, + GetValue, + nullptr, + 0, + napi_default, + 0}, + DECLARE_NODE_API_PROPERTY("plusOne", PlusOne), + DECLARE_NODE_API_PROPERTY("multiply", Multiply), + }; + + napi_value cons; + NODE_API_CALL_RETURN_VOID( + env, + napi_define_class(env, + "MyObject", + -1, + New, + nullptr, + sizeof(properties) / sizeof(napi_property_descriptor), + properties, + &cons)); + + NODE_API_CALL_RETURN_VOID(env, + napi_create_reference(env, cons, 1, &constructor)); + + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, exports, "MyObject", cons)); +} + +napi_value MyObject::New(napi_env env, napi_callback_info info) { + napi_value new_target; + NODE_API_CALL(env, napi_get_new_target(env, info, &new_target)); + bool is_constructor = (new_target != nullptr); + + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + if (is_constructor) { + // Invoked as constructor: `new MyObject(...)` + double value = 0; + + napi_valuetype valuetype; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype)); + + if (valuetype != napi_undefined) { + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value)); + } + + MyObject* obj = new MyObject(value); + + obj->env_ = env; + NODE_API_CALL(env, + napi_wrap(env, + _this, + obj, + MyObject::Destructor, + nullptr /* finalize_hint */, + &obj->wrapper_)); + + return _this; + } + + // Invoked as plain function `MyObject(...)`, turn into construct call. + argc = 1; + napi_value argv[1] = {args[0]}; + + napi_value cons; + NODE_API_CALL(env, napi_get_reference_value(env, constructor, &cons)); + + napi_value instance; + NODE_API_CALL(env, napi_new_instance(env, cons, argc, argv, &instance)); + + return instance; +} + +napi_value MyObject::GetValue(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, + napi_get_cb_info(env, info, nullptr, nullptr, &_this, nullptr)); + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + napi_value num; + NODE_API_CALL(env, napi_create_double(env, obj->value_, &num)); + + return num; +} + +napi_value MyObject::SetValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + NODE_API_CALL(env, napi_get_value_double(env, args[0], &obj->value_)); + + return nullptr; +} + +napi_value MyObject::PlusOne(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, + napi_get_cb_info(env, info, nullptr, nullptr, &_this, nullptr)); + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + obj->value_ += 1; + + napi_value num; + NODE_API_CALL(env, napi_create_double(env, obj->value_, &num)); + + return num; +} + +napi_value MyObject::Multiply(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + double multiple = 1; + if (argc >= 1) { + NODE_API_CALL(env, napi_get_value_double(env, args[0], &multiple)); + } + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + napi_value cons; + NODE_API_CALL(env, napi_get_reference_value(env, constructor, &cons)); + + const int kArgCount = 1; + napi_value argv[kArgCount]; + NODE_API_CALL(env, napi_create_double(env, obj->value_ * multiple, argv)); + + napi_value instance; + NODE_API_CALL(env, napi_new_instance(env, cons, kArgCount, argv, &instance)); + + return instance; +} + +// This finalizer should never be invoked. +void ObjectWrapDanglingReferenceFinalizer(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) { + assert(0 && "unreachable"); +} + +napi_ref dangling_ref; +napi_value ObjectWrapDanglingReference(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)); + + // Create a napi_wrap and remove it immediately, whilst leaving the out-param + // ref dangling (not deleted). + NODE_API_CALL(env, + napi_wrap(env, + args[0], + nullptr, + ObjectWrapDanglingReferenceFinalizer, + nullptr, + &dangling_ref)); + NODE_API_CALL(env, napi_remove_wrap(env, args[0], nullptr)); + + return args[0]; +} + +napi_value ObjectWrapDanglingReferenceTest(napi_env env, + napi_callback_info info) { + napi_value out; + napi_value ret; + NODE_API_CALL(env, napi_get_reference_value(env, dangling_ref, &out)); + + if (out == nullptr) { + // If the napi_ref has been invalidated, delete it. + NODE_API_CALL(env, napi_delete_reference(env, dangling_ref)); + NODE_API_CALL(env, napi_get_boolean(env, true, &ret)); + } else { + // The dangling napi_ref is still valid. + NODE_API_CALL(env, napi_get_boolean(env, false, &ret)); + } + return ret; +} + +static napi_value GetFinalizerCallCount(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + FinalizerData* data; + napi_value result; + + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr)); + NODE_API_CALL(env, + napi_get_instance_data(env, reinterpret_cast(&data))); + NODE_API_CALL(env, napi_create_int32(env, *data, &result)); + return result; +} + +static void finalizeData(napi_env env, void* data, void* hint) { + delete reinterpret_cast(data); +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + FinalizerData* data = new FinalizerData; + *data = 0; + NODE_API_CALL(env, napi_set_instance_data(env, data, finalizeData, nullptr)); + + MyObject::Init(env, exports); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("objectWrapDanglingReference", + ObjectWrapDanglingReference), + DECLARE_NODE_API_PROPERTY("objectWrapDanglingReferenceTest", + ObjectWrapDanglingReferenceTest), + DECLARE_NODE_API_PROPERTY("getFinalizerCallCount", GetFinalizerCallCount), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.h b/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.h new file mode 100644 index 00000000..fcb2e575 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/myobject.h @@ -0,0 +1,28 @@ +#ifndef TEST_JS_NATIVE_API_6_OBJECT_WRAP_MYOBJECT_H_ +#define TEST_JS_NATIVE_API_6_OBJECT_WRAP_MYOBJECT_H_ + +#include + +class MyObject { + public: + static void Init(napi_env env, napi_value exports); + static void Destructor(node_api_basic_env env, + void* nativeObject, + void* finalize_hint); + + private: + explicit MyObject(double value_ = 0); + ~MyObject(); + + static napi_value New(napi_env env, napi_callback_info info); + static napi_value GetValue(napi_env env, napi_callback_info info); + static napi_value SetValue(napi_env env, napi_callback_info info); + static napi_value PlusOne(napi_env env, napi_callback_info info); + static napi_value Multiply(napi_env env, napi_callback_info info); + static napi_ref constructor; + double value_; + napi_env env_; + napi_ref wrapper_; +}; + +#endif // TEST_JS_NATIVE_API_6_OBJECT_WRAP_MYOBJECT_H_ diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.cc b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.cc new file mode 100644 index 00000000..1c8594c8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.cc @@ -0,0 +1,99 @@ +#include "nested_wrap.h" +#include "../common.h" +#include "../entry_point.h" + +napi_ref NestedWrap::constructor{}; +static int finalization_count = 0; + +NestedWrap::NestedWrap() {} + +NestedWrap::~NestedWrap() { + napi_delete_reference(env_, wrapper_); + + // Delete the nested reference as well. + napi_delete_reference(env_, nested_); +} + +void NestedWrap::Destructor(node_api_basic_env env, + void* nativeObject, + void* /*finalize_hint*/) { + // Once this destructor is called, it cancels all pending + // finalizers for the object by deleting the references. + NestedWrap* obj = static_cast(nativeObject); + delete obj; + + finalization_count++; +} + +void NestedWrap::Init(napi_env env, napi_value exports) { + napi_value cons; + NODE_API_CALL_RETURN_VOID( + env, + napi_define_class( + env, "NestedWrap", -1, New, nullptr, 0, nullptr, &cons)); + + NODE_API_CALL_RETURN_VOID(env, + napi_create_reference(env, cons, 1, &constructor)); + + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, exports, "NestedWrap", cons)); +} + +napi_value NestedWrap::New(napi_env env, napi_callback_info info) { + napi_value new_target; + NODE_API_CALL(env, napi_get_new_target(env, info, &new_target)); + bool is_constructor = (new_target != nullptr); + NODE_API_BASIC_ASSERT_BASE( + is_constructor, "Constructor called without new", nullptr); + + napi_value this_val; + NODE_API_CALL(env, + napi_get_cb_info(env, info, 0, nullptr, &this_val, nullptr)); + + NestedWrap* obj = new NestedWrap(); + + obj->env_ = env; + NODE_API_CALL(env, + napi_wrap(env, + this_val, + obj, + NestedWrap::Destructor, + nullptr /* finalize_hint */, + &obj->wrapper_)); + + // Create a second napi_ref to be deleted in the destructor. + NODE_API_CALL(env, + napi_add_finalizer(env, + this_val, + obj, + NestedWrap::Destructor, + nullptr /* finalize_hint */, + &obj->nested_)); + + return this_val; +} + +static napi_value GetFinalizerCallCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalization_count, &result)); + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + NestedWrap::Init(env, exports); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("getFinalizerCallCount", GetFinalizerCallCount), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.h b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.h new file mode 100644 index 00000000..584f24de --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.h @@ -0,0 +1,33 @@ +#ifndef TEST_JS_NATIVE_API_6_OBJECT_WRAP_NESTED_WRAP_H_ +#define TEST_JS_NATIVE_API_6_OBJECT_WRAP_NESTED_WRAP_H_ + +#include + +/** + * Test that an napi_ref can be nested inside another ObjectWrap. + * + * This test shows a critical case where a finalizer deletes an napi_ref + * whose finalizer is also scheduled. + */ + +class NestedWrap { + public: + static void Init(napi_env env, napi_value exports); + static void Destructor(node_api_basic_env env, + void* nativeObject, + void* finalize_hint); + + private: + explicit NestedWrap(); + ~NestedWrap(); + + static napi_value New(napi_env env, napi_callback_info info); + + static napi_ref constructor; + + napi_env env_{}; + napi_ref wrapper_{}; + napi_ref nested_{}; +}; + +#endif // TEST_JS_NATIVE_API_6_OBJECT_WRAP_NESTED_WRAP_H_ diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.js b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.js new file mode 100644 index 00000000..726c6931 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/nested_wrap.js @@ -0,0 +1,20 @@ +// Flags: --expose-gc + +'use strict'; +const common = require('../../common'); +const { gcUntil } = require('../../common/gc'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/nested_wrap`); + +// This test verifies that ObjectWrap and napi_ref can be nested and finalized +// correctly with a non-basic finalizer. +(() => { + let obj = new addon.NestedWrap(); + obj = null; + // Silent eslint about unused variables. + assert.strictEqual(obj, null); +})(); + +gcUntil('object-wrap-ref', () => { + return addon.getFinalizerCallCount() === 1; +}); diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/test-basic-finalizer.js b/Tests/NodeApi/test/js-native-api/6_object_wrap/test-basic-finalizer.js new file mode 100644 index 00000000..5a7ccff4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/test-basic-finalizer.js @@ -0,0 +1,24 @@ +// Flags: --expose-gc + +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/myobject_basic_finalizer`); + +// This test verifies that ObjectWrap can be correctly finalized with a node_api_basic_finalizer +// in the current JS loop tick +(() => { + let obj = new addon.MyObject(9); + obj = null; + // Silent eslint about unused variables. + assert.strictEqual(obj, null); +})(); + +for (let i = 0; i < 10; ++i) { + global.gc(); + if (addon.getFinalizerCallCount() === 1) { + break; + } +} + +assert.strictEqual(addon.getFinalizerCallCount(), 1); diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/test-object-wrap-ref.js b/Tests/NodeApi/test/js-native-api/6_object_wrap/test-object-wrap-ref.js new file mode 100644 index 00000000..8f236410 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/test-object-wrap-ref.js @@ -0,0 +1,14 @@ +// Flags: --expose-gc + +'use strict'; +const common = require('../../common'); +const addon = require(`./build/${common.buildType}/myobject`); +const { gcUntil } = require('../../common/gc'); + +(function scope() { + addon.objectWrapDanglingReference({}); +})(); + +gcUntil('object-wrap-ref', () => { + return addon.objectWrapDanglingReferenceTest(); +}); diff --git a/Tests/NodeApi/test/js-native-api/6_object_wrap/test.js b/Tests/NodeApi/test/js-native-api/6_object_wrap/test.js new file mode 100644 index 00000000..809fddf2 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/6_object_wrap/test.js @@ -0,0 +1,48 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/myobject`); + +const getterOnlyErrorRE = + /^TypeError: Cannot (set|assign to) property .*( of #<.*>)? which has only a getter$/; + +const valueDescriptor = Object.getOwnPropertyDescriptor( + addon.MyObject.prototype, 'value'); +const valueReadonlyDescriptor = Object.getOwnPropertyDescriptor( + addon.MyObject.prototype, 'valueReadonly'); +const plusOneDescriptor = Object.getOwnPropertyDescriptor( + addon.MyObject.prototype, 'plusOne'); +assert.strictEqual(typeof valueDescriptor.get, 'function'); +assert.strictEqual(typeof valueDescriptor.set, 'function'); +assert.strictEqual(valueDescriptor.value, undefined); +assert.strictEqual(valueDescriptor.enumerable, false); +assert.strictEqual(valueDescriptor.configurable, false); +assert.strictEqual(typeof valueReadonlyDescriptor.get, 'function'); +assert.strictEqual(valueReadonlyDescriptor.set, undefined); +assert.strictEqual(valueReadonlyDescriptor.value, undefined); +assert.strictEqual(valueReadonlyDescriptor.enumerable, false); +assert.strictEqual(valueReadonlyDescriptor.configurable, false); + +assert.strictEqual(plusOneDescriptor.get, undefined); +assert.strictEqual(plusOneDescriptor.set, undefined); +assert.strictEqual(typeof plusOneDescriptor.value, 'function'); +assert.strictEqual(plusOneDescriptor.enumerable, false); +assert.strictEqual(plusOneDescriptor.configurable, false); + +const obj = new addon.MyObject(9); +assert.strictEqual(obj.value, 9); +obj.value = 10; +assert.strictEqual(obj.value, 10); +assert.strictEqual(obj.valueReadonly, 10); +assert.throws(() => { obj.valueReadonly = 14; }, getterOnlyErrorRE); +assert.strictEqual(obj.plusOne(), 11); +assert.strictEqual(obj.plusOne(), 12); +assert.strictEqual(obj.plusOne(), 13); + +assert.strictEqual(obj.multiply().value, 13); +assert.strictEqual(obj.multiply(10).value, 130); + +const newobj = obj.multiply(-1); +assert.strictEqual(newobj.value, -13); +assert.strictEqual(newobj.valueReadonly, -13); +assert.notStrictEqual(obj, newobj); diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/7_factory_wrap.cc b/Tests/NodeApi/test/js-native-api/7_factory_wrap/7_factory_wrap.cc new file mode 100644 index 00000000..b0ff0063 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/7_factory_wrap.cc @@ -0,0 +1,32 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "myobject.h" + +napi_value CreateObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)); + + napi_value instance; + NODE_API_CALL(env, MyObject::NewInstance(env, args[0], &instance)); + + return instance; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + NODE_API_CALL(env, MyObject::Init(env)); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_GETTER("finalizeCount", MyObject::GetFinalizeCount), + DECLARE_NODE_API_PROPERTY("createObject", CreateObject), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/7_factory_wrap/CMakeLists.txt new file mode 100644 index 00000000..1ca18670 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(7_factory_wrap + SOURCES + 7_factory_wrap.cc + myobject.cc +) diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/binding.gyp b/Tests/NodeApi/test/js-native-api/7_factory_wrap/binding.gyp new file mode 100644 index 00000000..f51f7823 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "7_factory_wrap", + "sources": [ + "7_factory_wrap.cc", + "myobject.cc" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.cc b/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.cc new file mode 100644 index 00000000..142c2dab --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.cc @@ -0,0 +1,101 @@ +#include "myobject.h" +#include "../common.h" + +static int finalize_count = 0; + +MyObject::MyObject() : env_(nullptr), wrapper_(nullptr) {} + +MyObject::~MyObject() { napi_delete_reference(env_, wrapper_); } + +void MyObject::Destructor(node_api_basic_env env, + void* nativeObject, + void* /*finalize_hint*/) { + ++finalize_count; + MyObject* obj = static_cast(nativeObject); + delete obj; +} + +napi_value MyObject::GetFinalizeCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalize_count, &result)); + return result; +} + +napi_ref MyObject::constructor; + +napi_status MyObject::Init(napi_env env) { + napi_status status; + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("plusOne", PlusOne), + }; + + napi_value cons; + status = napi_define_class( + env, "MyObject", -1, New, nullptr, 1, properties, &cons); + if (status != napi_ok) return status; + + status = napi_create_reference(env, cons, 1, &constructor); + if (status != napi_ok) return status; + + return napi_ok; +} + +napi_value MyObject::New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + napi_valuetype valuetype; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype)); + + MyObject* obj = new MyObject(); + + if (valuetype == napi_undefined) { + obj->counter_ = 0; + } else { + NODE_API_CALL(env, napi_get_value_uint32(env, args[0], &obj->counter_)); + } + + obj->env_ = env; + NODE_API_CALL(env, + napi_wrap( + env, _this, obj, MyObject::Destructor, nullptr /* finalize_hint */, + &obj->wrapper_)); + + return _this; +} + +napi_status MyObject::NewInstance(napi_env env, + napi_value arg, + napi_value* instance) { + napi_status status; + + const int argc = 1; + napi_value argv[argc] = {arg}; + + napi_value cons; + status = napi_get_reference_value(env, constructor, &cons); + if (status != napi_ok) return status; + + status = napi_new_instance(env, cons, argc, argv, instance); + if (status != napi_ok) return status; + + return napi_ok; +} + +napi_value MyObject::PlusOne(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, + napi_get_cb_info(env, info, nullptr, nullptr, &_this, nullptr)); + + MyObject* obj; + NODE_API_CALL(env, napi_unwrap(env, _this, reinterpret_cast(&obj))); + + obj->counter_ += 1; + + napi_value num; + NODE_API_CALL(env, napi_create_uint32(env, obj->counter_, &num)); + + return num; +} diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.h b/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.h new file mode 100644 index 00000000..aa2b199a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/myobject.h @@ -0,0 +1,27 @@ +#ifndef TEST_JS_NATIVE_API_7_FACTORY_WRAP_MYOBJECT_H_ +#define TEST_JS_NATIVE_API_7_FACTORY_WRAP_MYOBJECT_H_ + +#include + +class MyObject { + public: + static napi_status Init(napi_env env); + static void + Destructor(node_api_basic_env env, void *nativeObject, void *finalize_hint); + static napi_value GetFinalizeCount(napi_env env, napi_callback_info info); + static napi_status + NewInstance(napi_env env, napi_value arg, napi_value *instance); + + private: + MyObject(); + ~MyObject(); + + static napi_ref constructor; + static napi_value New(napi_env env, napi_callback_info info); + static napi_value PlusOne(napi_env env, napi_callback_info info); + uint32_t counter_; + napi_env env_; + napi_ref wrapper_; +}; + +#endif // TEST_JS_NATIVE_API_7_FACTORY_WRAP_MYOBJECT_H_ diff --git a/Tests/NodeApi/test/js-native-api/7_factory_wrap/test.js b/Tests/NodeApi/test/js-native-api/7_factory_wrap/test.js new file mode 100644 index 00000000..23840b36 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/7_factory_wrap/test.js @@ -0,0 +1,27 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const assert = require('assert'); +const test = require(`./build/${common.buildType}/7_factory_wrap`); +const { gcUntil } = require('../../common/gc'); + +assert.strictEqual(test.finalizeCount, 0); +async function runGCTests() { + (() => { + const obj = test.createObject(10); + assert.strictEqual(obj.plusOne(), 11); + assert.strictEqual(obj.plusOne(), 12); + assert.strictEqual(obj.plusOne(), 13); + })(); + await gcUntil('test 1', () => (test.finalizeCount === 1)); + + (() => { + const obj2 = test.createObject(20); + assert.strictEqual(obj2.plusOne(), 21); + assert.strictEqual(obj2.plusOne(), 22); + assert.strictEqual(obj2.plusOne(), 23); + })(); + await gcUntil('test 2', () => (test.finalizeCount === 2)); +} +runGCTests(); diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/8_passing_wrapped.cc b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/8_passing_wrapped.cc new file mode 100644 index 00000000..f3328d93 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/8_passing_wrapped.cc @@ -0,0 +1,61 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "myobject.h" + +extern size_t finalize_count; + +static napi_value CreateObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)); + + napi_value instance; + NODE_API_CALL(env, MyObject::NewInstance(env, args[0], &instance)); + + return instance; +} + +static napi_value Add(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr)); + + MyObject* obj1; + NODE_API_CALL(env, + napi_unwrap(env, args[0], reinterpret_cast(&obj1))); + + MyObject* obj2; + NODE_API_CALL(env, + napi_unwrap(env, args[1], reinterpret_cast(&obj2))); + + napi_value sum; + NODE_API_CALL(env, napi_create_double(env, obj1->Val() + obj2->Val(), &sum)); + + return sum; +} + +static napi_value FinalizeCount(napi_env env, napi_callback_info info) { + napi_value return_value; + NODE_API_CALL(env, napi_create_uint32(env, finalize_count, &return_value)); + return return_value; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + MyObject::Init(env); + + napi_property_descriptor desc[] = { + DECLARE_NODE_API_PROPERTY("createObject", CreateObject), + DECLARE_NODE_API_PROPERTY("add", Add), + DECLARE_NODE_API_PROPERTY("finalizeCount", FinalizeCount), + }; + + NODE_API_CALL(env, + napi_define_properties(env, exports, sizeof(desc) / sizeof(*desc), desc)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/CMakeLists.txt new file mode 100644 index 00000000..3ee22eb8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(8_passing_wrapped + SOURCES + 8_passing_wrapped.cc + myobject.cc +) diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/binding.gyp b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/binding.gyp new file mode 100644 index 00000000..d043d0f5 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "8_passing_wrapped", + "sources": [ + "8_passing_wrapped.cc", + "myobject.cc" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.cc b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.cc new file mode 100644 index 00000000..ff352d3f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.cc @@ -0,0 +1,91 @@ +#include "myobject.h" +#include "../common.h" + +size_t finalize_count = 0; + +MyObject::MyObject() : env_(nullptr), wrapper_(nullptr) {} + +MyObject::~MyObject() { + finalize_count++; + napi_delete_reference(env_, wrapper_); +} + +void MyObject::Destructor( + node_api_basic_env env, + void *nativeObject, + void * /*finalize_hint*/) { + MyObject *obj = static_cast(nativeObject); + delete obj; +} + +napi_ref MyObject::constructor; + +napi_status MyObject::Init(napi_env env) { + napi_status status; + + napi_value cons; + status = + napi_define_class(env, "MyObject", -1, New, nullptr, 0, nullptr, &cons); + if (status != napi_ok) + return status; + + status = napi_create_reference(env, cons, 1, &constructor); + if (status != napi_ok) + return status; + + return napi_ok; +} + +napi_value MyObject::New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, &_this, nullptr)); + + MyObject *obj = new MyObject(); + + napi_valuetype valuetype; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype)); + + if (valuetype == napi_undefined) { + obj->val_ = 0; + } else { + NODE_API_CALL(env, napi_get_value_double(env, args[0], &obj->val_)); + } + + obj->env_ = env; + + // The below call to napi_wrap() must request a reference to the wrapped + // object via the out-parameter, because this ensures that we test the code + // path that deals with a reference that is destroyed from its own finalizer. + NODE_API_CALL( + env, + napi_wrap( + env, + _this, + obj, + MyObject::Destructor, + nullptr /* finalize_hint */, + &obj->wrapper_)); + + return _this; +} + +napi_status +MyObject::NewInstance(napi_env env, napi_value arg, napi_value *instance) { + napi_status status; + + const int argc = 1; + napi_value argv[argc] = {arg}; + + napi_value cons; + status = napi_get_reference_value(env, constructor, &cons); + if (status != napi_ok) + return status; + + status = napi_new_instance(env, cons, argc, argv, instance); + if (status != napi_ok) + return status; + + return napi_ok; +} diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.h b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.h new file mode 100644 index 00000000..bdde3fb4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/myobject.h @@ -0,0 +1,28 @@ +#ifndef TEST_JS_NATIVE_API_8_PASSING_WRAPPED_MYOBJECT_H_ +#define TEST_JS_NATIVE_API_8_PASSING_WRAPPED_MYOBJECT_H_ + +#include + +class MyObject { + public: + static napi_status Init(napi_env env); + static void + Destructor(node_api_basic_env env, void *nativeObject, void *finalize_hint); + static napi_status + NewInstance(napi_env env, napi_value arg, napi_value *instance); + double Val() const { + return val_; + } + + private: + MyObject(); + ~MyObject(); + + static napi_ref constructor; + static napi_value New(napi_env env, napi_callback_info info); + double val_; + napi_env env_; + napi_ref wrapper_; +}; + +#endif // TEST_JS_NATIVE_API_8_PASSING_WRAPPED_MYOBJECT_H_ diff --git a/Tests/NodeApi/test/js-native-api/8_passing_wrapped/test.js b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/test.js new file mode 100644 index 00000000..145828e6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/8_passing_wrapped/test.js @@ -0,0 +1,21 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${common.buildType}/8_passing_wrapped`); +const { gcUntil } = require('../../common/gc'); + +async function runTest() { + let obj1 = addon.createObject(10); + let obj2 = addon.createObject(20); + const result = addon.add(obj1, obj2); + assert.strictEqual(result, 30); + + // Make sure the native destructor gets called. + obj1 = null; + obj2 = null; + await gcUntil('8_passing_wrapped', + () => (addon.finalizeCount() === 2)); +} +runTest(); diff --git a/Tests/NodeApi/test/js-native-api/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/CMakeLists.txt new file mode 100644 index 00000000..a2426f51 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/CMakeLists.txt @@ -0,0 +1,7 @@ +if(JSR_NODE_API_BUILD_NATIVE_TESTS) + foreach(NODE_API_TEST_DIR ${JSR_NODE_API_NATIVE_TEST_DIRS}) + if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/${NODE_API_TEST_DIR}/CMakeLists.txt) + add_subdirectory(${NODE_API_TEST_DIR}) + endif() + endforeach() +endif() diff --git a/Tests/NodeApi/test/js-native-api/common-inl.h b/Tests/NodeApi/test/js-native-api/common-inl.h new file mode 100644 index 00000000..2a1a8fa6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/common-inl.h @@ -0,0 +1,71 @@ +#ifndef JS_NATIVE_API_COMMON_INL_H_ +#define JS_NATIVE_API_COMMON_INL_H_ + +#include +#include "common.h" + +#include + +inline void add_returned_status(napi_env env, + const char* key, + napi_value object, + const char* expected_message, + napi_status expected_status, + napi_status actual_status) { + char napi_message_string[100] = ""; + napi_value prop_value; + + if (actual_status != expected_status) { + snprintf(napi_message_string, + sizeof(napi_message_string), + "Invalid status [%d]", + actual_status); + } + + NODE_API_CALL_RETURN_VOID( + env, + napi_create_string_utf8( + env, + (actual_status == expected_status ? expected_message + : napi_message_string), + NAPI_AUTO_LENGTH, + &prop_value)); + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, object, key, prop_value)); +} + +inline void add_last_status(napi_env env, + const char* key, + napi_value return_value) { + napi_value prop_value; + napi_value exception; + const napi_extended_error_info* p_last_error; + NODE_API_CALL_RETURN_VOID(env, napi_get_last_error_info(env, &p_last_error)); + // Content of p_last_error can be updated in subsequent node-api calls. + // Retrieve it immediately. + const char* error_message = p_last_error->error_message == NULL + ? "napi_ok" + : p_last_error->error_message; + + bool is_exception_pending; + NODE_API_CALL_RETURN_VOID( + env, napi_is_exception_pending(env, &is_exception_pending)); + if (is_exception_pending) { + NODE_API_CALL_RETURN_VOID( + env, napi_get_and_clear_last_exception(env, &exception)); + char exception_key[50]; + snprintf(exception_key, sizeof(exception_key), "%s%s", key, "Exception"); + NODE_API_CALL_RETURN_VOID( + env, + napi_set_named_property(env, return_value, exception_key, exception)); + } + + NODE_API_CALL_RETURN_VOID( + env, + napi_create_string_utf8( + env, error_message, NAPI_AUTO_LENGTH, &prop_value)); + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, return_value, key, prop_value)); +} + +#endif // JS_NATIVE_API_COMMON_INL_H_ diff --git a/Tests/NodeApi/test/js-native-api/common.h b/Tests/NodeApi/test/js-native-api/common.h new file mode 100644 index 00000000..7c99da88 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/common.h @@ -0,0 +1,132 @@ +#ifndef JS_NATIVE_API_COMMON_H_ +#define JS_NATIVE_API_COMMON_H_ + +#include +#include // abort() + +// Empty value so that macros here are able to return NULL or void +#define NODE_API_RETVAL_NOTHING // Intentionally blank #define + +#define GET_AND_THROW_LAST_ERROR(env) \ + do { \ + const napi_extended_error_info *error_info; \ + napi_get_last_error_info((env), &error_info); \ + bool is_pending; \ + const char* err_message = error_info->error_message; \ + napi_is_exception_pending((env), &is_pending); \ + /* If an exception is already pending, don't rethrow it */ \ + if (!is_pending) { \ + const char* error_message = err_message != NULL ? \ + err_message : \ + "empty error message"; \ + napi_throw_error((env), NULL, error_message); \ + } \ + } while (0) + +// The basic version of GET_AND_THROW_LAST_ERROR. We cannot access any +// exceptions and we cannot fail by way of JS exception, so we abort. +#define FATALLY_FAIL_WITH_LAST_ERROR(env) \ + do { \ + const napi_extended_error_info* error_info; \ + napi_get_last_error_info((env), &error_info); \ + const char* err_message = error_info->error_message; \ + const char* error_message = \ + err_message != NULL ? err_message : "empty error message"; \ + fprintf(stderr, "%s\n", error_message); \ + abort(); \ + } while (0) + +#define NODE_API_ASSERT_BASE(env, assertion, message, ret_val) \ + do { \ + if (!(assertion)) { \ + napi_throw_error( \ + (env), \ + NULL, \ + "assertion (" #assertion ") failed: " message); \ + return ret_val; \ + } \ + } while (0) + +#define NODE_API_BASIC_ASSERT_BASE(assertion, message, ret_val) \ + do { \ + if (!(assertion)) { \ + fprintf(stderr, "assertion (" #assertion ") failed: " message); \ + abort(); \ + return ret_val; \ + } \ + } while (0) + +// Returns NULL on failed assertion. +// This is meant to be used inside napi_callback methods. +#define NODE_API_ASSERT(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, NULL) + +// Returns empty on failed assertion. +// This is meant to be used inside functions with void return type. +#define NODE_API_ASSERT_RETURN_VOID(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, NODE_API_RETVAL_NOTHING) + +#define NODE_API_BASIC_ASSERT_RETURN_VOID(assertion, message) \ + NODE_API_BASIC_ASSERT_BASE(assertion, message, NODE_API_RETVAL_NOTHING) + +#define NODE_API_CALL_BASE(env, the_call, ret_val) \ + do { \ + if ((the_call) != napi_ok) { \ + GET_AND_THROW_LAST_ERROR((env)); \ + return ret_val; \ + } \ + } while (0) + +#define NODE_API_BASIC_CALL_BASE(env, the_call, ret_val) \ + do { \ + if ((the_call) != napi_ok) { \ + FATALLY_FAIL_WITH_LAST_ERROR((env)); \ + return ret_val; \ + } \ + } while (0) + +// Returns NULL if the_call doesn't return napi_ok. +#define NODE_API_CALL(env, the_call) \ + NODE_API_CALL_BASE(env, the_call, NULL) + +// Returns empty if the_call doesn't return napi_ok. +#define NODE_API_CALL_RETURN_VOID(env, the_call) \ + NODE_API_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) + +#define NODE_API_BASIC_CALL_RETURN_VOID(env, the_call) \ + NODE_API_BASIC_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) + +#define NODE_API_CHECK_STATUS(the_call) \ + do { \ + napi_status status = (the_call); \ + if (status != napi_ok) { \ + return status; \ + } \ + } while (0) + +#define NODE_API_ASSERT_STATUS(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, napi_generic_failure) + +#define DECLARE_NODE_API_PROPERTY(name, func) \ + { (name), NULL, (func), NULL, NULL, NULL, napi_default, NULL } + +#define DECLARE_NODE_API_GETTER(name, func) \ + { (name), NULL, NULL, (func), NULL, NULL, napi_default, NULL } + +#define DECLARE_NODE_API_PROPERTY_VALUE(name, value) \ + { (name), NULL, NULL, NULL, NULL, (value), napi_default, NULL } + +static inline void add_returned_status(napi_env env, + const char* key, + napi_value object, + const char* expected_message, + napi_status expected_status, + napi_status actual_status); + +static inline void add_last_status(napi_env env, + const char* key, + napi_value return_value); + +#include "common-inl.h" + +#endif // JS_NATIVE_API_COMMON_H_ diff --git a/Tests/NodeApi/test/js-native-api/entry_point.h b/Tests/NodeApi/test/js-native-api/entry_point.h new file mode 100644 index 00000000..2e74d6c0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/entry_point.h @@ -0,0 +1,12 @@ +#ifndef JS_NATIVE_API_ENTRY_POINT_H_ +#define JS_NATIVE_API_ENTRY_POINT_H_ + +#include + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports); +EXTERN_C_END + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) + +#endif // JS_NATIVE_API_ENTRY_POINT_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_array/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_array/CMakeLists.txt new file mode 100644 index 00000000..bda269e6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_array/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_array + SOURCES + test_array.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_array/binding.gyp b/Tests/NodeApi/test/js-native-api/test_array/binding.gyp new file mode 100644 index 00000000..69545b66 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_array/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_array", + "sources": [ + "test_array.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_array/test.js b/Tests/NodeApi/test/js-native-api/test_array/test.js new file mode 100644 index 00000000..26bcb18f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_array/test.js @@ -0,0 +1,61 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for arrays +const test_array = require(`./build/${common.buildType}/test_array`); + +const array = [ + 1, + 9, + 48, + 13493, + 9459324, + { name: 'hello' }, + [ + 'world', + 'node', + 'abi', + ], +]; + +assert.throws( + () => { + test_array.TestGetElement(array, array.length + 1); + }, + /^Error: assertion \(\(\(uint32_t\)index < length\)\) failed: Index out of bounds!$/, +); + +assert.throws( + () => { + test_array.TestGetElement(array, -2); + }, + /^Error: assertion \(index >= 0\) failed: Invalid index\. Expects a positive integer\.$/, +); + +array.forEach(function(element, index) { + assert.strictEqual(test_array.TestGetElement(array, index), element); +}); + + +assert.deepStrictEqual(test_array.New(array), array); + +assert(test_array.TestHasElement(array, 0)); +assert.strictEqual(test_array.TestHasElement(array, array.length + 1), false); + +assert(test_array.NewWithLength(0) instanceof Array); +assert(test_array.NewWithLength(1) instanceof Array); +// Check max allowed length for an array 2^32 -1 +// TODO: Hermes does not allow such big arrays +// assert(test_array.NewWithLength(4294967295) instanceof Array); + +{ + // Verify that array elements can be deleted. + const arr = ['a', 'b', 'c', 'd']; + + assert.strictEqual(arr.length, 4); + assert.strictEqual(2 in arr, true); + assert.strictEqual(test_array.TestDeleteElement(arr, 2), true); + assert.strictEqual(arr.length, 4); + assert.strictEqual(2 in arr, false); +} diff --git a/Tests/NodeApi/test/js-native-api/test_array/test_array.c b/Tests/NodeApi/test/js-native-api/test_array/test_array.c new file mode 100644 index 00000000..7a34af20 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_array/test_array.c @@ -0,0 +1,188 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value TestGetElement(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an array as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects an integer as second argument."); + + napi_value array = args[0]; + int32_t index; + NODE_API_CALL(env, napi_get_value_int32(env, args[1], &index)); + + NODE_API_ASSERT(env, index >= 0, "Invalid index. Expects a positive integer."); + + bool isarray; + NODE_API_CALL(env, napi_is_array(env, array, &isarray)); + + if (!isarray) { + return NULL; + } + + uint32_t length; + NODE_API_CALL(env, napi_get_array_length(env, array, &length)); + + NODE_API_ASSERT(env, ((uint32_t)index < length), "Index out of bounds!"); + + napi_value ret; + NODE_API_CALL(env, napi_get_element(env, array, index, &ret)); + + return ret; +} + +static napi_value TestHasElement(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an array as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects an integer as second argument."); + + napi_value array = args[0]; + int32_t index; + NODE_API_CALL(env, napi_get_value_int32(env, args[1], &index)); + + bool isarray; + NODE_API_CALL(env, napi_is_array(env, array, &isarray)); + + if (!isarray) { + return NULL; + } + + bool has_element; + NODE_API_CALL(env, napi_has_element(env, array, index, &has_element)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_element, &ret)); + + return ret; +} + +static napi_value TestDeleteElement(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an array as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects an integer as second argument."); + + napi_value array = args[0]; + int32_t index; + bool result; + napi_value ret; + + NODE_API_CALL(env, napi_get_value_int32(env, args[1], &index)); + NODE_API_CALL(env, napi_is_array(env, array, &result)); + + if (!result) { + return NULL; + } + + NODE_API_CALL(env, napi_delete_element(env, array, index, &result)); + NODE_API_CALL(env, napi_get_boolean(env, result, &ret)); + + return ret; +} + +static napi_value New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an array as first argument."); + + napi_value ret; + NODE_API_CALL(env, napi_create_array(env, &ret)); + + uint32_t i, length; + NODE_API_CALL(env, napi_get_array_length(env, args[0], &length)); + + for (i = 0; i < length; i++) { + napi_value e; + NODE_API_CALL(env, napi_get_element(env, args[0], i, &e)); + NODE_API_CALL(env, napi_set_element(env, ret, i, e)); + } + + return ret; +} + +static napi_value NewWithLength(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects an integer the first argument."); + + int32_t array_length; + NODE_API_CALL(env, napi_get_value_int32(env, args[0], &array_length)); + + napi_value ret; + NODE_API_CALL(env, napi_create_array_with_length(env, array_length, &ret)); + + return ret; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("TestGetElement", TestGetElement), + DECLARE_NODE_API_PROPERTY("TestHasElement", TestHasElement), + DECLARE_NODE_API_PROPERTY("TestDeleteElement", TestDeleteElement), + DECLARE_NODE_API_PROPERTY("New", New), + DECLARE_NODE_API_PROPERTY("NewWithLength", NewWithLength), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_bigint/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_bigint/CMakeLists.txt new file mode 100644 index 00000000..7027e3be --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_bigint + SOURCES + test_bigint.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_bigint/binding.gyp b/Tests/NodeApi/test/js-native-api/test_bigint/binding.gyp new file mode 100644 index 00000000..6dc71015 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_bigint", + "sources": [ + "test_bigint.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_bigint/test.js b/Tests/NodeApi/test/js-native-api/test_bigint/test.js new file mode 100644 index 00000000..50febf14 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint/test.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const { + IsLossless, + TestInt64, + TestUint64, + TestWords, + CreateTooBigBigInt, + MakeBigIntWordsThrow, +} = require(`./build/${common.buildType}/test_bigint`); + +[ + 0n, + -0n, + 1n, + -1n, + 100n, + 2121n, + -1233n, + 986583n, + -976675n, + 98765432213456789876546896323445679887645323232436587988766545658n, + -4350987086545760976737453646576078997096876957864353245245769809n, +].forEach((num) => { + if (num > -(2n ** 63n) && num < 2n ** 63n) { + assert.strictEqual(TestInt64(num), num); + assert.strictEqual(IsLossless(num, true), true); + } else { + assert.strictEqual(IsLossless(num, true), false); + } + + if (num >= 0 && num < 2n ** 64n) { + assert.strictEqual(TestUint64(num), num); + assert.strictEqual(IsLossless(num, false), true); + } else { + assert.strictEqual(IsLossless(num, false), false); + } + + assert.strictEqual(num, TestWords(num)); +}); + +assert.throws(() => CreateTooBigBigInt(), { + name: 'Error', + message: 'Invalid argument', +}); + +// Test that we correctly forward exceptions from the engine. +assert.throws(() => MakeBigIntWordsThrow(), { + name: 'RangeError', + message: 'Maximum BigInt size exceeded', +}); diff --git a/Tests/NodeApi/test/js-native-api/test_bigint/test_bigint.c b/Tests/NodeApi/test/js-native-api/test_bigint/test_bigint.c new file mode 100644 index 00000000..203bc3a7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_bigint/test_bigint.c @@ -0,0 +1,159 @@ +#include +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value IsLossless(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool is_signed; + NODE_API_CALL(env, napi_get_value_bool(env, args[1], &is_signed)); + + bool lossless; + + if (is_signed) { + int64_t input; + NODE_API_CALL(env, napi_get_value_bigint_int64(env, args[0], &input, &lossless)); + } else { + uint64_t input; + NODE_API_CALL(env, napi_get_value_bigint_uint64(env, args[0], &input, &lossless)); + } + + napi_value output; + NODE_API_CALL(env, napi_get_boolean(env, lossless, &output)); + + return output; +} + +static napi_value TestInt64(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_bigint, + "Wrong type of arguments. Expects a bigint as first argument."); + + int64_t input; + bool lossless; + NODE_API_CALL(env, napi_get_value_bigint_int64(env, args[0], &input, &lossless)); + + napi_value output; + NODE_API_CALL(env, napi_create_bigint_int64(env, input, &output)); + + return output; +} + +static napi_value TestUint64(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_bigint, + "Wrong type of arguments. Expects a bigint as first argument."); + + uint64_t input; + bool lossless; + NODE_API_CALL(env, napi_get_value_bigint_uint64( + env, args[0], &input, &lossless)); + + napi_value output; + NODE_API_CALL(env, napi_create_bigint_uint64(env, input, &output)); + + return output; +} + +static napi_value TestWords(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_bigint, + "Wrong type of arguments. Expects a bigint as first argument."); + + size_t expected_word_count; + NODE_API_CALL(env, napi_get_value_bigint_words( + env, args[0], NULL, &expected_word_count, NULL)); + + int sign_bit; + size_t word_count = 10; + uint64_t words[10]; + + NODE_API_CALL(env, napi_get_value_bigint_words( + env, args[0], &sign_bit, &word_count, words)); + + NODE_API_ASSERT(env, word_count == expected_word_count, + "word counts do not match"); + + napi_value output; + NODE_API_CALL(env, napi_create_bigint_words( + env, sign_bit, word_count, words, &output)); + + return output; +} + +// throws RangeError +static napi_value CreateTooBigBigInt(napi_env env, napi_callback_info info) { + int sign_bit = 0; + size_t word_count = SIZE_MAX; + uint64_t words[10] = {0}; + + napi_value output; + + NODE_API_CALL(env, napi_create_bigint_words( + env, sign_bit, word_count, words, &output)); + + return output; +} + +// Test that we correctly forward exceptions from the engine. +static napi_value MakeBigIntWordsThrow(napi_env env, napi_callback_info info) { + uint64_t words[10] = {0}; + napi_value output; + + napi_status status = napi_create_bigint_words(env, + 0, + INT_MAX, + words, + &output); + if (status != napi_pending_exception) + napi_throw_error(env, NULL, "Expected status `napi_pending_exception`"); + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("IsLossless", IsLossless), + DECLARE_NODE_API_PROPERTY("TestInt64", TestInt64), + DECLARE_NODE_API_PROPERTY("TestUint64", TestUint64), + DECLARE_NODE_API_PROPERTY("TestWords", TestWords), + DECLARE_NODE_API_PROPERTY("CreateTooBigBigInt", CreateTooBigBigInt), + DECLARE_NODE_API_PROPERTY("MakeBigIntWordsThrow", MakeBigIntWordsThrow), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_cannot_run_js/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/CMakeLists.txt new file mode 100644 index 00000000..c708e9cb --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/CMakeLists.txt @@ -0,0 +1,13 @@ +add_node_api_module(test_cannot_run_js + SOURCES + test_cannot_run_js.c + DEFINES + "NAPI_VERSION=10" +) + +add_node_api_module(test_pending_exception + SOURCES + test_cannot_run_js.c + DEFINES + "NAPI_VERSION=9" +) diff --git a/Tests/NodeApi/test/js-native-api/test_cannot_run_js/binding.gyp b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/binding.gyp new file mode 100644 index 00000000..51ff8ccb --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/binding.gyp @@ -0,0 +1,18 @@ +{ + "targets": [ + { + "target_name": "test_cannot_run_js", + "sources": [ + "test_cannot_run_js.c" + ], + "defines": [ "NAPI_VERSION=10" ], + }, + { + "target_name": "test_pending_exception", + "sources": [ + "test_cannot_run_js.c" + ], + "defines": [ "NAPI_VERSION=9" ], + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test.js b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test.js new file mode 100644 index 00000000..31c82480 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test.js @@ -0,0 +1,24 @@ +'use strict'; + +// Test that `napi_call_function()` returns `napi_cannot_run_js` in experimental +// mode and `napi_pending_exception` otherwise. This test calls the add-on's +// `createRef()` method, which creates a strong reference to a JS function. When +// the process exits, it calls all reference finalizers. The finalizer for the +// strong reference created herein will attempt to call `napi_get_property()` on +// a property of the global object and will abort the process if the API doesn't +// return the correct status. + +const { buildType, mustNotCall } = require('../../common'); +const addon_v8 = require(`./build/${buildType}/test_pending_exception`); +const addon_new = require(`./build/${buildType}/test_cannot_run_js`); + +function runTests(addon, isVersion8) { + addon.createRef(mustNotCall()); +} + +function runAllTests() { + runTests(addon_v8, /* isVersion8 */ true); + runTests(addon_new, /* isVersion8 */ false); +} + +runAllTests(); diff --git a/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test_cannot_run_js.c b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test_cannot_run_js.c new file mode 100644 index 00000000..8ca44c23 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_cannot_run_js/test_cannot_run_js.c @@ -0,0 +1,66 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "stdlib.h" + +static void Finalize(napi_env env, void* data, void* hint) { + napi_value global, set_timeout; + napi_ref* ref = data; + + NODE_API_BASIC_ASSERT_RETURN_VOID( + napi_delete_reference(env, *ref) == napi_ok, + "deleting reference in finalizer should succeed"); + NODE_API_BASIC_ASSERT_RETURN_VOID( + napi_get_global(env, &global) == napi_ok, + "getting global reference in finalizer should succeed"); + napi_status result = + napi_get_named_property(env, global, "setTimeout", &set_timeout); + + // The finalizer could be invoked either from check callbacks (as native + // immediates) if the event loop is still running (where napi_ok is returned) + // or during environment shutdown (where napi_cannot_run_js or + // napi_pending_exception is returned). This is not deterministic from + // the point of view of the addon. + +#if NAPI_VERSION > 9 + NODE_API_BASIC_ASSERT_RETURN_VOID( + result == napi_cannot_run_js || result == napi_ok, + "getting named property from global in finalizer should succeed " + "or return napi_cannot_run_js"); +#else + NODE_API_BASIC_ASSERT_RETURN_VOID( + result == napi_pending_exception || result == napi_ok, + "getting named property from global in finalizer should succeed " + "or return napi_pending_exception"); +#endif // NAPI_VERSION > 9 + free(ref); +} + +static napi_value CreateRef(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value cb; + napi_valuetype value_type; + napi_ref* ref = malloc(sizeof(*ref)); + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &cb, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Function takes only one argument"); + NODE_API_CALL(env, napi_typeof(env, cb, &value_type)); + NODE_API_ASSERT( + env, value_type == napi_function, "argument must be function"); + NODE_API_CALL(env, napi_add_finalizer(env, cb, ref, Finalize, NULL, ref)); + return cb; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("createRef", CreateRef), + }; + + NODE_API_CALL( + env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_constructor/CMakeLists.txt new file mode 100644 index 00000000..0582345e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(test_constructor + SOURCES + test_constructor.c + test_null.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/binding.gyp b/Tests/NodeApi/test/js-native-api/test_constructor/binding.gyp new file mode 100644 index 00000000..af0c5d10 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "test_constructor", + "sources": [ + "test_constructor.c", + "test_null.c", + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test.js b/Tests/NodeApi/test/js-native-api/test_constructor/test.js new file mode 100644 index 00000000..4ef41794 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test.js @@ -0,0 +1,62 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +const getterOnlyErrorRE = + /^TypeError: Cannot (set|assign to) property .*( of #<.*>)? which has only a getter$/; + +// Testing api calls for a constructor that defines properties +const TestConstructor = require(`./build/${common.buildType}/test_constructor`); +const test_object = new TestConstructor(); + +assert.strictEqual(test_object.echo('hello'), 'hello'); + +test_object.readwriteValue = 1; +assert.strictEqual(test_object.readwriteValue, 1); +test_object.readwriteValue = 2; +assert.strictEqual(test_object.readwriteValue, 2); + +assert.throws(() => { test_object.readonlyValue = 3; }, + /^TypeError: Cannot assign to read(-| )only property 'readonlyValue'.*(of object '#')?/); + +assert.ok(test_object.hiddenValue); + +// Properties with napi_enumerable attribute should be enumerable. +const propertyNames = []; +for (const name in test_object) { + propertyNames.push(name); +} +assert.ok(propertyNames.includes('echo')); +assert.ok(propertyNames.includes('readwriteValue')); +assert.ok(propertyNames.includes('readonlyValue')); +assert.ok(!propertyNames.includes('hiddenValue')); +assert.ok(!propertyNames.includes('readwriteAccessor1')); +assert.ok(!propertyNames.includes('readwriteAccessor2')); +assert.ok(!propertyNames.includes('readonlyAccessor1')); +assert.ok(!propertyNames.includes('readonlyAccessor2')); + +// The napi_writable attribute should be ignored for accessors. +test_object.readwriteAccessor1 = 1; +assert.strictEqual(test_object.readwriteAccessor1, 1); +assert.strictEqual(test_object.readonlyAccessor1, 1); +assert.throws(() => { test_object.readonlyAccessor1 = 3; }, getterOnlyErrorRE); +test_object.readwriteAccessor2 = 2; +assert.strictEqual(test_object.readwriteAccessor2, 2); +assert.strictEqual(test_object.readonlyAccessor2, 2); +assert.throws(() => { test_object.readonlyAccessor2 = 3; }, getterOnlyErrorRE); + +// Validate that static properties are on the class as opposed +// to the instance +assert.strictEqual(TestConstructor.staticReadonlyAccessor1, 10); +assert.strictEqual(test_object.staticReadonlyAccessor1, undefined); + +// Verify that passing NULL to napi_define_class() results in the correct +// error. +assert.deepStrictEqual(TestConstructor.TestDefineClass(), { + envIsNull: 'Invalid argument', + nameIsNull: 'Invalid argument', + cbIsNull: 'Invalid argument', + cbDataIsNull: 'napi_ok', + propertiesIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument' +}); diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test2.js b/Tests/NodeApi/test/js-native-api/test_constructor/test2.js new file mode 100644 index 00000000..125af81c --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test2.js @@ -0,0 +1,8 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for a constructor that defines properties +const TestConstructor = + require(`./build/${common.buildType}/test_constructor`).constructorName; +assert.strictEqual(TestConstructor.name, 'MyObject'); diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test_constructor.c b/Tests/NodeApi/test/js-native-api/test_constructor/test_constructor.c new file mode 100644 index 00000000..0c52bc31 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test_constructor.c @@ -0,0 +1,200 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +static double value_ = 1; +static double static_value_ = 10; + +static napi_value TestDefineClass(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value result, return_value; + + napi_property_descriptor property_descriptor = { + "TestDefineClass", + NULL, + TestDefineClass, + NULL, + NULL, + NULL, + napi_enumerable | napi_static, + NULL}; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + status = napi_define_class(NULL, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + &property_descriptor, + &result); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + status); + + napi_define_class(env, + NULL, + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + &property_descriptor, + &result); + + add_last_status(env, "nameIsNull", return_value); + + napi_define_class(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + NULL, + NULL, + 1, + &property_descriptor, + &result); + + add_last_status(env, "cbIsNull", return_value); + + napi_define_class(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + &property_descriptor, + &result); + + add_last_status(env, "cbDataIsNull", return_value); + + napi_define_class(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + NULL, + &result); + + add_last_status(env, "propertiesIsNull", return_value); + + + napi_define_class(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestDefineClass, + NULL, + 1, + &property_descriptor, + NULL); + + add_last_status(env, "resultIsNull", return_value); + + return return_value; +} + +static napi_value GetValue(napi_env env, napi_callback_info info) { + size_t argc = 0; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, NULL, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 0, "Wrong number of arguments"); + + napi_value number; + NODE_API_CALL(env, napi_create_double(env, value_, &number)); + + return number; +} + +static napi_value SetValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value_)); + + return NULL; +} + +static napi_value Echo(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + return args[0]; +} + +static napi_value New(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, NULL, NULL, &_this, NULL)); + + return _this; +} + +static napi_value GetStaticValue(napi_env env, napi_callback_info info) { + size_t argc = 0; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, NULL, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 0, "Wrong number of arguments"); + + napi_value number; + NODE_API_CALL(env, napi_create_double(env, static_value_, &number)); + + return number; +} + + +static napi_value NewExtra(napi_env env, napi_callback_info info) { + napi_value _this; + NODE_API_CALL(env, napi_get_cb_info(env, info, NULL, NULL, &_this, NULL)); + + return _this; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value number, cons; + NODE_API_CALL(env, napi_create_double(env, value_, &number)); + + NODE_API_CALL(env, napi_define_class( + env, "MyObject_Extra", 8, NewExtra, NULL, 0, NULL, &cons)); + + napi_property_descriptor properties[] = { + { "echo", NULL, Echo, NULL, NULL, NULL, napi_enumerable, NULL }, + { "readwriteValue", NULL, NULL, NULL, NULL, number, + napi_enumerable | napi_writable, NULL }, + { "readonlyValue", NULL, NULL, NULL, NULL, number, napi_enumerable, + NULL }, + { "hiddenValue", NULL, NULL, NULL, NULL, number, napi_default, NULL }, + { "readwriteAccessor1", NULL, NULL, GetValue, SetValue, NULL, napi_default, + NULL }, + { "readwriteAccessor2", NULL, NULL, GetValue, SetValue, NULL, + napi_writable, NULL }, + { "readonlyAccessor1", NULL, NULL, GetValue, NULL, NULL, napi_default, + NULL }, + { "readonlyAccessor2", NULL, NULL, GetValue, NULL, NULL, napi_writable, + NULL }, + { "staticReadonlyAccessor1", NULL, NULL, GetStaticValue, NULL, NULL, + napi_default | napi_static, NULL}, + { "constructorName", NULL, NULL, NULL, NULL, cons, + napi_enumerable | napi_static, NULL }, + { "TestDefineClass", NULL, TestDefineClass, NULL, NULL, NULL, + napi_enumerable | napi_static, NULL }, + }; + + NODE_API_CALL(env, napi_define_class(env, "MyObject", NAPI_AUTO_LENGTH, New, + NULL, sizeof(properties)/sizeof(*properties), properties, &cons)); + + init_test_null(env, cons); + + return cons; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test_null.c b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.c new file mode 100644 index 00000000..acbe5982 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.c @@ -0,0 +1,111 @@ +#include + +#include "../common.h" +#include "test_null.h" + +static int some_data = 0; + +static napi_value TestConstructor(napi_env env, napi_callback_info info) { + return NULL; +} + +static napi_value TestDefineClass(napi_env env, napi_callback_info info) { + napi_value return_value, cons; + + const napi_property_descriptor prop = + DECLARE_NODE_API_PROPERTY("testConstructor", TestConstructor); + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_define_class(NULL, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 1, + &prop, + &cons)); + + napi_define_class(env, + NULL, + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 1, + &prop, + &cons); + add_last_status(env, "nameIsNull", return_value); + + napi_define_class( + env, "TestClass", 0, TestConstructor, &some_data, 1, &prop, &cons); + add_last_status(env, "lengthIsZero", return_value); + + napi_define_class( + env, "TestClass", NAPI_AUTO_LENGTH, NULL, &some_data, 1, &prop, &cons); + add_last_status(env, "nativeSideIsNull", return_value); + + napi_define_class(env, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + NULL, + 1, + &prop, + &cons); + add_last_status(env, "dataIsNull", return_value); + + napi_define_class(env, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 0, + &prop, + &cons); + add_last_status(env, "propsLengthIsZero", return_value); + + napi_define_class(env, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 1, + NULL, + &cons); + add_last_status(env, "propsIsNull", return_value); + + napi_define_class(env, + "TestClass", + NAPI_AUTO_LENGTH, + TestConstructor, + &some_data, + 1, + &prop, + NULL); + add_last_status(env, "resultIsNull", return_value); + + return return_value; +} + +void init_test_null(napi_env env, napi_value exports) { + napi_value test_null; + + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("testDefineClass", TestDefineClass), + }; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID( + env, + napi_define_properties(env, + test_null, + sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, exports, "testNull", test_null)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test_null.h b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.h new file mode 100644 index 00000000..b142570d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_constructor/test_null.js b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.js new file mode 100644 index 00000000..f944953e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_constructor/test_null.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Test passing NULL to object-related N-APIs. +const { testNull } = require(`./build/${common.buildType}/test_constructor`); +const expectedResult = { + envIsNull: 'Invalid argument', + nameIsNull: 'Invalid argument', + lengthIsZero: 'napi_ok', + nativeSideIsNull: 'Invalid argument', + dataIsNull: 'napi_ok', + propsLengthIsZero: 'napi_ok', + propsIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}; + +assert.deepStrictEqual(testNull.testDefineClass(), expectedResult); diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_conversions/CMakeLists.txt new file mode 100644 index 00000000..732de7c6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(test_conversions + SOURCES + test_conversions.c + test_null.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/binding.gyp b/Tests/NodeApi/test/js-native-api/test_conversions/binding.gyp new file mode 100644 index 00000000..a7be5290 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "test_conversions", + "sources": [ + "test_conversions.c", + "test_null.c", + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/test.js b/Tests/NodeApi/test/js-native-api/test_conversions/test.js new file mode 100644 index 00000000..b5d047a4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/test.js @@ -0,0 +1,218 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const test = require(`./build/${common.buildType}/test_conversions`); + +const boolExpected = /boolean was expected/; +const numberExpected = /number was expected/; +const stringExpected = /string was expected/; + +const testSym = Symbol('test'); + +assert.strictEqual(test.asBool(false), false); +assert.strictEqual(test.asBool(true), true); +assert.throws(() => test.asBool(undefined), boolExpected); +assert.throws(() => test.asBool(null), boolExpected); +assert.throws(() => test.asBool(Number.NaN), boolExpected); +assert.throws(() => test.asBool(0), boolExpected); +assert.throws(() => test.asBool(''), boolExpected); +assert.throws(() => test.asBool('0'), boolExpected); +assert.throws(() => test.asBool(1), boolExpected); +assert.throws(() => test.asBool('1'), boolExpected); +assert.throws(() => test.asBool('true'), boolExpected); +assert.throws(() => test.asBool({}), boolExpected); +assert.throws(() => test.asBool([]), boolExpected); +assert.throws(() => test.asBool(testSym), boolExpected); + +[test.asInt32, test.asUInt32, test.asInt64].forEach((asInt) => { + assert.strictEqual(asInt(0), 0); + assert.strictEqual(asInt(1), 1); + assert.strictEqual(asInt(1.0), 1); + assert.strictEqual(asInt(1.1), 1); + assert.strictEqual(asInt(1.9), 1); + assert.strictEqual(asInt(0.9), 0); + assert.strictEqual(asInt(999.9), 999); + assert.strictEqual(asInt(Number.NaN), 0); + assert.throws(() => asInt(undefined), numberExpected); + assert.throws(() => asInt(null), numberExpected); + assert.throws(() => asInt(false), numberExpected); + assert.throws(() => asInt(''), numberExpected); + assert.throws(() => asInt('1'), numberExpected); + assert.throws(() => asInt({}), numberExpected); + assert.throws(() => asInt([]), numberExpected); + assert.throws(() => asInt(testSym), numberExpected); +}); + +assert.strictEqual(test.asInt32(-1), -1); +assert.strictEqual(test.asInt64(-1), -1); +assert.strictEqual(test.asUInt32(-1), Math.pow(2, 32) - 1); + +assert.strictEqual(test.asDouble(0), 0); +assert.strictEqual(test.asDouble(1), 1); +assert.strictEqual(test.asDouble(1.0), 1.0); +assert.strictEqual(test.asDouble(1.1), 1.1); +assert.strictEqual(test.asDouble(1.9), 1.9); +assert.strictEqual(test.asDouble(0.9), 0.9); +assert.strictEqual(test.asDouble(999.9), 999.9); +assert.strictEqual(test.asDouble(-1), -1); +assert.ok(Number.isNaN(test.asDouble(Number.NaN))); +assert.throws(() => test.asDouble(undefined), numberExpected); +assert.throws(() => test.asDouble(null), numberExpected); +assert.throws(() => test.asDouble(false), numberExpected); +assert.throws(() => test.asDouble(''), numberExpected); +assert.throws(() => test.asDouble('1'), numberExpected); +assert.throws(() => test.asDouble({}), numberExpected); +assert.throws(() => test.asDouble([]), numberExpected); +assert.throws(() => test.asDouble(testSym), numberExpected); + +assert.strictEqual(test.asString(''), ''); +assert.strictEqual(test.asString('test'), 'test'); +assert.throws(() => test.asString(undefined), stringExpected); +assert.throws(() => test.asString(null), stringExpected); +assert.throws(() => test.asString(false), stringExpected); +assert.throws(() => test.asString(1), stringExpected); +assert.throws(() => test.asString(1.1), stringExpected); +assert.throws(() => test.asString(Number.NaN), stringExpected); +assert.throws(() => test.asString({}), stringExpected); +assert.throws(() => test.asString([]), stringExpected); +assert.throws(() => test.asString(testSym), stringExpected); + +assert.strictEqual(test.toBool(true), true); +assert.strictEqual(test.toBool(1), true); +assert.strictEqual(test.toBool(-1), true); +assert.strictEqual(test.toBool('true'), true); +assert.strictEqual(test.toBool('false'), true); +assert.strictEqual(test.toBool({}), true); +assert.strictEqual(test.toBool([]), true); +assert.strictEqual(test.toBool(testSym), true); +assert.strictEqual(test.toBool(false), false); +assert.strictEqual(test.toBool(undefined), false); +assert.strictEqual(test.toBool(null), false); +assert.strictEqual(test.toBool(0), false); +assert.strictEqual(test.toBool(Number.NaN), false); +assert.strictEqual(test.toBool(''), false); + +assert.strictEqual(test.toNumber(0), 0); +assert.strictEqual(test.toNumber(1), 1); +assert.strictEqual(test.toNumber(1.1), 1.1); +assert.strictEqual(test.toNumber(-1), -1); +assert.strictEqual(test.toNumber('0'), 0); +assert.strictEqual(test.toNumber('1'), 1); +assert.strictEqual(test.toNumber('1.1'), 1.1); +assert.strictEqual(test.toNumber([]), 0); +assert.strictEqual(test.toNumber(false), 0); +assert.strictEqual(test.toNumber(null), 0); +assert.strictEqual(test.toNumber(''), 0); +assert.ok(Number.isNaN(test.toNumber(Number.NaN))); +assert.ok(Number.isNaN(test.toNumber({}))); +assert.ok(Number.isNaN(test.toNumber(undefined))); +assert.throws(() => test.toNumber(testSym), TypeError); + +assert.deepStrictEqual({}, test.toObject({})); +assert.deepStrictEqual({ 'test': 1 }, test.toObject({ 'test': 1 })); +assert.deepStrictEqual([], test.toObject([])); +assert.deepStrictEqual([ 1, 2, 3 ], test.toObject([ 1, 2, 3 ])); +assert.deepStrictEqual(new Boolean(false), test.toObject(false)); +assert.deepStrictEqual(new Boolean(true), test.toObject(true)); +assert.deepStrictEqual(new String(''), test.toObject('')); +assert.deepStrictEqual(new Number(0), test.toObject(0)); +assert.deepStrictEqual(new Number(Number.NaN), test.toObject(Number.NaN)); +assert.deepStrictEqual(new Object(testSym), test.toObject(testSym)); +assert.notStrictEqual(test.toObject(false), false); +assert.notStrictEqual(test.toObject(true), true); +assert.notStrictEqual(test.toObject(''), ''); +assert.notStrictEqual(test.toObject(0), 0); +assert.ok(!Number.isNaN(test.toObject(Number.NaN))); + +assert.strictEqual(test.toString(''), ''); +assert.strictEqual(test.toString('test'), 'test'); +assert.strictEqual(test.toString(undefined), 'undefined'); +assert.strictEqual(test.toString(null), 'null'); +assert.strictEqual(test.toString(false), 'false'); +assert.strictEqual(test.toString(true), 'true'); +assert.strictEqual(test.toString(0), '0'); +assert.strictEqual(test.toString(1.1), '1.1'); +assert.strictEqual(test.toString(Number.NaN), 'NaN'); +assert.strictEqual(test.toString({}), '[object Object]'); +assert.strictEqual(test.toString({ toString: () => 'test' }), 'test'); +assert.strictEqual(test.toString([]), ''); +assert.strictEqual(test.toString([ 1, 2, 3 ]), '1,2,3'); +assert.throws(() => test.toString(testSym), TypeError); + +assert.deepStrictEqual(test.testNull.getValueBool(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A boolean was expected', +}); + +assert.deepStrictEqual(test.testNull.getValueInt32(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A number was expected', +}); + +assert.deepStrictEqual(test.testNull.getValueUint32(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A number was expected', +}); + +assert.deepStrictEqual(test.testNull.getValueInt64(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A number was expected', +}); + + +assert.deepStrictEqual(test.testNull.getValueDouble(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'A number was expected', +}); + +assert.deepStrictEqual(test.testNull.coerceToBool(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'napi_ok', +}); + +assert.deepStrictEqual(test.testNull.coerceToObject(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'napi_ok', +}); + +assert.deepStrictEqual(test.testNull.coerceToString(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + inputTypeCheck: 'napi_ok', +}); + +assert.deepStrictEqual(test.testNull.getValueStringUtf8(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + wrongTypeIn: 'A string was expected', + bufAndOutLengthIsNull: 'Invalid argument', +}); + +assert.deepStrictEqual(test.testNull.getValueStringLatin1(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + wrongTypeIn: 'A string was expected', + bufAndOutLengthIsNull: 'Invalid argument', +}); + +assert.deepStrictEqual(test.testNull.getValueStringUtf16(), { + envIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', + wrongTypeIn: 'A string was expected', + bufAndOutLengthIsNull: 'Invalid argument', +}); diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/test_conversions.c b/Tests/NodeApi/test/js-native-api/test_conversions/test_conversions.c new file mode 100644 index 00000000..2db42970 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/test_conversions.c @@ -0,0 +1,158 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +static napi_value AsBool(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool value; + NODE_API_CALL(env, napi_get_value_bool(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_get_boolean(env, value, &output)); + + return output; +} + +static napi_value AsInt32(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int32_t value; + NODE_API_CALL(env, napi_get_value_int32(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_create_int32(env, value, &output)); + + return output; +} + +static napi_value AsUInt32(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + uint32_t value; + NODE_API_CALL(env, napi_get_value_uint32(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, value, &output)); + + return output; +} + +static napi_value AsInt64(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + int64_t value; + NODE_API_CALL(env, napi_get_value_int64(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_create_int64(env, (double)value, &output)); + + return output; +} + +static napi_value AsDouble(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + double value; + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value)); + + napi_value output; + NODE_API_CALL(env, napi_create_double(env, value, &output)); + + return output; +} + +static napi_value AsString(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + char value[100]; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[0], value, sizeof(value), NULL)); + + napi_value output; + NODE_API_CALL(env, napi_create_string_utf8( + env, value, NAPI_AUTO_LENGTH, &output)); + + return output; +} + +static napi_value ToBool(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value output; + NODE_API_CALL(env, napi_coerce_to_bool(env, args[0], &output)); + + return output; +} + +static napi_value ToNumber(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value output; + NODE_API_CALL(env, napi_coerce_to_number(env, args[0], &output)); + + return output; +} + +static napi_value ToObject(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value output; + NODE_API_CALL(env, napi_coerce_to_object(env, args[0], &output)); + + return output; +} + +static napi_value ToString(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value output; + NODE_API_CALL(env, napi_coerce_to_string(env, args[0], &output)); + + return output; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("asBool", AsBool), + DECLARE_NODE_API_PROPERTY("asInt32", AsInt32), + DECLARE_NODE_API_PROPERTY("asUInt32", AsUInt32), + DECLARE_NODE_API_PROPERTY("asInt64", AsInt64), + DECLARE_NODE_API_PROPERTY("asDouble", AsDouble), + DECLARE_NODE_API_PROPERTY("asString", AsString), + DECLARE_NODE_API_PROPERTY("toBool", ToBool), + DECLARE_NODE_API_PROPERTY("toNumber", ToNumber), + DECLARE_NODE_API_PROPERTY("toObject", ToObject), + DECLARE_NODE_API_PROPERTY("toString", ToString), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + init_test_null(env, exports); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/test_null.c b/Tests/NodeApi/test/js-native-api/test_conversions/test_null.c new file mode 100644 index 00000000..e08b986a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/test_null.c @@ -0,0 +1,102 @@ +#include + +#include "../common.h" +#include "test_null.h" + +#define GEN_NULL_CHECK_BINDING(binding_name, output_type, api) \ + static napi_value binding_name(napi_env env, napi_callback_info info) { \ + napi_value return_value; \ + output_type result; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + add_returned_status(env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + api(NULL, return_value, &result)); \ + api(env, NULL, &result); \ + add_last_status(env, "valueIsNull", return_value); \ + api(env, return_value, NULL); \ + add_last_status(env, "resultIsNull", return_value); \ + api(env, return_value, &result); \ + add_last_status(env, "inputTypeCheck", return_value); \ + return return_value; \ + } + +GEN_NULL_CHECK_BINDING(GetValueBool, bool, napi_get_value_bool) +GEN_NULL_CHECK_BINDING(GetValueInt32, int32_t, napi_get_value_int32) +GEN_NULL_CHECK_BINDING(GetValueUint32, uint32_t, napi_get_value_uint32) +GEN_NULL_CHECK_BINDING(GetValueInt64, int64_t, napi_get_value_int64) +GEN_NULL_CHECK_BINDING(GetValueDouble, double, napi_get_value_double) +GEN_NULL_CHECK_BINDING(CoerceToBool, napi_value, napi_coerce_to_bool) +GEN_NULL_CHECK_BINDING(CoerceToObject, napi_value, napi_coerce_to_object) +GEN_NULL_CHECK_BINDING(CoerceToString, napi_value, napi_coerce_to_string) + +#define GEN_NULL_CHECK_STRING_BINDING(binding_name, arg_type, api) \ + static napi_value binding_name(napi_env env, napi_callback_info info) { \ + napi_value return_value; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + arg_type buf1[4]; \ + size_t length1 = 3; \ + add_returned_status(env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + api(NULL, return_value, buf1, length1, &length1)); \ + arg_type buf2[4]; \ + size_t length2 = 3; \ + api(env, NULL, buf2, length2, &length2); \ + add_last_status(env, "valueIsNull", return_value); \ + api(env, return_value, NULL, 3, NULL); \ + add_last_status(env, "wrongTypeIn", return_value); \ + napi_value string; \ + NODE_API_CALL(env, \ + napi_create_string_utf8(env, \ + "Something", \ + NAPI_AUTO_LENGTH, \ + &string)); \ + api(env, string, NULL, 3, NULL); \ + add_last_status(env, "bufAndOutLengthIsNull", return_value); \ + return return_value; \ + } + +GEN_NULL_CHECK_STRING_BINDING(GetValueStringUtf8, + char, + napi_get_value_string_utf8) +GEN_NULL_CHECK_STRING_BINDING(GetValueStringLatin1, + char, + napi_get_value_string_latin1) +GEN_NULL_CHECK_STRING_BINDING(GetValueStringUtf16, + char16_t, + napi_get_value_string_utf16) + +void init_test_null(napi_env env, napi_value exports) { + napi_value test_null; + + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("getValueBool", GetValueBool), + DECLARE_NODE_API_PROPERTY("getValueInt32", GetValueInt32), + DECLARE_NODE_API_PROPERTY("getValueUint32", GetValueUint32), + DECLARE_NODE_API_PROPERTY("getValueInt64", GetValueInt64), + DECLARE_NODE_API_PROPERTY("getValueDouble", GetValueDouble), + DECLARE_NODE_API_PROPERTY("coerceToBool", CoerceToBool), + DECLARE_NODE_API_PROPERTY("coerceToObject", CoerceToObject), + DECLARE_NODE_API_PROPERTY("coerceToString", CoerceToString), + DECLARE_NODE_API_PROPERTY("getValueStringUtf8", GetValueStringUtf8), + DECLARE_NODE_API_PROPERTY("getValueStringLatin1", GetValueStringLatin1), + DECLARE_NODE_API_PROPERTY("getValueStringUtf16", GetValueStringUtf16), + }; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID(env, napi_define_properties( + env, test_null, sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + + const napi_property_descriptor test_null_set = { + "testNull", NULL, NULL, NULL, NULL, test_null, napi_enumerable, NULL + }; + + NODE_API_CALL_RETURN_VOID(env, + napi_define_properties(env, exports, 1, &test_null_set)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_conversions/test_null.h b/Tests/NodeApi/test/js-native-api/test_conversions/test_null.h new file mode 100644 index 00000000..fe6ad77a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_conversions/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_CONVERSIONS_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_CONVERSIONS_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_CONVERSIONS_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_dataview/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_dataview/CMakeLists.txt new file mode 100644 index 00000000..d809ba9a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_dataview/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_dataview + SOURCES + test_dataview.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_dataview/binding.gyp b/Tests/NodeApi/test/js-native-api/test_dataview/binding.gyp new file mode 100644 index 00000000..a8b4f1d4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_dataview/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_dataview", + "sources": [ + "test_dataview.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_dataview/test.js b/Tests/NodeApi/test/js-native-api/test_dataview/test.js new file mode 100644 index 00000000..2bfd109d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_dataview/test.js @@ -0,0 +1,24 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for arrays +const test_dataview = require(`./build/${common.buildType}/test_dataview`); + +// Test for creating dataview +{ + const buffer = new ArrayBuffer(128); + const template = Reflect.construct(DataView, [buffer]); + + const theDataview = test_dataview.CreateDataViewFromJSDataView(template); + assert.ok(theDataview instanceof DataView, + `Expect ${theDataview} to be a DataView`); +} + +// Test for creating dataview with invalid range +{ + const buffer = new ArrayBuffer(128); + assert.throws(() => { + test_dataview.CreateDataView(buffer, 10, 200); + }, RangeError); +} diff --git a/Tests/NodeApi/test/js-native-api/test_dataview/test_dataview.c b/Tests/NodeApi/test/js-native-api/test_dataview/test_dataview.c new file mode 100644 index 00000000..20a840de --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_dataview/test_dataview.c @@ -0,0 +1,102 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value CreateDataView(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args [3]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 3, "Wrong number of arguments"); + + napi_valuetype valuetype0; + napi_value arraybuffer = args[0]; + + NODE_API_CALL(env, napi_typeof(env, arraybuffer, &valuetype0)); + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects a ArrayBuffer as the first " + "argument."); + + bool is_arraybuffer; + NODE_API_CALL(env, napi_is_arraybuffer(env, arraybuffer, &is_arraybuffer)); + NODE_API_ASSERT(env, is_arraybuffer, + "Wrong type of arguments. Expects a ArrayBuffer as the first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects a number as second argument."); + + size_t byte_offset = 0; + NODE_API_CALL(env, napi_get_value_uint32(env, args[1], (uint32_t*)(&byte_offset))); + + napi_valuetype valuetype2; + NODE_API_CALL(env, napi_typeof(env, args[2], &valuetype2)); + + NODE_API_ASSERT(env, valuetype2 == napi_number, + "Wrong type of arguments. Expects a number as third argument."); + + size_t length = 0; + NODE_API_CALL(env, napi_get_value_uint32(env, args[2], (uint32_t*)(&length))); + + napi_value output_dataview; + NODE_API_CALL(env, + napi_create_dataview(env, length, arraybuffer, + byte_offset, &output_dataview)); + + return output_dataview; +} + +static napi_value CreateDataViewFromJSDataView(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args [1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + napi_valuetype valuetype; + napi_value input_dataview = args[0]; + + NODE_API_CALL(env, napi_typeof(env, input_dataview, &valuetype)); + NODE_API_ASSERT(env, valuetype == napi_object, + "Wrong type of arguments. Expects a DataView as the first " + "argument."); + + bool is_dataview; + NODE_API_CALL(env, napi_is_dataview(env, input_dataview, &is_dataview)); + NODE_API_ASSERT(env, is_dataview, + "Wrong type of arguments. Expects a DataView as the first " + "argument."); + size_t byte_offset = 0; + size_t length = 0; + napi_value buffer; + NODE_API_CALL(env, + napi_get_dataview_info(env, input_dataview, &length, NULL, + &buffer, &byte_offset)); + + napi_value output_dataview; + NODE_API_CALL(env, + napi_create_dataview(env, length, buffer, + byte_offset, &output_dataview)); + + + return output_dataview; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("CreateDataView", CreateDataView), + DECLARE_NODE_API_PROPERTY("CreateDataViewFromJSDataView", + CreateDataViewFromJSDataView) + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_date/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_date/CMakeLists.txt new file mode 100644 index 00000000..9c9736c8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_date/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_date + SOURCES + test_date.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_date/binding.gyp b/Tests/NodeApi/test/js-native-api/test_date/binding.gyp new file mode 100644 index 00000000..e08eaf6d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_date/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_date", + "sources": [ + "test_date.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_date/test.js b/Tests/NodeApi/test/js-native-api/test_date/test.js new file mode 100644 index 00000000..637f9f94 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_date/test.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../../common'); + +// This tests the date-related n-api calls + +const assert = require('assert'); +const test_date = require(`./build/${common.buildType}/test_date`); + +const dateTypeTestDate = test_date.createDate(1549183351); +assert.strictEqual(test_date.isDate(dateTypeTestDate), true); + +assert.strictEqual(test_date.isDate(new Date(1549183351)), true); + +assert.strictEqual(test_date.isDate(2.4), false); +assert.strictEqual(test_date.isDate('not a date'), false); +assert.strictEqual(test_date.isDate(undefined), false); +assert.strictEqual(test_date.isDate(null), false); +assert.strictEqual(test_date.isDate({}), false); + +assert.strictEqual(test_date.getDateValue(new Date(1549183351)), 1549183351); diff --git a/Tests/NodeApi/test/js-native-api/test_date/test_date.c b/Tests/NodeApi/test/js-native-api/test_date/test_date.c new file mode 100644 index 00000000..a9eeb4f0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_date/test_date.c @@ -0,0 +1,64 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value createDate(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + double time; + NODE_API_CALL(env, napi_get_value_double(env, args[0], &time)); + + napi_value date; + NODE_API_CALL(env, napi_create_date(env, time, &date)); + + return date; +} + +static napi_value isDate(napi_env env, napi_callback_info info) { + napi_value date, result; + size_t argc = 1; + bool is_date; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &date, NULL, NULL)); + NODE_API_CALL(env, napi_is_date(env, date, &is_date)); + NODE_API_CALL(env, napi_get_boolean(env, is_date, &result)); + + return result; +} + +static napi_value getDateValue(napi_env env, napi_callback_info info) { + napi_value date, result; + size_t argc = 1; + double value; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &date, NULL, NULL)); + NODE_API_CALL(env, napi_get_date_value(env, date, &value)); + NODE_API_CALL(env, napi_create_double(env, value, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("createDate", createDate), + DECLARE_NODE_API_PROPERTY("isDate", isDate), + DECLARE_NODE_API_PROPERTY("getDateValue", getDateValue), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_error/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_error/CMakeLists.txt new file mode 100644 index 00000000..955fd2a0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_error/CMakeLists.txt @@ -0,0 +1,6 @@ +add_node_api_module(test_error + SOURCES + test_error.c + DEFINES + "NAPI_VERSION=9" +) diff --git a/Tests/NodeApi/test/js-native-api/test_error/binding.gyp b/Tests/NodeApi/test/js-native-api/test_error/binding.gyp new file mode 100644 index 00000000..f0448028 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_error/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_error", + "sources": [ + "test_error.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_error/test.js b/Tests/NodeApi/test/js-native-api/test_error/test.js new file mode 100644 index 00000000..f6ba3799 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_error/test.js @@ -0,0 +1,148 @@ +'use strict'; + +const common = require('../../common'); +const test_error = require(`./build/${common.buildType}/test_error`); +const assert = require('assert'); +const theError = new Error('Some error'); +const theTypeError = new TypeError('Some type error'); +const theSyntaxError = new SyntaxError('Some syntax error'); +const theRangeError = new RangeError('Some type error'); +const theReferenceError = new ReferenceError('Some reference error'); +const theURIError = new URIError('Some URI error'); +const theEvalError = new EvalError('Some eval error'); + +class MyError extends Error { } +const myError = new MyError('Some MyError'); + +// Test that native error object is correctly classed +assert.strictEqual(test_error.checkError(theError), true); + +// Test that native type error object is correctly classed +assert.strictEqual(test_error.checkError(theTypeError), true); + +// Test that native syntax error object is correctly classed +assert.strictEqual(test_error.checkError(theSyntaxError), true); + +// Test that native range error object is correctly classed +assert.strictEqual(test_error.checkError(theRangeError), true); + +// Test that native reference error object is correctly classed +assert.strictEqual(test_error.checkError(theReferenceError), true); + +// Test that native URI error object is correctly classed +assert.strictEqual(test_error.checkError(theURIError), true); + +// Test that native eval error object is correctly classed +assert.strictEqual(test_error.checkError(theEvalError), true); + +// Test that class derived from native error is correctly classed +assert.strictEqual(test_error.checkError(myError), true); + +// Test that non-error object is correctly classed +assert.strictEqual(test_error.checkError({}), false); + +// Test that non-error primitive is correctly classed +assert.strictEqual(test_error.checkError('non-object'), false); + +assert.throws(() => { + test_error.throwExistingError(); +}, /^Error: existing error$/); + +assert.throws(() => { + test_error.throwError(); +}, /^Error: error$/); + +assert.throws(() => { + test_error.throwRangeError(); +}, /^RangeError: range error$/); + +assert.throws(() => { + test_error.throwTypeError(); +}, /^TypeError: type error$/); + +assert.throws(() => { + test_error.throwSyntaxError(); +}, /^SyntaxError: syntax error$/); + +[42, {}, [], Symbol('xyzzy'), true, 'ball', undefined, null, NaN] + .forEach((value) => assert.throws( + () => test_error.throwArbitrary(value), + (err) => { + assert.strictEqual(err, value); + return true; + }, + )); + +assert.throws( + () => test_error.throwErrorCode(), + { + code: 'ERR_TEST_CODE', + message: 'Error [error]', + }); + +assert.throws( + () => test_error.throwRangeErrorCode(), + { + code: 'ERR_TEST_CODE', + message: 'RangeError [range error]', + }); + +assert.throws( + () => test_error.throwTypeErrorCode(), + { + code: 'ERR_TEST_CODE', + message: 'TypeError [type error]', + }); + +assert.throws( + () => test_error.throwSyntaxErrorCode(), + { + code: 'ERR_TEST_CODE', + message: 'SyntaxError [syntax error]', + }); + +let error = test_error.createError(); +assert.ok(error instanceof Error, 'expected error to be an instance of Error'); +assert.strictEqual(error.message, 'error'); + +error = test_error.createRangeError(); +assert.ok(error instanceof RangeError, + 'expected error to be an instance of RangeError'); +assert.strictEqual(error.message, 'range error'); + +error = test_error.createTypeError(); +assert.ok(error instanceof TypeError, + 'expected error to be an instance of TypeError'); +assert.strictEqual(error.message, 'type error'); + +error = test_error.createSyntaxError(); +assert.ok(error instanceof SyntaxError, + 'expected error to be an instance of SyntaxError'); +assert.strictEqual(error.message, 'syntax error'); + +error = test_error.createErrorCode(); +assert.ok(error instanceof Error, 'expected error to be an instance of Error'); +assert.strictEqual(error.code, 'ERR_TEST_CODE'); +assert.strictEqual(error.message, 'Error [error]'); +assert.strictEqual(error.name, 'Error'); + +error = test_error.createRangeErrorCode(); +assert.ok(error instanceof RangeError, + 'expected error to be an instance of RangeError'); +assert.strictEqual(error.message, 'RangeError [range error]'); +assert.strictEqual(error.code, 'ERR_TEST_CODE'); +assert.strictEqual(error.name, 'RangeError'); + +error = test_error.createTypeErrorCode(); +assert.ok(error instanceof TypeError, + 'expected error to be an instance of TypeError'); +assert.strictEqual(error.message, 'TypeError [type error]'); +assert.strictEqual(error.code, 'ERR_TEST_CODE'); +assert.strictEqual(error.name, 'TypeError'); + +error = test_error.createSyntaxErrorCode(); +assert.ok(error instanceof SyntaxError, + 'expected error to be an instance of SyntaxError'); +assert.strictEqual(error.message, 'SyntaxError [syntax error]'); +assert.strictEqual(error.code, 'ERR_TEST_CODE'); +assert.strictEqual(error.name, 'SyntaxError'); diff --git a/Tests/NodeApi/test/js-native-api/test_error/test_error.c b/Tests/NodeApi/test/js-native-api/test_error/test_error.c new file mode 100644 index 00000000..fc4b8758 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_error/test_error.c @@ -0,0 +1,197 @@ +#define NAPI_VERSION 9 +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value checkError(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool r; + NODE_API_CALL(env, napi_is_error(env, args[0], &r)); + + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, r, &result)); + + return result; +} + +static napi_value throwExistingError(napi_env env, napi_callback_info info) { + napi_value message; + napi_value error; + NODE_API_CALL(env, napi_create_string_utf8( + env, "existing error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_error(env, NULL, message, &error)); + NODE_API_CALL(env, napi_throw(env, error)); + return NULL; +} + +static napi_value throwError(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, napi_throw_error(env, NULL, "error")); + return NULL; +} + +static napi_value throwRangeError(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, napi_throw_range_error(env, NULL, "range error")); + return NULL; +} + +static napi_value throwTypeError(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, napi_throw_type_error(env, NULL, "type error")); + return NULL; +} + +static napi_value throwSyntaxError(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, node_api_throw_syntax_error(env, NULL, "syntax error")); + return NULL; +} + +static napi_value throwErrorCode(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, napi_throw_error(env, "ERR_TEST_CODE", "Error [error]")); + return NULL; +} + +static napi_value throwRangeErrorCode(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, + napi_throw_range_error(env, "ERR_TEST_CODE", "RangeError [range error]")); + return NULL; +} + +static napi_value throwTypeErrorCode(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, + napi_throw_type_error(env, "ERR_TEST_CODE", "TypeError [type error]")); + return NULL; +} + +static napi_value throwSyntaxErrorCode(napi_env env, napi_callback_info info) { + NODE_API_CALL(env, + node_api_throw_syntax_error(env, "ERR_TEST_CODE", "SyntaxError [syntax error]")); + return NULL; +} + +static napi_value createError(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + NODE_API_CALL(env, napi_create_string_utf8( + env, "error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_error(env, NULL, message, &result)); + return result; +} + +static napi_value createRangeError(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + NODE_API_CALL(env, napi_create_string_utf8( + env, "range error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_range_error(env, NULL, message, &result)); + return result; +} + +static napi_value createTypeError(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + NODE_API_CALL(env, napi_create_string_utf8( + env, "type error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_type_error(env, NULL, message, &result)); + return result; +} + +static napi_value createSyntaxError(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + NODE_API_CALL(env, napi_create_string_utf8( + env, "syntax error", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, node_api_create_syntax_error(env, NULL, message, &result)); + return result; +} + +static napi_value createErrorCode(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + napi_value code; + NODE_API_CALL(env, napi_create_string_utf8( + env, "Error [error]", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8( + env, "ERR_TEST_CODE", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, napi_create_error(env, code, message, &result)); + return result; +} + +static napi_value createRangeErrorCode(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + napi_value code; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "RangeError [range error]", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8( + env, "ERR_TEST_CODE", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, napi_create_range_error(env, code, message, &result)); + return result; +} + +static napi_value createTypeErrorCode(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + napi_value code; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "TypeError [type error]", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8( + env, "ERR_TEST_CODE", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, napi_create_type_error(env, code, message, &result)); + return result; +} + +static napi_value createSyntaxErrorCode(napi_env env, napi_callback_info info) { + napi_value result; + napi_value message; + napi_value code; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "SyntaxError [syntax error]", NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8( + env, "ERR_TEST_CODE", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, node_api_create_syntax_error(env, code, message, &result)); + return result; +} + +static napi_value throwArbitrary(napi_env env, napi_callback_info info) { + napi_value arbitrary; + size_t argc = 1; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arbitrary, NULL, NULL)); + NODE_API_CALL(env, napi_throw(env, arbitrary)); + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("checkError", checkError), + DECLARE_NODE_API_PROPERTY("throwExistingError", throwExistingError), + DECLARE_NODE_API_PROPERTY("throwError", throwError), + DECLARE_NODE_API_PROPERTY("throwRangeError", throwRangeError), + DECLARE_NODE_API_PROPERTY("throwTypeError", throwTypeError), + DECLARE_NODE_API_PROPERTY("throwSyntaxError", throwSyntaxError), + DECLARE_NODE_API_PROPERTY("throwErrorCode", throwErrorCode), + DECLARE_NODE_API_PROPERTY("throwRangeErrorCode", throwRangeErrorCode), + DECLARE_NODE_API_PROPERTY("throwTypeErrorCode", throwTypeErrorCode), + DECLARE_NODE_API_PROPERTY("throwSyntaxErrorCode", throwSyntaxErrorCode), + DECLARE_NODE_API_PROPERTY("throwArbitrary", throwArbitrary), + DECLARE_NODE_API_PROPERTY("createError", createError), + DECLARE_NODE_API_PROPERTY("createRangeError", createRangeError), + DECLARE_NODE_API_PROPERTY("createTypeError", createTypeError), + DECLARE_NODE_API_PROPERTY("createSyntaxError", createSyntaxError), + DECLARE_NODE_API_PROPERTY("createErrorCode", createErrorCode), + DECLARE_NODE_API_PROPERTY("createRangeErrorCode", createRangeErrorCode), + DECLARE_NODE_API_PROPERTY("createTypeErrorCode", createTypeErrorCode), + DECLARE_NODE_API_PROPERTY("createSyntaxErrorCode", createSyntaxErrorCode), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_exception/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_exception/CMakeLists.txt new file mode 100644 index 00000000..4d8494f9 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_exception + SOURCES + test_exception.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_exception/binding.gyp b/Tests/NodeApi/test/js-native-api/test_exception/binding.gyp new file mode 100644 index 00000000..a453505d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_exception", + "sources": [ + "test_exception.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_exception/test.js b/Tests/NodeApi/test/js-native-api/test_exception/test.js new file mode 100644 index 00000000..3e070fed --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/test.js @@ -0,0 +1,115 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const assert = require('assert'); +const theError = new Error('Some error'); + +// The test module throws an error during Init, but in order for its exports to +// not be lost, it attaches them to the error's "bindings" property. This way, +// we can make sure that exceptions thrown during the module initialization +// phase are propagated through require() into JavaScript. +// https://github.com/nodejs/node/issues/19437 +const test_exception = (function() { + let resultingException; + try { + require(`./build/${common.buildType}/test_exception`); + } catch (anException) { + resultingException = anException; + } + assert.strictEqual(resultingException.message, 'Error during Init'); + return resultingException.binding; +})(); + +{ + const throwTheError = () => { throw theError; }; + + // Test that the native side successfully captures the exception + let returnedError = test_exception.returnException(throwTheError); + assert.strictEqual(returnedError, theError); + + // Test that the native side passes the exception through + assert.throws( + () => { test_exception.allowException(throwTheError); }, + (err) => err === theError, + ); + + // Test that the exception thrown above was marked as pending + // before it was handled on the JS side + const exception_pending = test_exception.wasPending(); + assert.strictEqual(exception_pending, true, + 'Exception not pending as expected,' + + ` .wasPending() returned ${exception_pending}`); + + // Test that the native side does not capture a non-existing exception + returnedError = test_exception.returnException(common.mustCall()); + assert.strictEqual(returnedError, undefined, + 'Returned error should be undefined when no exception is' + + ` thrown, but ${returnedError} was passed`); +} + + +{ + const throwTheError = class { constructor() { throw theError; } }; + + // Test that the native side successfully captures the exception + let returnedError = test_exception.constructReturnException(throwTheError); + assert.strictEqual(returnedError, theError); + + // Test that the native side passes the exception through + assert.throws( + () => { test_exception.constructAllowException(throwTheError); }, + (err) => err === theError, + ); + + // Test that the exception thrown above was marked as pending + // before it was handled on the JS side + const exception_pending = test_exception.wasPending(); + assert.strictEqual(exception_pending, true, + 'Exception not pending as expected,' + + ` .wasPending() returned ${exception_pending}`); + + // Test that the native side does not capture a non-existing exception + returnedError = test_exception.constructReturnException(common.mustCall()); + assert.strictEqual(returnedError, undefined, + 'Returned error should be undefined when no exception is' + + ` thrown, but ${returnedError} was passed`); +} + +{ + // Test that no exception appears that was not thrown by us + let caughtError; + try { + test_exception.allowException(common.mustCall()); + } catch (anError) { + caughtError = anError; + } + assert.strictEqual(caughtError, undefined, + 'No exception originated on the native side, but' + + ` ${caughtError} was passed`); + + // Test that the exception state remains clear when no exception is thrown + const exception_pending = test_exception.wasPending(); + assert.strictEqual(exception_pending, false, + 'Exception state did not remain clear as expected,' + + ` .wasPending() returned ${exception_pending}`); +} + +{ + // Test that no exception appears that was not thrown by us + let caughtError; + try { + test_exception.constructAllowException(common.mustCall()); + } catch (anError) { + caughtError = anError; + } + assert.strictEqual(caughtError, undefined, + 'No exception originated on the native side, but' + + ` ${caughtError} was passed`); + + // Test that the exception state remains clear when no exception is thrown + const exception_pending = test_exception.wasPending(); + assert.strictEqual(exception_pending, false, + 'Exception state did not remain clear as expected,' + + ` .wasPending() returned ${exception_pending}`); +} diff --git a/Tests/NodeApi/test/js-native-api/test_exception/testFinalizerException.js b/Tests/NodeApi/test/js-native-api/test_exception/testFinalizerException.js new file mode 100644 index 00000000..dce63624 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/testFinalizerException.js @@ -0,0 +1,31 @@ +'use strict'; +if (process.argv[2] === 'child') { + const common = require('../../common'); + // Trying, catching the exception, and finding the bindings at the `Error`'s + // `binding` property is done intentionally, because we're also testing what + // happens when the add-on entry point throws. See test.js. + try { + require(`./build/${common.buildType}/test_exception`); + } catch (anException) { + anException.binding.createExternal(); + } + + // Collect garbage 10 times. At least one of those should throw the exception + // and cause the whole process to bail with it, its text printed to stderr and + // asserted by the parent process to match expectations. + let gcCount = 10; + (function gcLoop() { + global.gc(); + if (--gcCount > 0) { + setImmediate(() => gcLoop()); + } + })(); +} else { + const assert = require('assert'); + const { spawnSync } = require('child_process'); + const child = spawnSync(process.execPath, [ + '--expose-gc', __filename, 'child', + ]); + assert.strictEqual(child.signal, null); + assert.match(child.stderr.toString(), /Error during Finalize/m); +} \ No newline at end of file diff --git a/Tests/NodeApi/test/js-native-api/test_exception/test_exception.c b/Tests/NodeApi/test/js-native-api/test_exception/test_exception.c new file mode 100644 index 00000000..de1eb42a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_exception/test_exception.c @@ -0,0 +1,116 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static bool exceptionWasPending = false; +static int num = 0x23432; + +static napi_value returnException(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + + napi_value result; + napi_status status = napi_call_function(env, global, args[0], 0, 0, &result); + if (status == napi_pending_exception) { + napi_value ex; + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &ex)); + return ex; + } + + return NULL; +} + +static napi_value constructReturnException(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result; + napi_status status = napi_new_instance(env, args[0], 0, 0, &result); + if (status == napi_pending_exception) { + napi_value ex; + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &ex)); + return ex; + } + + return NULL; +} + +static napi_value allowException(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + + napi_value result; + napi_call_function(env, global, args[0], 0, 0, &result); + // Ignore status and check napi_is_exception_pending() instead. + + NODE_API_CALL(env, napi_is_exception_pending(env, &exceptionWasPending)); + return NULL; +} + +static napi_value constructAllowException(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result; + napi_new_instance(env, args[0], 0, 0, &result); + // Ignore status and check napi_is_exception_pending() instead. + + NODE_API_CALL(env, napi_is_exception_pending(env, &exceptionWasPending)); + return NULL; +} + +static napi_value wasPending(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, exceptionWasPending, &result)); + + return result; +} + +static void finalizer(napi_env env, void *data, void *hint) { + NODE_API_CALL_RETURN_VOID(env, + napi_throw_error(env, NULL, "Error during Finalize")); +} + +static napi_value createExternal(napi_env env, napi_callback_info info) { + napi_value external; + + NODE_API_CALL(env, + napi_create_external(env, &num, finalizer, NULL, &external)); + + return external; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("returnException", returnException), + DECLARE_NODE_API_PROPERTY("allowException", allowException), + DECLARE_NODE_API_PROPERTY("constructReturnException", constructReturnException), + DECLARE_NODE_API_PROPERTY("constructAllowException", constructAllowException), + DECLARE_NODE_API_PROPERTY("wasPending", wasPending), + DECLARE_NODE_API_PROPERTY("createExternal", createExternal), + }; + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + napi_value error, code, message; + NODE_API_CALL(env, napi_create_string_utf8(env, "Error during Init", + NAPI_AUTO_LENGTH, &message)); + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &code)); + NODE_API_CALL(env, napi_create_error(env, code, message, &error)); + NODE_API_CALL(env, napi_set_named_property(env, error, "binding", exports)); + NODE_API_CALL(env, napi_throw(env, error)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_finalizer/CMakeLists.txt new file mode 100644 index 00000000..ce560dad --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/CMakeLists.txt @@ -0,0 +1,7 @@ +add_node_api_module(test_finalizer + SOURCES + test_finalizer.c + DEFINES + NAPI_EXPERIMENTAL + NODE_API_EXPERIMENTAL_NO_WARNING +) diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/binding.gyp b/Tests/NodeApi/test/js-native-api/test_finalizer/binding.gyp new file mode 100644 index 00000000..8553fd2d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "test_finalizer", + "defines": [ "NAPI_EXPERIMENTAL" ], + "sources": [ + "test_finalizer.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/test.js b/Tests/NodeApi/test/js-native-api/test_finalizer/test.js new file mode 100644 index 00000000..3edf53ce --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/test.js @@ -0,0 +1,45 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const test_finalizer = require(`./build/${common.buildType}/test_finalizer`); +const assert = require('assert'); + +const { gcUntil } = require('../../common/gc'); + +// The goal of this test is to show that we can run "pure" finalizers in the +// current JS loop tick. Thus, we do not use gcUntil function works +// asynchronously using micro tasks. +// We use IIFE for the obj scope instead of {} to be compatible with +// non-V8 JS engines that do not support scoped variables. +(() => { + const obj = {}; + test_finalizer.addFinalizer(obj); +})(); + +for (let i = 0; i < 10; ++i) { + global.gc(); + if (test_finalizer.getFinalizerCallCount() === 1) { + break; + } +} + +assert.strictEqual(test_finalizer.getFinalizerCallCount(), 1); + +// The finalizer that access JS cannot run synchronously. They are run in the +// next JS loop tick. Thus, we must use gcUntil. +async function runAsyncTests() { + // We do not use common.mustCall() because we want to see the finalizer + // called in response to GC and not as a part of env destruction. + let js_is_called = false; + // We use IIFE for the obj scope instead of {} to be compatible with + // non-V8 JS engines that do not support scoped variables. + (() => { + const obj = {}; + test_finalizer.addFinalizerWithJS(obj, () => { js_is_called = true; }); + })(); + await gcUntil('ensure JS finalizer called', + () => (test_finalizer.getFinalizerCallCount() === 2)); + assert(js_is_called); +} +runAsyncTests(); diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/test_fatal_finalize.js b/Tests/NodeApi/test/js-native-api/test_finalizer/test_fatal_finalize.js new file mode 100644 index 00000000..6b725414 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/test_fatal_finalize.js @@ -0,0 +1,35 @@ +"use strict"; +const common = require("../../common"); + +if (process.argv[2] === "child") { + const test_finalizer = require(`./build/${common.buildType}/test_finalizer`); + + (() => { + const obj = {}; + test_finalizer.addFinalizerFailOnJS(obj); + })(); + + // Collect garbage 10 times. At least one of those should throw the exception + // and cause the whole process to bail with it, its text printed to stderr and + // asserted by the parent process to match expectations. + let gcCount = 10; + (function gcLoop() { + global.gc(); + if (--gcCount > 0) { + setImmediate(() => gcLoop()); + } + })(); +} else { + const assert = require("assert"); + const { spawnSync } = require("child_process"); + const child = spawnSync(process.execPath, [ + "--expose-gc", + __filename, + "child", + ]); + assert(common.nodeProcessAborted(child.status, child.signal)); + assert.match( + child.stderr.toString(), + /Finalizer is calling a function that may affect GC state/ + ); +} diff --git a/Tests/NodeApi/test/js-native-api/test_finalizer/test_finalizer.c b/Tests/NodeApi/test/js-native-api/test_finalizer/test_finalizer.c new file mode 100644 index 00000000..0d829eee --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_finalizer/test_finalizer.c @@ -0,0 +1,148 @@ +#include +#include +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +typedef struct { + int32_t finalize_count; + napi_ref js_func; +} FinalizerData; + +static void finalizerOnlyCallback(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) { + FinalizerData* data = (FinalizerData*)finalize_data; + int32_t count = ++data->finalize_count; + + // It is safe to access instance data + NODE_API_BASIC_CALL_RETURN_VOID(env, + napi_get_instance_data(env, (void**)&data)); + NODE_API_BASIC_ASSERT_RETURN_VOID(count == data->finalize_count, + "Expected to be the same FinalizerData"); +} + +static void finalizerCallingJSCallback(napi_env env, + void* finalize_data, + void* finalize_hint) { + napi_value js_func, undefined; + FinalizerData* data = (FinalizerData*)finalize_data; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, data->js_func, &js_func)); + NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, undefined, js_func, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_func)); + data->js_func = NULL; + ++data->finalize_count; +} + +// Schedule async finalizer to run JavaScript-touching code. +static void finalizerWithJSCallback(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) { + NODE_API_BASIC_CALL_RETURN_VOID( + env, + node_api_post_finalizer( + env, finalizerCallingJSCallback, finalize_data, finalize_hint)); +} + +static void finalizerWithFailedJSCallback(node_api_basic_env basic_env, + void* finalize_data, + void* finalize_hint) { + // Intentionally cast to a napi_env to test the fatal failure. + napi_env env = (napi_env)basic_env; + napi_value obj; + FinalizerData* data = (FinalizerData*)finalize_data; + ++data->finalize_count; + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &obj)); +} + +static napi_value addFinalizer(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1] = {0}; + FinalizerData* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, + napi_add_finalizer( + env, argv[0], data, finalizerOnlyCallback, NULL, NULL)); + return NULL; +} + +// This finalizer is going to call JavaScript from finalizer and succeed. +static napi_value addFinalizerWithJS(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2] = {0}; + napi_valuetype arg_type; + FinalizerData* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, napi_typeof(env, argv[1], &arg_type)); + NODE_API_ASSERT( + env, arg_type == napi_function, "Expected function as the second arg"); + NODE_API_CALL(env, napi_create_reference(env, argv[1], 1, &data->js_func)); + NODE_API_CALL(env, + napi_add_finalizer( + env, argv[0], data, finalizerWithJSCallback, NULL, NULL)); + return NULL; +} + +// This finalizer is going to call JavaScript from finalizer and fail. +static napi_value addFinalizerFailOnJS(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1] = {0}; + FinalizerData* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL( + env, + napi_add_finalizer( + env, argv[0], data, finalizerWithFailedJSCallback, NULL, NULL)); + return NULL; +} + +static napi_value getFinalizerCallCount(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + FinalizerData* data; + napi_value result; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, napi_create_int32(env, data->finalize_count, &result)); + return result; +} + +static void finalizeData(napi_env env, void* data, void* hint) { + free(data); +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + FinalizerData* data = (FinalizerData*)malloc(sizeof(FinalizerData)); + NODE_API_ASSERT(env, data != NULL, "Failed to allocate memory"); + memset(data, 0, sizeof(FinalizerData)); + NODE_API_CALL(env, napi_set_instance_data(env, data, finalizeData, NULL)); + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("addFinalizer", addFinalizer), + DECLARE_NODE_API_PROPERTY("addFinalizerWithJS", addFinalizerWithJS), + DECLARE_NODE_API_PROPERTY("addFinalizerFailOnJS", addFinalizerFailOnJS), + DECLARE_NODE_API_PROPERTY("getFinalizerCallCount", + getFinalizerCallCount)}; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_function/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_function/CMakeLists.txt new file mode 100644 index 00000000..ee290680 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_function/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_function + SOURCES + test_function.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_function/binding.gyp b/Tests/NodeApi/test/js-native-api/test_function/binding.gyp new file mode 100644 index 00000000..7cd97f9d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_function/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_function", + "sources": [ + "test_function.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_function/test.js b/Tests/NodeApi/test/js-native-api/test_function/test.js new file mode 100644 index 00000000..4899fe33 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_function/test.js @@ -0,0 +1,52 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for function +const test_function = require(`./build/${common.buildType}/test_function`); + +function func1() { + return 1; +} +assert.strictEqual(test_function.TestCall(func1), 1); + +function func2() { + console.log('hello world!'); + return null; +} +assert.strictEqual(test_function.TestCall(func2), null); + +function func3(input) { + return input + 1; +} +assert.strictEqual(test_function.TestCall(func3, 1), 2); + +function func4(input) { + return func3(input); +} +assert.strictEqual(test_function.TestCall(func4, 1), 2); + +assert.strictEqual(test_function.TestName.name, 'Name'); +assert.strictEqual(test_function.TestNameShort.name, 'Name_'); + +let tracked_function = test_function.MakeTrackedFunction(common.mustCall()); +assert(!!tracked_function); +tracked_function = null; +global.gc(); + +assert.deepStrictEqual(test_function.TestCreateFunctionParameters(), { + envIsNull: 'Invalid argument', + nameIsNull: 'napi_ok', + cbIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +assert.throws( + () => test_function.TestBadReturnExceptionPending(), + { + code: 'throwing exception', + name: 'Error', + }, +); diff --git a/Tests/NodeApi/test/js-native-api/test_function/test_function.c b/Tests/NodeApi/test/js-native-api/test_function/test_function.c new file mode 100644 index 00000000..be660034 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_function/test_function.c @@ -0,0 +1,204 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value TestCreateFunctionParameters(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value result, return_value; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + status = napi_create_function(NULL, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestCreateFunctionParameters, + NULL, + &result); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + status); + + napi_create_function(env, + NULL, + NAPI_AUTO_LENGTH, + TestCreateFunctionParameters, + NULL, + &result); + + add_last_status(env, "nameIsNull", return_value); + + napi_create_function(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + NULL, + NULL, + &result); + + add_last_status(env, "cbIsNull", return_value); + + napi_create_function(env, + "TrackedFunction", + NAPI_AUTO_LENGTH, + TestCreateFunctionParameters, + NULL, + NULL); + + add_last_status(env, "resultIsNull", return_value); + + return return_value; +} + +static napi_value TestCallFunction(napi_env env, napi_callback_info info) { + size_t argc = 10; + napi_value args[10]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc > 0, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_function, + "Wrong type of arguments. Expects a function as first argument."); + + napi_value* argv = args + 1; + argc = argc - 1; + + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + + napi_value result; + NODE_API_CALL(env, napi_call_function(env, global, args[0], argc, argv, &result)); + + return result; +} + +static napi_value TestFunctionName(napi_env env, napi_callback_info info) { + return NULL; +} + +static void finalize_function(napi_env env, void* data, void* hint) { + napi_ref ref = data; + + // Retrieve the JavaScript undefined value. + napi_value undefined; + NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + + // Retrieve the JavaScript function we must call. + napi_value js_function; + NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, ref, &js_function)); + + // Call the JavaScript function to indicate that the generated JavaScript + // function is about to be gc-ed. + NODE_API_CALL_RETURN_VOID(env, + napi_call_function(env, undefined, js_function, 0, NULL, NULL)); + + // Destroy the persistent reference to the function we just called so as to + // properly clean up. + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, ref)); +} + +static napi_value MakeTrackedFunction(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value js_finalize_cb; + napi_valuetype arg_type; + + // Retrieve and validate from the arguments the function we will use to + // indicate to JavaScript that the function we are about to create is about to + // be gc-ed. + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, &js_finalize_cb, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + NODE_API_CALL(env, napi_typeof(env, js_finalize_cb, &arg_type)); + NODE_API_ASSERT(env, arg_type == napi_function, "Argument must be a function"); + + // Dynamically create a function. + napi_value result; + NODE_API_CALL(env, + napi_create_function( + env, "TrackedFunction", NAPI_AUTO_LENGTH, TestFunctionName, NULL, + &result)); + + // Create a strong reference to the function we will call when the tracked + // function is about to be gc-ed. + napi_ref js_finalize_cb_ref; + NODE_API_CALL(env, + napi_create_reference(env, js_finalize_cb, 1, &js_finalize_cb_ref)); + + // Attach a finalizer to the dynamically created function and pass it the + // strong reference we created in the previous step. + NODE_API_CALL(env, + napi_wrap( + env, result, js_finalize_cb_ref, finalize_function, NULL, NULL)); + + return result; +} + +static napi_value TestBadReturnExceptionPending(napi_env env, napi_callback_info info) { + napi_throw_error(env, "throwing exception", "throwing exception"); + + // addons should only ever return a valid napi_value even if an + // exception occurs, but we have seen that the C++ wrapper + // with exceptions enabled sometimes returns an invalid value + // when an exception is thrown. Test that we ignore the return + // value then an exception is pending. We use 0xFFFFFFFF as a value + // that should never be a valid napi_value and node seems to + // crash if it is not ignored indicating that it is indeed invalid. + return (napi_value)(0xFFFFFFFFF); +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value fn1; + NODE_API_CALL(env, napi_create_function( + env, NULL, NAPI_AUTO_LENGTH, TestCallFunction, NULL, &fn1)); + + napi_value fn2; + NODE_API_CALL(env, napi_create_function( + env, "Name", NAPI_AUTO_LENGTH, TestFunctionName, NULL, &fn2)); + + napi_value fn3; + NODE_API_CALL(env, napi_create_function( + env, "Name_extra", 5, TestFunctionName, NULL, &fn3)); + + napi_value fn4; + NODE_API_CALL(env, + napi_create_function( + env, "MakeTrackedFunction", NAPI_AUTO_LENGTH, MakeTrackedFunction, + NULL, &fn4)); + + napi_value fn5; + NODE_API_CALL(env, + napi_create_function( + env, "TestCreateFunctionParameters", NAPI_AUTO_LENGTH, + TestCreateFunctionParameters, NULL, &fn5)); + + napi_value fn6; + NODE_API_CALL(env, + napi_create_function( + env, "TestBadReturnExceptionPending", NAPI_AUTO_LENGTH, + TestBadReturnExceptionPending, NULL, &fn6)); + + NODE_API_CALL(env, napi_set_named_property(env, exports, "TestCall", fn1)); + NODE_API_CALL(env, napi_set_named_property(env, exports, "TestName", fn2)); + NODE_API_CALL(env, + napi_set_named_property(env, exports, "TestNameShort", fn3)); + NODE_API_CALL(env, + napi_set_named_property(env, exports, "MakeTrackedFunction", fn4)); + + NODE_API_CALL(env, + napi_set_named_property( + env, exports, "TestCreateFunctionParameters", fn5)); + + NODE_API_CALL(env, + napi_set_named_property( + env, exports, "TestBadReturnExceptionPending", fn6)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_general/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_general/CMakeLists.txt new file mode 100644 index 00000000..3b3532fd --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_general + SOURCES + test_general.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_general/binding.gyp b/Tests/NodeApi/test/js-native-api/test_general/binding.gyp new file mode 100644 index 00000000..71a4fb6b --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_general", + "sources": [ + "test_general.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_general/test.js b/Tests/NodeApi/test/js-native-api/test_general/test.js new file mode 100644 index 00000000..3bf87a55 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/test.js @@ -0,0 +1,97 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const test_general = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +const val1 = '1'; +const val2 = 1; +const val3 = 1; + +class BaseClass { +} + +class ExtendedClass extends BaseClass { +} + +const baseObject = new BaseClass(); +const extendedObject = new ExtendedClass(); + +// Test napi_strict_equals +assert.ok(test_general.testStrictEquals(val1, val1)); +assert.strictEqual(test_general.testStrictEquals(val1, val2), false); +assert.ok(test_general.testStrictEquals(val2, val3)); + +// Test napi_get_prototype +assert.strictEqual(test_general.testGetPrototype(baseObject), + Object.getPrototypeOf(baseObject)); +assert.strictEqual(test_general.testGetPrototype(extendedObject), + Object.getPrototypeOf(extendedObject)); +// Prototypes for base and extended should be different. +assert.notStrictEqual(test_general.testGetPrototype(baseObject), + test_general.testGetPrototype(extendedObject)); + +// Test version management functions +assert.strictEqual(test_general.testGetVersion(), 8); + +[ + 123, + 'test string', + function() {}, + new Object(), + true, + undefined, + Symbol(), +].forEach((val) => { + assert.strictEqual(test_general.testNapiTypeof(val), typeof val); +}); + +// Since typeof in js return object need to validate specific case +// for null +assert.strictEqual(test_general.testNapiTypeof(null), 'null'); + +// Assert that wrapping twice fails. +const x = {}; +test_general.wrap(x); +assert.throws(() => test_general.wrap(x), + { name: 'Error', message: 'Invalid argument' }); +// Clean up here, otherwise derefItemWasCalled() will be polluted. +test_general.removeWrap(x); + +// Ensure that wrapping, removing the wrap, and then wrapping again works. +const y = {}; +test_general.wrap(y); +test_general.removeWrap(y); +// Wrapping twice succeeds if a remove_wrap() separates the instances +test_general.wrap(y); +// Clean up here, otherwise derefItemWasCalled() will be polluted. +test_general.removeWrap(y); + +// Test napi_adjust_external_memory +// TODO: (vmoroz) Hermes does not implement that API. +// const adjustedValue = test_general.testAdjustExternalMemory(); +// assert.strictEqual(typeof adjustedValue, 'number'); +// assert(adjustedValue > 0); + +async function runGCTests() { + // Ensure that garbage collecting an object with a wrapped native item results + // in the finalize callback being called. + // TODO: (vmoroz) Restore after Hermes GC is fixed. + // assert.strictEqual(test_general.derefItemWasCalled(), false); + // (() => test_general.wrap({}))(); + // await common.gcUntil('deref_item() was called upon garbage collecting a ' + + // 'wrapped object.', + // () => test_general.derefItemWasCalled()); + + // Ensure that removing a wrap and garbage collecting does not fire the + // finalize callback. + let z = {}; + test_general.testFinalizeWrap(z); + test_general.removeWrap(z); + z = null; + await common.gcUntil( + 'finalize callback was not called upon garbage collection.', + () => (!test_general.finalizeWasCalled())); +} +runGCTests(); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testEnvCleanup.js b/Tests/NodeApi/test/js-native-api/test_general/testEnvCleanup.js new file mode 100644 index 00000000..ce59768b --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testEnvCleanup.js @@ -0,0 +1,57 @@ +'use strict'; + +if (process.argv[2] === 'child') { + const common = require('../../common'); + const test_general = require(`./build/${common.buildType}/test_general`); + + // The second argument to `envCleanupWrap()` is an index into the global + // static string array named `env_cleanup_finalizer_messages` on the native + // side. A reverse mapping is reproduced here for clarity. + const finalizerMessages = { + 'simple wrap': 0, + 'wrap, removeWrap': 1, + 'first wrap': 2, + 'second wrap': 3, + }; + + // We attach the three objects we will test to `module.exports` to ensure they + // will not be garbage-collected before the process exits. + + // Make sure the finalizer for a simple wrap will be called at env cleanup. + module.exports['simple wrap'] = + test_general.envCleanupWrap({}, finalizerMessages['simple wrap']); + + // Make sure that a removed wrap does not result in a call to its finalizer at + // env cleanup. + module.exports['wrap, removeWrap'] = + test_general.envCleanupWrap({}, finalizerMessages['wrap, removeWrap']); + test_general.removeWrap(module.exports['wrap, removeWrap']); + + // Make sure that only the latest attached version of a re-wrapped item's + // finalizer gets called at env cleanup. + module.exports['first wrap'] = + test_general.envCleanupWrap({}, finalizerMessages['first wrap']); + test_general.removeWrap(module.exports['first wrap']); + test_general.envCleanupWrap(module.exports['first wrap'], + finalizerMessages['second wrap']); +} else { + const assert = require('assert'); + const { spawnSync } = require('child_process'); + + const child = spawnSync(process.execPath, [__filename, 'child'], { + stdio: [ process.stdin, 'pipe', process.stderr ], + }); + + // Grab the child's output and construct an object whose keys are the rows of + // the output and whose values are `true`, so we can compare the output while + // ignoring the order in which the lines of it were produced. + assert.deepStrictEqual( + child.stdout.toString().split(/\r\n|\r|\n/g).reduce((obj, item) => + Object.assign(obj, item ? { [item]: true } : {}), {}), { + 'finalize at env cleanup for simple wrap': true, + 'finalize at env cleanup for second wrap': true, + }); + + // Ensure that the child exited successfully. + assert.strictEqual(child.status, 0); +} diff --git a/Tests/NodeApi/test/js-native-api/test_general/testFinalizer.js b/Tests/NodeApi/test/js-native-api/test_general/testFinalizer.js new file mode 100644 index 00000000..3eefe142 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testFinalizer.js @@ -0,0 +1,38 @@ +'use strict'; +// Flags: --expose-gc + +const common = require('../../common'); +const test_general = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +(function() { + let finalized = {}; + const callback = common.mustCall(2); + + // Add two items to be finalized and ensure the callback is called for each. + test_general.addFinalizerOnly(finalized, callback); + test_general.addFinalizerOnly(finalized, callback); + + // Ensure attached items cannot be retrieved. + assert.throws(() => test_general.unwrap(finalized), + { name: 'Error', message: 'Invalid argument' }); + + // Ensure attached items cannot be removed. + assert.throws(() => test_general.removeWrap(finalized), + { name: 'Error', message: 'Invalid argument' }); +})(); +global.gc(); + +// Add an item to an object that is already wrapped, and ensure that its +// finalizer as well as the wrap finalizer gets called. +async function testFinalizeAndWrap() { + assert.strictEqual(test_general.derefItemWasCalled(), false); + (function() { + let finalizeAndWrap = {}; + test_general.wrap(finalizeAndWrap); + test_general.addFinalizerOnly(finalizeAndWrap, common.mustCall()); + })(); + await common.gcUntil('test finalize and wrap', + () => test_general.derefItemWasCalled()); +} +testFinalizeAndWrap(); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testGlobals.js b/Tests/NodeApi/test/js-native-api/test_general/testGlobals.js new file mode 100644 index 00000000..34188e08 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testGlobals.js @@ -0,0 +1,8 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +const test_globals = require(`./build/${common.buildType}/test_general`); + +assert.strictEqual(test_globals.getUndefined(), undefined); +assert.strictEqual(test_globals.getNull(), null); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testInstanceOf.js b/Tests/NodeApi/test/js-native-api/test_general/testInstanceOf.js new file mode 100644 index 00000000..c9b98fa8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testInstanceOf.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Addon is referenced through the eval expression in testFile +const addon = require(`./build/${common.buildType}/test_general`); + +// We can only perform this test if we have a working Symbol.hasInstance +if (typeof Symbol !== 'undefined' && 'hasInstance' in Symbol && + typeof Symbol.hasInstance === 'symbol') { + + function compareToNative(theObject, theConstructor) { + assert.strictEqual( + addon.doInstanceOf(theObject, theConstructor), + (theObject instanceof theConstructor), + ); + } + + function MyClass() {} + Object.defineProperty(MyClass, Symbol.hasInstance, { + value: function(candidate) { + return 'mark' in candidate; + }, + }); + + function MySubClass() {} + MySubClass.prototype = new MyClass(); + + let x = new MySubClass(); + let y = new MySubClass(); + x.mark = true; + + compareToNative(x, MySubClass); + compareToNative(y, MySubClass); + compareToNative(x, MyClass); + compareToNative(y, MyClass); + + x = new MyClass(); + y = new MyClass(); + x.mark = true; + + compareToNative(x, MySubClass); + compareToNative(y, MySubClass); + compareToNative(x, MyClass); + compareToNative(y, MyClass); +} diff --git a/Tests/NodeApi/test/js-native-api/test_general/testNapiRun.js b/Tests/NodeApi/test/js-native-api/test_general/testNapiRun.js new file mode 100644 index 00000000..6d4f4662 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testNapiRun.js @@ -0,0 +1,14 @@ +'use strict'; + +const common = require('../../common'); +const assert = require('assert'); + +// `addon` is referenced through the eval expression in testFile +const addon = require(`./build/${common.buildType}/test_general`); + +const testCase = '(41.92 + 0.08);'; +const expected = 42; +const actual = addon.testNapiRun(testCase); + +assert.strictEqual(actual, expected); +assert.throws(() => addon.testNapiRun({ abc: 'def' }), /string was expected/); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testNapiStatus.js b/Tests/NodeApi/test/js-native-api/test_general/testNapiStatus.js new file mode 100644 index 00000000..5ad97a34 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testNapiStatus.js @@ -0,0 +1,8 @@ +'use strict'; + +const common = require('../../common'); +const addon = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +addon.createNapiError(); +assert(addon.testNapiErrorCleanup(), 'napi_status cleaned up for second call'); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof.js b/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof.js new file mode 100644 index 00000000..0b476e1e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof.js @@ -0,0 +1,121 @@ +// Copyright 2008 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +const common = require('../../common'); +const addon = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +// The following assert functions are referenced by v8's unit tests +// See for instance deps/v8/test/mjsunit/instanceof.js +// eslint-disable-next-line no-unused-vars +function assertTrue(assertion) { + return assert.strictEqual(assertion, true); +} + +// eslint-disable-next-line no-unused-vars +function assertFalse(assertion) { + assert.strictEqual(assertion, false); +} + +// eslint-disable-next-line no-unused-vars +function assertEquals(leftHandSide, rightHandSide) { + assert.strictEqual(leftHandSide, rightHandSide); +} + +// eslint-disable-next-line no-unused-vars +function assertThrows(statement) { + assert.throws(function() { + eval(statement); + }, Error); +} + +assertTrue(addon.doInstanceOf({}, Object)); +assertTrue(addon.doInstanceOf([], Object)); + +assertFalse(addon.doInstanceOf({}, Array)); +assertTrue(addon.doInstanceOf([], Array)); + +function TestChains() { + var A = {}; + var B = {}; + var C = {}; + B.__proto__ = A; + C.__proto__ = B; + + function F() { } + F.prototype = A; + assertTrue(addon.doInstanceOf(C, F)); + assertTrue(addon.doInstanceOf(B, F)); + assertFalse(addon.doInstanceOf(A, F)); + + F.prototype = B; + assertTrue(addon.doInstanceOf(C, F)); + assertFalse(addon.doInstanceOf(B, F)); + assertFalse(addon.doInstanceOf(A, F)); + + F.prototype = C; + assertFalse(addon.doInstanceOf(C, F)); + assertFalse(addon.doInstanceOf(B, F)); + assertFalse(addon.doInstanceOf(A, F)); +} + +TestChains(); + + +function TestExceptions() { + function F() { } + var items = [ 1, new Number(42), + true, + 'string', new String('hest'), + {}, [], + F, new F(), + Object, String ]; + + var exceptions = 0; + var instanceofs = 0; + + for (var i = 0; i < items.length; i++) { + for (var j = 0; j < items.length; j++) { + try { + if (addon.doInstanceOf(items[i], items[j])) instanceofs++; + } catch (e) { + assertTrue(addon.doInstanceOf(e, TypeError)); + exceptions++; + } + } + } + assertEquals(10, instanceofs); + assertEquals(88, exceptions); + + // Make sure to throw an exception if the function prototype + // isn't a proper JavaScript object. + function G() { } + G.prototype = undefined; + assertThrows("addon.doInstanceOf({}, G)"); +} + +TestExceptions(); diff --git a/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof2.js b/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof2.js new file mode 100644 index 00000000..360b28c8 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/testV8Instanceof2.js @@ -0,0 +1,341 @@ +// Copyright 2010 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +const common = require('../../common'); +const addon = require(`./build/${common.buildType}/test_general`); +const assert = require('assert'); + +function assertTrue(assertion) { + return assert.strictEqual(assertion, true); +} + +function assertEquals(leftHandSide, rightHandSide) { + assert.strictEqual(leftHandSide, rightHandSide); +} + +var except = "exception"; + +var correct_answer_index = 0; +var correct_answers = [ + false, false, true, true, false, false, true, true, + true, false, false, true, true, false, false, true, + false, true, true, false, false, true, true, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, false, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, false, true, true, false, + true, true, false, false, false, true, true, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, true, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, false, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, true, true, false, + true, false, false, true, true, true, false, false, + false, true, true, false, false, true, true, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, true, true, false, + true, false, false, true, false, true, true, false, + false, true, true, false, false, true, true, false, + true, true, false, false, false, true, true, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, true, false, + false, false, except, except, false, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, true, true, false, false, + false, true, true, false, false, false, true, true, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, false, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, false, false, true, true, + true, true, false, false, false, false, true, true, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, true, true, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, true, true, false, false, + false, true, true, false, false, false, true, true, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, false, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, false, false, true, true, + true, true, false, false, false, false, true, true, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, true, true, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except]; + +for (var i = 0; i < 256; i++) { + Test(i & 1, i & 2, i & 4, i & 8, i & 0x10, i & 0x20, i & 0x40, i & 0x80); +} + + +function InstanceTest(x, func) { + try { + var answer = addon.doInstanceOf(x, func); + assertEquals(correct_answers[correct_answer_index], answer); + } catch (e) { + assertTrue(/prototype/.test(e)); + assertEquals(correct_answers[correct_answer_index], except); + } + correct_answer_index++; +} + + +function Test(a, b, c, d, e, f, g, h) { + var Foo = function() { } + var Bar = function() { } + + if (c) Foo.prototype = 12; + if (d) Bar.prototype = 13; + var x = a ? new Foo() : new Bar(); + var y = b ? new Foo() : new Bar(); + InstanceTest(x, Foo); + InstanceTest(y, Foo); + InstanceTest(x, Bar); + InstanceTest(y, Bar); + if (e) x.__proto__ = Bar.prototype; + if (f) y.__proto__ = Foo.prototype; + if (g) { + x.__proto__ = y; + } else { + if (h) y.__proto__ = x + } + InstanceTest(x, Foo); + InstanceTest(y, Foo); + InstanceTest(x, Bar); + InstanceTest(y, Bar); +} diff --git a/Tests/NodeApi/test/js-native-api/test_general/test_general.c b/Tests/NodeApi/test/js-native-api/test_general/test_general.c new file mode 100644 index 00000000..2e130af6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_general/test_general.c @@ -0,0 +1,315 @@ +// we define NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED here to +// validate that it can be used as a form of test itself. It is +// not related to any of the other tests +// defined in the file +#define NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED +#include +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value testStrictEquals(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool bool_result; + napi_value result; + NODE_API_CALL(env, napi_strict_equals(env, args[0], args[1], &bool_result)); + NODE_API_CALL(env, napi_get_boolean(env, bool_result, &result)); + + return result; +} + +static napi_value testGetPrototype(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value result; + NODE_API_CALL(env, napi_get_prototype(env, args[0], &result)); + + return result; +} + +static napi_value testGetVersion(napi_env env, napi_callback_info info) { + uint32_t version; + napi_value result; + NODE_API_CALL(env, napi_get_version(env, &version)); + NODE_API_CALL(env, napi_create_uint32(env, version, &result)); + return result; +} + +static napi_value doInstanceOf(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + bool instanceof; + NODE_API_CALL(env, napi_instanceof(env, args[0], args[1], &instanceof)); + + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, instanceof, &result)); + + return result; +} + +static napi_value getNull(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_get_null(env, &result)); + return result; +} + +static napi_value getUndefined(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_get_undefined(env, &result)); + return result; +} + +static napi_value createNapiError(napi_env env, napi_callback_info info) { + napi_value value; + NODE_API_CALL(env, napi_create_string_utf8(env, "xyz", 3, &value)); + + double double_value; + napi_status status = napi_get_value_double(env, value, &double_value); + + NODE_API_ASSERT(env, status != napi_ok, "Failed to produce error condition"); + + const napi_extended_error_info *error_info = 0; + NODE_API_CALL(env, napi_get_last_error_info(env, &error_info)); + + NODE_API_ASSERT(env, error_info->error_code == status, + "Last error info code should match last status"); + NODE_API_ASSERT(env, error_info->error_message, + "Last error info message should not be null"); + + return NULL; +} + +static napi_value testNapiErrorCleanup(napi_env env, napi_callback_info info) { + const napi_extended_error_info *error_info = 0; + NODE_API_CALL(env, napi_get_last_error_info(env, &error_info)); + + napi_value result; + bool is_ok = error_info->error_code == napi_ok; + NODE_API_CALL(env, napi_get_boolean(env, is_ok, &result)); + + return result; +} + +static napi_value testNapiTypeof(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_valuetype argument_type; + NODE_API_CALL(env, napi_typeof(env, args[0], &argument_type)); + + napi_value result = NULL; + if (argument_type == napi_number) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "number", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_string) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "string", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_function) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "function", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_object) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "object", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_boolean) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "boolean", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_undefined) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "undefined", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_symbol) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "symbol", NAPI_AUTO_LENGTH, &result)); + } else if (argument_type == napi_null) { + NODE_API_CALL(env, napi_create_string_utf8( + env, "null", NAPI_AUTO_LENGTH, &result)); + } + return result; +} + +static bool deref_item_called = false; +static void deref_item(napi_env env, void* data, void* hint) { + (void) hint; + + NODE_API_ASSERT_RETURN_VOID(env, data == &deref_item_called, + "Finalize callback was called with the correct pointer"); + + deref_item_called = true; +} + +static napi_value deref_item_was_called(napi_env env, napi_callback_info info) { + napi_value it_was_called; + + NODE_API_CALL(env, napi_get_boolean(env, deref_item_called, &it_was_called)); + + return it_was_called; +} + +static napi_value wrap_first_arg(napi_env env, + napi_callback_info info, + napi_finalize finalizer, + void* data) { + size_t argc = 1; + napi_value to_wrap; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &to_wrap, NULL, NULL)); + NODE_API_CALL(env, napi_wrap(env, to_wrap, data, finalizer, NULL, NULL)); + + return to_wrap; +} + +static napi_value wrap(napi_env env, napi_callback_info info) { + deref_item_called = false; + return wrap_first_arg(env, info, deref_item, &deref_item_called); +} + +static napi_value unwrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value wrapped; + void* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &wrapped, NULL, NULL)); + NODE_API_CALL(env, napi_unwrap(env, wrapped, &data)); + + return NULL; +} + +static napi_value remove_wrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value wrapped; + void* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &wrapped, NULL, NULL)); + NODE_API_CALL(env, napi_remove_wrap(env, wrapped, &data)); + + return NULL; +} + +static bool finalize_called = false; +static void test_finalize(napi_env env, void* data, void* hint) { + finalize_called = true; +} + +static napi_value test_finalize_wrap(napi_env env, napi_callback_info info) { + return wrap_first_arg(env, info, test_finalize, NULL); +} + +static napi_value finalize_was_called(napi_env env, napi_callback_info info) { + napi_value it_was_called; + + NODE_API_CALL(env, napi_get_boolean(env, finalize_called, &it_was_called)); + + return it_was_called; +} + +static napi_value testAdjustExternalMemory(napi_env env, napi_callback_info info) { + napi_value result; + int64_t adjustedValue; + + NODE_API_CALL(env, napi_adjust_external_memory(env, 1, &adjustedValue)); + NODE_API_CALL(env, napi_create_double(env, (double)adjustedValue, &result)); + + return result; +} + +static napi_value testNapiRun(napi_env env, napi_callback_info info) { + napi_value script, result; + size_t argc = 1; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &script, NULL, NULL)); + + NODE_API_CALL(env, napi_run_script(env, script, &result)); + + return result; +} + +static void finalizer_only_callback(napi_env env, void* data, void* hint) { + napi_ref js_cb_ref = data; + napi_value js_cb, undefined; + NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, js_cb_ref, &js_cb)); + NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + NODE_API_CALL_RETURN_VOID(env, + napi_call_function(env, undefined, js_cb, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, js_cb_ref)); +} + +static napi_value add_finalizer_only(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2]; + napi_ref js_cb_ref; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_create_reference(env, argv[1], 1, &js_cb_ref)); + NODE_API_CALL(env, + napi_add_finalizer( + env, argv[0], js_cb_ref, finalizer_only_callback, NULL, NULL)); + return NULL; +} + +static const char* env_cleanup_finalizer_messages[] = { + "simple wrap", + "wrap, removeWrap", + "first wrap", + "second wrap" +}; + +static void cleanup_env_finalizer(napi_env env, void* data, void* hint) { + (void) env; + (void) hint; + + printf("finalize at env cleanup for %s\n", + env_cleanup_finalizer_messages[(uintptr_t)data]); +} + +static napi_value env_cleanup_wrap(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2]; + uint32_t value; + uintptr_t ptr_value; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + + NODE_API_CALL(env, napi_get_value_uint32(env, argv[1], &value)); + + ptr_value = value; + return wrap_first_arg(env, info, cleanup_env_finalizer, (void*)ptr_value); +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("testStrictEquals", testStrictEquals), + DECLARE_NODE_API_PROPERTY("testGetPrototype", testGetPrototype), + DECLARE_NODE_API_PROPERTY("testGetVersion", testGetVersion), + DECLARE_NODE_API_PROPERTY("testNapiRun", testNapiRun), + DECLARE_NODE_API_PROPERTY("doInstanceOf", doInstanceOf), + DECLARE_NODE_API_PROPERTY("getUndefined", getUndefined), + DECLARE_NODE_API_PROPERTY("getNull", getNull), + DECLARE_NODE_API_PROPERTY("createNapiError", createNapiError), + DECLARE_NODE_API_PROPERTY("testNapiErrorCleanup", testNapiErrorCleanup), + DECLARE_NODE_API_PROPERTY("testNapiTypeof", testNapiTypeof), + DECLARE_NODE_API_PROPERTY("wrap", wrap), + DECLARE_NODE_API_PROPERTY("envCleanupWrap", env_cleanup_wrap), + DECLARE_NODE_API_PROPERTY("unwrap", unwrap), + DECLARE_NODE_API_PROPERTY("removeWrap", remove_wrap), + DECLARE_NODE_API_PROPERTY("addFinalizerOnly", add_finalizer_only), + DECLARE_NODE_API_PROPERTY("testFinalizeWrap", test_finalize_wrap), + DECLARE_NODE_API_PROPERTY("finalizeWasCalled", finalize_was_called), + DECLARE_NODE_API_PROPERTY("derefItemWasCalled", deref_item_was_called), + DECLARE_NODE_API_PROPERTY("testAdjustExternalMemory", testAdjustExternalMemory) + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_handle_scope/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_handle_scope/CMakeLists.txt new file mode 100644 index 00000000..579119a3 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_handle_scope/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_handle_scope + SOURCES + test_handle_scope.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_handle_scope/binding.gyp b/Tests/NodeApi/test/js-native-api/test_handle_scope/binding.gyp new file mode 100644 index 00000000..dd6dd63a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_handle_scope/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_handle_scope", + "sources": [ + "test_handle_scope.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_handle_scope/test.js b/Tests/NodeApi/test/js-native-api/test_handle_scope/test.js new file mode 100644 index 00000000..46aa12f6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_handle_scope/test.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing handle scope api calls +const testHandleScope = + require(`./build/${common.buildType}/test_handle_scope`); + +testHandleScope.NewScope(); + +assert.ok(testHandleScope.NewScopeEscape() instanceof Object); + +testHandleScope.NewScopeEscapeTwice(); + +assert.throws( + () => { + testHandleScope.NewScopeWithException(() => { throw new RangeError(); }); + }, + RangeError); diff --git a/Tests/NodeApi/test/js-native-api/test_handle_scope/test_handle_scope.c b/Tests/NodeApi/test/js-native-api/test_handle_scope/test_handle_scope.c new file mode 100644 index 00000000..7c5eb4a4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_handle_scope/test_handle_scope.c @@ -0,0 +1,86 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +// these tests validate the handle scope functions in the normal +// flow. Forcing gc behavior to fully validate they are doing +// the right right thing would be quite hard so we keep it +// simple for now. + +static napi_value NewScope(napi_env env, napi_callback_info info) { + napi_handle_scope scope; + napi_value output = NULL; + + NODE_API_CALL(env, napi_open_handle_scope(env, &scope)); + NODE_API_CALL(env, napi_create_object(env, &output)); + NODE_API_CALL(env, napi_close_handle_scope(env, scope)); + return NULL; +} + +static napi_value NewScopeEscape(napi_env env, napi_callback_info info) { + napi_escapable_handle_scope scope; + napi_value output = NULL; + napi_value escapee = NULL; + + NODE_API_CALL(env, napi_open_escapable_handle_scope(env, &scope)); + NODE_API_CALL(env, napi_create_object(env, &output)); + NODE_API_CALL(env, napi_escape_handle(env, scope, output, &escapee)); + NODE_API_CALL(env, napi_close_escapable_handle_scope(env, scope)); + return escapee; +} + +static napi_value NewScopeEscapeTwice(napi_env env, napi_callback_info info) { + napi_escapable_handle_scope scope; + napi_value output = NULL; + napi_value escapee = NULL; + napi_status status; + + NODE_API_CALL(env, napi_open_escapable_handle_scope(env, &scope)); + NODE_API_CALL(env, napi_create_object(env, &output)); + NODE_API_CALL(env, napi_escape_handle(env, scope, output, &escapee)); + status = napi_escape_handle(env, scope, output, &escapee); + NODE_API_ASSERT(env, status == napi_escape_called_twice, "Escaping twice fails"); + NODE_API_CALL(env, napi_close_escapable_handle_scope(env, scope)); + return NULL; +} + +static napi_value NewScopeWithException(napi_env env, napi_callback_info info) { + napi_handle_scope scope; + size_t argc; + napi_value exception_function; + napi_status status; + napi_value output = NULL; + + NODE_API_CALL(env, napi_open_handle_scope(env, &scope)); + NODE_API_CALL(env, napi_create_object(env, &output)); + + argc = 1; + NODE_API_CALL(env, napi_get_cb_info( + env, info, &argc, &exception_function, NULL, NULL)); + + status = napi_call_function( + env, output, exception_function, 0, NULL, NULL); + NODE_API_ASSERT(env, status == napi_pending_exception, + "Function should have thrown."); + + // Closing a handle scope should still work while an exception is pending. + NODE_API_CALL(env, napi_close_handle_scope(env, scope)); + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("NewScope", NewScope), + DECLARE_NODE_API_PROPERTY("NewScopeEscape", NewScopeEscape), + DECLARE_NODE_API_PROPERTY("NewScopeEscapeTwice", NewScopeEscapeTwice), + DECLARE_NODE_API_PROPERTY("NewScopeWithException", NewScopeWithException), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_instance_data/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_instance_data/CMakeLists.txt new file mode 100644 index 00000000..feda8f59 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_instance_data/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_instance_data + SOURCES + test_instance_data.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_instance_data/binding.gyp b/Tests/NodeApi/test/js-native-api/test_instance_data/binding.gyp new file mode 100644 index 00000000..f1c77b30 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_instance_data/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_instance_data", + "sources": [ + "test_instance_data.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_instance_data/test.js b/Tests/NodeApi/test/js-native-api/test_instance_data/test.js new file mode 100644 index 00000000..043da9f1 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_instance_data/test.js @@ -0,0 +1,41 @@ +'use strict'; +// Test API calls for instance data. + +const common = require('../../common'); +const assert = require('assert'); + +if (module !== require.main) { + // When required as a module, run the tests. + const test_instance_data = + require(`./build/${common.buildType}/test_instance_data`); + + // Print to stdout when the environment deletes the instance data. This output + // is checked by the parent process. + test_instance_data.setPrintOnDelete(); + + // Test that instance data can be accessed from a binding. + assert.strictEqual(test_instance_data.increment(), 42); + + // Test that the instance data can be accessed from a finalizer. + // TODO: (vmoroz) Restore after Hermes fixes GC. + // test_instance_data.objectWithFinalizer(common.mustCall()); + // global.gc(); +} else { + // When launched as a script, run tests in either a child process or in a + // worker thread. + const requireAs = require('../../common/require-as'); + const runOptions = { stdio: ['inherit', 'pipe', 'inherit'] }; + + function checkOutput(child) { + assert.strictEqual(child.status, 0); + assert.strictEqual( + (child.stdout.toString().split(/\r\n?|\n/) || [])[0], + 'deleting addon data'); + } + + // Run tests in a child process. + checkOutput(requireAs(__filename, ['--expose-gc'], runOptions, 'child')); + + // Run tests in a worker thread in a child process. + checkOutput(requireAs(__filename, ['--expose-gc'], runOptions, 'worker')); +} diff --git a/Tests/NodeApi/test/js-native-api/test_instance_data/test_instance_data.c b/Tests/NodeApi/test/js-native-api/test_instance_data/test_instance_data.c new file mode 100644 index 00000000..bf354983 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_instance_data/test_instance_data.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +typedef struct { + size_t value; + bool print; + napi_ref js_cb_ref; +} AddonData; + +static napi_value Increment(napi_env env, napi_callback_info info) { + AddonData* data; + napi_value result; + + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, napi_create_uint32(env, ++data->value, &result)); + + return result; +} + +static void DeleteAddonData(napi_env env, void* raw_data, void* hint) { + AddonData* data = raw_data; + if (data->print) { + printf("deleting addon data\n"); + } + if (data->js_cb_ref != NULL) { + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref)); + } + free(data); +} + +static napi_value SetPrintOnDelete(napi_env env, napi_callback_info info) { + AddonData* data; + + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + data->print = true; + + return NULL; +} + +static void TestFinalizer(napi_env env, void* raw_data, void* hint) { + (void) raw_data; + (void) hint; + + AddonData* data; + NODE_API_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data)); + napi_value js_cb, undefined; + NODE_API_CALL_RETURN_VOID(env, + napi_get_reference_value(env, data->js_cb_ref, &js_cb)); + NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + NODE_API_CALL_RETURN_VOID(env, + napi_call_function(env, undefined, js_cb, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref)); + data->js_cb_ref = NULL; +} + +static napi_value ObjectWithFinalizer(napi_env env, napi_callback_info info) { + AddonData* data; + napi_value result, js_cb; + size_t argc = 1; + + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_ASSERT(env, data->js_cb_ref == NULL, "reference must be NULL"); + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL)); + NODE_API_CALL(env, napi_create_object(env, &result)); + NODE_API_CALL(env, + napi_add_finalizer(env, result, NULL, TestFinalizer, NULL, NULL)); + NODE_API_CALL(env, napi_create_reference(env, js_cb, 1, &data->js_cb_ref)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + AddonData* data = malloc(sizeof(*data)); + data->value = 41; + data->print = false; + data->js_cb_ref = NULL; + + NODE_API_CALL(env, napi_set_instance_data(env, data, DeleteAddonData, NULL)); + + napi_property_descriptor props[] = { + DECLARE_NODE_API_PROPERTY("increment", Increment), + DECLARE_NODE_API_PROPERTY("setPrintOnDelete", SetPrintOnDelete), + DECLARE_NODE_API_PROPERTY("objectWithFinalizer", ObjectWithFinalizer), + }; + + NODE_API_CALL(env, + napi_define_properties( + env, exports, sizeof(props) / sizeof(*props), props)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_new_target/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_new_target/CMakeLists.txt new file mode 100644 index 00000000..1cee2625 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_new_target/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_new_target + SOURCES + test_new_target.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_new_target/binding.gyp b/Tests/NodeApi/test/js-native-api/test_new_target/binding.gyp new file mode 100644 index 00000000..baa0e3c6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_new_target/binding.gyp @@ -0,0 +1,11 @@ +{ + 'targets': [ + { + 'target_name': 'test_new_target', + 'defines': [ 'V8_DEPRECATION_WARNINGS=1' ], + 'sources': [ + 'test_new_target.c' + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_new_target/test.js b/Tests/NodeApi/test/js-native-api/test_new_target/test.js new file mode 100644 index 00000000..06d5ef36 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_new_target/test.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../../common'); +const assert = require('assert'); +const binding = require(`./build/${common.buildType}/test_new_target`); + +class Class extends binding.BaseClass { + constructor() { + super(); + this.method(); + } + method() { + this.ok = true; + } +} + +assert.ok(new Class() instanceof binding.BaseClass); +assert.ok(new Class().ok); +assert.ok(binding.OrdinaryFunction()); +assert.ok( + new binding.Constructor(binding.Constructor) instanceof binding.Constructor); diff --git a/Tests/NodeApi/test/js-native-api/test_new_target/test_new_target.c b/Tests/NodeApi/test/js-native-api/test_new_target/test_new_target.c new file mode 100644 index 00000000..02ceb992 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_new_target/test_new_target.c @@ -0,0 +1,92 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value BaseClass(napi_env env, napi_callback_info info) { + napi_value newTargetArg; + NODE_API_CALL(env, napi_get_new_target(env, info, &newTargetArg)); + napi_value thisArg; + NODE_API_CALL(env, napi_get_cb_info(env, info, NULL, NULL, &thisArg, NULL)); + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + + // this !== new.target since we are being invoked through super() + bool result; + NODE_API_CALL(env, napi_strict_equals(env, newTargetArg, thisArg, &result)); + NODE_API_ASSERT(env, !result, "this !== new.target"); + + // new.target !== undefined because we should be called as a new expression + NODE_API_ASSERT(env, newTargetArg != NULL, "newTargetArg != NULL"); + NODE_API_CALL(env, napi_strict_equals(env, newTargetArg, undefined, &result)); + NODE_API_ASSERT(env, !result, "new.target !== undefined"); + + return thisArg; +} + +static napi_value Constructor(napi_env env, napi_callback_info info) { + bool result; + napi_value newTargetArg; + NODE_API_CALL(env, napi_get_new_target(env, info, &newTargetArg)); + size_t argc = 1; + napi_value argv; + napi_value thisArg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &argv, &thisArg, NULL)); + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + + // new.target !== undefined because we should be called as a new expression + NODE_API_ASSERT(env, newTargetArg != NULL, "newTargetArg != NULL"); + NODE_API_CALL(env, napi_strict_equals(env, newTargetArg, undefined, &result)); + NODE_API_ASSERT(env, !result, "new.target !== undefined"); + + // arguments[0] should be Constructor itself (test harness passed it) + NODE_API_CALL(env, napi_strict_equals(env, newTargetArg, argv, &result)); + NODE_API_ASSERT(env, result, "new.target === Constructor"); + + return thisArg; +} + +static napi_value OrdinaryFunction(napi_env env, napi_callback_info info) { + napi_value newTargetArg; + NODE_API_CALL(env, napi_get_new_target(env, info, &newTargetArg)); + + NODE_API_ASSERT(env, newTargetArg == NULL, "newTargetArg == NULL"); + + napi_value _true; + NODE_API_CALL(env, napi_get_boolean(env, true, &_true)); + return _true; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value baseClass, constructor; + NODE_API_CALL( + env, + napi_define_class( + env, + "BaseClass", + NAPI_AUTO_LENGTH, + BaseClass, + NULL, + 0, + NULL, + &baseClass)); + NODE_API_CALL( + env, + napi_define_class( + env, + "Constructor", + NAPI_AUTO_LENGTH, + Constructor, + NULL, + 0, + NULL, + &constructor)); + const napi_property_descriptor desc[] = { + DECLARE_NODE_API_PROPERTY_VALUE("BaseClass", baseClass), + DECLARE_NODE_API_PROPERTY("OrdinaryFunction", OrdinaryFunction), + DECLARE_NODE_API_PROPERTY_VALUE("Constructor", constructor)}; + NODE_API_CALL(env, napi_define_properties(env, exports, 3, desc)); + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_number/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_number/CMakeLists.txt new file mode 100644 index 00000000..87946947 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_module(test_number + SOURCES + test_number.c + test_null.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_number/binding.gyp b/Tests/NodeApi/test/js-native-api/test_number/binding.gyp new file mode 100644 index 00000000..31707404 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "test_number", + "sources": [ + "test_number.c", + "test_null.c", + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_number/test.js b/Tests/NodeApi/test/js-native-api/test_number/test.js new file mode 100644 index 00000000..6c0f0f3e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test.js @@ -0,0 +1,134 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const test_number = require(`./build/${common.buildType}/test_number`); + + +// Testing api calls for number +function testNumber(num) { + assert.strictEqual(num, test_number.Test(num)); +} + +testNumber(0); +testNumber(-0); +testNumber(1); +testNumber(-1); +testNumber(100); +testNumber(2121); +testNumber(-1233); +testNumber(986583); +testNumber(-976675); + +/* eslint-disable no-loss-of-precision */ +testNumber( + 98765432213456789876546896323445679887645323232436587988766545658); +testNumber( + -4350987086545760976737453646576078997096876957864353245245769809); +/* eslint-enable no-loss-of-precision */ +testNumber(Number.MIN_SAFE_INTEGER); +testNumber(Number.MAX_SAFE_INTEGER); +testNumber(Number.MAX_SAFE_INTEGER + 10); + +testNumber(Number.MIN_VALUE); +testNumber(Number.MAX_VALUE); +testNumber(Number.MAX_VALUE + 10); + +testNumber(Number.POSITIVE_INFINITY); +testNumber(Number.NEGATIVE_INFINITY); +testNumber(Number.NaN); + +function testUint32(input, expected = input) { + assert.strictEqual(expected, test_number.TestUint32Truncation(input)); +} + +// Test zero +testUint32(0.0, 0); +testUint32(-0.0, 0); + +// Test overflow scenarios +testUint32(4294967295); +testUint32(4294967296, 0); +testUint32(4294967297, 1); +testUint32(17 * 4294967296 + 1, 1); +testUint32(-1, 0xffffffff); + +// Validate documented behavior when value is retrieved as 32-bit integer with +// `napi_get_value_int32` +function testInt32(input, expected = input) { + assert.strictEqual(expected, test_number.TestInt32Truncation(input)); +} + +// Test zero +testInt32(0.0, 0); +testInt32(-0.0, 0); + +// Test min/max int32 range +testInt32(-Math.pow(2, 31)); +testInt32(Math.pow(2, 31) - 1); + +// Test overflow scenarios +testInt32(4294967297, 1); +testInt32(4294967296, 0); +testInt32(4294967295, -1); +testInt32(4294967296 * 5 + 3, 3); + +// Test min/max safe integer range +testInt32(Number.MIN_SAFE_INTEGER, 1); +testInt32(Number.MAX_SAFE_INTEGER, -1); + +// Test within int64_t range (with precision loss) +testInt32(-Math.pow(2, 63) + (Math.pow(2, 9) + 1), 1024); +testInt32(Math.pow(2, 63) - (Math.pow(2, 9) + 1), -1024); + +// Test min/max double value +testInt32(-Number.MIN_VALUE, 0); +testInt32(Number.MIN_VALUE, 0); +testInt32(-Number.MAX_VALUE, 0); +testInt32(Number.MAX_VALUE, 0); + +// Test outside int64_t range +testInt32(-Math.pow(2, 63) + (Math.pow(2, 9)), 0); +testInt32(Math.pow(2, 63) - (Math.pow(2, 9)), 0); + +// Test non-finite numbers +testInt32(Number.POSITIVE_INFINITY, 0); +testInt32(Number.NEGATIVE_INFINITY, 0); +testInt32(Number.NaN, 0); + +// Validate documented behavior when value is retrieved as 64-bit integer with +// `napi_get_value_int64` +function testInt64(input, expected = input) { + assert.strictEqual(expected, test_number.TestInt64Truncation(input)); +} + +// Both V8 and ChakraCore return a sentinel value of `0x8000000000000000` when +// the conversion goes out of range, but V8 treats it as unsigned in some cases. +const RANGEERROR_POSITIVE = Math.pow(2, 63); +const RANGEERROR_NEGATIVE = -Math.pow(2, 63); + +// Test zero +testInt64(0.0, 0); +testInt64(-0.0, 0); + +// Test min/max safe integer range +testInt64(Number.MIN_SAFE_INTEGER); +testInt64(Number.MAX_SAFE_INTEGER); + +// Test within int64_t range (with precision loss) +testInt64(-Math.pow(2, 63) + (Math.pow(2, 9) + 1)); +testInt64(Math.pow(2, 63) - (Math.pow(2, 9) + 1)); + +// Test min/max double value +testInt64(-Number.MIN_VALUE, 0); +testInt64(Number.MIN_VALUE, 0); +testInt64(-Number.MAX_VALUE, RANGEERROR_NEGATIVE); +testInt64(Number.MAX_VALUE, RANGEERROR_POSITIVE); + +// Test outside int64_t range +testInt64(-Math.pow(2, 63) + (Math.pow(2, 9)), RANGEERROR_NEGATIVE); +testInt64(Math.pow(2, 63) - (Math.pow(2, 9)), RANGEERROR_POSITIVE); + +// Test non-finite numbers +testInt64(Number.POSITIVE_INFINITY, 0); +testInt64(Number.NEGATIVE_INFINITY, 0); +testInt64(Number.NaN, 0); diff --git a/Tests/NodeApi/test/js-native-api/test_number/test_null.c b/Tests/NodeApi/test/js-native-api/test_number/test_null.c new file mode 100644 index 00000000..20d479c9 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test_null.c @@ -0,0 +1,77 @@ +#include + +#include "../common.h" + +// Unifies the way the macros declare values. +typedef double double_t; + +#define BINDING_FOR_CREATE(initial_capital, lowercase) \ + static napi_value Create##initial_capital(napi_env env, \ + napi_callback_info info) { \ + napi_value return_value, call_result; \ + lowercase##_t value = 42; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + add_returned_status(env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + napi_create_##lowercase(NULL, value, &call_result)); \ + napi_create_##lowercase(env, value, NULL); \ + add_last_status(env, "resultIsNull", return_value); \ + return return_value; \ + } + +#define BINDING_FOR_GET_VALUE(initial_capital, lowercase) \ + static napi_value GetValue##initial_capital(napi_env env, \ + napi_callback_info info) { \ + napi_value return_value, call_result; \ + lowercase##_t value = 42; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + NODE_API_CALL(env, napi_create_##lowercase(env, value, &call_result)); \ + add_returned_status( \ + env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + napi_get_value_##lowercase(NULL, call_result, &value)); \ + napi_get_value_##lowercase(env, NULL, &value); \ + add_last_status(env, "valueIsNull", return_value); \ + napi_get_value_##lowercase(env, call_result, NULL); \ + add_last_status(env, "resultIsNull", return_value); \ + return return_value; \ + } + +BINDING_FOR_CREATE(Double, double) +BINDING_FOR_CREATE(Int32, int32) +BINDING_FOR_CREATE(Uint32, uint32) +BINDING_FOR_CREATE(Int64, int64) +BINDING_FOR_GET_VALUE(Double, double) +BINDING_FOR_GET_VALUE(Int32, int32) +BINDING_FOR_GET_VALUE(Uint32, uint32) +BINDING_FOR_GET_VALUE(Int64, int64) + +void init_test_null(napi_env env, napi_value exports) { + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("createDouble", CreateDouble), + DECLARE_NODE_API_PROPERTY("createInt32", CreateInt32), + DECLARE_NODE_API_PROPERTY("createUint32", CreateUint32), + DECLARE_NODE_API_PROPERTY("createInt64", CreateInt64), + DECLARE_NODE_API_PROPERTY("getValueDouble", GetValueDouble), + DECLARE_NODE_API_PROPERTY("getValueInt32", GetValueInt32), + DECLARE_NODE_API_PROPERTY("getValueUint32", GetValueUint32), + DECLARE_NODE_API_PROPERTY("getValueInt64", GetValueInt64), + }; + napi_value test_null; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID( + env, + napi_define_properties(env, + test_null, + sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + NODE_API_CALL_RETURN_VOID( + env, napi_set_named_property(env, exports, "testNull", test_null)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_number/test_null.h b/Tests/NodeApi/test/js-native-api/test_number/test_null.h new file mode 100644 index 00000000..695d8971 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_NUMBER_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_NUMBER_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_NUMBER_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_number/test_null.js b/Tests/NodeApi/test/js-native-api/test_number/test_null.js new file mode 100644 index 00000000..c09801ac --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test_null.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const { testNull } = require(`./build/${common.buildType}/test_number`); + +const expectedCreateResult = { + envIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}; +const expectedGetValueResult = { + envIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}; +[ 'Double', 'Int32', 'Uint32', 'Int64' ].forEach((typeName) => { + assert.deepStrictEqual(testNull['create' + typeName](), expectedCreateResult); + assert.deepStrictEqual(testNull['getValue' + typeName](), expectedGetValueResult); +}); diff --git a/Tests/NodeApi/test/js-native-api/test_number/test_number.c b/Tests/NodeApi/test/js-native-api/test_number/test_number.c new file mode 100644 index 00000000..3b3ba29f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_number/test_number.c @@ -0,0 +1,110 @@ +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +static napi_value Test(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + double input; + NODE_API_CALL(env, napi_get_value_double(env, args[0], &input)); + + napi_value output; + NODE_API_CALL(env, napi_create_double(env, input, &output)); + + return output; +} + +static napi_value TestUint32Truncation(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + uint32_t input; + NODE_API_CALL(env, napi_get_value_uint32(env, args[0], &input)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, input, &output)); + + return output; +} + +static napi_value TestInt32Truncation(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + int32_t input; + NODE_API_CALL(env, napi_get_value_int32(env, args[0], &input)); + + napi_value output; + NODE_API_CALL(env, napi_create_int32(env, input, &output)); + + return output; +} + +static napi_value TestInt64Truncation(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_number, + "Wrong type of arguments. Expects a number as first argument."); + + int64_t input; + NODE_API_CALL(env, napi_get_value_int64(env, args[0], &input)); + + napi_value output; + NODE_API_CALL(env, napi_create_int64(env, input, &output)); + + return output; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("Test", Test), + DECLARE_NODE_API_PROPERTY("TestInt32Truncation", TestInt32Truncation), + DECLARE_NODE_API_PROPERTY("TestUint32Truncation", TestUint32Truncation), + DECLARE_NODE_API_PROPERTY("TestInt64Truncation", TestInt64Truncation), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + init_test_null(env, exports); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_object/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_object/CMakeLists.txt new file mode 100644 index 00000000..e8f81166 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/CMakeLists.txt @@ -0,0 +1,10 @@ +add_node_api_module(test_object + SOURCES + test_null.c + test_object.c +) + +add_node_api_module(test_exceptions + SOURCES + test_exceptions.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_object/binding.gyp b/Tests/NodeApi/test/js-native-api/test_object/binding.gyp new file mode 100644 index 00000000..37ea4931 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/binding.gyp @@ -0,0 +1,17 @@ +{ + "targets": [ + { + "target_name": "test_object", + "sources": [ + "test_null.c", + "test_object.c" + ] + }, + { + "target_name": "test_exceptions", + "sources": [ + "test_exceptions.c", + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_object/test.js b/Tests/NodeApi/test/js-native-api/test_object/test.js new file mode 100644 index 00000000..8ca961a1 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test.js @@ -0,0 +1,393 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for objects +const test_object = require(`./build/${common.buildType}/test_object`); + + +const object = { + hello: 'world', + array: [ + 1, 94, 'str', 12.321, { test: 'obj in arr' }, + ], + newObject: { + test: 'obj in obj', + }, +}; + +assert.strictEqual(test_object.Get(object, 'hello'), 'world'); +assert.strictEqual(test_object.GetNamed(object, 'hello'), 'world'); +assert.deepStrictEqual(test_object.Get(object, 'array'), + [1, 94, 'str', 12.321, { test: 'obj in arr' }]); +assert.deepStrictEqual(test_object.Get(object, 'newObject'), + { test: 'obj in obj' }); + +assert(test_object.Has(object, 'hello')); +assert(test_object.HasNamed(object, 'hello')); +assert(test_object.Has(object, 'array')); +assert(test_object.Has(object, 'newObject')); + +const newObject = test_object.New(); +assert(test_object.Has(newObject, 'test_number')); +assert.strictEqual(newObject.test_number, 987654321); +assert.strictEqual(newObject.test_string, 'test string'); + +{ + // Verify that napi_get_property() walks the prototype chain. + function MyObject() { + this.foo = 42; + this.bar = 43; + } + + MyObject.prototype.bar = 44; + MyObject.prototype.baz = 45; + + const obj = new MyObject(); + + assert.strictEqual(test_object.Get(obj, 'foo'), 42); + assert.strictEqual(test_object.Get(obj, 'bar'), 43); + assert.strictEqual(test_object.Get(obj, 'baz'), 45); + assert.strictEqual(test_object.Get(obj, 'toString'), + Object.prototype.toString); +} + +{ + // Verify that napi_has_own_property() fails if property is not a name. + [true, false, null, undefined, {}, [], 0, 1, () => { }].forEach((value) => { + assert.throws(() => { + test_object.HasOwn({}, value); + }, /^Error: A string or symbol was expected$/); + }); +} + +{ + // Verify that napi_has_own_property() does not walk the prototype chain. + const symbol1 = Symbol(); + const symbol2 = Symbol(); + + function MyObject() { + this.foo = 42; + this.bar = 43; + this[symbol1] = 44; + } + + MyObject.prototype.bar = 45; + MyObject.prototype.baz = 46; + MyObject.prototype[symbol2] = 47; + + const obj = new MyObject(); + + assert.strictEqual(test_object.HasOwn(obj, 'foo'), true); + assert.strictEqual(test_object.HasOwn(obj, 'bar'), true); + assert.strictEqual(test_object.HasOwn(obj, symbol1), true); + assert.strictEqual(test_object.HasOwn(obj, 'baz'), false); + assert.strictEqual(test_object.HasOwn(obj, 'toString'), false); + assert.strictEqual(test_object.HasOwn(obj, symbol2), false); +} + +{ + // test_object.Inflate increases all properties by 1 + const cube = { + x: 10, + y: 10, + z: 10, + }; + + assert.deepStrictEqual(test_object.Inflate(cube), { x: 11, y: 11, z: 11 }); + assert.deepStrictEqual(test_object.Inflate(cube), { x: 12, y: 12, z: 12 }); + assert.deepStrictEqual(test_object.Inflate(cube), { x: 13, y: 13, z: 13 }); + cube.t = 13; + assert.deepStrictEqual( + test_object.Inflate(cube), { x: 14, y: 14, z: 14, t: 14 }); + + const sym1 = Symbol('1'); + const sym2 = Symbol('2'); + const sym3 = Symbol('3'); + const sym4 = Symbol('4'); + const object2 = { + [sym1]: '@@iterator', + [sym2]: sym3, + }; + + assert(test_object.Has(object2, sym1)); + assert(test_object.Has(object2, sym2)); + assert.strictEqual(test_object.Get(object2, sym1), '@@iterator'); + assert.strictEqual(test_object.Get(object2, sym2), sym3); + assert(test_object.Set(object2, 'string', 'value')); + assert(test_object.SetNamed(object2, 'named_string', 'value')); + assert(test_object.Set(object2, sym4, 123)); + assert(test_object.Has(object2, 'string')); + assert(test_object.HasNamed(object2, 'named_string')); + assert(test_object.Has(object2, sym4)); + assert.strictEqual(test_object.Get(object2, 'string'), 'value'); + assert.strictEqual(test_object.Get(object2, sym4), 123); +} + +{ + // Wrap a pointer in a JS object, then verify the pointer can be unwrapped. + const wrapper = {}; + test_object.Wrap(wrapper); + + assert(test_object.Unwrap(wrapper)); +} + +{ + // Verify that wrapping doesn't break an object's prototype chain. + const wrapper = {}; + const protoA = { protoA: true }; + Object.setPrototypeOf(wrapper, protoA); + test_object.Wrap(wrapper); + + assert(test_object.Unwrap(wrapper)); + assert(wrapper.protoA); +} + +{ + // Verify the pointer can be unwrapped after inserting in the prototype chain. + const wrapper = {}; + const protoA = { protoA: true }; + Object.setPrototypeOf(wrapper, protoA); + test_object.Wrap(wrapper); + + const protoB = { protoB: true }; + Object.setPrototypeOf(protoB, Object.getPrototypeOf(wrapper)); + Object.setPrototypeOf(wrapper, protoB); + + assert(test_object.Unwrap(wrapper)); + assert(wrapper.protoA, true); + assert(wrapper.protoB, true); +} + +{ + // Verify that objects can be type-tagged and type-tag-checked. + const obj1 = test_object.TypeTaggedInstance(0); + const obj2 = test_object.TypeTaggedInstance(1); + const obj3 = test_object.TypeTaggedInstance(2); + const obj4 = test_object.TypeTaggedInstance(3); + const external = test_object.TypeTaggedExternal(2); + const plainExternal = test_object.PlainExternal(); + + // Verify that we do not allow type tag indices greater than the largest + // available index. + assert.throws(() => test_object.TypeTaggedInstance(39), { + name: 'RangeError', + message: 'Invalid type index', + }); + assert.throws(() => test_object.TypeTaggedExternal(39), { + name: 'RangeError', + message: 'Invalid type index', + }); + + // Verify that type tags are correctly accepted. + assert.strictEqual(test_object.CheckTypeTag(0, obj1), true); + assert.strictEqual(test_object.CheckTypeTag(1, obj2), true); + assert.strictEqual(test_object.CheckTypeTag(2, obj3), true); + assert.strictEqual(test_object.CheckTypeTag(3, obj4), true); + assert.strictEqual(test_object.CheckTypeTag(2, external), true); + + // Verify that wrongly tagged objects are rejected. + assert.strictEqual(test_object.CheckTypeTag(0, obj2), false); + assert.strictEqual(test_object.CheckTypeTag(1, obj1), false); + assert.strictEqual(test_object.CheckTypeTag(0, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(1, obj4), false); + assert.strictEqual(test_object.CheckTypeTag(2, obj4), false); + assert.strictEqual(test_object.CheckTypeTag(3, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(4, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(0, external), false); + assert.strictEqual(test_object.CheckTypeTag(1, external), false); + assert.strictEqual(test_object.CheckTypeTag(3, external), false); + assert.strictEqual(test_object.CheckTypeTag(4, external), false); + + // Verify that untagged objects are rejected. + assert.strictEqual(test_object.CheckTypeTag(0, {}), false); + assert.strictEqual(test_object.CheckTypeTag(1, {}), false); + assert.strictEqual(test_object.CheckTypeTag(0, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(1, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(2, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(3, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(4, plainExternal), false); +} + +{ + // Verify that normal and nonexistent properties can be deleted. + const sym = Symbol(); + const obj = { foo: 'bar', [sym]: 'baz' }; + + assert.strictEqual('foo' in obj, true); + assert.strictEqual(sym in obj, true); + assert.strictEqual('does_not_exist' in obj, false); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual('foo' in obj, false); + assert.strictEqual(sym in obj, true); + assert.strictEqual('does_not_exist' in obj, false); + assert.strictEqual(test_object.Delete(obj, sym), true); + assert.strictEqual('foo' in obj, false); + assert.strictEqual(sym in obj, false); + assert.strictEqual('does_not_exist' in obj, false); +} + +{ + // Verify that non-configurable properties are not deleted. + const obj = {}; + + Object.defineProperty(obj, 'foo', { configurable: false }); + assert.strictEqual(test_object.Delete(obj, 'foo'), false); + assert.strictEqual('foo' in obj, true); +} + +{ + // Verify that prototype properties are not deleted. + function Foo() { + this.foo = 'bar'; + } + + Foo.prototype.foo = 'baz'; + + const obj = new Foo(); + + assert.strictEqual(obj.foo, 'bar'); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual(obj.foo, 'baz'); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual(obj.foo, 'baz'); +} + +{ + // Verify that napi_get_property_names gets the right set of property names, + // i.e.: includes prototypes, only enumerable properties, skips symbols, + // and includes indices and converts them to strings. + + const object = { __proto__: { + inherited: 1, + } }; + + const fooSymbol = Symbol('foo'); + + object.normal = 2; + object[fooSymbol] = 3; + Object.defineProperty(object, 'unenumerable', { + value: 4, + enumerable: false, + writable: true, + configurable: true, + }); + Object.defineProperty(object, 'writable', { + value: 4, + enumerable: true, + writable: true, + configurable: false, + }); + Object.defineProperty(object, 'configurable', { + value: 4, + enumerable: true, + writable: false, + configurable: true, + }); + object[5] = 5; + + assert.deepStrictEqual(test_object.GetPropertyNames(object), + ['5', + 'normal', + 'writable', + 'configurable', + 'inherited']); + + assert.deepStrictEqual(test_object.GetSymbolNames(object), + [fooSymbol]); + + assert.deepStrictEqual(test_object.GetEnumerableWritableNames(object), + ['5', + 'normal', + 'writable', + fooSymbol, + 'inherited']); + + assert.deepStrictEqual(test_object.GetOwnWritableNames(object), + ['5', + 'normal', + 'unenumerable', + 'writable', + fooSymbol]); + + assert.deepStrictEqual(test_object.GetEnumerableConfigurableNames(object), + ['5', + 'normal', + 'configurable', + fooSymbol, + 'inherited']); + + assert.deepStrictEqual(test_object.GetOwnConfigurableNames(object), + ['5', + 'normal', + 'unenumerable', + 'configurable', + fooSymbol]); +} + +// Verify that passing NULL to napi_set_property() results in the correct +// error. +assert.deepStrictEqual(test_object.TestSetProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}); + +// Verify that passing NULL to napi_has_property() results in the correct +// error. +assert.deepStrictEqual(test_object.TestHasProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +// Verify that passing NULL to napi_get_property() results in the correct +// error. +assert.deepStrictEqual(test_object.TestGetProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +{ + const obj = { x: 'a', y: 'b', z: 'c' }; + + test_object.TestSeal(obj); + + assert.strictEqual(Object.isSealed(obj), true); + + assert.throws(() => { + obj.w = 'd'; + }, /(Cannot add property w, object is not extensible)|(TypeError: Cannot add new property 'w')/); + + assert.throws(() => { + delete obj.x; + }, /(Cannot delete property 'x' of #)|(TypeError: Property is not configurable)/); + + // Sealed objects allow updating existing properties, + // so this should not throw. + obj.x = 'd'; +} + +{ + const obj = { x: 10, y: 10, z: 10 }; + + test_object.TestFreeze(obj); + + assert.strictEqual(Object.isFrozen(obj), true); + + assert.throws(() => { + obj.x = 10; + }, /(Cannot assign to read only property 'x' of object '#)|(TypeError: Cannot assign to read-only property 'x')/); + + assert.throws(() => { + obj.w = 15; + }, /(Cannot add property w, object is not extensible)|(TypeError: Cannot add new property 'w')/); + + assert.throws(() => { + delete obj.x; + }, /(Cannot delete property 'x' of #)|(TypeError: Property is not configurable)/); +} diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.c b/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.c new file mode 100644 index 00000000..7474d49e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.c @@ -0,0 +1,82 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value TestExceptions(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value target = args[0]; + napi_value exception, key, value; + napi_status status; + bool is_exception_pending; + bool bool_result; + + NODE_API_CALL(env, + napi_create_string_utf8(env, "key", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL( + env, napi_create_string_utf8(env, "value", NAPI_AUTO_LENGTH, &value)); + +#define PROCEDURE(call) \ + { \ + status = (call); \ + NODE_API_ASSERT( \ + env, status == napi_pending_exception, "expect exception pending"); \ + NODE_API_CALL(env, napi_is_exception_pending(env, &is_exception_pending)); \ + NODE_API_ASSERT(env, is_exception_pending, "expect exception pending"); \ + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception)); \ + } + // discard the exception values. + + // properties + PROCEDURE(napi_set_property(env, target, key, value)); + PROCEDURE(napi_set_named_property(env, target, "key", value)); + PROCEDURE(napi_has_property(env, target, key, &bool_result)); + PROCEDURE(napi_has_own_property(env, target, key, &bool_result)); + PROCEDURE(napi_has_named_property(env, target, "key", &bool_result)); + PROCEDURE(napi_get_property(env, target, key, &value)); + PROCEDURE(napi_get_named_property(env, target, "key", &value)); + PROCEDURE(napi_delete_property(env, target, key, &bool_result)); + + // elements + PROCEDURE(napi_set_element(env, target, 0, value)); + PROCEDURE(napi_has_element(env, target, 0, &bool_result)); + PROCEDURE(napi_get_element(env, target, 0, &value)); + PROCEDURE(napi_delete_element(env, target, 0, &bool_result)); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY_VALUE("key", value), + }; + PROCEDURE(napi_define_properties( + env, target, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + PROCEDURE(napi_get_all_property_names(env, + target, + napi_key_own_only, + napi_key_enumerable, + napi_key_keep_numbers, + &value)); + PROCEDURE(napi_get_property_names(env, target, &value)); + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("testExceptions", TestExceptions), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.js b/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.js new file mode 100644 index 00000000..2d5f10ab --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_exceptions.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common'); + +// Test +const { testExceptions } = require(`./build/${common.buildType}/test_exceptions`); + +function throws() { + throw new Error('foobar'); +} +testExceptions(new Proxy({}, { + get: common.mustCallAtLeast(throws, 1), + getOwnPropertyDescriptor: common.mustCallAtLeast(throws, 1), + defineProperty: common.mustCallAtLeast(throws, 1), + deleteProperty: common.mustCallAtLeast(throws, 1), + has: common.mustCallAtLeast(throws, 1), + set: common.mustCallAtLeast(throws, 1), + ownKeys: common.mustCallAtLeast(throws, 1), +})); diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_null.c b/Tests/NodeApi/test/js-native-api/test_object/test_null.c new file mode 100644 index 00000000..4fd4e95e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_null.c @@ -0,0 +1,400 @@ +#include + +#include "../common.h" +#include "test_null.h" + +static napi_value SetProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object, key; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, + napi_create_string_utf8(env, "someString", NAPI_AUTO_LENGTH, &key)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_set_property(NULL, object, key, object)); + + napi_set_property(env, NULL, key, object); + add_last_status(env, "objectIsNull", return_value); + + napi_set_property(env, object, NULL, object); + add_last_status(env, "keyIsNull", return_value); + + napi_set_property(env, object, key, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object, key, prop; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, + napi_create_string_utf8(env, "someString", NAPI_AUTO_LENGTH, &key)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_property(NULL, object, key, &prop)); + + napi_get_property(env, NULL, key, &prop); + add_last_status(env, "objectIsNull", return_value); + + napi_get_property(env, object, NULL, &prop); + add_last_status(env, "keyIsNull", return_value); + + napi_get_property(env, object, key, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value TestBoolValuedPropApi(napi_env env, + napi_status (*api)(napi_env, napi_value, napi_value, bool*)) { + napi_value return_value, object, key; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, + napi_create_string_utf8(env, "someString", NAPI_AUTO_LENGTH, &key)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + api(NULL, object, key, &result)); + + api(env, NULL, key, &result); + add_last_status(env, "objectIsNull", return_value); + + api(env, object, NULL, &result); + add_last_status(env, "keyIsNull", return_value); + + api(env, object, key, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value HasProperty(napi_env env, napi_callback_info info) { + return TestBoolValuedPropApi(env, napi_has_property); +} + +static napi_value HasOwnProperty(napi_env env, napi_callback_info info) { + return TestBoolValuedPropApi(env, napi_has_own_property); +} + +static napi_value DeleteProperty(napi_env env, napi_callback_info info) { + return TestBoolValuedPropApi(env, napi_delete_property); +} + +static napi_value SetNamedProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_set_named_property(NULL, object, "key", object)); + + napi_set_named_property(env, NULL, "key", object); + add_last_status(env, "objectIsNull", return_value); + + napi_set_named_property(env, object, NULL, object); + add_last_status(env, "keyIsNull", return_value); + + napi_set_named_property(env, object, "key", NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetNamedProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object, prop; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_named_property(NULL, object, "key", &prop)); + + napi_get_named_property(env, NULL, "key", &prop); + add_last_status(env, "objectIsNull", return_value); + + napi_get_named_property(env, object, NULL, &prop); + add_last_status(env, "keyIsNull", return_value); + + napi_get_named_property(env, object, "key", NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value HasNamedProperty(napi_env env, napi_callback_info info) { + napi_value return_value, object; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_has_named_property(NULL, object, "key", &result)); + + napi_has_named_property(env, NULL, "key", &result); + add_last_status(env, "objectIsNull", return_value); + + napi_has_named_property(env, object, NULL, &result); + add_last_status(env, "keyIsNull", return_value); + + napi_has_named_property(env, object, "key", NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value SetElement(napi_env env, napi_callback_info info) { + napi_value return_value, object; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_set_element(NULL, object, 0, object)); + + napi_set_element(env, NULL, 0, object); + add_last_status(env, "objectIsNull", return_value); + + napi_set_property(env, object, 0, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetElement(napi_env env, napi_callback_info info) { + napi_value return_value, object, prop; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_element(NULL, object, 0, &prop)); + + napi_get_property(env, NULL, 0, &prop); + add_last_status(env, "objectIsNull", return_value); + + napi_get_property(env, object, 0, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value TestBoolValuedElementApi(napi_env env, + napi_status (*api)(napi_env, napi_value, uint32_t, bool*)) { + napi_value return_value, object; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + NODE_API_CALL(env, napi_create_object(env, &object)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + api(NULL, object, 0, &result)); + + api(env, NULL, 0, &result); + add_last_status(env, "objectIsNull", return_value); + + api(env, object, 0, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value HasElement(napi_env env, napi_callback_info info) { + return TestBoolValuedElementApi(env, napi_has_element); +} + +static napi_value DeleteElement(napi_env env, napi_callback_info info) { + return TestBoolValuedElementApi(env, napi_delete_element); +} + +static napi_value DefineProperties(napi_env env, napi_callback_info info) { + napi_value object, return_value; + + napi_property_descriptor desc = { + "prop", NULL, DefineProperties, NULL, NULL, NULL, napi_enumerable, NULL + }; + + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_define_properties(NULL, object, 1, &desc)); + + napi_define_properties(env, NULL, 1, &desc); + add_last_status(env, "objectIsNull", return_value); + + napi_define_properties(env, object, 1, NULL); + add_last_status(env, "descriptorListIsNull", return_value); + + desc.utf8name = NULL; + napi_define_properties(env, object, 1, NULL); + add_last_status(env, "utf8nameIsNull", return_value); + desc.utf8name = "prop"; + + desc.method = NULL; + napi_define_properties(env, object, 1, NULL); + add_last_status(env, "methodIsNull", return_value); + desc.method = DefineProperties; + + return return_value; +} + +static napi_value GetPropertyNames(napi_env env, napi_callback_info info) { + napi_value return_value, props; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_property_names(NULL, return_value, &props)); + + napi_get_property_names(env, NULL, &props); + add_last_status(env, "objectIsNull", return_value); + + napi_get_property_names(env, return_value, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetAllPropertyNames(napi_env env, napi_callback_info info) { + napi_value return_value, props; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_all_property_names(NULL, + return_value, + napi_key_own_only, + napi_key_writable, + napi_key_keep_numbers, + &props)); + + napi_get_all_property_names(env, + NULL, + napi_key_own_only, + napi_key_writable, + napi_key_keep_numbers, + &props); + add_last_status(env, "objectIsNull", return_value); + + napi_get_all_property_names(env, + return_value, + napi_key_own_only, + napi_key_writable, + napi_key_keep_numbers, + NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +static napi_value GetPrototype(napi_env env, napi_callback_info info) { + napi_value return_value, proto; + + NODE_API_CALL(env, napi_create_object(env, &return_value)); + + add_returned_status(env, + "envIsNull", + return_value, + "Invalid argument", + napi_invalid_arg, + napi_get_prototype(NULL, return_value, &proto)); + + napi_get_prototype(env, NULL, &proto); + add_last_status(env, "objectIsNull", return_value); + + napi_get_prototype(env, return_value, NULL); + add_last_status(env, "valueIsNull", return_value); + + return return_value; +} + +void init_test_null(napi_env env, napi_value exports) { + napi_value test_null; + + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("setProperty", SetProperty), + DECLARE_NODE_API_PROPERTY("getProperty", GetProperty), + DECLARE_NODE_API_PROPERTY("hasProperty", HasProperty), + DECLARE_NODE_API_PROPERTY("hasOwnProperty", HasOwnProperty), + DECLARE_NODE_API_PROPERTY("deleteProperty", DeleteProperty), + DECLARE_NODE_API_PROPERTY("setNamedProperty", SetNamedProperty), + DECLARE_NODE_API_PROPERTY("getNamedProperty", GetNamedProperty), + DECLARE_NODE_API_PROPERTY("hasNamedProperty", HasNamedProperty), + DECLARE_NODE_API_PROPERTY("setElement", SetElement), + DECLARE_NODE_API_PROPERTY("getElement", GetElement), + DECLARE_NODE_API_PROPERTY("hasElement", HasElement), + DECLARE_NODE_API_PROPERTY("deleteElement", DeleteElement), + DECLARE_NODE_API_PROPERTY("defineProperties", DefineProperties), + DECLARE_NODE_API_PROPERTY("getPropertyNames", GetPropertyNames), + DECLARE_NODE_API_PROPERTY("getAllPropertyNames", GetAllPropertyNames), + DECLARE_NODE_API_PROPERTY("getPrototype", GetPrototype), + }; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID(env, napi_define_properties( + env, test_null, sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + + const napi_property_descriptor test_null_set = { + "testNull", NULL, NULL, NULL, NULL, test_null, napi_enumerable, NULL + }; + + NODE_API_CALL_RETURN_VOID(env, + napi_define_properties(env, exports, 1, &test_null_set)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_null.h b/Tests/NodeApi/test/js-native-api/test_object/test_null.h new file mode 100644 index 00000000..b142570d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_OBJECT_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_null.js b/Tests/NodeApi/test/js-native-api/test_object/test_null.js new file mode 100644 index 00000000..dcf688c5 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_null.js @@ -0,0 +1,53 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Test passing NULL to object-related N-APIs. +const { testNull } = require(`./build/${common.buildType}/test_object`); + +const expectedForProperty = { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}; +assert.deepStrictEqual(testNull.setProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.getProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.hasProperty(), expectedForProperty); +// eslint-disable-next-line no-prototype-builtins +assert.deepStrictEqual(testNull.hasOwnProperty(), expectedForProperty); +// It's OK not to want the result of a deletion. +assert.deepStrictEqual(testNull.deleteProperty(), + Object.assign({}, + expectedForProperty, + { valueIsNull: 'napi_ok' })); +assert.deepStrictEqual(testNull.setNamedProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.getNamedProperty(), expectedForProperty); +assert.deepStrictEqual(testNull.hasNamedProperty(), expectedForProperty); + +const expectedForElement = { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}; +assert.deepStrictEqual(testNull.setElement(), expectedForElement); +assert.deepStrictEqual(testNull.getElement(), expectedForElement); +assert.deepStrictEqual(testNull.hasElement(), expectedForElement); +// It's OK not to want the result of a deletion. +assert.deepStrictEqual(testNull.deleteElement(), + Object.assign({}, + expectedForElement, + { valueIsNull: 'napi_ok' })); + +assert.deepStrictEqual(testNull.defineProperties(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + descriptorListIsNull: 'Invalid argument', + utf8nameIsNull: 'Invalid argument', + methodIsNull: 'Invalid argument', +}); + +// `expectedForElement` also works for the APIs below. +assert.deepStrictEqual(testNull.getPropertyNames(), expectedForElement); +assert.deepStrictEqual(testNull.getAllPropertyNames(), expectedForElement); +assert.deepStrictEqual(testNull.getPrototype(), expectedForElement); diff --git a/Tests/NodeApi/test/js-native-api/test_object/test_object.c b/Tests/NodeApi/test/js-native-api/test_object/test_object.c new file mode 100644 index 00000000..4d53e09f --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_object/test_object.c @@ -0,0 +1,755 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +static int test_value = 3; + +static napi_value Get(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as second."); + + napi_value object = args[0]; + napi_value output; + NODE_API_CALL(env, napi_get_property(env, object, args[1], &output)); + + return output; +} + +static napi_value GetNamed(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, value_type1 == napi_string, + "Wrong type of arguments. Expects a string as second."); + + napi_value object = args[0]; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT(env, key_length <= 255, + "Cannot accommodate keys longer than 255 bytes"); + napi_value output; + NODE_API_CALL(env, napi_get_named_property(env, object, key, &output)); + + return output; +} + +static napi_value GetPropertyNames(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL(env, napi_get_property_names(env, args[0], &output)); + + return output; +} + +static napi_value GetSymbolNames(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL(env, + napi_get_all_property_names( + env, args[0], napi_key_include_prototypes, napi_key_skip_strings, + napi_key_numbers_to_strings, &output)); + + return output; +} + +static napi_value GetEnumerableWritableNames(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT( + env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL( + env, + napi_get_all_property_names(env, + args[0], + napi_key_include_prototypes, + napi_key_enumerable | napi_key_writable, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value GetOwnWritableNames(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT( + env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL(env, + napi_get_all_property_names(env, + args[0], + napi_key_own_only, + napi_key_writable, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value GetEnumerableConfigurableNames(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT( + env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL( + env, + napi_get_all_property_names(env, + args[0], + napi_key_include_prototypes, + napi_key_enumerable | napi_key_configurable, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value GetOwnConfigurableNames(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT( + env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value output; + NODE_API_CALL(env, + napi_get_all_property_names(env, + args[0], + napi_key_own_only, + napi_key_configurable, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value Set(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 3, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as second."); + + NODE_API_CALL(env, napi_set_property(env, args[0], args[1], args[2])); + + napi_value valuetrue; + NODE_API_CALL(env, napi_get_boolean(env, true, &valuetrue)); + + return valuetrue; +} + +static napi_value SetNamed(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 3, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, value_type1 == napi_string, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT(env, key_length <= 255, + "Cannot accommodate keys longer than 255 bytes"); + + NODE_API_CALL(env, napi_set_named_property(env, args[0], key, args[2])); + + napi_value value_true; + NODE_API_CALL(env, napi_get_boolean(env, true, &value_true)); + + return value_true; +} + +static napi_value Has(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as second."); + + bool has_property; + NODE_API_CALL(env, napi_has_property(env, args[0], args[1], &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value HasNamed(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, value_type1 == napi_string || value_type1 == napi_symbol, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT(env, key_length <= 255, + "Cannot accommodate keys longer than 255 bytes"); + + bool has_property; + NODE_API_CALL(env, napi_has_named_property(env, args[0], key, &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value HasOwn(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + // napi_valuetype valuetype1; + // NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + // + // NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + // "Wrong type of arguments. Expects a string or symbol as second."); + + bool has_property; + NODE_API_CALL(env, napi_has_own_property(env, args[0], args[1], &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value Delete(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + NODE_API_ASSERT(env, valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as second."); + + bool result; + napi_value ret; + NODE_API_CALL(env, napi_delete_property(env, args[0], args[1], &result)); + NODE_API_CALL(env, napi_get_boolean(env, result, &ret)); + + return ret; +} + +static napi_value New(napi_env env, napi_callback_info info) { + napi_value ret; + NODE_API_CALL(env, napi_create_object(env, &ret)); + + napi_value num; + NODE_API_CALL(env, napi_create_int32(env, 987654321, &num)); + + NODE_API_CALL(env, napi_set_named_property(env, ret, "test_number", num)); + + napi_value str; + const char* str_val = "test string"; + size_t str_len = strlen(str_val); + NODE_API_CALL(env, napi_create_string_utf8(env, str_val, str_len, &str)); + + NODE_API_CALL(env, napi_set_named_property(env, ret, "test_string", str)); + + return ret; +} + +static napi_value Inflate(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first argument."); + + napi_value obj = args[0]; + napi_value propertynames; + NODE_API_CALL(env, napi_get_property_names(env, obj, &propertynames)); + + uint32_t i, length; + NODE_API_CALL(env, napi_get_array_length(env, propertynames, &length)); + + for (i = 0; i < length; i++) { + napi_value property_str; + NODE_API_CALL(env, napi_get_element(env, propertynames, i, &property_str)); + + napi_value value; + NODE_API_CALL(env, napi_get_property(env, obj, property_str, &value)); + + double double_val; + NODE_API_CALL(env, napi_get_value_double(env, value, &double_val)); + NODE_API_CALL(env, napi_create_double(env, double_val + 1, &value)); + NODE_API_CALL(env, napi_set_property(env, obj, property_str, value)); + } + + return obj; +} + +static napi_value Wrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + NODE_API_CALL(env, napi_wrap(env, arg, &test_value, NULL, NULL, NULL)); + return NULL; +} + +static napi_value Unwrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + void* data; + NODE_API_CALL(env, napi_unwrap(env, arg, &data)); + + bool is_expected = (data != NULL && *(int*)data == 3); + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, is_expected, &result)); + return result; +} + +static napi_value TestSetProperty(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value object, key, value; + + NODE_API_CALL(env, napi_create_object(env, &object)); + + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + + NODE_API_CALL(env, napi_create_object(env, &value)); + + status = napi_set_property(NULL, object, key, value); + + add_returned_status(env, + "envIsNull", + object, + "Invalid argument", + napi_invalid_arg, + status); + + napi_set_property(env, NULL, key, value); + + add_last_status(env, "objectIsNull", object); + + napi_set_property(env, object, NULL, value); + + add_last_status(env, "keyIsNull", object); + + napi_set_property(env, object, key, NULL); + + add_last_status(env, "valueIsNull", object); + + return object; +} + +static napi_value TestHasProperty(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value object, key; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &object)); + + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + + status = napi_has_property(NULL, object, key, &result); + + add_returned_status(env, + "envIsNull", + object, + "Invalid argument", + napi_invalid_arg, + status); + + napi_has_property(env, NULL, key, &result); + + add_last_status(env, "objectIsNull", object); + + napi_has_property(env, object, NULL, &result); + + add_last_status(env, "keyIsNull", object); + + napi_has_property(env, object, key, NULL); + + add_last_status(env, "resultIsNull", object); + + return object; +} + +static napi_value TestGetProperty(napi_env env, + napi_callback_info info) { + napi_status status; + napi_value object, key, result; + + NODE_API_CALL(env, napi_create_object(env, &object)); + + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + + NODE_API_CALL(env, napi_create_object(env, &result)); + + status = napi_get_property(NULL, object, key, &result); + + add_returned_status(env, + "envIsNull", + object, + "Invalid argument", + napi_invalid_arg, + status); + + napi_get_property(env, NULL, key, &result); + + add_last_status(env, "objectIsNull", object); + + napi_get_property(env, object, NULL, &result); + + add_last_status(env, "keyIsNull", object); + + napi_get_property(env, object, key, NULL); + + add_last_status(env, "resultIsNull", object); + + return object; +} + +static napi_value TestFreeze(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value object = args[0]; + NODE_API_CALL(env, napi_object_freeze(env, object)); + + return object; +} + +static napi_value TestSeal(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value object = args[0]; + NODE_API_CALL(env, napi_object_seal(env, object)); + + return object; +} + +// We create two type tags. They are basically 128-bit UUIDs. +#define TYPE_TAG_COUNT 5 +static const napi_type_tag type_tags[TYPE_TAG_COUNT] = { + {0xdaf987b3cc62481a, 0xb745b0497f299531}, + {0xbb7936c374084d9b, 0xa9548d0762eeedb9}, + {0xa5ed9ce2e4c00c38, 0}, + {0, 0}, + {0xa5ed9ce2e4c00c38, 0xdaf987b3cc62481a}, +}; +#define VALIDATE_TYPE_INDEX(env, type_index) \ + do { \ + if ((type_index) >= TYPE_TAG_COUNT) { \ + NODE_API_CALL((env), \ + napi_throw_range_error((env), \ + "NODE_API_TEST_INVALID_TYPE_INDEX", \ + "Invalid type index")); \ + } \ + } while (0) + +static napi_value +TypeTaggedInstance(napi_env env, napi_callback_info info) { + size_t argc = 1; + uint32_t type_index; + napi_value instance, which_type; + napi_type_tag tag; + + // Below we copy the tag before setting it to prevent bugs where a pointer + // to the tag (instead of the 128-bit tag value) is stored. + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &which_type, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, which_type, &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL(env, napi_create_object(env, &instance)); + tag = type_tags[type_index]; + NODE_API_CALL(env, napi_type_tag_object(env, instance, &tag)); + + // Since the tag passed to napi_type_tag_object() was copied to the stack, + // a type tagging implementation that uses a pointer instead of the + // tag value would end up pointing to stack memory. + // When CheckTypeTag() is called later on, it might be the case that this + // stack address has been left untouched by accident (if no subsequent + // function call has clobbered it), which means the pointer would still + // point to valid data. + // To make sure that tags are stored by value and not by reference, + // clear this copy; any implementation using a pointer would end up with + // random stack data or { 0, 0 }, but not the original tag value, and fail. + memset(&tag, 0, sizeof(tag)); + + return instance; +} + +// V8 will not allow us to construct an external with a NULL data value. +#define IN_LIEU_OF_NULL ((void*)0x1) + +static napi_value PlainExternal(napi_env env, napi_callback_info info) { + napi_value instance; + + NODE_API_CALL( + env, napi_create_external(env, IN_LIEU_OF_NULL, NULL, NULL, &instance)); + + return instance; +} + +static napi_value TypeTaggedExternal(napi_env env, napi_callback_info info) { + size_t argc = 1; + uint32_t type_index; + napi_value instance, which_type; + napi_type_tag tag; + + // See TypeTaggedInstance() for an explanation about why we copy the tag + // to the stack and why we call memset on it after the external is tagged. + + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, &which_type, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, which_type, &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL( + env, napi_create_external(env, IN_LIEU_OF_NULL, NULL, NULL, &instance)); + tag = type_tags[type_index]; + NODE_API_CALL(env, napi_type_tag_object(env, instance, &tag)); + + memset(&tag, 0, sizeof(tag)); + + return instance; +} + +static napi_value +CheckTypeTag(napi_env env, napi_callback_info info) { + size_t argc = 2; + bool result; + napi_value argv[2], js_result; + uint32_t type_index; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, argv[0], &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL(env, napi_check_object_type_tag(env, + argv[1], + &type_tags[type_index], + &result)); + NODE_API_CALL(env, napi_get_boolean(env, result, &js_result)); + + return js_result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("Get", Get), + DECLARE_NODE_API_PROPERTY("GetNamed", GetNamed), + DECLARE_NODE_API_PROPERTY("GetPropertyNames", GetPropertyNames), + DECLARE_NODE_API_PROPERTY("GetSymbolNames", GetSymbolNames), + DECLARE_NODE_API_PROPERTY("GetEnumerableWritableNames", + GetEnumerableWritableNames), + DECLARE_NODE_API_PROPERTY("GetOwnWritableNames", GetOwnWritableNames), + DECLARE_NODE_API_PROPERTY("GetEnumerableConfigurableNames", + GetEnumerableConfigurableNames), + DECLARE_NODE_API_PROPERTY("GetOwnConfigurableNames", + GetOwnConfigurableNames), + DECLARE_NODE_API_PROPERTY("Set", Set), + DECLARE_NODE_API_PROPERTY("SetNamed", SetNamed), + DECLARE_NODE_API_PROPERTY("Has", Has), + DECLARE_NODE_API_PROPERTY("HasNamed", HasNamed), + DECLARE_NODE_API_PROPERTY("HasOwn", HasOwn), + DECLARE_NODE_API_PROPERTY("Delete", Delete), + DECLARE_NODE_API_PROPERTY("New", New), + DECLARE_NODE_API_PROPERTY("Inflate", Inflate), + DECLARE_NODE_API_PROPERTY("Wrap", Wrap), + DECLARE_NODE_API_PROPERTY("Unwrap", Unwrap), + DECLARE_NODE_API_PROPERTY("TestSetProperty", TestSetProperty), + DECLARE_NODE_API_PROPERTY("TestHasProperty", TestHasProperty), + DECLARE_NODE_API_PROPERTY("TypeTaggedInstance", TypeTaggedInstance), + DECLARE_NODE_API_PROPERTY("TypeTaggedExternal", TypeTaggedExternal), + DECLARE_NODE_API_PROPERTY("PlainExternal", PlainExternal), + DECLARE_NODE_API_PROPERTY("CheckTypeTag", CheckTypeTag), + DECLARE_NODE_API_PROPERTY("TestGetProperty", TestGetProperty), + DECLARE_NODE_API_PROPERTY("TestFreeze", TestFreeze), + DECLARE_NODE_API_PROPERTY("TestSeal", TestSeal), + }; + + init_test_null(env, exports); + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_promise/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_promise/CMakeLists.txt new file mode 100644 index 00000000..0e82dff9 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_promise/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_promise + SOURCES + test_promise.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_promise/binding.gyp b/Tests/NodeApi/test/js-native-api/test_promise/binding.gyp new file mode 100644 index 00000000..c2b65f5a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_promise/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_promise", + "sources": [ + "test_promise.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_promise/test.js b/Tests/NodeApi/test/js-native-api/test_promise/test.js new file mode 100644 index 00000000..695fdcc2 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_promise/test.js @@ -0,0 +1,61 @@ +'use strict'; + +const common = require('../../common'); + +// This tests the promise-related n-api calls + +const assert = require('assert'); +const test_promise = require(`./build/${common.buildType}/test_promise`); + +// A resolution +{ + const expected_result = 42; + const promise = test_promise.createPromise(); + promise.then( + common.mustCall(function(result) { + assert.strictEqual(result, expected_result); + }), + common.mustNotCall()); + test_promise.concludeCurrentPromise(expected_result, true); +} + +// A rejection +{ + const expected_result = 'It\'s not you, it\'s me.'; + const promise = test_promise.createPromise(); + promise.then( + common.mustNotCall(), + common.mustCall(function(result) { + assert.strictEqual(result, expected_result); + })); + test_promise.concludeCurrentPromise(expected_result, false); +} + +// Chaining +{ + const expected_result = 'chained answer'; + const promise = test_promise.createPromise(); + promise.then( + common.mustCall(function(result) { + assert.strictEqual(result, expected_result); + }), + common.mustNotCall()); + test_promise.concludeCurrentPromise(Promise.resolve('chained answer'), true); +} + +const promiseTypeTestPromise = test_promise.createPromise(); +assert.strictEqual(test_promise.isPromise(promiseTypeTestPromise), true); +test_promise.concludeCurrentPromise(undefined, true); + +const rejectPromise = Promise.reject(-1); +const expected_reason = -1; +assert.strictEqual(test_promise.isPromise(rejectPromise), true); +rejectPromise.catch((reason) => { + assert.strictEqual(reason, expected_reason); +}); + +assert.strictEqual(test_promise.isPromise(2.4), false); +assert.strictEqual(test_promise.isPromise('I promise!'), false); +assert.strictEqual(test_promise.isPromise(undefined), false); +assert.strictEqual(test_promise.isPromise(null), false); +assert.strictEqual(test_promise.isPromise({}), false); diff --git a/Tests/NodeApi/test/js-native-api/test_promise/test_promise.c b/Tests/NodeApi/test/js-native-api/test_promise/test_promise.c new file mode 100644 index 00000000..1f0b5507 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_promise/test_promise.c @@ -0,0 +1,64 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +napi_deferred deferred = NULL; + +static napi_value createPromise(napi_env env, napi_callback_info info) { + napi_value promise; + + // We do not overwrite an existing deferred. + if (deferred != NULL) { + return NULL; + } + + NODE_API_CALL(env, napi_create_promise(env, &deferred, &promise)); + + return promise; +} + +static napi_value +concludeCurrentPromise(napi_env env, napi_callback_info info) { + napi_value argv[2]; + size_t argc = 2; + bool resolution; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_bool(env, argv[1], &resolution)); + if (resolution) { + NODE_API_CALL(env, napi_resolve_deferred(env, deferred, argv[0])); + } else { + NODE_API_CALL(env, napi_reject_deferred(env, deferred, argv[0])); + } + + deferred = NULL; + + return NULL; +} + +static napi_value isPromise(napi_env env, napi_callback_info info) { + napi_value promise, result; + size_t argc = 1; + bool is_promise; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &promise, NULL, NULL)); + NODE_API_CALL(env, napi_is_promise(env, promise, &is_promise)); + NODE_API_CALL(env, napi_get_boolean(env, is_promise, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("createPromise", createPromise), + DECLARE_NODE_API_PROPERTY("concludeCurrentPromise", concludeCurrentPromise), + DECLARE_NODE_API_PROPERTY("isPromise", isPromise), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_properties/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_properties/CMakeLists.txt new file mode 100644 index 00000000..18dcbf18 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_properties/CMakeLists.txt @@ -0,0 +1,6 @@ +add_node_api_module(test_properties + SOURCES + test_properties.c + DEFINES + "NAPI_VERSION=9" +) diff --git a/Tests/NodeApi/test/js-native-api/test_properties/binding.gyp b/Tests/NodeApi/test/js-native-api/test_properties/binding.gyp new file mode 100644 index 00000000..ab9d58db --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_properties/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_properties", + "sources": [ + "test_properties.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_properties/test.js b/Tests/NodeApi/test/js-native-api/test_properties/test.js new file mode 100644 index 00000000..6e035d67 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_properties/test.js @@ -0,0 +1,69 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const readonlyErrorRE = + /^TypeError: Cannot assign to read(-| )only property '.*'( of object '#')?$/; +const getterOnlyErrorRE = + /^TypeError: Cannot (set|assign to) property .*( of #)? which has only a getter$/; + +// Testing api calls for defining properties +const test_object = require(`./build/${common.buildType}/test_properties`); + +assert.strictEqual(test_object.echo('hello'), 'hello'); + +test_object.readwriteValue = 1; +assert.strictEqual(test_object.readwriteValue, 1); +test_object.readwriteValue = 2; +assert.strictEqual(test_object.readwriteValue, 2); + +assert.throws(() => { test_object.readonlyValue = 3; }, readonlyErrorRE); + +assert.ok(test_object.hiddenValue); + +// Properties with napi_enumerable attribute should be enumerable. +const propertyNames = []; +for (const name in test_object) { + propertyNames.push(name); +} +assert.ok(propertyNames.includes('echo')); +assert.ok(propertyNames.includes('readwriteValue')); +assert.ok(propertyNames.includes('readonlyValue')); +assert.ok(!propertyNames.includes('hiddenValue')); +assert.ok(propertyNames.includes('NameKeyValue')); +assert.ok(!propertyNames.includes('readwriteAccessor1')); +assert.ok(!propertyNames.includes('readwriteAccessor2')); +assert.ok(!propertyNames.includes('readonlyAccessor1')); +assert.ok(!propertyNames.includes('readonlyAccessor2')); + +// Validate property created with symbol +const start = 'Symbol('.length; +const end = start + 'NameKeySymbol'.length; +const symbolDescription = + String(Object.getOwnPropertySymbols(test_object)[0]).slice(start, end); +assert.strictEqual(symbolDescription, 'NameKeySymbol'); + +// The napi_writable attribute should be ignored for accessors. +const readwriteAccessor1Descriptor = + Object.getOwnPropertyDescriptor(test_object, 'readwriteAccessor1'); +const readonlyAccessor1Descriptor = + Object.getOwnPropertyDescriptor(test_object, 'readonlyAccessor1'); +assert.ok(readwriteAccessor1Descriptor.get != null); +assert.ok(readwriteAccessor1Descriptor.set != null); +assert.ok(readwriteAccessor1Descriptor.value === undefined); +assert.ok(readonlyAccessor1Descriptor.get != null); +assert.ok(readonlyAccessor1Descriptor.set === undefined); +assert.ok(readonlyAccessor1Descriptor.value === undefined); +test_object.readwriteAccessor1 = 1; +assert.strictEqual(test_object.readwriteAccessor1, 1); +assert.strictEqual(test_object.readonlyAccessor1, 1); +assert.throws(() => { test_object.readonlyAccessor1 = 3; }, getterOnlyErrorRE); +test_object.readwriteAccessor2 = 2; +assert.strictEqual(test_object.readwriteAccessor2, 2); +assert.strictEqual(test_object.readonlyAccessor2, 2); +assert.throws(() => { test_object.readonlyAccessor2 = 3; }, getterOnlyErrorRE); + +assert.strictEqual(test_object.hasNamedProperty(test_object, 'echo'), true); +assert.strictEqual(test_object.hasNamedProperty(test_object, 'hiddenValue'), + true); +assert.strictEqual(test_object.hasNamedProperty(test_object, 'doesnotexist'), + false); diff --git a/Tests/NodeApi/test/js-native-api/test_properties/test_properties.c b/Tests/NodeApi/test/js-native-api/test_properties/test_properties.c new file mode 100644 index 00000000..7b8e67a9 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_properties/test_properties.c @@ -0,0 +1,113 @@ +#define NAPI_VERSION 9 +#include +#include "../common.h" +#include "../entry_point.h" + +static double value_ = 1; + +static napi_value GetValue(napi_env env, napi_callback_info info) { + size_t argc = 0; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, NULL, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 0, "Wrong number of arguments"); + + napi_value number; + NODE_API_CALL(env, napi_create_double(env, value_, &number)); + + return number; +} + +static napi_value SetValue(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + NODE_API_CALL(env, napi_get_value_double(env, args[0], &value_)); + + return NULL; +} + +static napi_value Echo(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + return args[0]; +} + +static napi_value HasNamedProperty(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + // Extract the name of the property to check + char buffer[128]; + size_t copied; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[1], buffer, sizeof(buffer), &copied)); + + // do the check and create the boolean return value + bool value; + napi_value result; + NODE_API_CALL(env, napi_has_named_property(env, args[0], buffer, &value)); + NODE_API_CALL(env, napi_get_boolean(env, value, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value number; + NODE_API_CALL(env, napi_create_double(env, value_, &number)); + + napi_value name_value; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "NameKeyValue", NAPI_AUTO_LENGTH, &name_value)); + + napi_value symbol_description; + napi_value name_symbol; + NODE_API_CALL(env, + napi_create_string_utf8( + env, "NameKeySymbol", NAPI_AUTO_LENGTH, &symbol_description)); + NODE_API_CALL(env, + napi_create_symbol(env, symbol_description, &name_symbol)); + + napi_value name_symbol_descriptionless; + NODE_API_CALL(env, + napi_create_symbol(env, NULL, &name_symbol_descriptionless)); + + napi_value name_symbol_for; + NODE_API_CALL(env, node_api_symbol_for(env, + "NameKeySymbolFor", + NAPI_AUTO_LENGTH, + &name_symbol_for)); + + napi_property_descriptor properties[] = { + { "echo", 0, Echo, 0, 0, 0, napi_enumerable, 0 }, + { "readwriteValue", 0, 0, 0, 0, number, napi_enumerable | napi_writable, 0 }, + { "readonlyValue", 0, 0, 0, 0, number, napi_enumerable, 0}, + { "hiddenValue", 0, 0, 0, 0, number, napi_default, 0}, + { NULL, name_value, 0, 0, 0, number, napi_enumerable, 0}, + { NULL, name_symbol, 0, 0, 0, number, napi_enumerable, 0}, + { NULL, name_symbol_descriptionless, 0, 0, 0, number, napi_enumerable, 0}, + { NULL, name_symbol_for, 0, 0, 0, number, napi_enumerable, 0}, + { "readwriteAccessor1", 0, 0, GetValue, SetValue, 0, napi_default, 0}, + { "readwriteAccessor2", 0, 0, GetValue, SetValue, 0, napi_writable, 0}, + { "readonlyAccessor1", 0, 0, GetValue, NULL, 0, napi_default, 0}, + { "readonlyAccessor2", 0, 0, GetValue, NULL, 0, napi_writable, 0}, + { "hasNamedProperty", 0, HasNamedProperty, 0, 0, 0, napi_default, 0 }, + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_reference/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_reference/CMakeLists.txt new file mode 100644 index 00000000..2c0dd7b5 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/CMakeLists.txt @@ -0,0 +1,11 @@ +add_node_api_module(test_reference + SOURCES + test_reference.c + DEFINES + "NAPI_VERSION=9" +) + +add_node_api_module(test_finalizer + SOURCES + test_finalizer.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_reference/binding.gyp b/Tests/NodeApi/test/js-native-api/test_reference/binding.gyp new file mode 100644 index 00000000..2f2acb3a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/binding.gyp @@ -0,0 +1,16 @@ +{ + "targets": [ + { + "target_name": "test_reference", + "sources": [ + "test_reference.c" + ] + }, + { + "target_name": "test_finalizer", + "sources": [ + "test_finalizer.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_reference/test.js b/Tests/NodeApi/test/js-native-api/test_reference/test.js new file mode 100644 index 00000000..aa5c9953 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/test.js @@ -0,0 +1,158 @@ +'use strict'; +// Flags: --expose-gc + +const { buildType } = require('../../common'); +const { gcUntil } = require('../../common/gc'); +const assert = require('assert'); + +const test_reference = require(`./build/${buildType}/test_reference`); + +// This test script uses external values with finalizer callbacks +// in order to track when values get garbage-collected. Each invocation +// of a finalizer callback increments the finalizeCount property. +assert.strictEqual(test_reference.finalizeCount, 0); + +// Run each test function in sequence, +// with an async delay and GC call between each. +async function runTests() { + (() => { + const symbol = test_reference.createSymbol('testSym'); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, symbol); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolFor('testSymFor'); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, symbol); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolFor('testSymFor'); + test_reference.createReference(symbol, 1); + assert.strictEqual(test_reference.referenceValue, symbol); + assert.strictEqual(test_reference.referenceValue, Symbol.for('testSymFor')); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolForEmptyString(); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, Symbol.for('')); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolForEmptyString(); + test_reference.createReference(symbol, 1); + assert.strictEqual(test_reference.referenceValue, symbol); + assert.strictEqual(test_reference.referenceValue, Symbol.for('')); + })(); + test_reference.deleteReference(); + + assert.throws(() => test_reference.createSymbolForIncorrectLength(), + /Invalid argument/); + + (() => { + const value = test_reference.createExternal(); + assert.strictEqual(test_reference.finalizeCount, 0); + assert.strictEqual(typeof value, 'object'); + test_reference.checkExternal(value); + })(); + await gcUntil('External value without a finalizer', + () => (test_reference.finalizeCount === 0)); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + assert.strictEqual(typeof value, 'object'); + test_reference.checkExternal(value); + })(); + await gcUntil('External value with a finalizer', + () => (test_reference.finalizeCount === 1)); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 0); + assert.strictEqual(test_reference.referenceValue, value); + })(); + // Value should be GC'd because there is only a weak ref + await gcUntil('Weak reference', + () => (test_reference.referenceValue === undefined && + test_reference.finalizeCount === 1)); + test_reference.deleteReference(); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 1); + assert.strictEqual(test_reference.referenceValue, value); + })(); + // Value should NOT be GC'd because there is a strong ref + await gcUntil('Strong reference', + () => (test_reference.finalizeCount === 0)); + test_reference.deleteReference(); + await gcUntil('Strong reference (cont.d)', + () => (test_reference.finalizeCount === 1)); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 1); + })(); + // Value should NOT be GC'd because there is a strong ref + await gcUntil('Strong reference, increment then decrement to weak reference', + () => (test_reference.finalizeCount === 0)); + assert.strictEqual(test_reference.incrementRefcount(), 2); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-1)', + () => (test_reference.finalizeCount === 0)); + assert.strictEqual(test_reference.decrementRefcount(), 1); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-2)', + () => (test_reference.finalizeCount === 0)); + assert.strictEqual(test_reference.decrementRefcount(), 0); + // Value should be GC'd because the ref is now weak! + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-3)', + () => (test_reference.finalizeCount === 1)); + test_reference.deleteReference(); + // Value was already GC'd + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-4)', + () => (test_reference.finalizeCount === 1)); +} +runTests(); + +// This test creates a napi_ref on an object that has +// been wrapped by napi_wrap and for which the finalizer +// for the wrap calls napi_delete_ref on that napi_ref. +// +// Since both the wrap and the reference use the same +// object the finalizer for the wrap and reference +// may run in the same gc and in any order. +// +// It does that to validate that napi_delete_ref can be +// called before the finalizer has been run for the +// reference (there is a finalizer behind the scenes even +// though it cannot be passed to napi_create_reference). +// +// Since the order is not guaranteed, run the +// test a number of times maximize the chance that we +// get a run with the desired order for the test. +// +// 1000 reliably recreated the problem without the fix +// required to ensure delete could be called before +// the finalizer in manual testing. +for (let i = 0; i < 1000; i++) { + (() => { + const wrapObject = new Object(); + test_reference.validateDeleteBeforeFinalize(wrapObject); + })(); + global.gc(); +} diff --git a/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.c b/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.c new file mode 100644 index 00000000..0ce671b6 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.c @@ -0,0 +1,79 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static int test_value = 1; +static int finalize_count = 0; + +static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalize_count, &result)); + return result; +} +static void FinalizeExternalCallJs(napi_env env, void* data, void* hint) { + finalize_count++; + + int* actual_value = data; + NODE_API_ASSERT_RETURN_VOID( + env, + actual_value == &test_value, + "The correct pointer was passed to the finalizer"); + + napi_ref finalizer_ref = (napi_ref)hint; + napi_value js_finalizer; + napi_value recv; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, finalizer_ref, &js_finalizer)); + NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv)); + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref)); +} + +static napi_value CreateExternalWithJsFinalize(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + napi_value finalizer = args[0]; + napi_valuetype finalizer_valuetype; + NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype)); + NODE_API_ASSERT(env, + finalizer_valuetype == napi_function, + "Wrong type of first argument"); + napi_ref finalizer_ref; + NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref)); + + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + &test_value, + FinalizeExternalCallJs, + finalizer_ref, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_GETTER("finalizeCount", GetFinalizeCount), + DECLARE_NODE_API_PROPERTY("createExternalWithJsFinalize", + CreateExternalWithJsFinalize), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.js b/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.js new file mode 100644 index 00000000..0b973163 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/test_finalizer.js @@ -0,0 +1,24 @@ +'use strict'; +// Flags: --expose-gc --force-node-api-uncaught-exceptions-policy + +const common = require('../../common'); +const binding = require(`./build/${common.buildType}/test_finalizer`); +const assert = require('assert'); +const { gcUntil } = require('../../common/gc'); + +process.on('uncaughtException', common.mustCall((err) => { + assert.throws(() => { throw err; }, /finalizer error/); +})); + +(async function() { + (() => { + binding.createExternalWithJsFinalize( + common.mustCall(() => { + throw new Error('finalizer error'); + }) + ); + })(); + await gcUntil('External value calls finalizer', + () => (binding.finalizeCount === 1)); + +})().then(common.mustCall()); diff --git a/Tests/NodeApi/test/js-native-api/test_reference/test_reference.c b/Tests/NodeApi/test/js-native-api/test_reference/test_reference.c new file mode 100644 index 00000000..a66b8068 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference/test_reference.c @@ -0,0 +1,252 @@ +#define NAPI_VERSION 9 +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static int test_value = 1; +static int finalize_count = 0; +static napi_ref test_reference = NULL; + +static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalize_count, &result)); + return result; +} + +static void FinalizeExternal(napi_env env, void* data, void* hint) { + int *actual_value = data; + NODE_API_ASSERT_RETURN_VOID(env, actual_value == &test_value, + "The correct pointer was passed to the finalizer"); + finalize_count++; +} + +static napi_value CreateExternal(napi_env env, napi_callback_info info) { + int* data = &test_value; + + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + data, + NULL, /* finalize_cb */ + NULL, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +static napi_value CreateSymbol(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT( + env, argc == 1, "Expect one argument only (symbol description)"); + + napi_value result_symbol; + + NODE_API_CALL(env, napi_create_symbol(env, args[0], &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolFor(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + + char description[256]; + size_t description_length; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT( + env, argc == 1, "Expect one argument only (symbol description)"); + + NODE_API_CALL( + env, + napi_get_value_string_utf8( + env, args[0], description, sizeof(description), &description_length)); + NODE_API_ASSERT(env, + description_length <= 255, + "Cannot accommodate descriptions longer than 255 bytes"); + + napi_value result_symbol; + + NODE_API_CALL(env, + node_api_symbol_for( + env, description, description_length, &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolForEmptyString(napi_env env, napi_callback_info info) { + napi_value result_symbol; + NODE_API_CALL(env, node_api_symbol_for(env, NULL, 0, &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolForIncorrectLength(napi_env env, napi_callback_info info) { + napi_value result_symbol; + NODE_API_CALL(env, node_api_symbol_for(env, NULL, 5, &result_symbol)); + return result_symbol; +} + +static napi_value +CreateExternalWithFinalize(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + &test_value, + FinalizeExternal, + NULL, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +static napi_value CheckExternal(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Expected one argument."); + + napi_valuetype argtype; + NODE_API_CALL(env, napi_typeof(env, arg, &argtype)); + + NODE_API_ASSERT(env, argtype == napi_external, "Expected an external value."); + + void* data; + NODE_API_CALL(env, napi_get_value_external(env, arg, &data)); + + NODE_API_ASSERT(env, data != NULL && *(int*)data == test_value, + "An external data value of 1 was expected."); + + return NULL; +} + +static napi_value CreateReference(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference == NULL, + "The test allows only one reference at a time."); + + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Expected two arguments."); + + uint32_t initial_refcount; + NODE_API_CALL(env, napi_get_value_uint32(env, args[1], &initial_refcount)); + + NODE_API_CALL(env, + napi_create_reference(env, args[0], initial_refcount, &test_reference)); + + NODE_API_ASSERT(env, test_reference != NULL, + "A reference should have been created."); + + return NULL; +} + +static napi_value DeleteReference(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + NODE_API_CALL(env, napi_delete_reference(env, test_reference)); + test_reference = NULL; + return NULL; +} + +static napi_value IncrementRefcount(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + uint32_t refcount; + NODE_API_CALL(env, napi_reference_ref(env, test_reference, &refcount)); + + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, refcount, &result)); + return result; +} + +static napi_value DecrementRefcount(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + uint32_t refcount; + NODE_API_CALL(env, napi_reference_unref(env, test_reference, &refcount)); + + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, refcount, &result)); + return result; +} + +static napi_value GetReferenceValue(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + napi_value result; + NODE_API_CALL(env, napi_get_reference_value(env, test_reference, &result)); + return result; +} + +static void DeleteBeforeFinalizeFinalizer( + napi_env env, void* finalize_data, void* finalize_hint) { + napi_ref* ref = (napi_ref*)finalize_data; + napi_value value; + assert(napi_get_reference_value(env, *ref, &value) == napi_ok); + assert(value == NULL); + napi_delete_reference(env, *ref); + free(ref); +} + +static napi_value ValidateDeleteBeforeFinalize(napi_env env, napi_callback_info info) { + napi_value wrapObject; + size_t argc = 1; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &wrapObject, NULL, NULL)); + + napi_ref* ref_t = malloc(sizeof(napi_ref)); + NODE_API_CALL(env, + napi_wrap( + env, wrapObject, ref_t, DeleteBeforeFinalizeFinalizer, NULL, NULL)); + + // Create a reference that will be eligible for collection at the same + // time as the wrapped object by passing in the same wrapObject. + // This means that the FinalizeOrderValidation callback may be run + // before the finalizer for the newly created reference (there is a finalizer + // behind the scenes even though it cannot be passed to napi_create_reference) + // The Finalizer for the wrap (which is different than the finalizer + // for the reference) calls napi_delete_reference validating that + // napi_delete_reference can be called before the finalizer for the + // reference runs. + NODE_API_CALL(env, napi_create_reference(env, wrapObject, 0, ref_t)); + return wrapObject; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_GETTER("finalizeCount", GetFinalizeCount), + DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal), + DECLARE_NODE_API_PROPERTY("createExternalWithFinalize", + CreateExternalWithFinalize), + DECLARE_NODE_API_PROPERTY("checkExternal", CheckExternal), + DECLARE_NODE_API_PROPERTY("createReference", CreateReference), + DECLARE_NODE_API_PROPERTY("createSymbol", CreateSymbol), + DECLARE_NODE_API_PROPERTY("createSymbolFor", CreateSymbolFor), + DECLARE_NODE_API_PROPERTY("createSymbolForEmptyString", + CreateSymbolForEmptyString), + DECLARE_NODE_API_PROPERTY("createSymbolForIncorrectLength", + CreateSymbolForIncorrectLength), + DECLARE_NODE_API_PROPERTY("deleteReference", DeleteReference), + DECLARE_NODE_API_PROPERTY("incrementRefcount", IncrementRefcount), + DECLARE_NODE_API_PROPERTY("decrementRefcount", DecrementRefcount), + DECLARE_NODE_API_GETTER("referenceValue", GetReferenceValue), + DECLARE_NODE_API_PROPERTY("validateDeleteBeforeFinalize", + ValidateDeleteBeforeFinalize), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_reference_double_free/CMakeLists.txt new file mode 100644 index 00000000..3fe097a0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_reference_double_free + SOURCES + test_reference_double_free.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/binding.gyp b/Tests/NodeApi/test/js-native-api/test_reference_double_free/binding.gyp new file mode 100644 index 00000000..8e49ef22 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_reference_double_free", + "sources": [ + "test_reference_double_free.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/test.js b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test.js new file mode 100644 index 00000000..f9a465c5 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test.js @@ -0,0 +1,11 @@ +'use strict'; + +// This test makes no assertions. It tests a fix without which it will crash +// with a double free. + +const { buildType } = require('../../common'); + +const addon = require(`./build/${buildType}/test_reference_double_free`); + +{ new addon.MyObject(true); } +{ new addon.MyObject(false); } diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_reference_double_free.c b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_reference_double_free.c new file mode 100644 index 00000000..e99667a7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_reference_double_free.c @@ -0,0 +1,90 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static size_t g_call_count = 0; + +static void Destructor(napi_env env, void* data, void* nothing) { + napi_ref* ref = data; + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, *ref)); + free(ref); +} + +static void NoDeleteDestructor(napi_env env, void* data, void* hint) { + napi_ref* ref = data; + size_t* call_count = hint; + + // This destructor must be called exactly once. + if ((*call_count) > 0) abort(); + *call_count = ((*call_count) + 1); + free(ref); +} + +static napi_value New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value js_this, js_delete; + bool delete; + napi_ref* ref = malloc(sizeof(*ref)); + + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, &js_delete, &js_this, NULL)); + NODE_API_CALL(env, napi_get_value_bool(env, js_delete, &delete)); + + if (delete) { + NODE_API_CALL(env, + napi_wrap(env, js_this, ref, Destructor, NULL, ref)); + } else { + NODE_API_CALL(env, + napi_wrap(env, js_this, ref, NoDeleteDestructor, &g_call_count, ref)); + } + NODE_API_CALL(env, napi_reference_ref(env, *ref, NULL)); + + return js_this; +} + +static void NoopDeleter(napi_env env, void* data, void* hint) {} + +// Tests that calling napi_remove_wrap and napi_delete_reference consecutively +// doesn't crash the process. +// This is analogous to the test https://github.com/nodejs/node-addon-api/blob/main/test/objectwrap_constructor_exception.cc. +// In which the Napi::ObjectWrap<> is being destructed immediately after napi_wrap. +// As Napi::ObjectWrap<> is a subclass of Napi::Reference<>, napi_remove_wrap +// in the destructor of Napi::ObjectWrap<> is called before napi_delete_reference +// in the destructor of Napi::Reference<>. +static napi_value DeleteImmediately(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value js_obj; + napi_ref ref; + napi_valuetype type; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &js_obj, NULL, NULL)); + + NODE_API_CALL(env, napi_typeof(env, js_obj, &type)); + NODE_API_ASSERT(env, type == napi_object, "Expected object parameter"); + + NODE_API_CALL(env, napi_wrap(env, js_obj, NULL, NoopDeleter, NULL, &ref)); + NODE_API_CALL(env, napi_remove_wrap(env, js_obj, NULL)); + NODE_API_CALL(env, napi_delete_reference(env, ref)); + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_value myobj_ctor; + NODE_API_CALL(env, + napi_define_class( + env, "MyObject", NAPI_AUTO_LENGTH, New, NULL, 0, NULL, &myobj_ctor)); + NODE_API_CALL(env, + napi_set_named_property(env, exports, "MyObject", myobj_ctor)); + + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("deleteImmediately", DeleteImmediately), + }; + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_wrap.js b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_wrap.js new file mode 100644 index 00000000..f7f75094 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_reference_double_free/test_wrap.js @@ -0,0 +1,10 @@ +'use strict'; + +// This test makes no assertions. It tests that calling napi_remove_wrap and +// napi_delete_reference consecutively doesn't crash the process. + +const { buildType } = require('../../common'); + +const addon = require(`./build/${buildType}/test_reference_double_free`); + +addon.deleteImmediately({}); diff --git a/Tests/NodeApi/test/js-native-api/test_string/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_string/CMakeLists.txt new file mode 100644 index 00000000..5244fdba --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/CMakeLists.txt @@ -0,0 +1,7 @@ +add_node_api_module(test_string + SOURCES + test_string.c + test_null.c + DEFINES + "NAPI_VERSION=10" +) diff --git a/Tests/NodeApi/test/js-native-api/test_string/binding.gyp b/Tests/NodeApi/test/js-native-api/test_string/binding.gyp new file mode 100644 index 00000000..550e33b4 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/binding.gyp @@ -0,0 +1,14 @@ +{ + "targets": [ + { + "target_name": "test_string", + "sources": [ + "test_string.c", + "test_null.c", + ], + "defines": [ + "NAPI_VERSION=10", + ], + }, + ], +} diff --git a/Tests/NodeApi/test/js-native-api/test_string/test.js b/Tests/NodeApi/test/js-native-api/test_string/test.js new file mode 100644 index 00000000..5d03ba9d --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test.js @@ -0,0 +1,91 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for string +const test_string = require(`./build/${common.buildType}/test_string`); +// The insufficient buffer test case allocates a buffer of size 4, including +// the null terminator. +const kInsufficientIdx = 3; + +const asciiCases = [ + '', + 'hello world', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + '?!@#$%^&*()_+-=[]{}/.,<>\'"\\', +]; + +const latin1Cases = [ + { + str: '¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿', + utf8Length: 62, + utf8InsufficientIdx: 1, + }, + { + str: 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ', + utf8Length: 126, + utf8InsufficientIdx: 1, + }, +]; + +const unicodeCases = [ + { + str: '\u{2003}\u{2101}\u{2001}\u{202}\u{2011}', + utf8Length: 14, + utf8InsufficientIdx: 1, + }, +]; + +function testLatin1Cases(str) { + assert.strictEqual(test_string.TestLatin1(str), str); + assert.strictEqual(test_string.TestLatin1AutoLength(str), str); + assert.strictEqual(test_string.TestLatin1External(str), str); + assert.strictEqual(test_string.TestLatin1ExternalAutoLength(str), str); + assert.strictEqual(test_string.TestPropertyKeyLatin1(str), str); + assert.strictEqual(test_string.TestPropertyKeyLatin1AutoLength(str), str); + assert.strictEqual(test_string.Latin1Length(str), str.length); + + if (str !== '') { + assert.strictEqual(test_string.TestLatin1Insufficient(str), str.slice(0, kInsufficientIdx)); + } +} + +function testUnicodeCases(str, utf8Length, utf8InsufficientIdx) { + assert.strictEqual(test_string.TestUtf8(str), str); + assert.strictEqual(test_string.TestUtf16(str), str); + assert.strictEqual(test_string.TestUtf8AutoLength(str), str); + assert.strictEqual(test_string.TestUtf16AutoLength(str), str); + assert.strictEqual(test_string.TestUtf16External(str), str); + assert.strictEqual(test_string.TestUtf16ExternalAutoLength(str), str); + assert.strictEqual(test_string.TestPropertyKeyUtf8(str), str); + assert.strictEqual(test_string.TestPropertyKeyUtf8AutoLength(str), str); + assert.strictEqual(test_string.TestPropertyKeyUtf16(str), str); + assert.strictEqual(test_string.TestPropertyKeyUtf16AutoLength(str), str); + assert.strictEqual(test_string.Utf8Length(str), utf8Length); + assert.strictEqual(test_string.Utf16Length(str), str.length); + + if (str !== '') { + assert.strictEqual(test_string.TestUtf8Insufficient(str), str.slice(0, utf8InsufficientIdx)); + assert.strictEqual(test_string.TestUtf16Insufficient(str), str.slice(0, kInsufficientIdx)); + } +} + +asciiCases.forEach(testLatin1Cases); +asciiCases.forEach((str) => testUnicodeCases(str, str.length, kInsufficientIdx)); +latin1Cases.forEach((it) => testLatin1Cases(it.str)); +latin1Cases.forEach((it) => testUnicodeCases(it.str, it.utf8Length, it.utf8InsufficientIdx)); +unicodeCases.forEach((it) => testUnicodeCases(it.str, it.utf8Length, it.utf8InsufficientIdx)); + +assert.throws(() => { + test_string.TestLargeUtf8(); +}, /^Error: Invalid argument$/); + +assert.throws(() => { + test_string.TestLargeLatin1(); +}, /^Error: Invalid argument$/); + +assert.throws(() => { + test_string.TestLargeUtf16(); +}, /^Error: Invalid argument$/); + +test_string.TestMemoryCorruption(' '.repeat(64 * 1024)); diff --git a/Tests/NodeApi/test/js-native-api/test_string/test_null.c b/Tests/NodeApi/test/js-native-api/test_string/test_null.c new file mode 100644 index 00000000..84c1fc40 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test_null.c @@ -0,0 +1,71 @@ +#include + +#include "../common.h" +#include "test_null.h" + +#define DECLARE_TEST(charset, str_arg) \ + static napi_value \ + test_create_##charset(napi_env env, napi_callback_info info) { \ + napi_value return_value, result; \ + NODE_API_CALL(env, napi_create_object(env, &return_value)); \ + \ + add_returned_status(env, \ + "envIsNull", \ + return_value, \ + "Invalid argument", \ + napi_invalid_arg, \ + napi_create_string_##charset(NULL, \ + (str_arg), \ + NAPI_AUTO_LENGTH, \ + &result)); \ + \ + napi_create_string_##charset(env, NULL, NAPI_AUTO_LENGTH, &result); \ + add_last_status(env, "stringIsNullNonZeroLength", return_value); \ + \ + napi_create_string_##charset(env, NULL, 0, &result); \ + add_last_status(env, "stringIsNullZeroLength", return_value); \ + \ + napi_create_string_##charset(env, (str_arg), NAPI_AUTO_LENGTH, NULL); \ + add_last_status(env, "resultIsNull", return_value); \ + \ + return return_value; \ + } + +static const char16_t something[] = { + (char16_t)'s', + (char16_t)'o', + (char16_t)'m', + (char16_t)'e', + (char16_t)'t', + (char16_t)'h', + (char16_t)'i', + (char16_t)'n', + (char16_t)'g', + (char16_t)'\0' +}; + +DECLARE_TEST(utf8, "something") +DECLARE_TEST(latin1, "something") +DECLARE_TEST(utf16, something) + +void init_test_null(napi_env env, napi_value exports) { + napi_value test_null; + + const napi_property_descriptor test_null_props[] = { + DECLARE_NODE_API_PROPERTY("test_create_utf8", test_create_utf8), + DECLARE_NODE_API_PROPERTY("test_create_latin1", test_create_latin1), + DECLARE_NODE_API_PROPERTY("test_create_utf16", test_create_utf16), + }; + + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &test_null)); + NODE_API_CALL_RETURN_VOID(env, napi_define_properties( + env, test_null, sizeof(test_null_props) / sizeof(*test_null_props), + test_null_props)); + + const napi_property_descriptor test_null_set = { + "testNull", NULL, NULL, NULL, NULL, test_null, napi_enumerable, NULL + }; + + NODE_API_CALL_RETURN_VOID(env, + napi_define_properties(env, exports, 1, &test_null_set)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_string/test_null.h b/Tests/NodeApi/test/js-native-api/test_string/test_null.h new file mode 100644 index 00000000..95be6359 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test_null.h @@ -0,0 +1,8 @@ +#ifndef TEST_JS_NATIVE_API_TEST_STRING_TEST_NULL_H_ +#define TEST_JS_NATIVE_API_TEST_STRING_TEST_NULL_H_ + +#include + +void init_test_null(napi_env env, napi_value exports); + +#endif // TEST_JS_NATIVE_API_TEST_STRING_TEST_NULL_H_ diff --git a/Tests/NodeApi/test/js-native-api/test_string/test_null.js b/Tests/NodeApi/test/js-native-api/test_string/test_null.js new file mode 100644 index 00000000..71963009 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test_null.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Test passing NULL to object-related N-APIs. +const { testNull } = require(`./build/${common.buildType}/test_string`); + +const expectedResult = { + envIsNull: 'Invalid argument', + stringIsNullNonZeroLength: 'Invalid argument', + stringIsNullZeroLength: 'napi_ok', + resultIsNull: 'Invalid argument', +}; + +assert.deepStrictEqual(expectedResult, testNull.test_create_latin1()); +assert.deepStrictEqual(expectedResult, testNull.test_create_utf8()); +assert.deepStrictEqual(expectedResult, testNull.test_create_utf16()); diff --git a/Tests/NodeApi/test/js-native-api/test_string/test_string.c b/Tests/NodeApi/test/js-native-api/test_string/test_string.c new file mode 100644 index 00000000..c6874dc7 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_string/test_string.c @@ -0,0 +1,498 @@ +#include +#include // INT_MAX +#include +#include +#include "../common.h" +#include "../entry_point.h" +#include "test_null.h" + +enum length_type { actual_length, auto_length }; + +static napi_status validate_and_retrieve_single_string_arg( + napi_env env, napi_callback_info info, napi_value* arg) { + size_t argc = 1; + NODE_API_CHECK_STATUS(napi_get_cb_info(env, info, &argc, arg, NULL, NULL)); + + NODE_API_ASSERT_STATUS(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype; + NODE_API_CHECK_STATUS(napi_typeof(env, *arg, &valuetype)); + + NODE_API_ASSERT_STATUS(env, + valuetype == napi_string, + "Wrong type of argment. Expects a string."); + + return napi_ok; +} + +// These help us factor out code that is common between the bindings. +typedef napi_status (*OneByteCreateAPI)(napi_env, + const char*, + size_t, + napi_value*); +typedef napi_status (*OneByteGetAPI)( + napi_env, napi_value, char*, size_t, size_t*); +typedef napi_status (*TwoByteCreateAPI)(napi_env, + const char16_t*, + size_t, + napi_value*); +typedef napi_status (*TwoByteGetAPI)( + napi_env, napi_value, char16_t*, size_t, size_t*); + +// Test passing back the one-byte string we got from JS. +static napi_value TestOneByteImpl(napi_env env, + napi_callback_info info, + OneByteGetAPI get_api, + OneByteCreateAPI create_api, + enum length_type length_mode) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char buffer[128]; + size_t buffer_size = 128; + size_t copied; + + NODE_API_CALL(env, get_api(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + if (length_mode == auto_length) { + copied = NAPI_AUTO_LENGTH; + } + NODE_API_CALL(env, create_api(env, buffer, copied, &output)); + + return output; +} + +// Test passing back the two-byte string we got from JS. +static napi_value TestTwoByteImpl(napi_env env, + napi_callback_info info, + TwoByteGetAPI get_api, + TwoByteCreateAPI create_api, + enum length_type length_mode) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char16_t buffer[128]; + size_t buffer_size = 128; + size_t copied; + + NODE_API_CALL(env, get_api(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + if (length_mode == auto_length) { + copied = NAPI_AUTO_LENGTH; + } + NODE_API_CALL(env, create_api(env, buffer, copied, &output)); + + return output; +} + +static void free_string(node_api_basic_env env, void* data, void* hint) { + free(data); +} + +static napi_status create_external_latin1(napi_env env, + const char* string, + size_t length, + napi_value* result) { + napi_status status; + // Initialize to true, because that is the value we don't want. + bool copied = true; + char* string_copy; + const size_t actual_length = + (length == NAPI_AUTO_LENGTH ? strlen(string) : length); + const size_t length_bytes = (actual_length + 1) * sizeof(*string_copy); + string_copy = malloc(length_bytes); + memcpy(string_copy, string, length_bytes); + string_copy[actual_length] = 0; + + status = node_api_create_external_string_latin1( + env, string_copy, length, free_string, NULL, result, &copied); + // We do not want the string to be copied. + if (copied) { + return napi_generic_failure; + } + if (status != napi_ok) { + free(string_copy); + return status; + } + return napi_ok; +} + +// strlen for char16_t. Needed in case we're copying a string of length +// NAPI_AUTO_LENGTH. +static size_t strlen16(const char16_t* string) { + for (const char16_t* iter = string;; iter++) { + if (*iter == 0) { + return iter - string; + } + } + // We should never get here. + abort(); +} + +static napi_status create_external_utf16(napi_env env, + const char16_t* string, + size_t length, + napi_value* result) { + napi_status status; + // Initialize to true, because that is the value we don't want. + bool copied = true; + char16_t* string_copy; + const size_t actual_length = + (length == NAPI_AUTO_LENGTH ? strlen16(string) : length); + const size_t length_bytes = (actual_length + 1) * sizeof(*string_copy); + string_copy = malloc(length_bytes); + memcpy(string_copy, string, length_bytes); + string_copy[actual_length] = 0; + + status = node_api_create_external_string_utf16( + env, string_copy, length, free_string, NULL, result, &copied); + if (status != napi_ok) { + free(string_copy); + return status; + } + + return napi_ok; +} + +static napi_value TestLatin1(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + napi_create_string_latin1, + actual_length); +} + +static napi_value TestUtf8(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_utf8, + napi_create_string_utf8, + actual_length); +} + +static napi_value TestUtf16(napi_env env, napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + napi_create_string_utf16, + actual_length); +} + +static napi_value TestLatin1AutoLength(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + napi_create_string_latin1, + auto_length); +} + +static napi_value TestUtf8AutoLength(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_utf8, + napi_create_string_utf8, + auto_length); +} + +static napi_value TestUtf16AutoLength(napi_env env, napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + napi_create_string_utf16, + auto_length); +} + +static napi_value TestLatin1External(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + create_external_latin1, + actual_length); +} + +static napi_value TestUtf16External(napi_env env, napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + create_external_utf16, + actual_length); +} + +static napi_value TestLatin1ExternalAutoLength(napi_env env, + napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + create_external_latin1, + auto_length); +} + +static napi_value TestUtf16ExternalAutoLength(napi_env env, + napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + create_external_utf16, + auto_length); +} + +static napi_value TestLatin1Insufficient(napi_env env, + napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char buffer[4]; + size_t buffer_size = 4; + size_t copied; + + NODE_API_CALL( + env, + napi_get_value_string_latin1(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + NODE_API_CALL(env, napi_create_string_latin1(env, buffer, copied, &output)); + + return output; +} + +static napi_value TestUtf8Insufficient(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char buffer[4]; + size_t buffer_size = 4; + size_t copied; + + NODE_API_CALL( + env, + napi_get_value_string_utf8(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + NODE_API_CALL(env, napi_create_string_utf8(env, buffer, copied, &output)); + + return output; +} + +static napi_value TestUtf16Insufficient(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + char16_t buffer[4]; + size_t buffer_size = 4; + size_t copied; + + NODE_API_CALL( + env, + napi_get_value_string_utf16(env, args[0], buffer, buffer_size, &copied)); + + napi_value output; + NODE_API_CALL(env, napi_create_string_utf16(env, buffer, copied, &output)); + + return output; +} + +static napi_value TestPropertyKeyLatin1(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + node_api_create_property_key_latin1, + actual_length); +} + +static napi_value TestPropertyKeyLatin1AutoLength(napi_env env, + napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_latin1, + node_api_create_property_key_latin1, + auto_length); +} + +static napi_value TestPropertyKeyUtf8(napi_env env, napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_utf8, + node_api_create_property_key_utf8, + actual_length); +} + +static napi_value TestPropertyKeyUtf8AutoLength(napi_env env, + napi_callback_info info) { + return TestOneByteImpl(env, + info, + napi_get_value_string_utf8, + node_api_create_property_key_utf8, + auto_length); +} + +static napi_value TestPropertyKeyUtf16(napi_env env, napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + node_api_create_property_key_utf16, + actual_length); +} + +static napi_value TestPropertyKeyUtf16AutoLength(napi_env env, + napi_callback_info info) { + return TestTwoByteImpl(env, + info, + napi_get_value_string_utf16, + node_api_create_property_key_utf16, + auto_length); +} + +static napi_value Latin1Length(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + size_t length; + NODE_API_CALL(env, + napi_get_value_string_latin1(env, args[0], NULL, 0, &length)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, (uint32_t)length, &output)); + + return output; +} + +static napi_value Utf16Length(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + size_t length; + NODE_API_CALL(env, + napi_get_value_string_utf16(env, args[0], NULL, 0, &length)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, (uint32_t)length, &output)); + + return output; +} + +static napi_value Utf8Length(napi_env env, napi_callback_info info) { + napi_value args[1]; + NODE_API_CALL(env, validate_and_retrieve_single_string_arg(env, info, args)); + + size_t length; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, args[0], NULL, 0, &length)); + + napi_value output; + NODE_API_CALL(env, napi_create_uint32(env, (uint32_t)length, &output)); + + return output; +} + +static napi_value TestLargeUtf8(napi_env env, napi_callback_info info) { + napi_value output; + if (SIZE_MAX > INT_MAX) { + NODE_API_CALL( + env, napi_create_string_utf8(env, "", ((size_t)INT_MAX) + 1, &output)); + } else { + // just throw the expected error as there is nothing to test + // in this case since we can't overflow + NODE_API_CALL(env, napi_throw_error(env, NULL, "Invalid argument")); + } + + return output; +} + +static napi_value TestLargeLatin1(napi_env env, napi_callback_info info) { + napi_value output; + if (SIZE_MAX > INT_MAX) { + NODE_API_CALL( + env, + napi_create_string_latin1(env, "", ((size_t)INT_MAX) + 1, &output)); + } else { + // just throw the expected error as there is nothing to test + // in this case since we can't overflow + NODE_API_CALL(env, napi_throw_error(env, NULL, "Invalid argument")); + } + + return output; +} + +static napi_value TestLargeUtf16(napi_env env, napi_callback_info info) { + napi_value output; + if (SIZE_MAX > INT_MAX) { + NODE_API_CALL( + env, + napi_create_string_utf16( + env, ((const char16_t*)""), ((size_t)INT_MAX) + 1, &output)); + } else { + // just throw the expected error as there is nothing to test + // in this case since we can't overflow + NODE_API_CALL(env, napi_throw_error(env, NULL, "Invalid argument")); + } + + return output; +} + +static napi_value TestMemoryCorruption(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + + char buf[10] = {0}; + NODE_API_CALL(env, napi_get_value_string_utf8(env, args[0], buf, 0, NULL)); + + char zero[10] = {0}; + if (memcmp(buf, zero, sizeof(buf)) != 0) { + NODE_API_CALL(env, napi_throw_error(env, NULL, "Buffer overwritten")); + } + + return NULL; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("TestLatin1", TestLatin1), + DECLARE_NODE_API_PROPERTY("TestLatin1AutoLength", TestLatin1AutoLength), + DECLARE_NODE_API_PROPERTY("TestLatin1External", TestLatin1External), + DECLARE_NODE_API_PROPERTY("TestLatin1ExternalAutoLength", + TestLatin1ExternalAutoLength), + DECLARE_NODE_API_PROPERTY("TestLatin1Insufficient", + TestLatin1Insufficient), + DECLARE_NODE_API_PROPERTY("TestUtf8", TestUtf8), + DECLARE_NODE_API_PROPERTY("TestUtf8AutoLength", TestUtf8AutoLength), + DECLARE_NODE_API_PROPERTY("TestUtf8Insufficient", TestUtf8Insufficient), + DECLARE_NODE_API_PROPERTY("TestUtf16", TestUtf16), + DECLARE_NODE_API_PROPERTY("TestUtf16AutoLength", TestUtf16AutoLength), + DECLARE_NODE_API_PROPERTY("TestUtf16External", TestUtf16External), + DECLARE_NODE_API_PROPERTY("TestUtf16ExternalAutoLength", + TestUtf16ExternalAutoLength), + DECLARE_NODE_API_PROPERTY("TestUtf16Insufficient", TestUtf16Insufficient), + DECLARE_NODE_API_PROPERTY("Latin1Length", Latin1Length), + DECLARE_NODE_API_PROPERTY("Utf16Length", Utf16Length), + DECLARE_NODE_API_PROPERTY("Utf8Length", Utf8Length), + DECLARE_NODE_API_PROPERTY("TestLargeUtf8", TestLargeUtf8), + DECLARE_NODE_API_PROPERTY("TestLargeLatin1", TestLargeLatin1), + DECLARE_NODE_API_PROPERTY("TestLargeUtf16", TestLargeUtf16), + DECLARE_NODE_API_PROPERTY("TestMemoryCorruption", TestMemoryCorruption), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyLatin1", TestPropertyKeyLatin1), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyLatin1AutoLength", + TestPropertyKeyLatin1AutoLength), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyUtf8", TestPropertyKeyUtf8), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyUtf8AutoLength", + TestPropertyKeyUtf8AutoLength), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyUtf16", TestPropertyKeyUtf16), + DECLARE_NODE_API_PROPERTY("TestPropertyKeyUtf16AutoLength", + TestPropertyKeyUtf16AutoLength), + }; + + init_test_null(env, exports); + + NODE_API_CALL( + env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_symbol/CMakeLists.txt new file mode 100644 index 00000000..7e937424 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_symbol + SOURCES + test_symbol.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/binding.gyp b/Tests/NodeApi/test/js-native-api/test_symbol/binding.gyp new file mode 100644 index 00000000..6a5a7cad --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_symbol", + "sources": [ + "test_symbol.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/test1.js b/Tests/NodeApi/test/js-native-api/test_symbol/test1.js new file mode 100644 index 00000000..3a28437a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/test1.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for symbol +const test_symbol = require(`./build/${common.buildType}/test_symbol`); + +const sym = test_symbol.New('test'); +assert.strictEqual(sym.toString(), 'Symbol(test)'); + +const myObj = {}; +const fooSym = test_symbol.New('foo'); +const otherSym = test_symbol.New('bar'); +myObj.foo = 'bar'; +myObj[fooSym] = 'baz'; +myObj[otherSym] = 'bing'; +assert.strictEqual(myObj.foo, 'bar'); +assert.strictEqual(myObj[fooSym], 'baz'); +assert.strictEqual(myObj[otherSym], 'bing'); diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/test2.js b/Tests/NodeApi/test/js-native-api/test_symbol/test2.js new file mode 100644 index 00000000..026f2c68 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/test2.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for symbol +const test_symbol = require(`./build/${common.buildType}/test_symbol`); + +const fooSym = test_symbol.New('foo'); +assert.strictEqual(fooSym.toString(), 'Symbol(foo)'); + +const myObj = {}; +myObj.foo = 'bar'; +myObj[fooSym] = 'baz'; + +assert.deepStrictEqual(Object.keys(myObj), ['foo']); +assert.deepStrictEqual(Object.getOwnPropertyNames(myObj), ['foo']); +assert.deepStrictEqual(Object.getOwnPropertySymbols(myObj), [fooSym]); diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/test3.js b/Tests/NodeApi/test/js-native-api/test_symbol/test3.js new file mode 100644 index 00000000..186c561e --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/test3.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for symbol +const test_symbol = require(`./build/${common.buildType}/test_symbol`); + +assert.notStrictEqual(test_symbol.New(), test_symbol.New()); +assert.notStrictEqual(test_symbol.New('foo'), test_symbol.New('foo')); +assert.notStrictEqual(test_symbol.New('foo'), test_symbol.New('bar')); + +const foo1 = test_symbol.New('foo'); +const foo2 = test_symbol.New('foo'); +const object = { + [foo1]: 1, + [foo2]: 2, +}; +assert.strictEqual(object[foo1], 1); +assert.strictEqual(object[foo2], 2); diff --git a/Tests/NodeApi/test/js-native-api/test_symbol/test_symbol.c b/Tests/NodeApi/test/js-native-api/test_symbol/test_symbol.c new file mode 100644 index 00000000..58fcb85a --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_symbol/test_symbol.c @@ -0,0 +1,38 @@ +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value New(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + napi_value description = NULL; + if (argc >= 1) { + napi_valuetype valuetype; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype)); + + NODE_API_ASSERT(env, valuetype == napi_string, + "Wrong type of arguments. Expects a string."); + + description = args[0]; + } + + napi_value symbol; + NODE_API_CALL(env, napi_create_symbol(env, description, &symbol)); + + return symbol; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("New", New), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/js-native-api/test_typedarray/CMakeLists.txt b/Tests/NodeApi/test/js-native-api/test_typedarray/CMakeLists.txt new file mode 100644 index 00000000..093e92d0 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_typedarray/CMakeLists.txt @@ -0,0 +1,4 @@ +add_node_api_module(test_typedarray + SOURCES + test_typedarray.c +) diff --git a/Tests/NodeApi/test/js-native-api/test_typedarray/binding.gyp b/Tests/NodeApi/test/js-native-api/test_typedarray/binding.gyp new file mode 100644 index 00000000..a5ae5741 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_typedarray/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_typedarray", + "sources": [ + "test_typedarray.c" + ] + } + ] +} diff --git a/Tests/NodeApi/test/js-native-api/test_typedarray/test.js b/Tests/NodeApi/test/js-native-api/test_typedarray/test.js new file mode 100644 index 00000000..673bb5ce --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_typedarray/test.js @@ -0,0 +1,109 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for arrays +const test_typedarray = require(`./build/${common.buildType}/test_typedarray`); + +const byteArray = new Uint8Array(3); +byteArray[0] = 0; +byteArray[1] = 1; +byteArray[2] = 2; +assert.strictEqual(byteArray.length, 3); + +const doubleArray = new Float64Array(3); +doubleArray[0] = 0.0; +doubleArray[1] = 1.1; +doubleArray[2] = 2.2; +assert.strictEqual(doubleArray.length, 3); + +const byteResult = test_typedarray.Multiply(byteArray, 3); +assert.ok(byteResult instanceof Uint8Array); +assert.strictEqual(byteResult.length, 3); +assert.strictEqual(byteResult[0], 0); +assert.strictEqual(byteResult[1], 3); +assert.strictEqual(byteResult[2], 6); + +const doubleResult = test_typedarray.Multiply(doubleArray, -3); +assert.ok(doubleResult instanceof Float64Array); +assert.strictEqual(doubleResult.length, 3); +assert.strictEqual(doubleResult[0], -0); +assert.strictEqual(Math.round(10 * doubleResult[1]) / 10, -3.3); +assert.strictEqual(Math.round(10 * doubleResult[2]) / 10, -6.6); + +const externalResult = test_typedarray.External(); +assert.ok(externalResult instanceof Int8Array); +assert.strictEqual(externalResult.length, 3); +assert.strictEqual(externalResult[0], 0); +assert.strictEqual(externalResult[1], 1); +assert.strictEqual(externalResult[2], 2); + +// Validate creation of all kinds of TypedArrays +const buffer = new ArrayBuffer(128); +const arrayTypes = [ Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, + Uint16Array, Int32Array, Uint32Array, Float32Array, + Float64Array, BigInt64Array, BigUint64Array ]; + +arrayTypes.forEach((currentType) => { + const template = Reflect.construct(currentType, buffer); + const theArray = test_typedarray.CreateTypedArray(template, buffer); + + assert.ok(theArray instanceof currentType, + 'Type of new array should match that of the template. ' + + `Expected type: ${currentType.name}, ` + + `actual type: ${template.constructor.name}`); + assert.notStrictEqual(theArray, template); + assert.strictEqual(theArray.buffer, buffer); +}); + +arrayTypes.forEach((currentType) => { + const template = Reflect.construct(currentType, buffer); + assert.throws(() => { + test_typedarray.CreateTypedArray(template, buffer, 0, 136); + }, RangeError); +}); + +const nonByteArrayTypes = [ Int16Array, Uint16Array, Int32Array, Uint32Array, + Float32Array, Float64Array, + BigInt64Array, BigUint64Array ]; +nonByteArrayTypes.forEach((currentType) => { + const template = Reflect.construct(currentType, buffer); + assert.throws(() => { + test_typedarray.CreateTypedArray(template, buffer, + currentType.BYTES_PER_ELEMENT + 1, 1); + console.log(`start of offset ${currentType}`); + }, RangeError); +}); + +// Test detaching +arrayTypes.forEach((currentType) => { + const buffer = Reflect.construct(currentType, [8]); + assert.strictEqual(buffer.length, 8); + assert.ok(!test_typedarray.IsDetached(buffer.buffer)); + test_typedarray.Detach(buffer); + assert.ok(test_typedarray.IsDetached(buffer.buffer)); + assert.strictEqual(buffer.length, 0); +}); +{ + const buffer = test_typedarray.External(); + assert.ok(externalResult instanceof Int8Array); + assert.strictEqual(externalResult.length, 3); + assert.strictEqual(externalResult.byteLength, 3); + assert.ok(!test_typedarray.IsDetached(buffer.buffer)); + test_typedarray.Detach(buffer); + assert.ok(test_typedarray.IsDetached(buffer.buffer)); + assert.ok(externalResult instanceof Int8Array); + assert.strictEqual(buffer.length, 0); + assert.strictEqual(buffer.byteLength, 0); +} + +{ + const buffer = new ArrayBuffer(128); + assert.ok(!test_typedarray.IsDetached(buffer)); +} + +{ + const buffer = test_typedarray.NullArrayBuffer(); + assert.ok(buffer instanceof ArrayBuffer); + assert.ok(test_typedarray.IsDetached(buffer)); +} diff --git a/Tests/NodeApi/test/js-native-api/test_typedarray/test_typedarray.c b/Tests/NodeApi/test/js-native-api/test_typedarray/test_typedarray.c new file mode 100644 index 00000000..8aac9b52 --- /dev/null +++ b/Tests/NodeApi/test/js-native-api/test_typedarray/test_typedarray.c @@ -0,0 +1,249 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value Multiply(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects a typed array as first argument."); + + napi_value input_array = args[0]; + bool is_typedarray; + NODE_API_CALL(env, napi_is_typedarray(env, input_array, &is_typedarray)); + + NODE_API_ASSERT(env, is_typedarray, + "Wrong type of arguments. Expects a typed array as first argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_number, + "Wrong type of arguments. Expects a number as second argument."); + + double multiplier; + NODE_API_CALL(env, napi_get_value_double(env, args[1], &multiplier)); + + napi_typedarray_type type; + napi_value input_buffer; + size_t byte_offset; + size_t i, length; + NODE_API_CALL(env, napi_get_typedarray_info( + env, input_array, &type, &length, NULL, &input_buffer, &byte_offset)); + + void* data; + size_t byte_length; + NODE_API_CALL(env, napi_get_arraybuffer_info( + env, input_buffer, &data, &byte_length)); + + napi_value output_buffer; + void* output_ptr = NULL; + NODE_API_CALL(env, napi_create_arraybuffer( + env, byte_length, &output_ptr, &output_buffer)); + + napi_value output_array; + NODE_API_CALL(env, napi_create_typedarray( + env, type, length, output_buffer, byte_offset, &output_array)); + + if (type == napi_uint8_array) { + uint8_t* input_bytes = (uint8_t*)(data) + byte_offset; + uint8_t* output_bytes = (uint8_t*)(output_ptr); + for (i = 0; i < length; i++) { + output_bytes[i] = (uint8_t)(input_bytes[i] * multiplier); + } + } else if (type == napi_float64_array) { + double* input_doubles = (double*)((uint8_t*)(data) + byte_offset); + double* output_doubles = (double*)(output_ptr); + for (i = 0; i < length; i++) { + output_doubles[i] = input_doubles[i] * multiplier; + } + } else { + napi_throw_error(env, NULL, + "Typed array was of a type not expected by test."); + return NULL; + } + + return output_array; +} + +static void FinalizeCallback(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) +{ + free(finalize_data); +} + +static napi_value External(napi_env env, napi_callback_info info) { + const uint8_t nElem = 3; + int8_t* externalData = malloc(nElem*sizeof(int8_t)); + externalData[0] = 0; + externalData[1] = 1; + externalData[2] = 2; + + napi_value output_buffer; + NODE_API_CALL(env, napi_create_external_arraybuffer( + env, + externalData, + nElem*sizeof(int8_t), + FinalizeCallback, + NULL, // finalize_hint + &output_buffer)); + + napi_value output_array; + NODE_API_CALL(env, napi_create_typedarray(env, + napi_int8_array, + nElem, + output_buffer, + 0, + &output_array)); + + return output_array; +} + + +static napi_value NullArrayBuffer(napi_env env, napi_callback_info info) { + static void* data = NULL; + napi_value arraybuffer; + NODE_API_CALL(env, + napi_create_external_arraybuffer(env, data, 0, NULL, NULL, &arraybuffer)); + return arraybuffer; +} + +static napi_value CreateTypedArray(napi_env env, napi_callback_info info) { + size_t argc = 4; + napi_value args[4]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2 || argc == 4, "Wrong number of arguments"); + + napi_value input_array = args[0]; + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, input_array, &valuetype0)); + + NODE_API_ASSERT(env, valuetype0 == napi_object, + "Wrong type of arguments. Expects a typed array as first argument."); + + bool is_typedarray; + NODE_API_CALL(env, napi_is_typedarray(env, input_array, &is_typedarray)); + + NODE_API_ASSERT(env, is_typedarray, + "Wrong type of arguments. Expects a typed array as first argument."); + + napi_valuetype valuetype1; + napi_value input_buffer = args[1]; + NODE_API_CALL(env, napi_typeof(env, input_buffer, &valuetype1)); + + NODE_API_ASSERT(env, valuetype1 == napi_object, + "Wrong type of arguments. Expects an array buffer as second argument."); + + bool is_arraybuffer; + NODE_API_CALL(env, napi_is_arraybuffer(env, input_buffer, &is_arraybuffer)); + + NODE_API_ASSERT(env, is_arraybuffer, + "Wrong type of arguments. Expects an array buffer as second argument."); + + napi_typedarray_type type; + napi_value in_array_buffer; + size_t byte_offset; + size_t length; + NODE_API_CALL(env, napi_get_typedarray_info( + env, input_array, &type, &length, NULL, &in_array_buffer, &byte_offset)); + + if (argc == 4) { + napi_valuetype valuetype2; + NODE_API_CALL(env, napi_typeof(env, args[2], &valuetype2)); + + NODE_API_ASSERT(env, valuetype2 == napi_number, + "Wrong type of arguments. Expects a number as third argument."); + + uint32_t uint32_length; + NODE_API_CALL(env, napi_get_value_uint32(env, args[2], &uint32_length)); + length = uint32_length; + + napi_valuetype valuetype3; + NODE_API_CALL(env, napi_typeof(env, args[3], &valuetype3)); + + NODE_API_ASSERT(env, valuetype3 == napi_number, + "Wrong type of arguments. Expects a number as third argument."); + + uint32_t uint32_byte_offset; + NODE_API_CALL(env, napi_get_value_uint32(env, args[3], &uint32_byte_offset)); + byte_offset = uint32_byte_offset; + } + + napi_value output_array; + NODE_API_CALL(env, napi_create_typedarray( + env, type, length, input_buffer, byte_offset, &output_array)); + + return output_array; +} + +static napi_value Detach(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments."); + + bool is_typedarray; + NODE_API_CALL(env, napi_is_typedarray(env, args[0], &is_typedarray)); + NODE_API_ASSERT( + env, is_typedarray, + "Wrong type of arguments. Expects a typedarray as first argument."); + + napi_value arraybuffer; + NODE_API_CALL(env, + napi_get_typedarray_info( + env, args[0], NULL, NULL, NULL, &arraybuffer, NULL)); + NODE_API_CALL(env, napi_detach_arraybuffer(env, arraybuffer)); + + return NULL; +} + +static napi_value IsDetached(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments."); + + napi_value array_buffer = args[0]; + bool is_arraybuffer; + NODE_API_CALL(env, napi_is_arraybuffer(env, array_buffer, &is_arraybuffer)); + NODE_API_ASSERT(env, is_arraybuffer, + "Wrong type of arguments. Expects an array buffer as first argument."); + + bool is_detached; + NODE_API_CALL(env, + napi_is_detached_arraybuffer(env, array_buffer, &is_detached)); + + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, is_detached, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("Multiply", Multiply), + DECLARE_NODE_API_PROPERTY("External", External), + DECLARE_NODE_API_PROPERTY("NullArrayBuffer", NullArrayBuffer), + DECLARE_NODE_API_PROPERTY("CreateTypedArray", CreateTypedArray), + DECLARE_NODE_API_PROPERTY("Detach", Detach), + DECLARE_NODE_API_PROPERTY("IsDetached", IsDetached), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/Tests/NodeApi/test/package.json b/Tests/NodeApi/test/package.json new file mode 100644 index 00000000..21023796 --- /dev/null +++ b/Tests/NodeApi/test/package.json @@ -0,0 +1,14 @@ +{ + "name": "hermes-node-api-test-packages", + "private": true, + "scripts": { + "postinstall": "tar -c -f node_modules.tar node_modules & npx hasha -a sha1 node_modules.tar > node_modules.sha1" + }, + "devDependencies": { + "@babel/cli": "^7.28.0", + "@babel/core": "^7.28.0", + "@babel/runtime": "^7.28.2", + "@react-native/babel-preset": "^0.80.2", + "hasha-cli": "^6.0.0" + } +} diff --git a/Tests/NodeApi/test/yarn.lock b/Tests/NodeApi/test/yarn.lock new file mode 100644 index 00000000..b5fb0864 --- /dev/null +++ b/Tests/NodeApi/test/yarn.lock @@ -0,0 +1,1326 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/cli@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.28.0.tgz#26959456cbedff569a2c3ac909e8a268ca6cb7e2" + integrity sha512-CYrZG7FagtE8ReKDBfItxnrEBf2khq2eTMnPuqO8UVN0wzhp1eMX1wfda8b1a32l2aqYLwRRIOGNovm8FVzmMw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.28" + commander "^6.2.0" + convert-source-map "^2.0.0" + fs-readdir-recursive "^1.1.0" + glob "^7.2.0" + make-dir "^2.1.0" + slash "^2.0.0" + optionalDependencies: + "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3" + chokidar "^3.6.0" + +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790" + integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== + +"@babel/core@^7.25.2", "@babel/core@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.0.tgz#55dad808d5bf3445a108eefc88ea3fdf034749a4" + integrity sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.0" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.27.3" + "@babel/helpers" "^7.27.6" + "@babel/parser" "^7.28.0" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.0" + "@babel/types" "^7.28.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.0.tgz#9cc2f7bd6eb054d77dc66c2664148a0c5118acd2" + integrity sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg== + dependencies: + "@babel/parser" "^7.28.0" + "@babel/types" "^7.28.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz#5bee4262a6ea5ddc852d0806199eb17ca3de9281" + integrity sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.27.1" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" + integrity sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + regexpu-core "^6.2.0" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.5": + version "0.6.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz#742ccf1cb003c07b48859fc9fa2c1bbe40e5f753" + integrity sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg== + dependencies: + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + debug "^4.4.1" + lodash.debounce "^4.0.8" + resolve "^1.22.10" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-member-expression-to-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" + integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz#db0bbcfba5802f9ef7870705a7ef8788508ede02" + integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.3" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-replace-supers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helper-wrap-function@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz#b88285009c31427af318d4fe37651cd62a142409" + integrity sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ== + dependencies: + "@babel/template" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helpers@^7.27.6": + version "7.28.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.2.tgz#80f0918fecbfebea9af856c419763230040ee850" + integrity sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.2" + +"@babel/parser@^7.27.2", "@babel/parser@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.0.tgz#979829fbab51a29e13901e5a80713dbcb840825e" + integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g== + dependencies: + "@babel/types" "^7.28.0" + +"@babel/plugin-proposal-export-default-from@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz#59b050b0e5fdc366162ab01af4fcbac06ea40919" + integrity sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-default-from@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.27.1.tgz#8efed172e79ab657c7fa4d599224798212fb7e18" + integrity sha512-eBC/3KSekshx19+N40MzjWqJd7KTEdOoLesAfa4IDFI8eRz5a47i5Oszus6zG/cwIXN63YhgLOMSSNJx49sENg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-flow@^7.12.1", "@babel/plugin-syntax-flow@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz#6c83cf0d7d635b716827284b7ecd5aead9237662" + integrity sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" + integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-typescript@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" + integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-arrow-functions@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-async-generator-functions@^7.25.4": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz#1276e6c7285ab2cd1eccb0bc7356b7a69ff842c2" + integrity sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.28.0" + +"@babel/plugin-transform-async-to-generator@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" + integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + +"@babel/plugin-transform-block-scoping@^7.25.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz#e7c50cbacc18034f210b93defa89638666099451" + integrity sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-class-properties@^7.25.4": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-classes@^7.25.4": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz#12fa46cffc32a6e084011b650539e880add8a0f8" + integrity sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-globals" "^7.28.0" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/traverse" "^7.28.0" + +"@babel/plugin-transform-computed-properties@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" + integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/template" "^7.27.1" + +"@babel/plugin-transform-destructuring@^7.24.8", "@babel/plugin-transform-destructuring@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz#0f156588f69c596089b7d5b06f5af83d9aa7f97a" + integrity sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.0" + +"@babel/plugin-transform-flow-strip-types@^7.25.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz#5def3e1e7730f008d683144fb79b724f92c5cdf9" + integrity sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-flow" "^7.27.1" + +"@babel/plugin-transform-for-of@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-function-name@^7.25.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-transform-literals@^7.25.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-logical-assignment-operators@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz#890cb20e0270e0e5bebe3f025b434841c32d5baa" + integrity sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-commonjs@^7.24.8": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" + integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" + integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-numeric-separator@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" + integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-object-rest-spread@^7.24.7": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz#d23021857ffd7cd809f54d624299b8086402ed8d" + integrity sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA== + dependencies: + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.0" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/traverse" "^7.28.0" + +"@babel/plugin-transform-optional-catch-binding@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" + integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-optional-chaining@^7.24.8": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f" + integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-parameters@^7.24.7", "@babel/plugin-transform-parameters@^7.27.7": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" + integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-private-methods@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" + integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-private-property-in-object@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" + integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-display-name@^7.24.7": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz#6f20a7295fea7df42eb42fed8f896813f5b934de" + integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-self@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-source@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx@^7.25.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz#1023bc94b78b0a2d68c82b5e96aed573bcfb9db0" + integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/plugin-transform-regenerator@^7.24.7": + version "7.28.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz#bde80603442ff4bb4e910bc8b35485295d556ab1" + integrity sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-runtime@^7.24.7": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.0.tgz#462e79008cc7bdac03e4c5e1765b9de2bcd31c21" + integrity sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + babel-plugin-polyfill-corejs2 "^0.4.14" + babel-plugin-polyfill-corejs3 "^0.13.0" + babel-plugin-polyfill-regenerator "^0.6.5" + semver "^6.3.1" + +"@babel/plugin-transform-shorthand-properties@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-spread@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" + integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-sticky-regex@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-typescript@^7.25.2": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz#796cbd249ab56c18168b49e3e1d341b72af04a6b" + integrity sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.27.1" + +"@babel/plugin-transform-unicode-regex@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/runtime@^7.28.2": + version "7.28.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.2.tgz#2ae5a9d51cc583bd1f5673b3bb70d6d819682473" + integrity sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA== + +"@babel/template@^7.25.0", "@babel/template@^7.27.1", "@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3", "@babel/traverse@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.0.tgz#518aa113359b062042379e333db18380b537e34b" + integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.0" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.0" + debug "^4.3.1" + +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.0", "@babel/types@^7.28.2": + version "7.28.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" + integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.12" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz#2234ce26c62889f03db3d7fea43c1932ab3e927b" + integrity sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz#7358043433b2e5da569aa02cbc4c121da3af27d7" + integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.29" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz#a58d31eaadaf92c6695680b2e1d464a9b8fbf7fc" + integrity sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3": + version "2.1.8-no-fsevents.3" + resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" + integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ== + +"@react-native/babel-plugin-codegen@0.80.2": + version "0.80.2" + resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.80.2.tgz#14964a7c7058e25df60b41b4dd6b8a3714a33440" + integrity sha512-q0XzdrdDebPwt5tEi2MSo90kpEcs4e3ZZskrbxda081DEjHhgm3bbIxAiW3BxY6adOf/eXxgOhKEGWTfG2me6g== + dependencies: + "@babel/traverse" "^7.25.3" + "@react-native/codegen" "0.80.2" + +"@react-native/babel-preset@^0.80.2": + version "0.80.2" + resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.80.2.tgz#8c5ba82a37c044c22cf92e613eb8be01310782e5" + integrity sha512-vLtS8YJV0nAnOZ8kVJBaXzHlwvoMXpYB4/NBR1BuAesE+WTiAkXpDFnKSkXBHoS03d/5HYNVcW8VRaB2f0Jmtw== + dependencies: + "@babel/core" "^7.25.2" + "@babel/plugin-proposal-export-default-from" "^7.24.7" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-default-from" "^7.24.7" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-transform-arrow-functions" "^7.24.7" + "@babel/plugin-transform-async-generator-functions" "^7.25.4" + "@babel/plugin-transform-async-to-generator" "^7.24.7" + "@babel/plugin-transform-block-scoping" "^7.25.0" + "@babel/plugin-transform-class-properties" "^7.25.4" + "@babel/plugin-transform-classes" "^7.25.4" + "@babel/plugin-transform-computed-properties" "^7.24.7" + "@babel/plugin-transform-destructuring" "^7.24.8" + "@babel/plugin-transform-flow-strip-types" "^7.25.2" + "@babel/plugin-transform-for-of" "^7.24.7" + "@babel/plugin-transform-function-name" "^7.25.1" + "@babel/plugin-transform-literals" "^7.25.2" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" + "@babel/plugin-transform-numeric-separator" "^7.24.7" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-optional-catch-binding" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.8" + "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.24.7" + "@babel/plugin-transform-private-property-in-object" "^7.24.7" + "@babel/plugin-transform-react-display-name" "^7.24.7" + "@babel/plugin-transform-react-jsx" "^7.25.2" + "@babel/plugin-transform-react-jsx-self" "^7.24.7" + "@babel/plugin-transform-react-jsx-source" "^7.24.7" + "@babel/plugin-transform-regenerator" "^7.24.7" + "@babel/plugin-transform-runtime" "^7.24.7" + "@babel/plugin-transform-shorthand-properties" "^7.24.7" + "@babel/plugin-transform-spread" "^7.24.7" + "@babel/plugin-transform-sticky-regex" "^7.24.7" + "@babel/plugin-transform-typescript" "^7.25.2" + "@babel/plugin-transform-unicode-regex" "^7.24.7" + "@babel/template" "^7.25.0" + "@react-native/babel-plugin-codegen" "0.80.2" + babel-plugin-syntax-hermes-parser "0.28.1" + babel-plugin-transform-flow-enums "^0.0.2" + react-refresh "^0.14.0" + +"@react-native/codegen@0.80.2": + version "0.80.2" + resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.80.2.tgz#2e5dc975400d41b84c7393d73cfe32f47b12d82e" + integrity sha512-eYad9ex9/RS6oFbbpu6LxsczktbhfJbJlTvtRlcWLJjJbFTeNr5Q7CgBT2/m5VtpxnJ/0YdmZ9vdazsJ2yp9kw== + dependencies: + glob "^7.1.1" + hermes-parser "0.28.1" + invariant "^2.2.4" + nullthrows "^1.1.1" + yargs "^17.6.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +babel-plugin-polyfill-corejs2@^0.4.14: + version "0.4.14" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz#8101b82b769c568835611542488d463395c2ef8f" + integrity sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg== + dependencies: + "@babel/compat-data" "^7.27.7" + "@babel/helper-define-polyfill-provider" "^0.6.5" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz#bb7f6aeef7addff17f7602a08a6d19a128c30164" + integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.5" + core-js-compat "^3.43.0" + +babel-plugin-polyfill-regenerator@^0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz#32752e38ab6f6767b92650347bf26a31b16ae8c5" + integrity sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.5" + +babel-plugin-syntax-hermes-parser@0.28.1: + version "0.28.1" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.28.1.tgz#9e80a774ddb8038307a62316486669c668fb3568" + integrity sha512-meT17DOuUElMNsL5LZN56d+KBp22hb0EfxWfuPUeoSi54e40v1W4C2V36P75FpsH9fVEfDKpw5Nnkahc8haSsQ== + dependencies: + hermes-parser "0.28.1" + +babel-plugin-transform-flow-enums@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz#d1d0cc9bdc799c850ca110d0ddc9f21b9ec3ef25" + integrity sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ== + dependencies: + "@babel/plugin-syntax-flow" "^7.12.1" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0, browserslist@^4.25.1: + version "4.25.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.1.tgz#ba9e8e6f298a1d86f829c9b975e07948967bb111" + integrity sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw== + dependencies: + caniuse-lite "^1.0.30001726" + electron-to-chromium "^1.5.173" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + +caniuse-lite@^1.0.30001726: + version "1.0.30001727" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz#22e9706422ad37aa50556af8c10e40e2d93a8b85" + integrity sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q== + +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.43.0: + version "3.44.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.44.0.tgz#62b9165b97e4cbdb8bca16b14818e67428b4a0f8" + integrity sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA== + dependencies: + browserslist "^4.25.1" + +debug@^4.1.0, debug@^4.3.1, debug@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + +electron-to-chromium@^1.5.173: + version "1.5.192" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz#6dfc57a41846a57b18f9c0121821a6df1e165cc1" + integrity sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +fs-readdir-recursive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" + integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.1, glob@^7.2.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +hasha-cli@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/hasha-cli/-/hasha-cli-6.0.0.tgz#f39fb9fae2a9cfaab6755b690c22e26d67e107ce" + integrity sha512-T5NbVb/ksS9aFTcWijOoQnsqLPBGfdjw+ii4QVKWKaCWTYsxBtbuXlsQBy+igndxLDjESjrlK7l+hSO67aIL3Q== + dependencies: + hasha "^6.0.0" + meow "^12.1.1" + +hasha@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-6.0.0.tgz#bdf1231ae40b406121c09c13705e5b38c1bb607c" + integrity sha512-MLydoyGp9QJcjlhE5lsLHXYpWayjjWqkavzju2ZWD2tYa1CgmML1K1gWAu22BLFa2eZ0OfvJ/DlfoVjaD54U2Q== + dependencies: + is-stream "^3.0.0" + type-fest "^4.7.1" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hermes-estree@0.28.1: + version "0.28.1" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.28.1.tgz#631e6db146b06e62fc1c630939acf4a3c77d1b24" + integrity sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ== + +hermes-parser@0.28.1: + version "0.28.1" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.28.1.tgz#17b9e6377f334b6870a1f6da2e123fdcd0b605ac" + integrity sha512-nf8o+hE8g7UJWParnccljHumE9Vlq8F7MqIdeahl+4x0tvCUJYRrT0L7h0MMg/X9YJmkNwsfbaNNrzPtFXOscg== + dependencies: + hermes-estree "0.28.1" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +meow@^12.1.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-12.1.1.tgz#e558dddbab12477b69b2e9a2728c327f191bace6" + integrity sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +nullthrows@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" + integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +react-refresh@^0.14.0: + version "0.14.2" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" + integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== + dependencies: + jsesc "~3.0.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +resolve@^1.22.10: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +type-fest@^4.7.1: + version "4.41.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.6.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" diff --git a/Tests/NodeApi/test_basics.cpp b/Tests/NodeApi/test_basics.cpp new file mode 100644 index 00000000..ea00acc8 --- /dev/null +++ b/Tests/NodeApi/test_basics.cpp @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include + +#include +#include +#include "child_process.h" +#include "test_main.h" + +namespace fs = std::filesystem; + +namespace node_api_tests { + +class BasicsTest : public TestFixtureBase { + protected: + void SetUp() override { basics_js_dir_ = js_root_dir_ / "basics"; } + + ProcessResult RunScript(std::string_view script_filename) noexcept { + return SpawnSync(node_lite_path_.string(), + {(basics_js_dir_ / script_filename).string()}); + } + + bool StringContains(std::string_view str, std::string_view substr) { + return str.find(substr) != std::string::npos; + } + + private: + fs::path basics_js_dir_; +}; + +TEST_F(BasicsTest, TestHello) { + ProcessResult result = RunScript("hello.js"); + ASSERT_TRUE(StringContains(result.std_output, "Hello")); +} + +TEST_F(BasicsTest, TestThrowString) { + ProcessResult result = RunScript("throw_string.js"); + ASSERT_TRUE(StringContains(result.std_error, "Script failed")); +} + +TEST_F(BasicsTest, TestAsyncResolved) { + ProcessResult result = RunScript("async_resolved.js"); + ASSERT_TRUE(StringContains(result.std_output, "test async calling")); + ASSERT_TRUE( + StringContains(result.std_output, "Expected: test async resolved")); +} + +TEST_F(BasicsTest, TestAsyncRejected) { + ProcessResult result = RunScript("async_rejected.js"); + ASSERT_TRUE(StringContains(result.std_output, "test async calling")); + ASSERT_TRUE( + StringContains(result.std_error, "Expected: test async rejected")); +} + +TEST_F(BasicsTest, TestMustCallSuccess) { + ProcessResult result = RunScript("mustcall_success.js"); + ASSERT_TRUE(result.status == 0); +} + +TEST_F(BasicsTest, TestMustCallFailure) { + ProcessResult result = RunScript("mustcall_failure.js"); + ASSERT_TRUE(result.status != 0); + ASSERT_TRUE( + StringContains(result.std_error, "Mismatched noop function calls")); +} + +TEST_F(BasicsTest, TestMustNotCallSuccess) { + ProcessResult result = RunScript("mustnotcall_success.js"); + ASSERT_TRUE(result.status == 0); +} + +TEST_F(BasicsTest, TestMustNotCallFailure) { + ProcessResult result = RunScript("mustnotcall_failure.js"); + ASSERT_TRUE(result.status != 0); + ASSERT_TRUE( + StringContains(result.std_error, "Function should not have been called")); +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/test_main.cpp b/Tests/NodeApi/test_main.cpp new file mode 100644 index 00000000..b13cd5d3 --- /dev/null +++ b/Tests/NodeApi/test_main.cpp @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "test_main.h" + +#include +#include + +#include +#include +#include +#include +#include +#include "child_process.h" +#include "string_utils.h" + +namespace fs = std::filesystem; + +namespace node_api_tests { + +/*static*/ std::filesystem::path TestFixtureBase::node_lite_path_; +/*static*/ std::filesystem::path TestFixtureBase::js_root_dir_; + +std::filesystem::path ResolveNodeLitePath(const std::filesystem::path& exePath); + +/*static*/ void TestFixtureBase::InitializeGlobals( + const char* test_exe_path) noexcept { + fs::path exePath = fs::canonical(test_exe_path); + + fs::path nodeLitePath = + ResolveNodeLitePath(fs::path(test_exe_path)); + if (!fs::exists(nodeLitePath)) { + std::cerr << "Error: Cannot find node_lite executable at " + << nodeLitePath << std::endl; + exit(1); + } + TestFixtureBase::node_lite_path_ = nodeLitePath; + + fs::path testRootPath = exePath.parent_path(); + fs::path rootJsPath = testRootPath / "test"; + if (!fs::exists(rootJsPath)) { + testRootPath = testRootPath.parent_path(); + rootJsPath = testRootPath / "test"; + } + if (!fs::exists(rootJsPath)) { + std::cerr << "Error: Cannot find test directory." << std::endl; + exit(1); + } + TestFixtureBase::js_root_dir_ = rootJsPath; +} + +// Forward declaration +int EvaluateJSFile(int argc, char** argv); + +struct NodeApiTestFixture : TestFixtureBase { + explicit NodeApiTestFixture(fs::path testProcess, fs::path jsFilePath) + : m_testProcess(std::move(testProcess)), + m_jsFilePath(std::move(jsFilePath)) {} + static void SetUpTestSuite() {} + static void TearDownTestSuite() {} + void SetUp() override {} + void TearDown() override {} + + void TestBody() override { + ProcessResult result = + SpawnSync(m_testProcess.string(), {m_jsFilePath.string()}); + if (result.status == 0) { + return; + } + if (!result.std_error.empty()) { + std::stringstream errorStream(result.std_error); + std::vector errorLines; + std::string line; + while (std::getline(errorStream, line)) { + if (!line.empty() && line[line.size() - 1] == '\r') { + line.erase(line.size() - 1, std::string::npos); + } + errorLines.push_back(line); + } + if (errorLines.size() >= 3) { + std::string file = errorLines[0].find("file:") == 0 + ? errorLines[0].substr(std::size("file:") - 1) + : ""; + int line = errorLines[1].find("line:") == 0 + ? std::stoi(errorLines[1].substr(std::size("line:") - 1)) + : 0; + std::string message = errorLines[2]; + std::stringstream details; + for (size_t i = 3; i < errorLines.size(); i++) { + details << errorLines[i] << std::endl; + } + GTEST_MESSAGE_AT_(file.c_str(), + line, + message.c_str(), + ::testing::TestPartResult::kFatalFailure) + << details.str(); + return; + } + } + ASSERT_EQ(result.status, 0); + } + + static void RegisterNodeApiTests(); + + private: + fs::path m_testProcess; + fs::path m_jsFilePath; +}; + +std::string SanitizeName(const std::string& name) { + return ReplaceAll(ReplaceAll(name, "-", "_"), ".", "_"); +} + +#ifdef NODE_API_TESTS_HAVE_NATIVE_MODULES +const std::unordered_set& GetEnabledNativeModuleFolders() { + static const std::unordered_set modules = []() { + std::unordered_set result; +#ifdef NODE_API_AVAILABLE_NATIVE_TESTS + std::stringstream stream(NODE_API_AVAILABLE_NATIVE_TESTS); + std::string entry; + while (std::getline(stream, entry, ',')) { + if (!entry.empty()) { + result.insert(entry); + } + } +#endif + return result; + }(); + return modules; +} +#endif + +std::filesystem::path ResolveNodeLitePath(const std::filesystem::path& exePath) { + fs::path nodeLitePath = exePath; + nodeLitePath.replace_filename("node_lite"); +#if defined(_WIN32) + nodeLitePath += ".exe"; +#endif + return nodeLitePath; +} + +} // namespace node_api_tests + +namespace node_api_tests { + +void NodeApiTestFixture::RegisterNodeApiTests() { + for (const fs::directory_entry& dir_entry : + fs::recursive_directory_iterator(js_root_dir_)) { + if (!dir_entry.is_regular_file() || + dir_entry.path().extension() != ".js") { + continue; + } + + fs::path jsFilePath = dir_entry.path(); + fs::path suitePath = jsFilePath.parent_path().parent_path(); + std::string suiteFolder = suitePath.filename().string(); + + bool includeTest = false; + if (suiteFolder == "basics") { + includeTest = true; + } else if (suiteFolder == "js-native-api") { +#ifdef NODE_API_TESTS_HAVE_NATIVE_MODULES + std::string moduleName = jsFilePath.parent_path().filename().string(); + if (GetEnabledNativeModuleFolders().count(moduleName) == 0) { + continue; + } + includeTest = true; +#else + includeTest = false; +#endif + } + + if (!includeTest) { + continue; + } + + std::string testSuiteName = SanitizeName(suiteFolder); + std::string testName = + SanitizeName(jsFilePath.parent_path().filename().string() + "_" + + jsFilePath.filename().string()); + + ::testing::RegisterTest( + testSuiteName.c_str(), + testName.c_str(), + nullptr, + nullptr, + jsFilePath.string().c_str(), + 1, + [jsFilePath]() { + return new NodeApiTestFixture(node_lite_path_, jsFilePath); + }); + } +} + +} // namespace node_api_tests + +int main(int argc, char** argv) { + node_api_tests::TestFixtureBase::InitializeGlobals(argv[0]); + ::testing::InitGoogleTest(&argc, argv); + node_api_tests::NodeApiTestFixture::RegisterNodeApiTests(); + return RUN_ALL_TESTS(); +} diff --git a/Tests/NodeApi/test_main.h b/Tests/NodeApi/test_main.h new file mode 100644 index 00000000..e0f34c32 --- /dev/null +++ b/Tests/NodeApi/test_main.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef NODE_API_TEST_TEST_MAIN_H +#define NODE_API_TEST_TEST_MAIN_H + +#include +#include + +namespace node_api_tests { + +class TestFixtureBase : public ::testing::Test { + public: + static void InitializeGlobals(const char* test_exe_path) noexcept; + + protected: + static std::filesystem::path node_lite_path_; + static std::filesystem::path js_root_dir_; +}; + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_TEST_MAIN_H \ No newline at end of file From 300d73d3969c88ee2aa1bf83d44ff5a1db669493 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 12 Oct 2025 01:19:17 -0700 Subject: [PATCH 3/6] =?UTF-8?q?Android=20tests=20now=20pass.=20StdoutLogge?= =?UTF-8?q?r=20was=20holding=20on=20to=20destroyed=20mutex=20handles=20dur?= =?UTF-8?q?ing=20instrumentation,=20causing=20crashes,=20and=20now=20route?= =?UTF-8?q?=20console=20output=20through=20the=20new=20NodeLiteRuntime::Ca?= =?UTF-8?q?llbacks.=20On=20Android=20we=20forward=20stdout/stderr=20to=20l?= =?UTF-8?q?ogcat=20via=20callbacks=20to=20work=20around=20this=20for=20now?= =?UTF-8?q?.=20Added=20Android-specific=20shims=20(node=5Flite=5Fandroid.c?= =?UTF-8?q?pp,=20child=5Fprocess=5Fandroid.cpp)=20so=20native=20module=20l?= =?UTF-8?q?oading=20uses=20dlopen=20and=20JS=20=20child=5Fprocess.spawnSyn?= =?UTF-8?q?c=20safely=20reports=20=E2=80=9Cunsupported=E2=80=9D.=20Extende?= =?UTF-8?q?d=20the=20Node=E2=80=91API=20harness=20to=20allow=20in-process?= =?UTF-8?q?=20execution:=20RunNodeLiteScript=20captures=20output,=20SetNod?= =?UTF-8?q?eApiTestEnvironment=20lets=20the=20JNI=20layer=20provide=20a=20?= =?UTF-8?q?base=20directory=20and=20asset=20manager,=20and=20the=20GTest?= =?UTF-8?q?=20registration=20path=20uses=20that=20configuration=20instead?= =?UTF-8?q?=20of=20shelling=20out=20to=20the=20node=5Flite=20executable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/NodeApi/CMakeLists.txt | 20 +- Tests/NodeApi/node_lite.cpp | 420 ++++++++++++------ Tests/NodeApi/node_lite.h | 143 +++--- Tests/NodeApi/node_lite_jsruntimehost.cpp | 78 +++- Tests/NodeApi/test_basics.cpp | 28 +- Tests/NodeApi/test_main.cpp | 331 ++++++-------- Tests/NodeApi/test_main.h | 44 +- Tests/UnitTests/Android/app/build.gradle | 20 + .../com/jsruntimehost/unittests/Main.java | 10 +- .../com/jsruntimehost/unittests/Native.java | 1 + .../Android/app/src/main/cpp/CMakeLists.txt | 34 +- .../Android/app/src/main/cpp/JNI.cpp | 41 +- Tests/UnitTests/Shared/Shared.cpp | 173 ++++++++ Tests/UnitTests/Shared/Shared.h | 9 +- Tests/package-lock.json | 6 + 15 files changed, 944 insertions(+), 414 deletions(-) diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt index 48c5b5a8..1c4ce785 100644 --- a/Tests/NodeApi/CMakeLists.txt +++ b/Tests/NodeApi/CMakeLists.txt @@ -20,7 +20,10 @@ function(node_api_copy_test_sources TARGET_NAME) ) endfunction() -if(APPLE) +if(ANDROID) + set(NODE_LITE_PLATFORM_SRC node_lite_android.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process_android.cpp) +elseif(APPLE) set(NODE_LITE_PLATFORM_SRC node_lite_mac.cpp) set(NODE_LITE_CHILD_PROCESS_SRC child_process_mac.cpp) elseif(WIN32) @@ -68,13 +71,14 @@ target_link_libraries(node_lite node_api_copy_test_sources(node_lite) add_executable(NodeApiTests - ${NODE_LITE_CHILD_PROCESS_SRC} - child_process.h - string_utils.cpp - string_utils.h - test_basics.cpp - test_main.cpp - test_main.h + ${NODE_LITE_CHILD_PROCESS_SRC} + child_process.h + string_utils.cpp + string_utils.h + test_basics.cpp + test_main.cpp + main.cpp + test_main.h ) target_include_directories(NodeApiTests diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index 5401d39c..d82f3bb2 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -1,29 +1,50 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#include "node_lite.h" -#include "js_runtime_api.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "child_process.h" +#include "node_lite.h" +#include "js_runtime_api.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "child_process.h" namespace fs = std::filesystem; namespace node_api_tests { -namespace { - -NodeApiRef MakeNodeApiRef(napi_env env, napi_value value) { - napi_ref ref{}; - NODE_LITE_CALL(napi_create_reference(env, value, 1, &ref)); - return NodeApiRef(ref, NodeApiRefDeleter(env)); +namespace { + +std::mutex& ErrorHandlerMutex() { + static std::mutex mutex; + return mutex; +} + +void DefaultFatalErrorHandler(const NodeLiteFatalErrorInfo& info) { + if (!info.message.empty()) { + std::cerr << info.message; + if (!info.details.empty()) { + std::cerr << '\n' << info.details; + } + std::cerr << std::endl; + } else if (!info.details.empty()) { + std::cerr << info.details << std::endl; + } + std::exit(info.exit_code); +} + +NodeApiRef MakeNodeApiRef(napi_env env, napi_value value) { + napi_ref ref{}; + NODE_LITE_CALL(napi_create_reference(env, value, 1, &ref)); + return NodeApiRef(ref, NodeApiRefDeleter(env)); } template @@ -270,32 +291,39 @@ std::string NodeLiteModule::ReadModuleFileText(napi_env env) { } std::string jsFilePath = args[1]; - std::unique_ptr runtime = NodeLiteRuntime::Create( - std::move(taskRunner), js_root.string(), std::move(args)); + std::unique_ptr runtime = NodeLiteRuntime::Create( + std::move(taskRunner), + js_root.string(), + std::move(args), + NodeLiteRuntime::Callbacks{}); runtime->RunTestScript(jsFilePath); } /*static*/ std::unique_ptr NodeLiteRuntime::Create( - std::shared_ptr task_runner, - std::string js_root, - std::vector args) { - std::unique_ptr runtime = - std::make_unique(PrivateTag{}, - std::move(task_runner), - std::move(js_root), - std::move(args)); - runtime->Initialize(); - return runtime; -} - -NodeLiteRuntime::NodeLiteRuntime( - PrivateTag, - std::shared_ptr task_runner, - std::string js_root, - std::vector args) - : task_runner_(std::move(task_runner)), - js_root_(std::move(js_root)), - args_(std::move(args)) {} + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks) { + std::unique_ptr runtime = + std::make_unique(PrivateTag{}, + std::move(task_runner), + std::move(js_root), + std::move(args), + std::move(callbacks)); + runtime->Initialize(); + return std::unique_ptr(runtime.release()); +} + +NodeLiteRuntime::NodeLiteRuntime( + PrivateTag, + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks) + : task_runner_(std::move(task_runner)), + js_root_(std::move(js_root)), + args_(std::move(args)), + callbacks_(std::move(callbacks)) {} void NodeLiteRuntime::Initialize() { env_holder_ = @@ -554,9 +582,9 @@ void NodeLiteRuntime::DefineBuiltInModules() { } } -void NodeLiteRuntime::DefineGlobalFunctions() { - NodeApiHandleScope scope{env_}; - napi_value global = NodeApi::GetGlobal(env_); +void NodeLiteRuntime::DefineGlobalFunctions() { + NodeApiHandleScope scope{env_}; + napi_value global = NodeApi::GetGlobal(env_); // Add global.global NodeApi::SetProperty(env_, global, "global", global); @@ -680,27 +708,46 @@ void NodeLiteRuntime::DefineGlobalFunctions() { NodeApi::SetProperty(env_, global, "console", console_obj); // console.log() - NodeApi::SetMethod( - env_, console_obj, "log", [](napi_env env, span args) { - NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); - std::string message = NodeApi::ToStdString(env, args[0]); - std::cout << message << std::endl; - return nullptr; - }); + NodeApi::SetMethod( + env_, + console_obj, + "log", + [this](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + std::string message = NodeApi::ToStdString(env, args[0]); + EmitConsoleOutput(message, false); + return nullptr; + }); // console.error() NodeApi::SetMethod( env_, console_obj, "error", - [](napi_env env, span args) -> napi_value { - NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); - std::string message = NodeApi::ToStdString(env, args[0]); - std::cerr << message << std::endl; - return nullptr; - }); - } -} + [this](napi_env env, span args) -> napi_value { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + std::string message = NodeApi::ToStdString(env, args[0]); + EmitConsoleOutput(message, true); + return nullptr; + }); + } +} + +void NodeLiteRuntime::EmitConsoleOutput(const std::string& message, + bool is_error) { + const auto& callback = is_error ? callbacks_.stderr_callback + : callbacks_.stdout_callback; + if (callback) { + callback(message); + return; + } + + if (is_error) { + std::cerr << message << std::endl; + } else { + std::cout << message << std::endl; + } +} std::string NodeLiteRuntime::ProcessStack(std::string const& stack, std::string const& assertMethod) { @@ -863,27 +910,55 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { return *this; } -//============================================================================= -// NodeLiteErrorHandler implementation -//============================================================================= - -/*static*/ [[noreturn]] void NodeLiteErrorHandler::OnNodeApiFailed( - napi_env env, napi_status error_code) { - const char* errorMessage = "An exception is pending"; - if (NodeApi::IsExceptionPending(env)) { - error_code = napi_pending_exception; +//============================================================================= +// NodeLiteErrorHandler implementation +//============================================================================= + +/*static*/ NodeLiteErrorHandler::Handler NodeLiteErrorHandler::SetHandler( + Handler handler) noexcept { + std::lock_guard lock{ErrorHandlerMutex()}; + Handler previous = GetHandler(); + if (handler) { + GetHandler() = std::move(handler); + } else { + GetHandler() = DefaultFatalErrorHandler; + } + return previous; +} + +/*static*/ NodeLiteErrorHandler::Handler& NodeLiteErrorHandler::GetHandler() + noexcept { + static Handler handler = DefaultFatalErrorHandler; + return handler; +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::HandleFatalError( + NodeLiteFatalErrorInfo info) { + Handler handler_copy; + { + std::lock_guard lock{ErrorHandlerMutex()}; + handler_copy = GetHandler(); + } + handler_copy(info); + std::terminate(); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::OnNodeApiFailed( + napi_env env, napi_status error_code) { + const char* errorMessage = "An exception is pending"; + if (NodeApi::IsExceptionPending(env)) { + error_code = napi_pending_exception; } else { const napi_extended_error_info* error_info{}; napi_status status = napi_get_last_error_info(env, &error_info); if (status != napi_ok) { - NodeLiteErrorHandler::ExitWithMessage("", [&](std::ostream& os) { - os << "Failed to get last error info: " << status; - }); - } - errorMessage = error_info->error_message; - } - throw NodeLiteException(error_code, errorMessage); -} + NodeLiteErrorHandler::ExitWithMessage( + "", [&](std::ostream& os) { os << "Failed to get last error info: " << status; }); + } + errorMessage = error_info->error_message; + } + throw NodeLiteException(error_code, errorMessage); +} /*static*/ [[noreturn]] void NodeLiteErrorHandler::OnAssertFailed( napi_env env, char const* expr, char const* message) { @@ -912,17 +987,17 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { } std::string message = NodeApi::GetPropertyString(env, error, "message"); std::string stack = NodeApi::GetPropertyString(env, error, "stack"); - ExitWithMessage("JavaScript error", [&](std::ostream& os) { - os << "Exception: " << name << '\n' - << " Message: " << message << '\n' - << "Callstack: " << '\n' - << stack; - }); - } else { - std::string message = NodeApi::CoerceToString(env, error); - ExitWithMessage("JavaScript error", - [&](std::ostream& os) { os << " Message: " << message; }); - } + ExitWithMessage("JavaScript error", [&](std::ostream& os) { + os << "Exception: " << name << '\n' + << " Message: " << message << '\n' + << "Callstack: " << '\n' + << stack; + }); + } else { + std::string message = NodeApi::CoerceToString(env, error); + ExitWithMessage("JavaScript error", + [&](std::ostream& os) { os << " Message: " << message; }); + } } /*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithJSAssertError( @@ -946,38 +1021,37 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { << " Actual: " << actual << '\n'; } - ExitWithMessage("JavaScript assertion error", [&](std::ostream& os) { - os << "Exception: " << "AssertionError" << '\n' - << " Method: " << method_name << '\n' - << " Message: " << message << '\n' - << error_details.str(/*a filler for formatting*/) - << "Callstack: " << '\n' - << error_stack; - }); -} - -/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithMessage( - const std::string& message, - std::function get_error_details) noexcept { - std::ostringstream details_stream; - get_error_details(details_stream); - std::string details = details_stream.str(); - if (!message.empty()) { - std::cerr << message; - } - if (!details.empty()) { - if (!message.empty()) { - std::cerr << "\n"; - } - std::cerr << details; - } - std::cerr << std::endl; - exit(1); -} - -//============================================================================= -// NodeApi implementation -//============================================================================= + ExitWithMessage("JavaScript assertion error", [&](std::ostream& os) { + os << "Exception: " + << "AssertionError" << '\n' + << " Method: " << method_name << '\n' + << " Message: " << message << '\n' + << error_details.str(/*a filler for formatting*/) + << "Callstack: " << '\n' + << error_stack; + }); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithMessage( + const std::string& message, + std::function get_error_details, + int exit_code) noexcept { + std::ostringstream details_stream; + if (get_error_details) { + get_error_details(details_stream); + } + std::string details = details_stream.str(); + + HandleFatalError(NodeLiteFatalErrorInfo{ + .message = message, + .details = details, + .exit_code = exit_code, + }); +} + +//============================================================================= +// NodeApi implementation +//============================================================================= /*static*/ bool NodeApi::IsExceptionPending(napi_env env) { bool result{}; @@ -1236,11 +1310,11 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { return result; } -/*static*/ napi_value NodeApi::CreateFunction(napi_env env, - std::string_view name, - NodeApiCallback cb) { - napi_value result{}; - NODE_LITE_CALL(napi_create_function( +/*static*/ napi_value NodeApi::CreateFunction(napi_env env, + std::string_view name, + NodeApiCallback cb) { + napi_value result{}; + NODE_LITE_CALL(napi_create_function( env, name.data(), name.size(), @@ -1257,12 +1331,98 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { // TODO: (vmoroz) Find a way to delete it on close. new NodeApiCallback(std::move(cb)), &result)); - return result; -} - -} // namespace node_api_tests - -int main(int argc, char* argv[]) { - node_api_tests::NodeLiteRuntime::Run( + return result; +} + +ProcessResult RunNodeLiteScript(const std::filesystem::path& js_root, + const std::filesystem::path& script_path, + NodeLiteRuntime::Callbacks callbacks) { + ProcessResult result{}; + std::ostringstream stdout_stream; + std::ostringstream stderr_stream; + + NodeLiteRuntime::Callbacks effective_callbacks; + auto stdout_cb = callbacks.stdout_callback; + auto stderr_cb = callbacks.stderr_callback; + effective_callbacks.stdout_callback = + [stdout_cb, &stdout_stream](const std::string& message) { + if (stdout_cb) { + stdout_cb(message); + } + stdout_stream << message << '\n'; + }; + effective_callbacks.stderr_callback = + [stderr_cb, &stderr_stream](const std::string& message) { + if (stderr_cb) { + stderr_cb(message); + } + stderr_stream << message << '\n'; + }; + + auto fatal_handler = [&result](const NodeLiteFatalErrorInfo& info) { + result.status = info.exit_code; + if (!info.message.empty()) { + result.std_error = info.message; + } + if (!info.details.empty()) { + if (!result.std_error.empty()) { + result.std_error += '\n'; + } + result.std_error += info.details; + } + throw NodeLiteFatalError(info); + }; + + NodeLiteErrorHandler::Handler previous_handler = + NodeLiteErrorHandler::SetHandler(fatal_handler); + + try { + auto task_runner = std::make_shared(); + std::vector args{"node_lite", script_path.string()}; + auto runtime = NodeLiteRuntime::Create(std::move(task_runner), + js_root.string(), + std::move(args), + std::move(effective_callbacks)); + runtime->RunTestScript(script_path.string()); + result.status = 0; + } catch (const NodeLiteFatalError&) { + // Fatal error captured in result + } catch (const std::exception& e) { + NodeLiteErrorHandler::SetHandler(previous_handler); + result.status = -1; + result.std_error = e.what(); + return result; + } catch (...) { + NodeLiteErrorHandler::SetHandler(previous_handler); + result.status = -1; + result.std_error = "Unknown error"; + return result; + } + + NodeLiteErrorHandler::SetHandler(previous_handler); + + result.std_output = stdout_stream.str(); + if (!result.std_output.empty() && result.std_output.back() == '\n') { + result.std_output.pop_back(); + } + + std::string stderr_logs = stderr_stream.str(); + if (!stderr_logs.empty() && stderr_logs.back() == '\n') { + stderr_logs.pop_back(); + } + if (!stderr_logs.empty()) { + if (!result.std_error.empty()) { + result.std_error += '\n'; + } + result.std_error += stderr_logs; + } + + return result; +} + +} // namespace node_api_tests + +int main(int argc, char* argv[]) { + node_api_tests::NodeLiteRuntime::Run( std::vector(argv, argv + argc)); } diff --git a/Tests/NodeApi/node_lite.h b/Tests/NodeApi/node_lite.h index 5b9044dd..e8bba7e2 100644 --- a/Tests/NodeApi/node_lite.h +++ b/Tests/NodeApi/node_lite.h @@ -13,11 +13,12 @@ #include #include #include -#include -#include -#include -#include "compat.h" -#include "string_utils.h" +#include +#include +#include +#include "child_process.h" +#include "compat.h" +#include "string_utils.h" #define NAPI_EXPERIMENTAL #include "js_runtime_api.h" @@ -87,10 +88,31 @@ class NodeLiteException : public std::runtime_error { napi_status error_status_; }; -class NodeLiteErrorHandler { - public: - [[noreturn]] static void OnNodeApiFailed(napi_env env, - napi_status error_status); +struct NodeLiteFatalErrorInfo { + std::string message; + std::string details; + int exit_code{1}; +}; + +class NodeLiteFatalError : public std::runtime_error { + public: + explicit NodeLiteFatalError(NodeLiteFatalErrorInfo info) + : std::runtime_error{info.message.c_str()}, info_{std::move(info)} {} + + const NodeLiteFatalErrorInfo& info() const noexcept { return info_; } + + private: + NodeLiteFatalErrorInfo info_; +}; + +class NodeLiteErrorHandler { + public: + using Handler = std::function; + + static Handler SetHandler(Handler handler) noexcept; + + [[noreturn]] static void OnNodeApiFailed(napi_env env, + napi_status error_status); [[noreturn]] static void OnAssertFailed(napi_env env, char const* expr, @@ -99,16 +121,21 @@ class NodeLiteErrorHandler { [[noreturn]] static void ExitWithJSError(napi_env env, napi_value error) noexcept; - [[noreturn]] static void ExitWithJSAssertError(napi_env env, - napi_value error) noexcept; - - [[noreturn]] static void ExitWithMessage( - const std::string& message, - std::function get_error_details = nullptr) noexcept; -}; - -// Define NodeApiRef "smart pointer" for napi_ref as unique_ptr with a custom -// deleter. + [[noreturn]] static void ExitWithJSAssertError(napi_env env, + napi_value error) noexcept; + + [[noreturn]] static void ExitWithMessage( + const std::string& message, + std::function get_error_details = nullptr, + int exit_code = 1) noexcept; + + private: + static Handler& GetHandler() noexcept; + [[noreturn]] static void HandleFatalError(NodeLiteFatalErrorInfo info); +}; + +// Define NodeApiRef "smart pointer" for napi_ref as unique_ptr with a custom +// deleter. class NodeApiRefDeleter { public: NodeApiRefDeleter() noexcept; @@ -183,21 +210,28 @@ class NodeLiteModule { }; // The Node.js-like runtime that is enough to run Node-API tests. -class NodeLiteRuntime { - struct PrivateTag {}; - - public: - static std::unique_ptr Create( - std::shared_ptr task_runner, - std::string js_root, - std::vector args); - - explicit NodeLiteRuntime(PrivateTag tag, - std::shared_ptr task_runner, - std::string js_root, - std::vector args); - - static void Run(std::vector args); +class NodeLiteRuntime { + struct PrivateTag {}; + + public: + struct Callbacks { + std::function stdout_callback{}; + std::function stderr_callback{}; + }; + + static std::unique_ptr Create( + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks); + + explicit NodeLiteRuntime(PrivateTag tag, + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks); + + static void Run(std::vector args); NodeLiteModule& ResolveModule(const std::string& parent_module_path, const std::string& module_path); @@ -222,15 +256,17 @@ class NodeLiteRuntime { private: void Initialize(); - void DefineGlobalFunctions(); - void DefineBuiltInModules(); - - private: - std::shared_ptr task_runner_; - std::string js_root_; - std::vector args_; - std::unique_ptr env_holder_; - napi_env env_{}; + void DefineGlobalFunctions(); + void DefineBuiltInModules(); + void EmitConsoleOutput(const std::string& message, bool is_error); + + private: + std::shared_ptr task_runner_; + std::string js_root_; + std::vector args_; + Callbacks callbacks_{}; + std::unique_ptr env_holder_; + napi_env env_{}; std::unordered_map> registered_modules_; std::unordered_map node_js_modules_; @@ -355,11 +391,16 @@ class NodeApi { napi_value func, span args); - static napi_value CreateFunction(napi_env env, - std::string_view name, - NodeApiCallback cb); -}; - -} // namespace node_api_tests - -#endif // !NODE_API_TEST_NODE_LITE_H \ No newline at end of file + static napi_value CreateFunction(napi_env env, + std::string_view name, + NodeApiCallback cb); +}; + +ProcessResult RunNodeLiteScript( + const std::filesystem::path& js_root, + const std::filesystem::path& script_path, + NodeLiteRuntime::Callbacks callbacks = NodeLiteRuntime::Callbacks{}); + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_NODE_LITE_H diff --git a/Tests/NodeApi/node_lite_jsruntimehost.cpp b/Tests/NodeApi/node_lite_jsruntimehost.cpp index 6833672e..3e47e621 100644 --- a/Tests/NodeApi/node_lite_jsruntimehost.cpp +++ b/Tests/NodeApi/node_lite_jsruntimehost.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include #if defined(__APPLE__) @@ -12,6 +14,7 @@ #elif defined(__ANDROID__) #include #include "js_native_api_v8.h" +#include #endif namespace node_api_tests { @@ -28,12 +31,20 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { context_ = JSGlobalContextCreateInGroup(nullptr, nullptr); env_ = Napi::Attach(context_); #elif defined(__ANDROID__) - // TODO: Implement a dedicated V8 environment for Android Node-API tests. - // For now we surface a clear failure so we remember to provide a proper - // implementation before enabling Android execution. - (void)onUnhandledError_; - throw std::runtime_error( - "Android Node-API tests are not yet implemented for node_lite."); + V8Platform::EnsureInitialized(); + + allocator_.reset(v8::ArrayBuffer::Allocator::NewDefaultAllocator()); + v8::Isolate::CreateParams create_params; + create_params.array_buffer_allocator = allocator_.get(); + isolate_ = v8::Isolate::New(create_params); + + v8::Locker locker(isolate_); + v8::Isolate::Scope isolate_scope(isolate_); + v8::HandleScope handle_scope(isolate_); + v8::Local context = v8::Context::New(isolate_); + context_.Reset(isolate_, context); + v8::Context::Scope context_scope(context); + env_ = Napi::Attach(context); #else (void)onUnhandledError_; throw std::runtime_error( @@ -65,6 +76,37 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { JSGlobalContextRelease(context_); context_ = nullptr; } +#elif defined(__ANDROID__) + if (env_ != nullptr && isolate_ != nullptr) { + v8::Locker locker(isolate_); + v8::Isolate::Scope isolate_scope(isolate_); + v8::HandleScope handle_scope(isolate_); + v8::Local context = context_.Get(isolate_); + v8::Context::Scope context_scope(context); + + if (onUnhandledError_) { + bool hasPending = false; + if (napi_is_exception_pending(env_, &hasPending) == napi_ok && hasPending) { + napi_value error{}; + if (napi_get_and_clear_last_exception(env_, &error) == napi_ok) { + onUnhandledError_(env_, error); + } + } + } + + Napi::Env napiEnv{env_}; + Napi::Detach(napiEnv); + env_ = nullptr; + } + + context_.Reset(); + + if (isolate_ != nullptr) { + isolate_->Dispose(); + isolate_ = nullptr; + } + + allocator_.reset(); #endif } @@ -73,11 +115,35 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { private: #if defined(__APPLE__) JSGlobalContextRef context_{}; +#elif defined(__ANDROID__) + class V8Platform { + public: + static void EnsureInitialized() { + std::call_once(init_flag_, []() { + platform_ = v8::platform::NewDefaultPlatform(); + v8::V8::InitializePlatform(platform_.get()); + v8::V8::Initialize(); + }); + } + + private: + static std::once_flag init_flag_; + static std::unique_ptr platform_; + }; + + v8::Isolate* isolate_{nullptr}; + v8::Global context_; + std::unique_ptr allocator_{}; #endif napi_env env_{}; std::function onUnhandledError_{}; }; +#if defined(__ANDROID__) +std::once_flag JsRuntimeHostEnvHolder::V8Platform::init_flag_{}; +std::unique_ptr JsRuntimeHostEnvHolder::V8Platform::platform_{}; +#endif + } // namespace std::unique_ptr CreateEnvHolder( diff --git a/Tests/NodeApi/test_basics.cpp b/Tests/NodeApi/test_basics.cpp index ea00acc8..45d00a26 100644 --- a/Tests/NodeApi/test_basics.cpp +++ b/Tests/NodeApi/test_basics.cpp @@ -12,14 +12,26 @@ namespace fs = std::filesystem; namespace node_api_tests { -class BasicsTest : public TestFixtureBase { - protected: - void SetUp() override { basics_js_dir_ = js_root_dir_ / "basics"; } - - ProcessResult RunScript(std::string_view script_filename) noexcept { - return SpawnSync(node_lite_path_.string(), - {(basics_js_dir_ / script_filename).string()}); - } +class BasicsTest : public TestFixtureBase { + protected: + void SetUp() override { + const auto& config = Config(); + ASSERT_FALSE(config.js_root.empty()) + << "Node-API test root directory is not configured."; + basics_js_dir_ = config.js_root / "basics"; + } + + ProcessResult RunScript(std::string_view script_filename) noexcept { + const auto& config = Config(); + if (config.run_script) { + return config.run_script(basics_js_dir_ / fs::path{script_filename}); + } + + ProcessResult fallback{}; + fallback.status = 1; + fallback.std_error = "Node-API test runner is not configured."; + return fallback; + } bool StringContains(std::string_view str, std::string_view substr) { return str.find(substr) != std::string::npos; diff --git a/Tests/NodeApi/test_main.cpp b/Tests/NodeApi/test_main.cpp index b13cd5d3..93803cb8 100644 --- a/Tests/NodeApi/test_main.cpp +++ b/Tests/NodeApi/test_main.cpp @@ -1,201 +1,164 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -#include "test_main.h" - -#include -#include - +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "test_main.h" + +#include +#include + #include +#include #include #include -#include -#include -#include "child_process.h" -#include "string_utils.h" - -namespace fs = std::filesystem; - -namespace node_api_tests { - -/*static*/ std::filesystem::path TestFixtureBase::node_lite_path_; -/*static*/ std::filesystem::path TestFixtureBase::js_root_dir_; - -std::filesystem::path ResolveNodeLitePath(const std::filesystem::path& exePath); - -/*static*/ void TestFixtureBase::InitializeGlobals( - const char* test_exe_path) noexcept { - fs::path exePath = fs::canonical(test_exe_path); - - fs::path nodeLitePath = - ResolveNodeLitePath(fs::path(test_exe_path)); - if (!fs::exists(nodeLitePath)) { - std::cerr << "Error: Cannot find node_lite executable at " - << nodeLitePath << std::endl; - exit(1); - } - TestFixtureBase::node_lite_path_ = nodeLitePath; - - fs::path testRootPath = exePath.parent_path(); - fs::path rootJsPath = testRootPath / "test"; - if (!fs::exists(rootJsPath)) { - testRootPath = testRootPath.parent_path(); - rootJsPath = testRootPath / "test"; - } - if (!fs::exists(rootJsPath)) { - std::cerr << "Error: Cannot find test directory." << std::endl; - exit(1); - } - TestFixtureBase::js_root_dir_ = rootJsPath; -} - -// Forward declaration -int EvaluateJSFile(int argc, char** argv); - -struct NodeApiTestFixture : TestFixtureBase { - explicit NodeApiTestFixture(fs::path testProcess, fs::path jsFilePath) - : m_testProcess(std::move(testProcess)), - m_jsFilePath(std::move(jsFilePath)) {} - static void SetUpTestSuite() {} - static void TearDownTestSuite() {} - void SetUp() override {} - void TearDown() override {} - - void TestBody() override { - ProcessResult result = - SpawnSync(m_testProcess.string(), {m_jsFilePath.string()}); - if (result.status == 0) { - return; - } - if (!result.std_error.empty()) { - std::stringstream errorStream(result.std_error); - std::vector errorLines; - std::string line; - while (std::getline(errorStream, line)) { - if (!line.empty() && line[line.size() - 1] == '\r') { - line.erase(line.size() - 1, std::string::npos); - } - errorLines.push_back(line); - } - if (errorLines.size() >= 3) { - std::string file = errorLines[0].find("file:") == 0 - ? errorLines[0].substr(std::size("file:") - 1) - : ""; - int line = errorLines[1].find("line:") == 0 - ? std::stoi(errorLines[1].substr(std::size("line:") - 1)) - : 0; - std::string message = errorLines[2]; - std::stringstream details; - for (size_t i = 3; i < errorLines.size(); i++) { - details << errorLines[i] << std::endl; - } - GTEST_MESSAGE_AT_(file.c_str(), - line, - message.c_str(), - ::testing::TestPartResult::kFatalFailure) - << details.str(); - return; - } - } - ASSERT_EQ(result.status, 0); - } - - static void RegisterNodeApiTests(); - - private: - fs::path m_testProcess; - fs::path m_jsFilePath; -}; - +#include +#include +#include "string_utils.h" + +namespace fs = std::filesystem; + +namespace node_api_tests { + +namespace { + +NodeApiTestConfig g_test_config{}; +bool g_test_config_initialized = false; + +const NodeApiTestConfig& RequireConfig() noexcept { + if (!g_test_config_initialized) { + std::cerr << "[NodeApiTests] configuration not initialized." << std::endl; + std::abort(); + } + return g_test_config; +} + std::string SanitizeName(const std::string& name) { return ReplaceAll(ReplaceAll(name, "-", "_"), ".", "_"); } -#ifdef NODE_API_TESTS_HAVE_NATIVE_MODULES -const std::unordered_set& GetEnabledNativeModuleFolders() { - static const std::unordered_set modules = []() { - std::unordered_set result; -#ifdef NODE_API_AVAILABLE_NATIVE_TESTS - std::stringstream stream(NODE_API_AVAILABLE_NATIVE_TESTS); - std::string entry; - while (std::getline(stream, entry, ',')) { - if (!entry.empty()) { - result.insert(entry); - } - } -#endif - return result; - }(); - return modules; +} // namespace + +void InitializeNodeApiTests(const NodeApiTestConfig& config) noexcept { + g_test_config = config; + g_test_config_initialized = true; } -#endif - -std::filesystem::path ResolveNodeLitePath(const std::filesystem::path& exePath) { - fs::path nodeLitePath = exePath; - nodeLitePath.replace_filename("node_lite"); -#if defined(_WIN32) - nodeLitePath += ".exe"; -#endif - return nodeLitePath; -} - -} // namespace node_api_tests - -namespace node_api_tests { - -void NodeApiTestFixture::RegisterNodeApiTests() { - for (const fs::directory_entry& dir_entry : - fs::recursive_directory_iterator(js_root_dir_)) { - if (!dir_entry.is_regular_file() || - dir_entry.path().extension() != ".js") { - continue; + +const NodeApiTestConfig& GetNodeApiTestConfig() noexcept { + return RequireConfig(); +} + +const NodeApiTestConfig& TestFixtureBase::Config() noexcept { + return RequireConfig(); +} + +class NodeApiTestFixture : public TestFixtureBase { + public: + explicit NodeApiTestFixture(fs::path jsFilePath) + : m_jsFilePath(std::move(jsFilePath)) {} + + void TestBody() override { + const auto& config = Config(); + ASSERT_TRUE(static_cast(config.run_script)) + << "Node-API test runner is not configured."; + + ProcessResult result = config.run_script(m_jsFilePath); + if (result.status == 0) { + return; } - fs::path jsFilePath = dir_entry.path(); - fs::path suitePath = jsFilePath.parent_path().parent_path(); - std::string suiteFolder = suitePath.filename().string(); - - bool includeTest = false; - if (suiteFolder == "basics") { - includeTest = true; - } else if (suiteFolder == "js-native-api") { -#ifdef NODE_API_TESTS_HAVE_NATIVE_MODULES - std::string moduleName = jsFilePath.parent_path().filename().string(); - if (GetEnabledNativeModuleFolders().count(moduleName) == 0) { - continue; + if (!result.std_error.empty()) { + std::stringstream errorStream(result.std_error); + std::vector errorLines; + std::string line; + while (std::getline(errorStream, line)) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + errorLines.push_back(line); + } + if (errorLines.size() >= 3) { + std::string file = errorLines[0].rfind("file:", 0) == 0 + ? errorLines[0].substr(5) + : ""; + int lineNumber = errorLines[1].rfind("line:", 0) == 0 + ? std::stoi(errorLines[1].substr(5)) + : 0; + std::string message = errorLines[2]; + std::stringstream details; + for (size_t i = 3; i < errorLines.size(); ++i) { + details << errorLines[i] << std::endl; + } + GTEST_MESSAGE_AT_(file.c_str(), + lineNumber, + message.c_str(), + ::testing::TestPartResult::kFatalFailure) + << details.str(); + return; } - includeTest = true; -#else - includeTest = false; -#endif } - if (!includeTest) { - continue; + ASSERT_EQ(result.status, 0); + } + + static void Register() { + const auto& config = Config(); + const fs::path& js_root = config.js_root; + if (js_root.empty()) { + std::cerr << "[NodeApiTests] JS root directory not configured." << std::endl; + std::abort(); } - std::string testSuiteName = SanitizeName(suiteFolder); - std::string testName = - SanitizeName(jsFilePath.parent_path().filename().string() + "_" + - jsFilePath.filename().string()); - - ::testing::RegisterTest( - testSuiteName.c_str(), - testName.c_str(), - nullptr, - nullptr, - jsFilePath.string().c_str(), - 1, - [jsFilePath]() { - return new NodeApiTestFixture(node_lite_path_, jsFilePath); - }); + for (const fs::directory_entry& dir_entry : + fs::recursive_directory_iterator(js_root)) { + if (!dir_entry.is_regular_file() || + dir_entry.path().extension() != ".js") { + continue; + } + + fs::path jsFilePath = dir_entry.path(); + fs::path suitePath = jsFilePath.parent_path().parent_path(); + std::string suiteFolder = suitePath.filename().string(); + + bool includeTest = false; + if (suiteFolder == "basics") { + includeTest = true; + } else if (suiteFolder == "js-native-api") { + if (config.enabled_native_suites.empty()) { + continue; + } + std::string moduleName = jsFilePath.parent_path().filename().string(); + includeTest = + config.enabled_native_suites.find(moduleName) != + config.enabled_native_suites.end(); + } else { + continue; + } + + if (!includeTest) { + continue; + } + + std::string testSuiteName = SanitizeName(suiteFolder); + std::string testName = SanitizeName( + jsFilePath.parent_path().filename().string() + "_" + + jsFilePath.filename().string()); + + ::testing::RegisterTest( + testSuiteName.c_str(), + testName.c_str(), + nullptr, + nullptr, + jsFilePath.string().c_str(), + 1, + [jsFilePath]() { return new NodeApiTestFixture(jsFilePath); }); + } } + + private: + fs::path m_jsFilePath; +}; + +void RegisterNodeApiTests() { + NodeApiTestFixture::Register(); } - -} // namespace node_api_tests - -int main(int argc, char** argv) { - node_api_tests::TestFixtureBase::InitializeGlobals(argv[0]); - ::testing::InitGoogleTest(&argc, argv); - node_api_tests::NodeApiTestFixture::RegisterNodeApiTests(); - return RUN_ALL_TESTS(); -} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/test_main.h b/Tests/NodeApi/test_main.h index e0f34c32..a4fe4532 100644 --- a/Tests/NodeApi/test_main.h +++ b/Tests/NodeApi/test_main.h @@ -4,20 +4,30 @@ #ifndef NODE_API_TEST_TEST_MAIN_H #define NODE_API_TEST_TEST_MAIN_H -#include -#include - -namespace node_api_tests { - -class TestFixtureBase : public ::testing::Test { - public: - static void InitializeGlobals(const char* test_exe_path) noexcept; - - protected: - static std::filesystem::path node_lite_path_; - static std::filesystem::path js_root_dir_; -}; - -} // namespace node_api_tests - -#endif // !NODE_API_TEST_TEST_MAIN_H \ No newline at end of file +#include +#include +#include +#include + +#include "child_process.h" + +namespace node_api_tests { + +struct NodeApiTestConfig { + std::filesystem::path js_root; + std::function run_script; + std::unordered_set enabled_native_suites; +}; + +void InitializeNodeApiTests(const NodeApiTestConfig& config) noexcept; +const NodeApiTestConfig& GetNodeApiTestConfig() noexcept; +void RegisterNodeApiTests(); + +class TestFixtureBase : public ::testing::Test { + protected: + static const NodeApiTestConfig& Config() noexcept; +}; + +} // namespace node_api_tests + +#endif // !NODE_API_TEST_TEST_MAIN_H diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index 0151c32d..38557f6e 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -7,6 +7,8 @@ if (project.hasProperty("jsEngine")) { jsEngine = project.property("jsEngine") } +def nodeApiAssetsDir = "${project.buildDir}/generated/nodeapiassets" + android { namespace 'com.jsruntimehost.unittests' compileSdk 33 @@ -61,6 +63,12 @@ android { buildFeatures { viewBinding true } + + sourceSets { + main { + assets.srcDirs += [nodeApiAssetsDir] + } + } } dependencies { @@ -91,6 +99,11 @@ task copyScripts { } } +task copyNodeApiTests(type: Copy) { + from '../../../NodeApi/test' + into "${nodeApiAssetsDir}/NodeApi/test" +} + // Run copyScripts task after CMake external build // And make sure merging assets into output is performed after the scripts copy tasks.configureEach { task -> @@ -99,5 +112,12 @@ tasks.configureEach { task -> } if (task.name == 'mergeDebugAssets') { task.dependsOn(copyScripts) + task.dependsOn(copyNodeApiTests) + } + if (task.name == 'mergeReleaseAssets') { + task.dependsOn(copyScripts) + task.dependsOn(copyNodeApiTests) } } + +preBuild.dependsOn(copyNodeApiTests) diff --git a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java index 23c22307..464bbdef 100644 --- a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java +++ b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java @@ -2,6 +2,8 @@ import android.content.Context; +import java.io.File; + import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -23,6 +25,10 @@ public void javaScriptTests() { Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); assertEquals("com.jsruntimehost.unittests", appContext.getPackageName()); - assertEquals(0, Native.javaScriptTests(appContext)); + Context applicationContext = appContext.getApplicationContext(); + File baseDir = new File(applicationContext.getFilesDir(), "node_api_tests"); + Native.prepareNodeApiTests(applicationContext, baseDir.getAbsolutePath()); + + assertEquals(0, Native.javaScriptTests(applicationContext)); } -} \ No newline at end of file +} diff --git a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Native.java b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Native.java index 158c28e6..f8fa75ce 100644 --- a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Native.java +++ b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Native.java @@ -8,5 +8,6 @@ public class Native { System.loadLibrary("UnitTestsJNI"); } + public static native void prepareNodeApiTests(Context context, String baseDirPath); public static native int javaScriptTests(Context context); } diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index 3db2a37a..af0fbf67 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -15,15 +15,36 @@ FetchContent_MakeAvailable_With_Message(googletest) npm(install --silent WORKING_DIRECTORY ${TESTS_DIR}) +set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_mac.cpp) +set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_mac.cpp) + +if(ANDROID) + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_android.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_android.cpp) +elseif(WIN32) + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_windows.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process.cpp) +endif() + add_library(UnitTestsJNI SHARED JNI.cpp ${UNIT_TESTS_DIR}/Shared/Shared.h - ${UNIT_TESTS_DIR}/Shared/Shared.cpp) + ${UNIT_TESTS_DIR}/Shared/Shared.cpp + ${TESTS_DIR}/NodeApi/node_lite.cpp + ${NODE_LITE_PLATFORM_SRC} + ${NODE_LITE_CHILD_PROCESS_SRC} + ${TESTS_DIR}/NodeApi/node_lite_jsruntimehost.cpp + ${TESTS_DIR}/NodeApi/js_runtime_api.cpp + ${TESTS_DIR}/NodeApi/string_utils.cpp + ${TESTS_DIR}/NodeApi/test_main.cpp) target_compile_definitions(UnitTestsJNI PRIVATE JSRUNTIMEHOST_PLATFORM="${JSRUNTIMEHOST_PLATFORM}") target_include_directories(UnitTestsJNI - PRIVATE ${UNIT_TESTS_DIR}) + PRIVATE ${UNIT_TESTS_DIR} + PRIVATE ${TESTS_DIR}/NodeApi + PRIVATE ${TESTS_DIR}/NodeApi/include + PRIVATE ${REPO_ROOT_DIR}/Core/Node-API/Source) target_link_libraries(UnitTestsJNI PRIVATE log @@ -38,4 +59,11 @@ target_link_libraries(UnitTestsJNI PRIVATE XMLHttpRequest PRIVATE WebSocket PRIVATE gtest_main - PRIVATE Blob) + PRIVATE Blob + PRIVATE napi) + +if(ANDROID) + target_link_libraries(UnitTestsJNI PRIVATE dl) +endif() +target_compile_definitions(UnitTestsJNI + PRIVATE NODE_API_AVAILABLE_NATIVE_TESTS="2_function_arguments,3_callbacks,4_object_factory,5_function_factory") diff --git a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp index fe243eb5..bfecacf1 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp +++ b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include "Babylon/DebugTrace.h" #include @@ -17,17 +19,48 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz jclass webSocketClass{env->FindClass("com/jsruntimehost/unittests/WebSocket")}; java::websocket::WebSocketClient::InitializeJavaWebSocketClass(webSocketClass, env); - android::StdoutLogger::Start(); + jclass contextClass = env->GetObjectClass(context); + jmethodID getApplicationContext = env->GetMethodID(contextClass, "getApplicationContext", "()Landroid/content/Context;"); + jobject applicationContext = env->CallObjectMethod(context, getApplicationContext); + env->DeleteLocalRef(contextClass); - android::global::Initialize(javaVM, context); + android::global::Initialize(javaVM, applicationContext); + + env->DeleteLocalRef(applicationContext); Babylon::DebugTrace::EnableDebugTrace(true); Babylon::DebugTrace::SetTraceOutput([](const char* trace) { printf("%s\n", trace); fflush(stdout); }); auto testResult = RunTests(); - android::StdoutLogger::Stop(); - java::websocket::WebSocketClient::DestructJavaWebSocketClass(env); return testResult; } + +extern "C" JNIEXPORT void JNICALL +Java_com_jsruntimehost_unittests_Native_prepareNodeApiTests(JNIEnv* env, jclass, jobject context, jstring baseDirPath) +{ + AAssetManager* assetManager = nullptr; + if (context != nullptr) + { + jclass contextClass = env->GetObjectClass(context); + jmethodID getAssets = env->GetMethodID(contextClass, "getAssets", "()Landroid/content/res/AssetManager;"); + jobject assets = env->CallObjectMethod(context, getAssets); + env->DeleteLocalRef(contextClass); + if (assets != nullptr) + { + assetManager = AAssetManager_fromJava(env, assets); + env->DeleteLocalRef(assets); + } + } + + std::filesystem::path baseDir; + if (baseDirPath != nullptr) + { + const char* chars = env->GetStringUTFChars(baseDirPath, nullptr); + baseDir = chars; + env->ReleaseStringUTFChars(baseDirPath, chars); + } + + SetNodeApiTestEnvironment(assetManager, baseDir); +} diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index 79d4df92..c8f1fa9f 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -11,9 +11,169 @@ #include #include #include +#include +#include +#include +#include +#include + +#if defined(__ANDROID__) +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../../NodeApi/node_lite.h" +#include "../../NodeApi/test_main.h" +#endif namespace { +#if defined(__ANDROID__) + namespace + { + using namespace std::filesystem; + + void CopyAssetsRecursive(AAssetManager* manager, const std::string& asset_path, const path& destination) + { + AAssetDir* dir = AAssetManager_openDir(manager, asset_path.c_str()); + if (dir == nullptr) + { + return; + } + + const char* filename = nullptr; + while ((filename = AAssetDir_getNextFileName(dir)) != nullptr) + { + std::string child_asset = asset_path.empty() ? filename : asset_path + "/" + filename; + AAsset* asset = AAssetManager_open(manager, child_asset.c_str(), AASSET_MODE_STREAMING); + if (asset != nullptr) + { + create_directories(destination); + std::ofstream output(destination / filename, std::ios::binary); + char buffer[4096]; + int read = 0; + while ((read = AAsset_read(asset, buffer, sizeof(buffer))) > 0) + { + output.write(buffer, read); + } + AAsset_close(asset); + } + else + { + CopyAssetsRecursive(manager, child_asset, destination / filename); + } + } + + AAssetDir_close(dir); + } + + path GetFilesDir() + { + JNIEnv* env = android::global::GetEnvForCurrentThread(); + jobject context = android::global::GetAppContext(); + jclass contextClass = env->GetObjectClass(context); + jmethodID getFilesDir = env->GetMethodID(contextClass, "getFilesDir", "()Ljava/io/File;"); + jobject filesDir = env->CallObjectMethod(context, getFilesDir); + env->DeleteLocalRef(contextClass); + + jclass fileClass = env->GetObjectClass(filesDir); + jmethodID getAbsolutePath = env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;"); + jstring pathString = static_cast(env->CallObjectMethod(filesDir, getAbsolutePath)); + env->DeleteLocalRef(fileClass); + + const char* rawPath = env->GetStringUTFChars(pathString, nullptr); + path resultPath{rawPath}; + env->ReleaseStringUTFChars(pathString, rawPath); + env->DeleteLocalRef(pathString); + env->DeleteLocalRef(filesDir); + + return resultPath; + } + + std::unordered_set ParseNativeSuiteList() + { + std::unordered_set suites; +#ifdef NODE_API_AVAILABLE_NATIVE_TESTS + std::stringstream stream(NODE_API_AVAILABLE_NATIVE_TESTS); + std::string entry; + while (std::getline(stream, entry, ',')) + { + if (!entry.empty()) + { + suites.insert(entry); + } + } +#endif + return suites; + } + + std::optional& OverrideBaseDir() + { + static std::optional baseDirOverride{}; + return baseDirOverride; + } + + AAssetManager*& OverrideAssetManager() + { + static AAssetManager* assetManager{}; + return assetManager; + } + + void ConfigureNodeApiTests() + { + static std::once_flag onceFlag; + std::call_once(onceFlag, []() { + path baseDir; + if (OverrideBaseDir()) + { + baseDir = *OverrideBaseDir(); + } + else + { + baseDir = GetFilesDir() / "node_api_tests"; + } + std::error_code ec; + std::filesystem::remove_all(baseDir, ec); + std::filesystem::create_directories(baseDir); + + AAssetManager* assetManagerNative = OverrideAssetManager(); + if (assetManagerNative == nullptr) + { + auto assetManagerWrapper = android::global::GetAppContext().getAssets(); + assetManagerNative = assetManagerWrapper; + } + + if (assetManagerNative != nullptr) + { + CopyAssetsRecursive(assetManagerNative, "NodeApi/test", baseDir); + } + + node_api_tests::NodeApiTestConfig config{}; + config.js_root = baseDir; + config.run_script = [baseDir](const path& script) { + node_api_tests::NodeLiteRuntime::Callbacks callbacks; + callbacks.stdout_callback = [](const std::string& message) { + __android_log_write(ANDROID_LOG_INFO, "NodeApiTests", message.c_str()); + }; + callbacks.stderr_callback = [](const std::string& message) { + __android_log_write(ANDROID_LOG_ERROR, "NodeApiTests", message.c_str()); + }; + return node_api_tests::RunNodeLiteScript(baseDir, script, std::move(callbacks)); + }; + config.enabled_native_suites = ParseNativeSuiteList(); + + node_api_tests::InitializeNodeApiTests(config); + }); + } + } +#endif + const char* EnumToString(Babylon::Polyfills::Console::LogLevel logLevel) { switch (logLevel) @@ -117,6 +277,19 @@ TEST(Console, Log) int RunTests() { +#if defined(__ANDROID__) + ConfigureNodeApiTests(); +#endif testing::InitGoogleTest(); +#if defined(__ANDROID__) + node_api_tests::RegisterNodeApiTests(); +#endif return RUN_ALL_TESTS(); } +#if defined(__ANDROID__) +void SetNodeApiTestEnvironment(AAssetManager* assetManager, const std::filesystem::path& baseDir) +{ + OverrideAssetManager() = assetManager; + OverrideBaseDir() = baseDir; +} +#endif diff --git a/Tests/UnitTests/Shared/Shared.h b/Tests/UnitTests/Shared/Shared.h index b1610fa8..14abef97 100644 --- a/Tests/UnitTests/Shared/Shared.h +++ b/Tests/UnitTests/Shared/Shared.h @@ -1,3 +1,10 @@ #pragma once -int RunTests(); \ No newline at end of file +#include + +int RunTests(); + +#if defined(__ANDROID__) +struct AAssetManager; +void SetNodeApiTestEnvironment(AAssetManager* assetManager, const std::filesystem::path& baseDir); +#endif diff --git a/Tests/package-lock.json b/Tests/package-lock.json index e627a786..3964da22 100644 --- a/Tests/package-lock.json +++ b/Tests/package-lock.json @@ -111,6 +111,7 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2384,6 +2385,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2410,6 +2412,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2676,6 +2679,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -5152,6 +5156,7 @@ "integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -5201,6 +5206,7 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", From 4e73539cd2ae9e507b8223a7cc6367c68bdb44a7 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 12 Oct 2025 17:09:47 -0700 Subject: [PATCH 4/6] Run the macOS NodeApiTests under sanitizers, which found another bug -- a use-after-free. Will check sanitizers under Android next --- CMakeLists.txt | 26 +++++++++++- .../Source/js_native_api_javascriptcore.cc | 3 ++ .../Source/js_native_api_javascriptcore.h | 2 + Tests/NodeApi/CMakeLists.txt | 12 +++--- Tests/NodeApi/node_lite_jsruntimehost.cpp | 9 ++-- Tests/NodeApi/node_lite_windows.cpp | 3 +- Tests/UnitTests/Android/app/build.gradle | 15 ++++++- .../com/jsruntimehost/unittests/Main.java | 5 --- .../com/jsruntimehost/unittests/Native.java | 1 - .../Android/app/src/main/cpp/CMakeLists.txt | 12 +++--- .../Android/app/src/main/cpp/JNI.cpp | 42 +++++-------------- Tests/UnitTests/Shared/Shared.cpp | 1 - Tests/UnitTests/Shared/Shared.h | 5 --- 13 files changed, 72 insertions(+), 64 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 76a70d6f..89cd569e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,8 +13,8 @@ FetchContent_Declare(arcana.cpp GIT_REPOSITORY https://github.com/microsoft/arcana.cpp.git GIT_TAG 1a8a5d6e95413ed14b38a6ac9419048f9a9c8009) FetchContent_Declare(AndroidExtensions - GIT_REPOSITORY https://github.com/bghgary/AndroidExtensions.git - GIT_TAG 7d88a601fda9892791e7b4e994e375e049615688) + GIT_REPOSITORY https://github.com/matthargett/AndroidExtensions.git + GIT_TAG 4ffcc4ab149a02b9ee72622eee708b2753fd2093) FetchContent_Declare(asio GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git GIT_TAG f693a3eb7fe72a5f19b975289afc4f437d373d9c) @@ -72,6 +72,28 @@ option(JSRUNTIMEHOST_POLYFILL_URL "Include JsRuntimeHost Polyfill URL and URLSea option(JSRUNTIMEHOST_POLYFILL_ABORT_CONTROLLER "Include JsRuntimeHost Polyfills AbortController and AbortSignal." ON) option(JSRUNTIMEHOST_POLYFILL_WEBSOCKET "Include JsRuntimeHost Polyfill WebSocket." ON) option(JSRUNTIMEHOST_POLYFILL_BLOB "Include JsRuntimeHost Polyfill Blob." ON) +option(JSR_ENABLE_ASAN "Enable AddressSanitizer support." OFF) + +if(JSR_ENABLE_ASAN) + message(STATUS "JSR_ENABLE_ASAN=ON (appending address sanitizer flags)") + set(JSR_ASAN_COMPILE_FLAGS "-fsanitize=address -fno-omit-frame-pointer") + foreach(var CMAKE_C_FLAGS CMAKE_CXX_FLAGS) + if(NOT "${${var}}" MATCHES "-fsanitize=address") + set(${var} "${${var}} ${JSR_ASAN_COMPILE_FLAGS}") + endif() + set(${var} "${${var}}" CACHE STRING "" FORCE) + endforeach() + + set(JSR_ASAN_LINK_FLAGS "-fsanitize=address") + foreach(var CMAKE_EXE_LINKER_FLAGS CMAKE_SHARED_LINKER_FLAGS) + if(NOT "${${var}}" MATCHES "-fsanitize=address") + set(${var} "${${var}} ${JSR_ASAN_LINK_FLAGS}") + endif() + set(${var} "${${var}}" CACHE STRING "" FORCE) + endforeach() + message(STATUS "ASan compile flags: C=${CMAKE_C_FLAGS} CXX=${CMAKE_CXX_FLAGS}") + message(STATUS "ASan link flags: EXE=${CMAKE_EXE_LINKER_FLAGS} SHARED=${CMAKE_SHARED_LINKER_FLAGS}") +endif() # -------------------------------------------------- diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.cc b/Core/Node-API/Source/js_native_api_javascriptcore.cc index c0606e2a..b9995bda 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.cc +++ b/Core/Node-API/Source/js_native_api_javascriptcore.cc @@ -664,6 +664,9 @@ struct napi_ref__ { CHECK_NAPI(ReferenceInfo::GetObjectId(env, _value, &_objectId)); if (_objectId == 0) { CHECK_NAPI(ReferenceInfo::Initialize(env, _value, [value = _value](ReferenceInfo* info) { + if (info->Env()->shutting_down) { + return; + } auto entry{info->Env()->active_ref_values.find(value)}; // NOTE: The finalizer callback is actually on a "sentinel" JS object that is linked to the // actual JS object we are trying to track. This means it is possible for the tracked object diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index da74596e..77bc1180 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -14,6 +14,7 @@ struct napi_env__ { napi_extended_error_info last_error{nullptr, nullptr, 0, napi_ok}; std::unordered_map active_ref_values{}; std::list strong_refs{}; + bool shutting_down{false}; JSValueRef constructor_info_symbol{}; JSValueRef function_info_symbol{}; @@ -32,6 +33,7 @@ struct napi_env__ { } ~napi_env__() { + shutting_down = true; deinit_refs(); deinit_symbol(wrapper_info_symbol); deinit_symbol(reference_info_symbol); diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt index 1c4ce785..6b329067 100644 --- a/Tests/NodeApi/CMakeLists.txt +++ b/Tests/NodeApi/CMakeLists.txt @@ -21,17 +21,17 @@ function(node_api_copy_test_sources TARGET_NAME) endfunction() if(ANDROID) - set(NODE_LITE_PLATFORM_SRC node_lite_android.cpp) - set(NODE_LITE_CHILD_PROCESS_SRC child_process_android.cpp) + set(NODE_LITE_PLATFORM_SRC node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process_posix.cpp) elseif(APPLE) - set(NODE_LITE_PLATFORM_SRC node_lite_mac.cpp) - set(NODE_LITE_CHILD_PROCESS_SRC child_process_mac.cpp) + set(NODE_LITE_PLATFORM_SRC node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process_posix.cpp) elseif(WIN32) set(NODE_LITE_PLATFORM_SRC node_lite_windows.cpp) set(NODE_LITE_CHILD_PROCESS_SRC child_process.cpp) else() - set(NODE_LITE_PLATFORM_SRC node_lite_mac.cpp) - set(NODE_LITE_CHILD_PROCESS_SRC child_process_mac.cpp) + set(NODE_LITE_PLATFORM_SRC node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC child_process_posix.cpp) message(WARNING "Node-API node_lite platform not yet customized for ${CMAKE_SYSTEM_NAME}; using POSIX defaults.") endif() diff --git a/Tests/NodeApi/node_lite_jsruntimehost.cpp b/Tests/NodeApi/node_lite_jsruntimehost.cpp index 3e47e621..1c524f53 100644 --- a/Tests/NodeApi/node_lite_jsruntimehost.cpp +++ b/Tests/NodeApi/node_lite_jsruntimehost.cpp @@ -68,11 +68,14 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { } } + if (context_ != nullptr) { + JSGlobalContextRelease(context_); + context_ = nullptr; + } + Napi::Detach(napiEnv); env_ = nullptr; - } - - if (context_ != nullptr) { + } else if (context_ != nullptr) { JSGlobalContextRelease(context_); context_ = nullptr; } diff --git a/Tests/NodeApi/node_lite_windows.cpp b/Tests/NodeApi/node_lite_windows.cpp index 6b41d83a..4af34561 100644 --- a/Tests/NodeApi/node_lite_windows.cpp +++ b/Tests/NodeApi/node_lite_windows.cpp @@ -2,8 +2,7 @@ // Licensed under the MIT License. #include -#include "node_lite.h" -#include "string_utils.h" +#include "node_lite.h" namespace node_api_tests { diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index 38557f6e..5bea1370 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -8,6 +8,7 @@ if (project.hasProperty("jsEngine")) { } def nodeApiAssetsDir = "${project.buildDir}/generated/nodeapiassets" +def enableAsan = project.hasProperty("enableAsan") android { namespace 'com.jsruntimehost.unittests' @@ -28,11 +29,15 @@ android { externalNativeBuild { cmake { - arguments ( + def cmakeArgs = [ "-DANDROID_STL=c++_shared", "-DNAPI_JAVASCRIPT_ENGINE=${jsEngine}", "-DJSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR=ON" - ) + ] + if (enableAsan) { + cmakeArgs += "-DJSR_ENABLE_ASAN=ON" + } + arguments(*cmakeArgs) } } @@ -64,6 +69,12 @@ android { viewBinding true } + packagingOptions { + if (enableAsan) { + doNotStrip "**/*.so" + } + } + sourceSets { main { assets.srcDirs += [nodeApiAssetsDir] diff --git a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java index 464bbdef..069c698d 100644 --- a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java +++ b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Main.java @@ -2,8 +2,6 @@ import android.content.Context; -import java.io.File; - import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -26,9 +24,6 @@ public void javaScriptTests() { assertEquals("com.jsruntimehost.unittests", appContext.getPackageName()); Context applicationContext = appContext.getApplicationContext(); - File baseDir = new File(applicationContext.getFilesDir(), "node_api_tests"); - Native.prepareNodeApiTests(applicationContext, baseDir.getAbsolutePath()); - assertEquals(0, Native.javaScriptTests(applicationContext)); } } diff --git a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Native.java b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Native.java index f8fa75ce..158c28e6 100644 --- a/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Native.java +++ b/Tests/UnitTests/Android/app/src/androidTest/java/com/jsruntimehost/unittests/Native.java @@ -8,6 +8,5 @@ public class Native { System.loadLibrary("UnitTestsJNI"); } - public static native void prepareNodeApiTests(Context context, String baseDirPath); public static native int javaScriptTests(Context context); } diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index af0fbf67..41ce387a 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -15,15 +15,15 @@ FetchContent_MakeAvailable_With_Message(googletest) npm(install --silent WORKING_DIRECTORY ${TESTS_DIR}) -set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_mac.cpp) -set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_mac.cpp) +set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_posix.cpp) +set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_posix.cpp) -if(ANDROID) - set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_android.cpp) - set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_android.cpp) -elseif(WIN32) +if(WIN32) set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_windows.cpp) set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process.cpp) +elseif(APPLE) + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_mac.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_mac.cpp) endif() add_library(UnitTestsJNI SHARED diff --git a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp index bfecacf1..7f26e796 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp +++ b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp @@ -2,8 +2,6 @@ #include #include #include -#include -#include #include #include "Babylon/DebugTrace.h" #include @@ -24,7 +22,17 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz jobject applicationContext = env->CallObjectMethod(context, getApplicationContext); env->DeleteLocalRef(contextClass); - android::global::Initialize(javaVM, applicationContext); + jclass appContextClass = env->GetObjectClass(applicationContext); + jmethodID getAssets = env->GetMethodID(appContextClass, "getAssets", "()Landroid/content/res/AssetManager;"); + jobject assetManagerObj = env->CallObjectMethod(applicationContext, getAssets); + env->DeleteLocalRef(appContextClass); + + android::global::Initialize(javaVM, applicationContext, assetManagerObj); + + if (assetManagerObj != nullptr) + { + env->DeleteLocalRef(assetManagerObj); + } env->DeleteLocalRef(applicationContext); @@ -36,31 +44,3 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz java::websocket::WebSocketClient::DestructJavaWebSocketClass(env); return testResult; } - -extern "C" JNIEXPORT void JNICALL -Java_com_jsruntimehost_unittests_Native_prepareNodeApiTests(JNIEnv* env, jclass, jobject context, jstring baseDirPath) -{ - AAssetManager* assetManager = nullptr; - if (context != nullptr) - { - jclass contextClass = env->GetObjectClass(context); - jmethodID getAssets = env->GetMethodID(contextClass, "getAssets", "()Landroid/content/res/AssetManager;"); - jobject assets = env->CallObjectMethod(context, getAssets); - env->DeleteLocalRef(contextClass); - if (assets != nullptr) - { - assetManager = AAssetManager_fromJava(env, assets); - env->DeleteLocalRef(assets); - } - } - - std::filesystem::path baseDir; - if (baseDirPath != nullptr) - { - const char* chars = env->GetStringUTFChars(baseDirPath, nullptr); - baseDir = chars; - env->ReleaseStringUTFChars(baseDirPath, chars); - } - - SetNodeApiTestEnvironment(assetManager, baseDir); -} diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index c8f1fa9f..7f835274 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include diff --git a/Tests/UnitTests/Shared/Shared.h b/Tests/UnitTests/Shared/Shared.h index 14abef97..acc4548b 100644 --- a/Tests/UnitTests/Shared/Shared.h +++ b/Tests/UnitTests/Shared/Shared.h @@ -3,8 +3,3 @@ #include int RunTests(); - -#if defined(__ANDROID__) -struct AAssetManager; -void SetNodeApiTestEnvironment(AAssetManager* assetManager, const std::filesystem::path& baseDir); -#endif From 27ad00075b534464c2962db6df007d317a98e345 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 15 Oct 2025 14:29:56 -0700 Subject: [PATCH 5/6] Fix build errors. This deduplicates struct definitions that were inlined into our copy of the JSI code. --- Core/Node-API-JSI/Include/napi/napi.h | 37 +----- Tests/NodeApi/child_process_mac.cpp | 166 -------------------------- Tests/NodeApi/node_lite.cpp | 1 + Tests/NodeApi/node_lite_mac.cpp | 39 ------ 4 files changed, 2 insertions(+), 241 deletions(-) delete mode 100644 Tests/NodeApi/child_process_mac.cpp delete mode 100644 Tests/NodeApi/node_lite_mac.cpp diff --git a/Core/Node-API-JSI/Include/napi/napi.h b/Core/Node-API-JSI/Include/napi/napi.h index dbca8051..e5f0604d 100644 --- a/Core/Node-API-JSI/Include/napi/napi.h +++ b/Core/Node-API-JSI/Include/napi/napi.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -8,42 +9,6 @@ #include #include -// Copied from js_native_api_types.h (https://git.io/J8aI5) -typedef enum { - napi_default = 0, - napi_writable = 1 << 0, - napi_enumerable = 1 << 1, - napi_configurable = 1 << 2, -} napi_property_attributes; - -typedef enum { - // ES6 types (corresponds to typeof) - napi_undefined, - napi_null, - napi_boolean, - napi_number, - napi_string, - napi_symbol, - napi_object, - napi_function, - napi_external, -} napi_valuetype; - -typedef enum { - napi_int8_array, - napi_uint8_array, - napi_uint8_clamped_array, - napi_int16_array, - napi_uint16_array, - napi_int32_array, - napi_uint32_array, - napi_float32_array, - napi_float64_array, - // JSI doesn't support bigint. - // napi_bigint64_array, - // napi_biguint64_array, -} napi_typedarray_type; - struct napi_env__ { napi_env__(facebook::jsi::Runtime& rt) : rt{rt} diff --git a/Tests/NodeApi/child_process_mac.cpp b/Tests/NodeApi/child_process_mac.cpp deleted file mode 100644 index 8e5f0d18..00000000 --- a/Tests/NodeApi/child_process_mac.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "child_process.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Verify the condition. -// - If true, resume execution. -// - If false, print a message to stderr and exit the app with exit code 1. -#ifndef VerifyElseExit -#define VerifyElseExit(condition) \ - do { \ - if (!(condition)) { \ - ExitOnError(#condition, nullptr); \ - } \ - } while (false) -#endif - -// Verify the condition. -// - If true, resume execution. -// - If false, destroy the passed `posix_spawn_file_actions_t* actions`, then -// print a message to stderr and exit the app with exit code 1. -#ifndef VerifyElseExitWithCleanup -#define VerifyElseExitWithCleanup(condition, actions_ptr) \ - do { \ - if (!(condition)) { \ - ExitOnError(#condition, actions_ptr); \ - } \ - } while (false) -#endif - -extern char** environ; - -namespace node_api_tests { - -namespace { - -std::string ReadFromFd(int fd); -void ExitOnError(const char* message, posix_spawn_file_actions_t* actions); - -} // namespace - -ProcessResult SpawnSync(std::string_view command, - std::vector args) { - ProcessResult result{}; - - // These int arrays each comprise two file descriptors: { readEnd, writeEnd }. - int stdout_pipe[2], stderr_pipe[2]; - VerifyElseExit(pipe(stdout_pipe) == 0); - VerifyElseExit(pipe(stderr_pipe) == 0); - - posix_spawn_file_actions_t actions; - VerifyElseExit(posix_spawn_file_actions_init(&actions) == 0); - - VerifyElseExitWithCleanup(posix_spawn_file_actions_adddup2( - &actions, stdout_pipe[1], STDOUT_FILENO) == 0, - &actions); - VerifyElseExitWithCleanup(posix_spawn_file_actions_adddup2( - &actions, stderr_pipe[1], STDERR_FILENO) == 0, - &actions); - - VerifyElseExitWithCleanup( - posix_spawn_file_actions_addclose(&actions, stdout_pipe[0]) == 0, - &actions); - VerifyElseExitWithCleanup( - posix_spawn_file_actions_addclose(&actions, stderr_pipe[0]) == 0, - &actions); - - std::vector argv; - argv.push_back(strdup(std::string(command).c_str())); - for (const std::string& arg : args) { - argv.push_back(strdup(arg.c_str())); - } - argv.push_back(nullptr); - - pid_t pid; - VerifyElseExitWithCleanup( - posix_spawnp(&pid, argv[0], &actions, nullptr, argv.data(), environ) == 0, - &actions); - - posix_spawn_file_actions_destroy(&actions); - - // Close the write ends of the pipes. - close(stdout_pipe[1]); - close(stderr_pipe[1]); - - int wait_status; - pid_t waited_pid; - do { - waited_pid = waitpid(pid, &wait_status, 0); - } while (waited_pid == -1 && errno == EINTR); - - VerifyElseExit(waited_pid == pid); - - if (WIFEXITED(wait_status)) { - result.status = WEXITSTATUS(wait_status); - } else if (WIFSIGNALED(wait_status)) { - result.status = 128 + WTERMSIG(wait_status); - } else { - result.status = 1; - } - result.std_output = ReadFromFd(stdout_pipe[0]); - result.std_error = ReadFromFd(stderr_pipe[0]); - - // Close the read ends of the pipes. - close(stdout_pipe[0]); - close(stderr_pipe[0]); - - for (char* arg : argv) { - free(arg); - } - - return result; -} - -namespace { - -std::string ReadFromFd(int fd) { - std::string result; - constexpr size_t bufferSize = 4096; - char buffer[bufferSize]; - ssize_t bytesRead; - while (true) { - bytesRead = read(fd, buffer, bufferSize); - if (bytesRead > 0) { - result.append(buffer, bytesRead); - continue; - } - - if (bytesRead == 0) { - break; - } - - if (errno == EINTR) { - continue; - } - - ExitOnError("read", nullptr); - } - return result; -} - -// Format a readable error message, print it to console, and exit from the -// application. -void ExitOnError(const char* message, posix_spawn_file_actions_t* actions) { - int err = errno; - const char* err_msg = strerror(err); - - fprintf(stderr, "%s failed with error %d: %s\n", message, err, err_msg); - - if (actions != nullptr) { - posix_spawn_file_actions_destroy(actions); - } - - exit(1); -} - -} // namespace -} // namespace node_api_tests diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index d82f3bb2..0db78f9b 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include diff --git a/Tests/NodeApi/node_lite_mac.cpp b/Tests/NodeApi/node_lite_mac.cpp deleted file mode 100644 index ce8ffb17..00000000 --- a/Tests/NodeApi/node_lite_mac.cpp +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#include -#include "node_lite.h" -#include "string_utils.h" - -namespace node_api_tests { - -//============================================================================= -// NodeLitePlatform implementation -//============================================================================= - -/*static*/ void* NodeLitePlatform::LoadFunction( - napi_env env, - const std::filesystem::path& lib_path, - const std::string& function_name) noexcept { - void* library_handle = dlopen(lib_path.string().c_str(), RTLD_NOW | RTLD_LOCAL); - if (library_handle == nullptr) { - const char* error_message = dlerror(); - NODE_LITE_ASSERT(false, - "Failed to load dynamic library: %s. Error: %s", - lib_path.c_str(), - error_message != nullptr ? error_message : "Unknown error"); - return nullptr; - } - - dlerror(); // Clear any existing error state before dlsym. - void* symbol = dlsym(library_handle, function_name.c_str()); - const char* error_message = dlerror(); - NODE_LITE_ASSERT(error_message == nullptr, - "Failed to resolve symbol: %s in %s. Error: %s", - function_name.c_str(), - lib_path.c_str(), - error_message != nullptr ? error_message : "Unknown error"); - return symbol; -} - -} // namespace node_api_tests From 6824da26d9a180ad90d20a1ad7671d38ef72000f Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 15 Oct 2025 21:03:50 -0700 Subject: [PATCH 6/6] always build the napi tests --- CMakeLists.txt | 15 + Core/Node-API/CMakeLists.txt | 5 +- Tests/NodeApi/CMakeLists.txt | 31 +- Tests/NodeApi/child_process_android.cpp | 14 + Tests/NodeApi/child_process_posix.cpp | 189 ++++++ Tests/NodeApi/main.cpp | 81 +++ Tests/NodeApi/node_lite.cpp | 598 +++++++++--------- Tests/NodeApi/node_lite_android.cpp | 22 + Tests/NodeApi/node_lite_posix.cpp | 43 ++ Tests/UnitTests/Android/app/build.gradle | 69 ++ .../Android/app/src/main/cpp/CMakeLists.txt | 13 +- Tests/UnitTests/Android/gradle.properties | 2 +- 12 files changed, 762 insertions(+), 320 deletions(-) create mode 100644 Tests/NodeApi/child_process_android.cpp create mode 100644 Tests/NodeApi/child_process_posix.cpp create mode 100644 Tests/NodeApi/main.cpp create mode 100644 Tests/NodeApi/node_lite_android.cpp create mode 100644 Tests/NodeApi/node_lite_posix.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 89cd569e..b5d7a347 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,21 @@ endif() FetchContent_MakeAvailable_With_Message(arcana.cpp) set_property(TARGET arcana PROPERTY FOLDER Dependencies) +if(ANDROID) + FetchContent_GetProperties(AndroidExtensions) + if(NOT AndroidExtensions_POPULATED) + FetchContent_Populate(AndroidExtensions) + FetchContent_GetProperties(AndroidExtensions) + message(STATUS "Patching AndroidExtensions Globals.cpp in ${androidextensions_SOURCE_DIR}") + file(COPY + ${CMAKE_CURRENT_SOURCE_DIR}/patches/AndroidExtensions/Globals.cpp + DESTINATION ${androidextensions_SOURCE_DIR}/Source) + add_subdirectory(${androidextensions_SOURCE_DIR} ${androidextensions_BINARY_DIR}) + else() + add_subdirectory(${androidextensions_SOURCE_DIR} ${androidextensions_BINARY_DIR}) + endif() +endif() + if(JSRUNTIMEHOST_POLYFILL_XMLHTTPREQUEST) FetchContent_MakeAvailable_With_Message(UrlLib) set_property(TARGET UrlLib PROPERTY FOLDER Dependencies) diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 1e8b8611..5089d5d8 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -7,7 +7,10 @@ elseif(ANDROID) set(NAPI_JAVASCRIPT_ENGINE "V8" CACHE STRING "JavaScript engine for Node-API") elseif(UNIX) set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") - set(JAVASCRIPTCORE_LIBRARY "/usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.1.so" CACHE STRING "Path to the JavaScriptCore shared library") + find_library(JAVASCRIPTCORE_LIBRARY javascriptcoregtk-4.1) + if(NOT JAVASCRIPTCORE_LIBRARY) + message(FATAL_ERROR "JavaScriptCore library not found. Please install libwebkit2gtk-4.1-dev") + endif() else() message(FATAL_ERROR "Unable to select Node-API JavaScript engine for platform") endif() diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt index 6b329067..256782ae 100644 --- a/Tests/NodeApi/CMakeLists.txt +++ b/Tests/NodeApi/CMakeLists.txt @@ -1,15 +1,13 @@ set(NODE_API_TEST_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) -option(JSR_NODE_API_BUILD_NATIVE_TESTS "Build Node-API native addon test modules" OFF) +option(JSR_NODE_API_BUILD_NATIVE_TESTS "Build Node-API native addon test modules" ON) -if(JSR_NODE_API_BUILD_NATIVE_TESTS) - set(JSR_NODE_API_NATIVE_TEST_DIRS - 2_function_arguments - 3_callbacks - 4_object_factory - 5_function_factory - ) -endif() +set(JSR_NODE_API_NATIVE_TEST_DIRS + 2_function_arguments + 3_callbacks + 4_object_factory + 5_function_factory +) function(node_api_copy_test_sources TARGET_NAME) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD @@ -98,17 +96,26 @@ add_dependencies(NodeApiTests node_lite) add_custom_target(NodeApiModules) +# Always define which tests are available, regardless of whether we build the native modules +list(JOIN JSR_NODE_API_NATIVE_TEST_DIRS "," NODE_API_NATIVE_TESTS_STRING) +target_compile_definitions(node_lite + PRIVATE + NODE_API_AVAILABLE_NATIVE_TESTS=\"${NODE_API_NATIVE_TESTS_STRING}\" +) +target_compile_definitions(NodeApiTests + PRIVATE + NODE_API_AVAILABLE_NATIVE_TESTS=\"${NODE_API_NATIVE_TESTS_STRING}\" +) + +# Only define NODE_API_TESTS_HAVE_NATIVE_MODULES when actually building native modules if(JSR_NODE_API_BUILD_NATIVE_TESTS) - list(JOIN JSR_NODE_API_NATIVE_TEST_DIRS "," NODE_API_NATIVE_TESTS_STRING) target_compile_definitions(node_lite PRIVATE NODE_API_TESTS_HAVE_NATIVE_MODULES=1 - NODE_API_AVAILABLE_NATIVE_TESTS=\"${NODE_API_NATIVE_TESTS_STRING}\" ) target_compile_definitions(NodeApiTests PRIVATE NODE_API_TESTS_HAVE_NATIVE_MODULES=1 - NODE_API_AVAILABLE_NATIVE_TESTS=\"${NODE_API_NATIVE_TESTS_STRING}\" ) endif() diff --git a/Tests/NodeApi/child_process_android.cpp b/Tests/NodeApi/child_process_android.cpp new file mode 100644 index 00000000..cb70ccd1 --- /dev/null +++ b/Tests/NodeApi/child_process_android.cpp @@ -0,0 +1,14 @@ +#include "child_process.h" + +namespace node_api_tests { + +ProcessResult SpawnSync(std::string_view /*command*/, std::vector /*args*/) +{ + ProcessResult result{}; + result.status = -1; + result.std_error = "child_process.spawnSync is not supported on this platform."; + result.std_output.clear(); + return result; +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/child_process_posix.cpp b/Tests/NodeApi/child_process_posix.cpp new file mode 100644 index 00000000..bc461b0e --- /dev/null +++ b/Tests/NodeApi/child_process_posix.cpp @@ -0,0 +1,189 @@ +#include "child_process.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__ANDROID__) +#include +#endif + +#if !defined(__ANDROID__) +#include +#elif (__ANDROID_API__ >= 29) +#include +#endif + +#ifndef VerifyElseExit +#define VerifyElseExit(condition) \ + do { \ + if (!(condition)) { \ + ExitOnError(#condition, nullptr); \ + } \ + } while (false) +#endif + +#ifndef VerifyElseExitWithCleanup +#define VerifyElseExitWithCleanup(condition, actions_ptr) \ + do { \ + if (!(condition)) { \ + ExitOnError(#condition, actions_ptr); \ + } \ + } while (false) +#endif + +#if defined(__ANDROID__) && (__ANDROID_API__ < 29) + +namespace node_api_tests { + +ProcessResult SpawnSync(std::string_view /*command*/, + std::vector /*args*/) { + ProcessResult result{}; + result.status = -1; + result.std_error = "child_process.spawnSync is not supported on this platform."; + result.std_output.clear(); + return result; +} + +} // namespace node_api_tests + +#else + +extern char** environ; + +namespace node_api_tests { + +namespace { + +std::string ReadFromFd(int fd); +void ExitOnError(const char* message, posix_spawn_file_actions_t* actions); + +} // namespace + +ProcessResult SpawnSync(std::string_view command, + std::vector args) { + ProcessResult result{}; + + // These int arrays each comprise two file descriptors: { readEnd, writeEnd }. + int stdout_pipe[2], stderr_pipe[2]; + VerifyElseExit(pipe(stdout_pipe) == 0); + VerifyElseExit(pipe(stderr_pipe) == 0); + + posix_spawn_file_actions_t actions; + VerifyElseExit(posix_spawn_file_actions_init(&actions) == 0); + + VerifyElseExitWithCleanup(posix_spawn_file_actions_adddup2( + &actions, stdout_pipe[1], STDOUT_FILENO) == 0, + &actions); + VerifyElseExitWithCleanup(posix_spawn_file_actions_adddup2( + &actions, stderr_pipe[1], STDERR_FILENO) == 0, + &actions); + + VerifyElseExitWithCleanup( + posix_spawn_file_actions_addclose(&actions, stdout_pipe[0]) == 0, + &actions); + VerifyElseExitWithCleanup( + posix_spawn_file_actions_addclose(&actions, stderr_pipe[0]) == 0, + &actions); + + std::vector argv; + argv.push_back(strdup(std::string(command).c_str())); + for (const std::string& arg : args) { + argv.push_back(strdup(arg.c_str())); + } + argv.push_back(nullptr); + + pid_t pid; + VerifyElseExitWithCleanup( + posix_spawnp(&pid, argv[0], &actions, nullptr, argv.data(), environ) == 0, + &actions); + + posix_spawn_file_actions_destroy(&actions); + + // Close the write ends of the pipes. + close(stdout_pipe[1]); + close(stderr_pipe[1]); + + int wait_status; + pid_t waited_pid; + do { + waited_pid = waitpid(pid, &wait_status, 0); + } while (waited_pid == -1 && errno == EINTR); + + VerifyElseExit(waited_pid == pid); + + if (WIFEXITED(wait_status)) { + result.status = WEXITSTATUS(wait_status); + } else if (WIFSIGNALED(wait_status)) { + result.status = 128 + WTERMSIG(wait_status); + } else { + result.status = 1; + } + result.std_output = ReadFromFd(stdout_pipe[0]); + result.std_error = ReadFromFd(stderr_pipe[0]); + + // Close the read ends of the pipes. + close(stdout_pipe[0]); + close(stderr_pipe[0]); + + for (char* arg : argv) { + free(arg); + } + + return result; +} + +namespace { + +std::string ReadFromFd(int fd) { + std::string result; + constexpr size_t bufferSize = 4096; + char buffer[bufferSize]; + ssize_t bytesRead; + while (true) { + bytesRead = read(fd, buffer, bufferSize); + if (bytesRead > 0) { + result.append(buffer, bytesRead); + continue; + } + + if (bytesRead == 0) { + break; + } + + if (errno == EINTR) { + continue; + } + + ExitOnError("read", nullptr); + } + return result; +} + +// Format a readable error message, print it to console, and exit from the +// application. +void ExitOnError(const char* message, posix_spawn_file_actions_t* actions) { + int err = errno; + const char* err_msg = strerror(err); + + fprintf(stderr, "%s failed with error %d: %s\n", message, err, err_msg); + + if (actions != nullptr) { + posix_spawn_file_actions_destroy(actions); + } + + exit(1); +} + +} // namespace + +} // namespace node_api_tests + +#endif // __ANDROID__ diff --git a/Tests/NodeApi/main.cpp b/Tests/NodeApi/main.cpp new file mode 100644 index 00000000..1565693c --- /dev/null +++ b/Tests/NodeApi/main.cpp @@ -0,0 +1,81 @@ +#include "test_main.h" + +#include + +#include +#include +#include +#include + +#include "child_process.h" + +namespace fs = std::filesystem; + +namespace { + +fs::path ResolveNodeLitePath(const fs::path& exe_path) { + fs::path nodeLitePath = exe_path; + nodeLitePath.replace_filename("node_lite"); +#if defined(_WIN32) + nodeLitePath += ".exe"; +#endif + return nodeLitePath; +} + +fs::path ResolveTestsRoot(const fs::path& exe_path) { + fs::path testRootPath = exe_path.parent_path(); + fs::path js_root = testRootPath / "test"; + if (!fs::exists(js_root)) { + testRootPath = testRootPath.parent_path(); + js_root = testRootPath / "test"; + } + return js_root; +} + +std::unordered_set ParseEnabledNativeSuites() { + std::unordered_set suites; +#ifdef NODE_API_AVAILABLE_NATIVE_TESTS + std::stringstream stream(NODE_API_AVAILABLE_NATIVE_TESTS); + std::string entry; + while (std::getline(stream, entry, ',')) { + if (!entry.empty()) { + suites.insert(entry); + } + } +#endif + return suites; +} + +} // namespace + +int main(int argc, char** argv) { + fs::path exe_path = fs::canonical(argv[0]); + fs::path js_root = ResolveTestsRoot(exe_path); + if (!fs::exists(js_root)) { + std::cerr << "Error: Cannot find Node-API test directory." << std::endl; + return EXIT_FAILURE; + } + + fs::path node_lite_path = ResolveNodeLitePath(exe_path); + if (!fs::exists(node_lite_path)) { + std::cerr << "Error: Cannot find node_lite executable at " + << node_lite_path << std::endl; + return EXIT_FAILURE; + } + + node_api_tests::NodeApiTestConfig config{}; + config.js_root = js_root; + config.run_script = + [node_lite_path](const fs::path& script_path) + -> node_api_tests::ProcessResult { + return node_api_tests::SpawnSync(node_lite_path.string(), + {script_path.string()}); + }; + config.enabled_native_suites = ParseEnabledNativeSuites(); + + node_api_tests::InitializeNodeApiTests(config); + + ::testing::InitGoogleTest(&argc, argv); + node_api_tests::RegisterNodeApiTests(); + return RUN_ALL_TESTS(); +} diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index 0db78f9b..e7e6ffaf 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -1,51 +1,51 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#include "node_lite.h" -#include "js_runtime_api.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "child_process.h" +#include "node_lite.h" +#include "js_runtime_api.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "child_process.h" namespace fs = std::filesystem; namespace node_api_tests { -namespace { - -std::mutex& ErrorHandlerMutex() { - static std::mutex mutex; - return mutex; -} - -void DefaultFatalErrorHandler(const NodeLiteFatalErrorInfo& info) { - if (!info.message.empty()) { - std::cerr << info.message; - if (!info.details.empty()) { - std::cerr << '\n' << info.details; - } - std::cerr << std::endl; - } else if (!info.details.empty()) { - std::cerr << info.details << std::endl; - } - std::exit(info.exit_code); -} - -NodeApiRef MakeNodeApiRef(napi_env env, napi_value value) { - napi_ref ref{}; - NODE_LITE_CALL(napi_create_reference(env, value, 1, &ref)); - return NodeApiRef(ref, NodeApiRefDeleter(env)); +namespace { + +std::mutex& ErrorHandlerMutex() { + static std::mutex mutex; + return mutex; +} + +void DefaultFatalErrorHandler(const NodeLiteFatalErrorInfo& info) { + if (!info.message.empty()) { + std::cerr << info.message; + if (!info.details.empty()) { + std::cerr << '\n' << info.details; + } + std::cerr << std::endl; + } else if (!info.details.empty()) { + std::cerr << info.details << std::endl; + } + std::exit(info.exit_code); +} + +NodeApiRef MakeNodeApiRef(napi_env env, napi_value value) { + napi_ref ref{}; + NODE_LITE_CALL(napi_create_reference(env, value, 1, &ref)); + return NodeApiRef(ref, NodeApiRefDeleter(env)); } template @@ -292,39 +292,39 @@ std::string NodeLiteModule::ReadModuleFileText(napi_env env) { } std::string jsFilePath = args[1]; - std::unique_ptr runtime = NodeLiteRuntime::Create( - std::move(taskRunner), - js_root.string(), - std::move(args), - NodeLiteRuntime::Callbacks{}); + std::unique_ptr runtime = NodeLiteRuntime::Create( + std::move(taskRunner), + js_root.string(), + std::move(args), + NodeLiteRuntime::Callbacks{}); runtime->RunTestScript(jsFilePath); } /*static*/ std::unique_ptr NodeLiteRuntime::Create( - std::shared_ptr task_runner, - std::string js_root, - std::vector args, - Callbacks callbacks) { - std::unique_ptr runtime = - std::make_unique(PrivateTag{}, - std::move(task_runner), - std::move(js_root), - std::move(args), - std::move(callbacks)); - runtime->Initialize(); - return std::unique_ptr(runtime.release()); -} - -NodeLiteRuntime::NodeLiteRuntime( - PrivateTag, - std::shared_ptr task_runner, - std::string js_root, - std::vector args, - Callbacks callbacks) - : task_runner_(std::move(task_runner)), - js_root_(std::move(js_root)), - args_(std::move(args)), - callbacks_(std::move(callbacks)) {} + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks) { + std::unique_ptr runtime = + std::make_unique(PrivateTag{}, + std::move(task_runner), + std::move(js_root), + std::move(args), + std::move(callbacks)); + runtime->Initialize(); + return std::unique_ptr(runtime.release()); +} + +NodeLiteRuntime::NodeLiteRuntime( + PrivateTag, + std::shared_ptr task_runner, + std::string js_root, + std::vector args, + Callbacks callbacks) + : task_runner_(std::move(task_runner)), + js_root_(std::move(js_root)), + args_(std::move(args)), + callbacks_(std::move(callbacks)) {} void NodeLiteRuntime::Initialize() { env_holder_ = @@ -583,9 +583,9 @@ void NodeLiteRuntime::DefineBuiltInModules() { } } -void NodeLiteRuntime::DefineGlobalFunctions() { - NodeApiHandleScope scope{env_}; - napi_value global = NodeApi::GetGlobal(env_); +void NodeLiteRuntime::DefineGlobalFunctions() { + NodeApiHandleScope scope{env_}; + napi_value global = NodeApi::GetGlobal(env_); // Add global.global NodeApi::SetProperty(env_, global, "global", global); @@ -650,12 +650,8 @@ void NodeLiteRuntime::DefineGlobalFunctions() { // process.execPath NodeApi::SetPropertyString(env_, process_obj, "execPath", args_[0]); -// process.target_config -#ifdef NDEBUG + // process.target_config - always use "Release" to match CMAKE module output directory NodeApi::SetPropertyString(env_, process_obj, "target_config", "Release"); -#else - NodeApi::SetPropertyString(env_, process_obj, "target_config", "Debug"); -#endif // process.platform #ifdef WIN32 @@ -709,46 +705,46 @@ void NodeLiteRuntime::DefineGlobalFunctions() { NodeApi::SetProperty(env_, global, "console", console_obj); // console.log() - NodeApi::SetMethod( - env_, - console_obj, - "log", - [this](napi_env env, span args) { - NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); - std::string message = NodeApi::ToStdString(env, args[0]); - EmitConsoleOutput(message, false); - return nullptr; - }); + NodeApi::SetMethod( + env_, + console_obj, + "log", + [this](napi_env env, span args) { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + std::string message = NodeApi::ToStdString(env, args[0]); + EmitConsoleOutput(message, false); + return nullptr; + }); // console.error() NodeApi::SetMethod( env_, console_obj, "error", - [this](napi_env env, span args) -> napi_value { - NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); - std::string message = NodeApi::ToStdString(env, args[0]); - EmitConsoleOutput(message, true); - return nullptr; - }); - } -} - -void NodeLiteRuntime::EmitConsoleOutput(const std::string& message, - bool is_error) { - const auto& callback = is_error ? callbacks_.stderr_callback - : callbacks_.stdout_callback; - if (callback) { - callback(message); - return; - } - - if (is_error) { - std::cerr << message << std::endl; - } else { - std::cout << message << std::endl; - } -} + [this](napi_env env, span args) -> napi_value { + NODE_LITE_ASSERT(args.size() >= 1, "Expected at least 1 argument"); + std::string message = NodeApi::ToStdString(env, args[0]); + EmitConsoleOutput(message, true); + return nullptr; + }); + } +} + +void NodeLiteRuntime::EmitConsoleOutput(const std::string& message, + bool is_error) { + const auto& callback = is_error ? callbacks_.stderr_callback + : callbacks_.stdout_callback; + if (callback) { + callback(message); + return; + } + + if (is_error) { + std::cerr << message << std::endl; + } else { + std::cout << message << std::endl; + } +} std::string NodeLiteRuntime::ProcessStack(std::string const& stack, std::string const& assertMethod) { @@ -911,55 +907,55 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { return *this; } -//============================================================================= -// NodeLiteErrorHandler implementation -//============================================================================= - -/*static*/ NodeLiteErrorHandler::Handler NodeLiteErrorHandler::SetHandler( - Handler handler) noexcept { - std::lock_guard lock{ErrorHandlerMutex()}; - Handler previous = GetHandler(); - if (handler) { - GetHandler() = std::move(handler); - } else { - GetHandler() = DefaultFatalErrorHandler; - } - return previous; -} - -/*static*/ NodeLiteErrorHandler::Handler& NodeLiteErrorHandler::GetHandler() - noexcept { - static Handler handler = DefaultFatalErrorHandler; - return handler; -} - -/*static*/ [[noreturn]] void NodeLiteErrorHandler::HandleFatalError( - NodeLiteFatalErrorInfo info) { - Handler handler_copy; - { - std::lock_guard lock{ErrorHandlerMutex()}; - handler_copy = GetHandler(); - } - handler_copy(info); - std::terminate(); -} - -/*static*/ [[noreturn]] void NodeLiteErrorHandler::OnNodeApiFailed( - napi_env env, napi_status error_code) { - const char* errorMessage = "An exception is pending"; - if (NodeApi::IsExceptionPending(env)) { - error_code = napi_pending_exception; +//============================================================================= +// NodeLiteErrorHandler implementation +//============================================================================= + +/*static*/ NodeLiteErrorHandler::Handler NodeLiteErrorHandler::SetHandler( + Handler handler) noexcept { + std::lock_guard lock{ErrorHandlerMutex()}; + Handler previous = GetHandler(); + if (handler) { + GetHandler() = std::move(handler); + } else { + GetHandler() = DefaultFatalErrorHandler; + } + return previous; +} + +/*static*/ NodeLiteErrorHandler::Handler& NodeLiteErrorHandler::GetHandler() + noexcept { + static Handler handler = DefaultFatalErrorHandler; + return handler; +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::HandleFatalError( + NodeLiteFatalErrorInfo info) { + Handler handler_copy; + { + std::lock_guard lock{ErrorHandlerMutex()}; + handler_copy = GetHandler(); + } + handler_copy(info); + std::terminate(); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::OnNodeApiFailed( + napi_env env, napi_status error_code) { + const char* errorMessage = "An exception is pending"; + if (NodeApi::IsExceptionPending(env)) { + error_code = napi_pending_exception; } else { const napi_extended_error_info* error_info{}; napi_status status = napi_get_last_error_info(env, &error_info); if (status != napi_ok) { - NodeLiteErrorHandler::ExitWithMessage( - "", [&](std::ostream& os) { os << "Failed to get last error info: " << status; }); - } - errorMessage = error_info->error_message; - } - throw NodeLiteException(error_code, errorMessage); -} + NodeLiteErrorHandler::ExitWithMessage( + "", [&](std::ostream& os) { os << "Failed to get last error info: " << status; }); + } + errorMessage = error_info->error_message; + } + throw NodeLiteException(error_code, errorMessage); +} /*static*/ [[noreturn]] void NodeLiteErrorHandler::OnAssertFailed( napi_env env, char const* expr, char const* message) { @@ -988,17 +984,17 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { } std::string message = NodeApi::GetPropertyString(env, error, "message"); std::string stack = NodeApi::GetPropertyString(env, error, "stack"); - ExitWithMessage("JavaScript error", [&](std::ostream& os) { - os << "Exception: " << name << '\n' - << " Message: " << message << '\n' - << "Callstack: " << '\n' - << stack; - }); - } else { - std::string message = NodeApi::CoerceToString(env, error); - ExitWithMessage("JavaScript error", - [&](std::ostream& os) { os << " Message: " << message; }); - } + ExitWithMessage("JavaScript error", [&](std::ostream& os) { + os << "Exception: " << name << '\n' + << " Message: " << message << '\n' + << "Callstack: " << '\n' + << stack; + }); + } else { + std::string message = NodeApi::CoerceToString(env, error); + ExitWithMessage("JavaScript error", + [&](std::ostream& os) { os << " Message: " << message; }); + } } /*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithJSAssertError( @@ -1022,37 +1018,37 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { << " Actual: " << actual << '\n'; } - ExitWithMessage("JavaScript assertion error", [&](std::ostream& os) { - os << "Exception: " - << "AssertionError" << '\n' - << " Method: " << method_name << '\n' - << " Message: " << message << '\n' - << error_details.str(/*a filler for formatting*/) - << "Callstack: " << '\n' - << error_stack; - }); -} - -/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithMessage( - const std::string& message, - std::function get_error_details, - int exit_code) noexcept { - std::ostringstream details_stream; - if (get_error_details) { - get_error_details(details_stream); - } - std::string details = details_stream.str(); - - HandleFatalError(NodeLiteFatalErrorInfo{ - .message = message, - .details = details, - .exit_code = exit_code, - }); -} - -//============================================================================= -// NodeApi implementation -//============================================================================= + ExitWithMessage("JavaScript assertion error", [&](std::ostream& os) { + os << "Exception: " + << "AssertionError" << '\n' + << " Method: " << method_name << '\n' + << " Message: " << message << '\n' + << error_details.str(/*a filler for formatting*/) + << "Callstack: " << '\n' + << error_stack; + }); +} + +/*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithMessage( + const std::string& message, + std::function get_error_details, + int exit_code) noexcept { + std::ostringstream details_stream; + if (get_error_details) { + get_error_details(details_stream); + } + std::string details = details_stream.str(); + + HandleFatalError(NodeLiteFatalErrorInfo{ + .message = message, + .details = details, + .exit_code = exit_code, + }); +} + +//============================================================================= +// NodeApi implementation +//============================================================================= /*static*/ bool NodeApi::IsExceptionPending(napi_env env) { bool result{}; @@ -1277,11 +1273,11 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { return result; } -/*static*/ napi_value NodeApi::RunScript(napi_env env, napi_value script) { - napi_value result{}; - NODE_LITE_CALL(napi_run_script(env, script, nullptr, &result)); - return result; -} +/*static*/ napi_value NodeApi::RunScript(napi_env env, napi_value script) { + napi_value result{}; + NODE_LITE_CALL(napi_run_script(env, script, nullptr, &result)); + return result; +} /*static*/ napi_value NodeApi::RunScript(napi_env env, const std::string& code, @@ -1311,11 +1307,11 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { return result; } -/*static*/ napi_value NodeApi::CreateFunction(napi_env env, - std::string_view name, - NodeApiCallback cb) { - napi_value result{}; - NODE_LITE_CALL(napi_create_function( +/*static*/ napi_value NodeApi::CreateFunction(napi_env env, + std::string_view name, + NodeApiCallback cb) { + napi_value result{}; + NODE_LITE_CALL(napi_create_function( env, name.data(), name.size(), @@ -1332,98 +1328,98 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { // TODO: (vmoroz) Find a way to delete it on close. new NodeApiCallback(std::move(cb)), &result)); - return result; -} - -ProcessResult RunNodeLiteScript(const std::filesystem::path& js_root, - const std::filesystem::path& script_path, - NodeLiteRuntime::Callbacks callbacks) { - ProcessResult result{}; - std::ostringstream stdout_stream; - std::ostringstream stderr_stream; - - NodeLiteRuntime::Callbacks effective_callbacks; - auto stdout_cb = callbacks.stdout_callback; - auto stderr_cb = callbacks.stderr_callback; - effective_callbacks.stdout_callback = - [stdout_cb, &stdout_stream](const std::string& message) { - if (stdout_cb) { - stdout_cb(message); - } - stdout_stream << message << '\n'; - }; - effective_callbacks.stderr_callback = - [stderr_cb, &stderr_stream](const std::string& message) { - if (stderr_cb) { - stderr_cb(message); - } - stderr_stream << message << '\n'; - }; - - auto fatal_handler = [&result](const NodeLiteFatalErrorInfo& info) { - result.status = info.exit_code; - if (!info.message.empty()) { - result.std_error = info.message; - } - if (!info.details.empty()) { - if (!result.std_error.empty()) { - result.std_error += '\n'; - } - result.std_error += info.details; - } - throw NodeLiteFatalError(info); - }; - - NodeLiteErrorHandler::Handler previous_handler = - NodeLiteErrorHandler::SetHandler(fatal_handler); - - try { - auto task_runner = std::make_shared(); - std::vector args{"node_lite", script_path.string()}; - auto runtime = NodeLiteRuntime::Create(std::move(task_runner), - js_root.string(), - std::move(args), - std::move(effective_callbacks)); - runtime->RunTestScript(script_path.string()); - result.status = 0; - } catch (const NodeLiteFatalError&) { - // Fatal error captured in result - } catch (const std::exception& e) { - NodeLiteErrorHandler::SetHandler(previous_handler); - result.status = -1; - result.std_error = e.what(); - return result; - } catch (...) { - NodeLiteErrorHandler::SetHandler(previous_handler); - result.status = -1; - result.std_error = "Unknown error"; - return result; - } - - NodeLiteErrorHandler::SetHandler(previous_handler); - - result.std_output = stdout_stream.str(); - if (!result.std_output.empty() && result.std_output.back() == '\n') { - result.std_output.pop_back(); - } - - std::string stderr_logs = stderr_stream.str(); - if (!stderr_logs.empty() && stderr_logs.back() == '\n') { - stderr_logs.pop_back(); - } - if (!stderr_logs.empty()) { - if (!result.std_error.empty()) { - result.std_error += '\n'; - } - result.std_error += stderr_logs; - } - - return result; -} - -} // namespace node_api_tests - -int main(int argc, char* argv[]) { - node_api_tests::NodeLiteRuntime::Run( + return result; +} + +ProcessResult RunNodeLiteScript(const std::filesystem::path& js_root, + const std::filesystem::path& script_path, + NodeLiteRuntime::Callbacks callbacks) { + ProcessResult result{}; + std::ostringstream stdout_stream; + std::ostringstream stderr_stream; + + NodeLiteRuntime::Callbacks effective_callbacks; + auto stdout_cb = callbacks.stdout_callback; + auto stderr_cb = callbacks.stderr_callback; + effective_callbacks.stdout_callback = + [stdout_cb, &stdout_stream](const std::string& message) { + if (stdout_cb) { + stdout_cb(message); + } + stdout_stream << message << '\n'; + }; + effective_callbacks.stderr_callback = + [stderr_cb, &stderr_stream](const std::string& message) { + if (stderr_cb) { + stderr_cb(message); + } + stderr_stream << message << '\n'; + }; + + auto fatal_handler = [&result](const NodeLiteFatalErrorInfo& info) { + result.status = info.exit_code; + if (!info.message.empty()) { + result.std_error = info.message; + } + if (!info.details.empty()) { + if (!result.std_error.empty()) { + result.std_error += '\n'; + } + result.std_error += info.details; + } + throw NodeLiteFatalError(info); + }; + + NodeLiteErrorHandler::Handler previous_handler = + NodeLiteErrorHandler::SetHandler(fatal_handler); + + try { + auto task_runner = std::make_shared(); + std::vector args{"node_lite", script_path.string()}; + auto runtime = NodeLiteRuntime::Create(std::move(task_runner), + js_root.string(), + std::move(args), + std::move(effective_callbacks)); + runtime->RunTestScript(script_path.string()); + result.status = 0; + } catch (const NodeLiteFatalError&) { + // Fatal error captured in result + } catch (const std::exception& e) { + NodeLiteErrorHandler::SetHandler(previous_handler); + result.status = -1; + result.std_error = e.what(); + return result; + } catch (...) { + NodeLiteErrorHandler::SetHandler(previous_handler); + result.status = -1; + result.std_error = "Unknown error"; + return result; + } + + NodeLiteErrorHandler::SetHandler(previous_handler); + + result.std_output = stdout_stream.str(); + if (!result.std_output.empty() && result.std_output.back() == '\n') { + result.std_output.pop_back(); + } + + std::string stderr_logs = stderr_stream.str(); + if (!stderr_logs.empty() && stderr_logs.back() == '\n') { + stderr_logs.pop_back(); + } + if (!stderr_logs.empty()) { + if (!result.std_error.empty()) { + result.std_error += '\n'; + } + result.std_error += stderr_logs; + } + + return result; +} + +} // namespace node_api_tests + +int main(int argc, char* argv[]) { + node_api_tests::NodeLiteRuntime::Run( std::vector(argv, argv + argc)); } diff --git a/Tests/NodeApi/node_lite_android.cpp b/Tests/NodeApi/node_lite_android.cpp new file mode 100644 index 00000000..5476aeec --- /dev/null +++ b/Tests/NodeApi/node_lite_android.cpp @@ -0,0 +1,22 @@ +#include "node_lite.h" + +#include + +namespace node_api_tests { + +/*static*/ void* NodeLitePlatform::LoadFunction( + napi_env /*env*/, + const std::filesystem::path& lib_path, + const std::string& function_name) noexcept +{ + void* handle = dlopen(lib_path.string().c_str(), RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) + { + return nullptr; + } + + void* symbol = dlsym(handle, function_name.c_str()); + return symbol; +} + +} // namespace node_api_tests diff --git a/Tests/NodeApi/node_lite_posix.cpp b/Tests/NodeApi/node_lite_posix.cpp new file mode 100644 index 00000000..c2968514 --- /dev/null +++ b/Tests/NodeApi/node_lite_posix.cpp @@ -0,0 +1,43 @@ +#include +#if defined(__ANDROID__) +#include +#endif +#include "node_lite.h" + +namespace node_api_tests { + +/*static*/ void* NodeLitePlatform::LoadFunction( + napi_env env, + const std::filesystem::path& lib_path, + const std::string& function_name) noexcept { +#if defined(__ANDROID__) && (__ANDROID_API__ < 29) + void* library_handle = dlopen(lib_path.string().c_str(), RTLD_NOW | RTLD_LOCAL); + if (library_handle == nullptr) { + return nullptr; + } + + return dlsym(library_handle, function_name.c_str()); +#else + void* library_handle = dlopen(lib_path.string().c_str(), RTLD_NOW | RTLD_LOCAL); + if (library_handle == nullptr) { + const char* error_message = dlerror(); + NODE_LITE_ASSERT(false, + "Failed to load dynamic library: %s. Error: %s", + lib_path.c_str(), + error_message != nullptr ? error_message : "Unknown error"); + return nullptr; + } + + dlerror(); // Clear any existing error state before dlsym. + void* symbol = dlsym(library_handle, function_name.c_str()); + const char* error_message = dlerror(); + NODE_LITE_ASSERT(error_message == nullptr, + "Failed to resolve symbol: %s in %s. Error: %s", + function_name.c_str(), + lib_path.c_str(), + error_message != nullptr ? error_message : "Unknown error"); + return symbol; +#endif +} + +} // namespace node_api_tests diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index 5bea1370..242dd7d0 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -72,12 +72,16 @@ android { packagingOptions { if (enableAsan) { doNotStrip "**/*.so" + jniLibs.useLegacyPackaging true } } sourceSets { main { assets.srcDirs += [nodeApiAssetsDir] + if (enableAsan) { + jniLibs.srcDir "${buildDir}/generated/asanRuntime/jniLibs" + } } } } @@ -132,3 +136,68 @@ tasks.configureEach { task -> } preBuild.dependsOn(copyNodeApiTests) + +if (enableAsan) { + def hostTag = { + def osName = System.getProperty("os.name").toLowerCase() + if (osName.contains("mac") || osName.contains("darwin")) { + return "darwin-x86_64" + } else if (osName.contains("windows")) { + return "windows-x86_64" + } else { + return "linux-x86_64" + } + }.call() + + def asanRuntimeProvider = providers.provider { + def prebuiltRoot = new File(android.ndkDirectory, "toolchains/llvm/prebuilt/${hostTag}/lib64/clang") + if (!prebuiltRoot.exists()) { + throw new GradleException("Unable to locate clang libraries under ${prebuiltRoot}") + } + def versionDir = prebuiltRoot.listFiles().find { it.isDirectory() } + if (versionDir == null) { + throw new GradleException("Unable to determine clang version directory within ${prebuiltRoot}") + } + def runtimeFile = new File(versionDir, "lib/linux/libclang_rt.asan-aarch64-android.so") + if (!runtimeFile.exists()) { + throw new GradleException("Unable to locate ASan runtime library at ${runtimeFile}") + } + return runtimeFile + } + + def generatedAsanDir = layout.buildDirectory.dir("generated/asanRuntime/jniLibs/arm64-v8a") + + def prepareAsanRuntime = tasks.register("prepareAsanRuntime", Copy) { + from(asanRuntimeProvider) + into(generatedAsanDir) + } + + tasks.matching { it.name in ["mergeDebugNativeLibs", "mergeReleaseNativeLibs"] }.configureEach { + dependsOn(prepareAsanRuntime) + } + + def wrapScript = file("${projectDir}/../tools/wrap.sh") + + def pushAsanRuntime = tasks.register("pushAsanRuntime", Exec) { + dependsOn(prepareAsanRuntime) + commandLine("adb", "push", + generatedAsanDir.get().file("libclang_rt.asan-aarch64-android.so").asFile.absolutePath, + "/data/local/tmp/libclang_rt.asan-aarch64-android.so") + } + + def pushAsanWrapScript = tasks.register("pushAsanWrapScript", Exec) { + commandLine("adb", "push", wrapScript.absolutePath, "/data/local/tmp/wrap.sh") + } + + tasks.register("pushAsanArtifacts") { + dependsOn(pushAsanRuntime, pushAsanWrapScript) + doLast { + exec { + commandLine("adb", "shell", "chcon", "u:object_r:zygote_exec:s0", "/data/local/tmp/wrap.sh") + } + exec { + commandLine("adb", "shell", "chmod", "+x", "/data/local/tmp/wrap.sh") + } + } + } +} diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index 41ce387a..3f4119db 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -15,15 +15,18 @@ FetchContent_MakeAvailable_With_Message(googletest) npm(install --silent WORKING_DIRECTORY ${TESTS_DIR}) -set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_posix.cpp) -set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_posix.cpp) - if(WIN32) set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_windows.cpp) set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process.cpp) elseif(APPLE) - set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_mac.cpp) - set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_mac.cpp) + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_posix.cpp) +elseif(ANDROID) + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_android.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_android.cpp) +else() + set(NODE_LITE_PLATFORM_SRC ${TESTS_DIR}/NodeApi/node_lite_posix.cpp) + set(NODE_LITE_CHILD_PROCESS_SRC ${TESTS_DIR}/NodeApi/child_process_posix.cpp) endif() add_library(UnitTestsJNI SHARED diff --git a/Tests/UnitTests/Android/gradle.properties b/Tests/UnitTests/Android/gradle.properties index 25ceb3e4..3ec776e3 100644 --- a/Tests/UnitTests/Android/gradle.properties +++ b/Tests/UnitTests/Android/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects