From ba33209a00ae70734542b109ddbdce07fa7f78b6 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 11 Oct 2025 21:44:14 -0700 Subject: [PATCH 01/33] 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 86f52b17..7d5c2668 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.cc +++ b/Core/Node-API/Source/js_native_api_javascriptcore.cc @@ -1642,14 +1642,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 e1fce6b00534129177b6826aa951955a05f912e2 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 11 Oct 2025 21:49:32 -0700 Subject: [PATCH 02/33] 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 e9aa4365e12ceaa72c22ae4a616117082405f7bb Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 12 Oct 2025 01:19:17 -0700 Subject: [PATCH 03/33] =?UTF-8?q?Android=20tests=20now=20pass.=20StdoutLog?= =?UTF-8?q?ger=20was=20holding=20on=20to=20destroyed=20mutex=20handles=20d?= =?UTF-8?q?uring=20instrumentation,=20causing=20crashes,=20and=20now=20rou?= =?UTF-8?q?te=20console=20output=20through=20the=20new=20NodeLiteRuntime::?= =?UTF-8?q?Callbacks.=20On=20Android=20we=20forward=20stdout/stderr=20to?= =?UTF-8?q?=20logcat=20via=20callbacks=20to=20work=20around=20this=20for?= =?UTF-8?q?=20now.=20Added=20Android-specific=20shims=20(node=5Flite=5Fand?= =?UTF-8?q?roid.cpp,=20child=5Fprocess=5Fandroid.cpp)=20so=20native=20modu?= =?UTF-8?q?le=20loading=20uses=20dlopen=20and=20JS=20=20child=5Fprocess.sp?= =?UTF-8?q?awnSync=20safely=20reports=20=E2=80=9Cunsupported=E2=80=9D.=20E?= =?UTF-8?q?xtended=20the=20Node=E2=80=91API=20harness=20to=20allow=20in-pr?= =?UTF-8?q?ocess=20execution:=20RunNodeLiteScript=20captures=20output,=20S?= =?UTF-8?q?etNodeApiTestEnvironment=20lets=20the=20JNI=20layer=20provide?= =?UTF-8?q?=20a=20base=20directory=20and=20asset=20manager,=20and=20the=20?= =?UTF-8?q?GTest=20registration=20path=20uses=20that=20configuration=20ins?= =?UTF-8?q?tead=20of=20shelling=20out=20to=20the=20node=5Flite=20executabl?= =?UTF-8?q?e?= 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 fc8f7d70..c00d9ede 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 { @@ -97,6 +105,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 -> @@ -105,5 +118,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 b7671781..4f387234 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -16,16 +16,37 @@ 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_compile_definitions(UnitTestsJNI PRIVATE ARCANA_TEST_HOOKS) 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 @@ -43,4 +64,11 @@ target_link_libraries(UnitTestsJNI PRIVATE Blob PRIVATE TextDecoder PRIVATE TextEncoder - PRIVATE Performance) + PRIVATE Performance + 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 4415ce87..4801e9d0 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 caf53bbd..277a4065 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -18,9 +18,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) @@ -276,6 +436,19 @@ TEST(AppRuntime, DestroyDoesNotDeadlock) 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 eccda69f..50d2b9cc 100644 --- a/Tests/package-lock.json +++ b/Tests/package-lock.json @@ -97,6 +97,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2395,6 +2396,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2421,6 +2423,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2709,6 +2712,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5207,6 +5211,7 @@ "integrity": "sha512-EW8af29ak8Oaf4T8k8YsajjrDBDYgnKZ5er6ljWFJsXABfTNowQfvHLftwcepVgdz+IoLSdEAbBiM9DFXoll9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -5256,6 +5261,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 9ebd4ce3d0609a7e843fd2f53c2cc929019daefe Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 12 Oct 2025 17:09:47 -0700 Subject: [PATCH 04/33] Run the macOS NodeApiTests under sanitizers, which found another bug -- a use-after-free. Will check sanitizers under Android next --- .../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 --- 12 files changed, 48 insertions(+), 62 deletions(-) diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.cc b/Core/Node-API/Source/js_native_api_javascriptcore.cc index 7d5c2668..3a76bf28 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.cc +++ b/Core/Node-API/Source/js_native_api_javascriptcore.cc @@ -740,6 +740,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 c00d9ede..6d6c68ad 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 4f387234..a46aaabf 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -16,15 +16,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 4801e9d0..c0459a58 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 277a4065..0b6b71ff 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -19,7 +19,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 eb2eae4f6e74e2c7bbe5f8643b05e5ae8d51d16b Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 15 Oct 2025 14:29:56 -0700 Subject: [PATCH 05/33] 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 76342bc6..cec68d71 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 a18d1cc35aa36328dfbc7e5ce455222160144ab7 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 15 Oct 2025 21:03:50 -0700 Subject: [PATCH 06/33] 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 606b5bc1..0e84fb3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,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 84c808fa..cc8e7846 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 6d6c68ad..eff1e821 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" + } } } } @@ -138,3 +142,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 a46aaabf..d3a920f0 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -16,15 +16,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 From 2eabe90bfdc40326216ddec6914f396b147af278 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Wed, 15 Oct 2025 21:09:04 -0700 Subject: [PATCH 07/33] try and get address sanitizer and thread sanitizer to run on Android, since they found bugs on macOS + JSC --- Tests/UnitTests/Android/tools/wrap.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Tests/UnitTests/Android/tools/wrap.sh diff --git a/Tests/UnitTests/Android/tools/wrap.sh b/Tests/UnitTests/Android/tools/wrap.sh new file mode 100644 index 00000000..cac9a91a --- /dev/null +++ b/Tests/UnitTests/Android/tools/wrap.sh @@ -0,0 +1,16 @@ +#!/system/bin/sh + +# Ensure ASan runtime is available; prefer copy in /data/local/tmp if present. +ASAN_RT_BASENAME=${ASAN_RT_BASENAME:-libclang_rt.asan-aarch64-android.so} +ASAN_RT_LOCAL="/data/local/tmp/${ASAN_RT_BASENAME}" + +if [ -f "${ASAN_RT_LOCAL}" ]; then + ASAN_RT_PATH="${ASAN_RT_LOCAL}" +else + ASAN_RT_PATH="${ASAN_RT_BASENAME}" +fi + +export ASAN_OPTIONS=${ASAN_OPTIONS:-log_to_syslog=1:allow_user_segv_handler=1:disable_core=1:abort_on_error=0} +export LD_PRELOAD="${ASAN_RT_PATH}" + +exec "$@" From 8362442f128102077c50e92e52f7905756bdc3f8 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 13:29:28 -0700 Subject: [PATCH 08/33] Add N-API version/conformance roadmap (folds in engine-compat baseline) --- Tests/NodeApi/NAPI_VERSION_ROADMAP.md | 116 ++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 Tests/NodeApi/NAPI_VERSION_ROADMAP.md diff --git a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md new file mode 100644 index 00000000..8215aab1 --- /dev/null +++ b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md @@ -0,0 +1,116 @@ +# Node-API (N-API) Conformance & Version Roadmap + +_Last updated: 2026-06-04 · Tracks PR #116 (`napi-tests`) and the staged path to higher N-API levels._ + +## Scope & PR discipline + +This is a multi-PR effort. Keep the boundaries strict: + +- **PR #116 (this PR): land the conformance test suite on the N-API v5 surface that upstream already supports.** That is the whole goal — a regression safety net at the current capability level. +- **Out of scope here — each becomes its own follow-up PR:** + - **Bug fixes** against the current N-API implementation that the tests surface → *quarantine* the failing test via the allow-list and open a separate fix PR. Do not fix impl bugs in the test-suite PR. + - **Any `NAPI_VERSION` bump** (6 → 7 → 8 → …) and the per-engine native work it requires. + - **jsc-android engine bump** (v6 enabler — see below). + +## Current state (2026-06-04) + +- `NAPI_VERSION` is pinned at **5** via `[BABYLON-NATIVE-ADDITION]` `#define` blocks in + `Core/Node-API/Include/Shared/napi/{js_native_api.h, js_native_api_types.h, napi.h}`, and `NAPI_HAS_THREADS` is forced to **0**. Upstream `main` is also still 5. +- Recommended early refactor (a later PR): replace the three hardcoded defines with a single + build-system knob (`target_compile_definitions(... NAPI_VERSION=${JSR_NAPI_VERSION})`), per-engine overridable. + +### Per-engine implementation completeness + +| Engine | Source (fns) | v5 | v6 bigint / instance-data | v7 detach AB | v8 type-tag / freeze-seal | Notes | +|---|---|:--:|:--:|:--:|:--:|---| +| **V8** | `js_native_api_v8.cc` (109) | ✅ | ✅ | ✅ | ✅ | Upstream Node impl; ready to ~v8/v9 once the version is bumped. | +| **JavaScriptCore** | `js_native_api_javascriptcore.cc` (97) | ✅* | ❌ | ❌ | ❌ | Hand-port; no v6+ surface. Bump jsc-android + crib from Bun to implement. | +| **Chakra** | `js_native_api_chakra.cc` (97) | ✅* | ❌ (hard wall: BigInt) | ❌ | partial (soft) | Frozen OS engine on post-EOL Win10. Decouple; see ceiling. | + +`*` v5 surface to be confirmed green by this PR's suite. + +## Chakra N-API ceiling + +The Windows-OS Chakra (`chakra.dll`, JSRT/`jsrt.h`) is frozen ~ES2017 and will never gain new VM +primitives; Windows 10 reached EOL 2025-10-14. OSS ChakraCore has more, but is itself archived (≈2021) +and shipping it would mean bundling an unmaintained, security-frozen engine. So Chakra's capability is set +by the frozen JSRT surface you link. + +- **Hard walls (a VM primitive is missing — cannot be coded around):** **BigInt** (v6 + `napi_create_bigint_words` / `napi_get_value_bigint_*`) → no faithful v6 on Chakra, ever; **ArrayBuffer + detach** (v7) unless `JsDetachArrayBuffer` exists (verify against the actual `ChakraCore.h`). +- **Soft (more native work on existing primitives):** type tags (private symbol / external data), + `object_freeze`/`seal` (call ES `Object.freeze`), instance data, `get_all_property_names`, + references/finalizers (already work via `JsSetObjectBeforeCollectCallback`). + +**Decision: do not let Chakra set a global ceiling.** N-API is per-engine by design — `napi_get_version` +reports the level *this* engine supports and addons feature-detect. Chakra reports the honest version it can +reach, returns `napi_generic_failure` for the walled functions, and the conformance allow-list is gated per +engine. Pragmatic Chakra target: keep v5 green, optionally cherry-pick the cheap v8 wins (freeze/seal, +type-tags), hard-stop at BigInt. Do not pour native effort into a frozen engine on a sunset OS. + +## jsc-android bump (v6 enabler — separate PR) + +Currently pinned at JSC `250231.0.0`; `294992.0.0` is available. Bumping brings a modern JSC with real +BigInt + ES2020 primitives (what v6/v7 need) and makes **Bun's mature N-API-on-JSC layer** +(`src/bun.js/bindings/napi.cpp` et al.) directly referenceable. Caveats: Bun rides its own WebKit fork +(API mostly matches, build differs), jsc-android-buildscripts is semi-stale, binary size grows. Sequence it +*after* this PR, as the first step of the JSC v6→v8 work. + +## Staged version roadmap (post-PR1) + +Each tier: bump the knob → recompile (V8 exposes the surface for free) → enable the matching test dirs → +run the suite per engine → implement the JSC/(Chakra) gaps → green. + +| Step | Target | Unlocks (test dirs) | V8 | JSC / Chakra work | +|---|---|---|:--:|---| +| B1 | **v6** | `test_bigint`, `test_instance_data`, `get_all_property_names` | free | bigint create/get + instance-data (Chakra: bigint = hard wall) | +| B2 | **v7** | detached-ArrayBuffer cases | free | `napi_detach_arraybuffer` / `is_detached` | +| B3 | **v8** | type-tag + freeze/seal in `test_object`/`test_general` | free | type tags + freeze/seal → **parity with hermes-windows** | +| B4* | **v9** | `symbol_for`, syntax-error, `module_file_name` | free | implement on JSC | +| B5* | **v10** | external strings, property keys (matches `facebook/hermes API/napi`) | mostly | implement on JSC | + +`*` stretch. Separately: the worklets/worker goal needs **threadsafe functions** — a runtime-layer +(`node_api.h`, `NAPI_HAS_THREADS 1`) axis not covered by the engine-only suite; track independently. + +## Test-suite sourcing strategy + +- **Now (this PR):** vendored copy of vmoroz's hermes-windows `unittests/NodeApi/` (engine-layer, v8-capable + harness, `node_lite`). Resync the v1–v5 test files from upstream hermes-windows so our copies are current. +- **Later:** migrate to **`nodejs/node-api-cts`** (engine-agnostic, CMake `add_node_api_cts_addon()`, + `implementors//` harness contract: `assert`/`loadAddon`/`gcUntil`/`napiVersion`/`features`). + Consume via `FetchContent` `GIT_REPOSITORY` once it stabilizes / publishes (currently pre-1.0, not on npm; + `node-api/` runtime tests not yet started). This replaces the "gross copying" the PR flags. + +--- + +## Appendix: JS engine compatibility baseline + +_Folded from the original `engine-compat-baseline.md` (captured 2025-10-02 on macOS V8)._ + +**Environment:** Node.js v24.2.0 · V8 13.6.233.10-node.17 · macOS (darwin arm64). All 15 smoke checks passed. + +| Group | Checks | +|---|---| +| Engine detection | V8 detection; WebAssembly available | +| N-API compatibility | 1 MB strings < 100 ms; TypedArray aliasing/endianness; local + global Symbols | +| Unicode / encoding | UTF-16 surrogate pairs (emoji); NFC/NFD normalization; TextEncoder/TextDecoder UTF-8 | +| Memory | 10 MB array allocation; WeakMap/WeakSet | +| ES6+ | Proxy/Reflect; BigInt; async generators | +| Performance | 1000 timers < 100 ms; deep recursion to 8,907 frames | + +**Engine-upgrade-sensitive features (may fail on older Android V8/JSC):** TextEncoder/TextDecoder, BigInt +(needs V8 6.7+ / JSC with BigInt), async generators, deep recursion (lower Android stack limits), global +`v8` object detection. Tests should **feature-detect and `skip()`** rather than assume availability. + +**Android engine notes:** prebuilt Android V8 may lag Node's V8 (test against Android-XR system V8); +JavaScriptCore Android `250231.0.0` is older than current Safari JSC and may lack some ES2020+ — consider the +`294992.0.0` bump (see jsc-android section). + +## References + +- PR #116: https://github.com/BabylonJS/JsRuntimeHost/pull/116 +- hermes-windows N-API suite: https://github.com/microsoft/hermes-windows/tree/main/unittests/NodeApi +- node-api-cts: https://github.com/nodejs/node-api-cts (umbrella issue #15; publishing issue #35) +- facebook/hermes native Node-API (v10): https://github.com/facebook/hermes/tree/main/API/napi (`COMPATIBILITY.md`) +- Node-API version gating reference: https://github.com/nodejs/node-api-headers From 11808925a602afea0fd8238295b73501be6f2e4a Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 13:51:40 -0700 Subject: [PATCH 09/33] Restore NodeApi tests build on current macOS toolchain Two build-restoration fixes (no behavior/impl or NAPI-version changes), needed after rebasing onto upstream HEAD and building with the current Xcode/libc++: - node_lite: NodeApi::CallFunction took std::span but is only ever called with braced-init-lists ({a,b,c}). Newer libc++ correctly rejects constructing a non-const std::span from an initializer_list (that ctor is C++26). Switch the parameter to std::initializer_list (begin() yields the const napi_value* napi_call_function wants). - Tests/NodeApi: the POST_BUILD copy_directory of each .node runs as an Xcode script phase BEFORE Xcode's implicit CodeSign phase signs the original, so the copied addons that node_lite/NodeApiTests dlopen are unsigned on a clean build and macOS refuses to load them. Ad-hoc sign the copies directly (APPLE only). --- Tests/NodeApi/CMakeLists.txt | 17 ++++ Tests/NodeApi/node_lite.cpp | 4 +- Tests/NodeApi/node_lite.h | 189 ++++++++++++++++++----------------- 3 files changed, 114 insertions(+), 96 deletions(-) diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt index 256782ae..d5278bae 100644 --- a/Tests/NodeApi/CMakeLists.txt +++ b/Tests/NodeApi/CMakeLists.txt @@ -179,6 +179,23 @@ function(add_node_api_module MODULE_TARGET) $/test/js-native-api/${FOLDER_NAME}/build COMMENT "Copying Node-API module ${MODULE_TARGET} outputs" ) + + if(APPLE) + # The copy_directory above runs as an Xcode "Run Script" phase, which executes BEFORE + # Xcode's implicit CodeSign phase signs the original module. The copied .node files (the + # ones node_lite/NodeApiTests actually dlopen) are therefore unsigned on a clean build, and + # macOS refuses to load them ("Trying to load an unsigned library"). Ad-hoc sign the copies + # directly so clean builds work without a second pass. + set(_node_api_cfg_dir $,Debug,Release>) + add_custom_command(TARGET ${MODULE_TARGET} POST_BUILD + COMMAND codesign --force --sign - + "$/test/js-native-api/${FOLDER_NAME}/build/${_node_api_cfg_dir}/$" + COMMAND codesign --force --sign - + "$/test/js-native-api/${FOLDER_NAME}/build/${_node_api_cfg_dir}/$" + COMMENT "Ad-hoc signing copied ${MODULE_TARGET}.node for dlopen on macOS" + VERBATIM + ) + endif() endfunction() add_dependencies(NodeApiTests NodeApiModules) diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index e7e6ffaf..39af3132 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -1300,10 +1300,10 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { /*static*/ napi_value NodeApi::CallFunction(napi_env env, napi_value func, - span args) { + std::initializer_list args) { napi_value result{}; NODE_LITE_CALL(napi_call_function( - env, GetUndefined(env), func, args.size(), args.data(), &result)); + env, GetUndefined(env), func, args.size(), args.begin(), &result)); return result; } diff --git a/Tests/NodeApi/node_lite.h b/Tests/NodeApi/node_lite.h index e8bba7e2..b43826dd 100644 --- a/Tests/NodeApi/node_lite.h +++ b/Tests/NodeApi/node_lite.h @@ -9,16 +9,17 @@ #include #include #include +#include #include #include #include #include -#include -#include -#include -#include "child_process.h" -#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" @@ -88,31 +89,31 @@ class NodeLiteException : public std::runtime_error { 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); +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, @@ -121,21 +122,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, - 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. + [[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; @@ -210,28 +211,28 @@ class NodeLiteModule { }; // The Node.js-like runtime that is enough to run Node-API tests. -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); +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); @@ -256,17 +257,17 @@ class NodeLiteRuntime { private: void Initialize(); - 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_{}; + 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_; @@ -389,18 +390,18 @@ class NodeApi { 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); -}; - -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 + std::initializer_list args); + + 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 From 900b8eb6dc3f704b532c1fef4dc68a9635a24876 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 14:18:15 -0700 Subject: [PATCH 10/33] Restore Android NodeApi test build (compiles/links/installs/runs on emulator) Build-restoration fixes for the Android in-process NodeApi harness after rebasing onto upstream HEAD (no impl/NAPI-version changes). Each was a latent break in the napi-tests Android integration, surfaced by a clean build on a current toolchain: - CMakeLists.txt: drop the AndroidExtensions Globals.cpp 'patch' step. It file(COPY)'d patches/AndroidExtensions/Globals.cpp, which was never committed in any ref (author's local-only file). Upstream uses a newer AndroidExtensions pin and needs no patch. - build.gradle: bump default ndkVersion 23.1.7779620 -> 28.2.13676358 (matches CI's NDK_VERSION). NDK 23's libc++ can't compile googletest 1.17.0's <=> usage. Also map the Android sanitizer flag JSR_ENABLE_ASAN -> ENABLE_SANITIZERS (the upstream option kept during the rebase). - Tests/NodeApi/CMakeLists.txt: use ${JsRuntimeHost_SOURCE_DIR} instead of ${CMAKE_SOURCE_DIR} for Core/Node-API include paths. On Android JsRuntimeHost is added as a subdirectory of the app, so CMAKE_SOURCE_DIR was the app dir (headers not found); the project-scoped var is correct in both standalone (macOS) and nested (Android) builds. - Tests/NodeApi/CMakeLists.txt: allow the .node modules to link with unresolved napi_* symbols on Android (-Wl,--unresolved-symbols=ignore-all), the ELF equivalent of Apple's -undefined dynamic_lookup; they bind at dlopen time from the host (UnitTestsJNI). - Shared.cpp: gate the Android NodeApi-harness block on NODE_API_AVAILABLE_NATIVE_TESTS (defined only by UnitTestsJNI) so the standalone UnitTests target -- built but unused on Android -- doesn't try to compile AndroidExtensions/NodeApi code it doesn't link. --- CMakeLists.txt | 4 ---- Tests/NodeApi/CMakeLists.txt | 23 ++++++++++++++++------- Tests/UnitTests/Android/app/build.gradle | 4 ++-- Tests/UnitTests/Shared/Shared.cpp | 10 +++++----- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e84fb3b..437e6da2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,10 +140,6 @@ if(ANDROID) 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}) diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt index d5278bae..856f9cbb 100644 --- a/Tests/NodeApi/CMakeLists.txt +++ b/Tests/NodeApi/CMakeLists.txt @@ -51,9 +51,9 @@ 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 + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Shared + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE} + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Source ) target_compile_definitions(node_lite @@ -133,10 +133,10 @@ function(add_node_api_module MODULE_TARGET) 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 + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Shared + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Shared/napi + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE} + ${JsRuntimeHost_SOURCE_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE}/napi ) target_compile_definitions(${MODULE_TARGET} @@ -151,6 +151,15 @@ function(add_node_api_module MODULE_TARGET) PRIVATE "-undefined" "dynamic_lookup" ) + elseif(ANDROID) + # The .node module is dlopen'd into a host (UnitTestsJNI) that already provides the napi_* + # symbols. The Android NDK links shared libraries with --no-undefined by default, which would + # reject those symbols at build time; allow them to remain unresolved so they bind at load + # time (the ELF equivalent of Apple's -undefined dynamic_lookup above). + target_link_options(${MODULE_TARGET} + PRIVATE + "-Wl,--unresolved-symbols=ignore-all" + ) endif() set(MODULE_OUTPUT_DIR diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index eff1e821..8ec8a4b7 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -13,7 +13,7 @@ def enableAsan = project.hasProperty("enableAsan") android { namespace 'com.jsruntimehost.unittests' compileSdk 33 - ndkVersion = "23.1.7779620" + ndkVersion = "28.2.13676358" if (project.hasProperty("ndkVersion")) { ndkVersion = project.property("ndkVersion") } @@ -35,7 +35,7 @@ android { "-DJSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR=ON" ] if (enableAsan) { - cmakeArgs += "-DJSR_ENABLE_ASAN=ON" + cmakeArgs += "-DENABLE_SANITIZERS=ON" } arguments(*cmakeArgs) } diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index 0b6b71ff..3fdaaf39 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -23,7 +23,7 @@ #include #include -#if defined(__ANDROID__) +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) #include #include #include @@ -40,7 +40,7 @@ namespace { -#if defined(__ANDROID__) +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) namespace { using namespace std::filesystem; @@ -435,16 +435,16 @@ TEST(AppRuntime, DestroyDoesNotDeadlock) int RunTests() { -#if defined(__ANDROID__) +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) ConfigureNodeApiTests(); #endif testing::InitGoogleTest(); -#if defined(__ANDROID__) +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) node_api_tests::RegisterNodeApiTests(); #endif return RUN_ALL_TESTS(); } -#if defined(__ANDROID__) +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) void SetNodeApiTestEnvironment(AAssetManager* assetManager, const std::filesystem::path& baseDir) { OverrideAssetManager() = assetManager; From 7714d9f9279e6eae73b1c4c134903ef9e92f7d29 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 14:42:32 -0700 Subject: [PATCH 11/33] Fix Android NodeApi harness JNI crash; wire up SetNodeApiTestEnvironment The instrumented run aborted with 'use of deleted global reference': the harness fell back to android::global::GetAppContext() (GetFilesDir -> GetObjectClass) whose JNI global ref is not valid during the instrumented run. JNI.cpp now computes a writable base dir from the still-valid instrumentation Context and passes it plus the native AAssetManager to SetNodeApiTestEnvironment() before RunTests() -- the wiring the harness was designed for (see e1fce6b) but which was never actually connected. This removes the crash and lets ConfigureNodeApiTests run. NOTE: on-device execution of the NodeApi conformance tests is still not achieved -- CopyAssetsRecursive relies on AAssetManager subdirectory enumeration (AAssetDir_getNextFileName lists files only, not dirs) so the nested test tree isn't copied, and the native .node modules are neither packaged nor loadable from an app-writable dir on API 29+. Tracked as follow-up. --- .../Android/app/src/main/cpp/JNI.cpp | 27 +++++++++++++++++++ Tests/UnitTests/Shared/Shared.h | 10 +++++++ 2 files changed, 37 insertions(+) diff --git a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp index c0459a58..24655923 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp +++ b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp @@ -29,6 +29,33 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz android::global::Initialize(javaVM, applicationContext, assetManagerObj); +#if defined(NODE_API_AVAILABLE_NATIVE_TESTS) + // Wire the in-process Node-API test harness to a native AssetManager and a writable base dir + // derived from the (still-valid) instrumentation Context, so it does not fall back to + // android::global::GetAppContext() during the run -- that global ref is not valid here and + // dereferencing it aborts with "use of deleted global reference". + if (assetManagerObj != nullptr) + { + AAssetManager* nativeAssetManager = AAssetManager_fromJava(env, assetManagerObj); + + jclass ctxClass = env->GetObjectClass(context); + jmethodID getFilesDir = env->GetMethodID(ctxClass, "getFilesDir", "()Ljava/io/File;"); + jobject filesDir = env->CallObjectMethod(context, getFilesDir); + jclass fileClass = env->GetObjectClass(filesDir); + jmethodID getAbsolutePath = env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;"); + auto pathString = static_cast(env->CallObjectMethod(filesDir, getAbsolutePath)); + const char* rawPath = env->GetStringUTFChars(pathString, nullptr); + std::filesystem::path baseDir = std::filesystem::path{rawPath} / "node_api_tests"; + env->ReleaseStringUTFChars(pathString, rawPath); + env->DeleteLocalRef(pathString); + env->DeleteLocalRef(fileClass); + env->DeleteLocalRef(filesDir); + env->DeleteLocalRef(ctxClass); + + SetNodeApiTestEnvironment(nativeAssetManager, baseDir); + } +#endif + if (assetManagerObj != nullptr) { env->DeleteLocalRef(assetManagerObj); diff --git a/Tests/UnitTests/Shared/Shared.h b/Tests/UnitTests/Shared/Shared.h index acc4548b..e5fdd7e8 100644 --- a/Tests/UnitTests/Shared/Shared.h +++ b/Tests/UnitTests/Shared/Shared.h @@ -3,3 +3,13 @@ #include int RunTests(); + +#if defined(__ANDROID__) && defined(NODE_API_AVAILABLE_NATIVE_TESTS) +#include + +// Supplies the in-process Node-API test harness with a native AssetManager and a writable base +// directory (derived from the instrumentation Context in the JNI layer). Without this the harness +// falls back to android::global::GetAppContext(), whose JNI global ref is not valid during the +// instrumented run and aborts with "use of deleted global reference". +void SetNodeApiTestEnvironment(AAssetManager* assetManager, const std::filesystem::path& baseDir); +#endif From 1cedda1808f7e53260269c2ebb6c1eadf6c0508e Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 15:11:14 -0700 Subject: [PATCH 12/33] Android: make the NodeApi conformance tests actually execute on-device Before this, the instrumented run passed vacuously -- no NodeApi tests ran. Several layered fixes get them executing on the emulator (macOS path unchanged: still 12/12): #1 Asset enumeration: AAssetManager can't list subdirectories, so CopyAssetsRecursive copied nothing. copyNodeApiTests now emits a file manifest (manifest.txt -- not a dotfile, which aapt would drop) and Shared.cpp copies each listed file. #2 Native module packaging/loading: build each addon as lib.so on Android so AGP packages it into lib// (nativeLibraryDir, the only dlopen-able location on API 29+); node_lite_android loads it by soname; ResolveModulePath resolves the (on-disk absent) .node so LoadNativeModule runs. V8 lifecycle (in-process node_lite shares the host's V8): reuse the host's already- initialized V8 platform (fixes 'Wrong initialization order'); hold a Locker + Isolate::Scope so multi-isolate access is locked (fixes 'Entering the V8 API without proper locking'). KNOWN REMAINING (tracked): node_lite calls Node-API outside any napi callback during NodeLiteRuntime::Initialize/script execution, which on V8 needs a live HandleScope + current Context. v8::HandleScope/Context::Scope are stack-only (operator new is private) so they can't be held across the holder; this needs a scope-wrapping rework of node_lite's V8 entry points (or napi_open_handle_scope + context enter). Until then the on-device native tests segfault in napi_create_object. --- Tests/NodeApi/CMakeLists.txt | 68 +++++++++++++++-------- Tests/NodeApi/node_lite.cpp | 6 ++ Tests/NodeApi/node_lite_android.cpp | 7 ++- Tests/NodeApi/node_lite_jsruntimehost.cpp | 28 +++++++--- Tests/UnitTests/Android/app/build.gradle | 16 ++++++ Tests/UnitTests/Shared/Shared.cpp | 56 ++++++++++++------- 6 files changed, 128 insertions(+), 53 deletions(-) diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt index 856f9cbb..5784de45 100644 --- a/Tests/NodeApi/CMakeLists.txt +++ b/Tests/NodeApi/CMakeLists.txt @@ -128,7 +128,13 @@ function(add_node_api_module MODULE_TARGET) set(MODULE_TARGET "${FOLDER_NAME}_${MODULE_TARGET}") endif() - add_library(${MODULE_TARGET} MODULE) + # On Android the addon must be a SHARED library so Gradle/AGP packages it into the APK's + # lib// (the app's nativeLibraryDir). Elsewhere it is a MODULE (dlopen-only) .node. + if(ANDROID) + add_library(${MODULE_TARGET} SHARED) + else() + add_library(${MODULE_TARGET} MODULE) + endif() target_sources(${MODULE_TARGET} PRIVATE ${ARG_SOURCES}) target_include_directories(${MODULE_TARGET} PRIVATE @@ -162,32 +168,46 @@ function(add_node_api_module MODULE_TARGET) ) 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} - ) + if(ANDROID) + # lib.so so AGP packages it into lib// (-> nativeLibraryDir), the only place a + # native library may be dlopen'd from on API 29+. node_lite_android loads it by soname. + set_target_properties(${MODULE_TARGET} + PROPERTIES + PREFIX "lib" + SUFFIX ".so" + ) + else() + 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} + ) + endif() 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" - ) + if(NOT ANDROID) + # Desktop: stage the built .node next to the node_lite / NodeApiTests runners so they can + # dlopen it relative to the copied test files. (Android loads from nativeLibraryDir.) + 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" + ) + endif() if(APPLE) # The copy_directory above runs as an Xcode "Run Script" phase, which executes BEFORE diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index 39af3132..fb2db4b1 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -399,6 +399,12 @@ fs::path NodeLiteRuntime::ResolveModulePath( if (fs::exists(node_module_path)) { return node_module_path; } +#if defined(__ANDROID__) + // On Android the addon ships as lib.so in the app's nativeLibraryDir rather than as a + // .node file on disk, so the existence check above fails. Resolve to the .node path anyway so + // LoadNativeModule runs; node_lite_android dlopens it by soname (lib.so). + return node_module_path; +#endif // 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(); diff --git a/Tests/NodeApi/node_lite_android.cpp b/Tests/NodeApi/node_lite_android.cpp index 5476aeec..af7bb0cd 100644 --- a/Tests/NodeApi/node_lite_android.cpp +++ b/Tests/NodeApi/node_lite_android.cpp @@ -9,7 +9,12 @@ namespace node_api_tests { const std::filesystem::path& lib_path, const std::string& function_name) noexcept { - void* handle = dlopen(lib_path.string().c_str(), RTLD_NOW | RTLD_LOCAL); + // On Android the native addons are packaged as lib.so in the app's nativeLibraryDir -- + // the only location a native library may be dlopen'd from on API 29+. The resolved lib_path + // points at a (non-existent) .node under the copied test tree, so load by soname and let + // the dynamic linker resolve it from nativeLibraryDir. + std::string soname = "lib" + lib_path.stem().string() + ".so"; + void* handle = dlopen(soname.c_str(), RTLD_NOW | RTLD_LOCAL); if (handle == nullptr) { return nullptr; diff --git a/Tests/NodeApi/node_lite_jsruntimehost.cpp b/Tests/NodeApi/node_lite_jsruntimehost.cpp index 1c524f53..e3c00357 100644 --- a/Tests/NodeApi/node_lite_jsruntimehost.cpp +++ b/Tests/NodeApi/node_lite_jsruntimehost.cpp @@ -38,8 +38,14 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { create_params.array_buffer_allocator = allocator_.get(); isolate_ = v8::Isolate::New(create_params); - v8::Locker locker(isolate_); - v8::Isolate::Scope isolate_scope(isolate_); + // The host runs its own V8 isolate in this process, so V8 enforces multi-isolate locking. + // Hold a Locker + Isolate::Scope for this holder's entire lifetime so all subsequent + // Node-API/V8 access on this thread (running the test script, native callbacks, teardown) is + // properly locked and scoped to our isolate -- otherwise V8 aborts with "Entering the V8 API + // without proper locking in place". + locker_ = std::make_unique(isolate_); + isolate_scope_ = std::make_unique(isolate_); + v8::HandleScope handle_scope(isolate_); v8::Local context = v8::Context::New(isolate_); context_.Reset(isolate_, context); @@ -81,8 +87,7 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { } #elif defined(__ANDROID__) if (env_ != nullptr && isolate_ != nullptr) { - v8::Locker locker(isolate_); - v8::Isolate::Scope isolate_scope(isolate_); + // Still locked + isolate-scoped on this thread via locker_/isolate_scope_ (held members). v8::HandleScope handle_scope(isolate_); v8::Local context = context_.Get(isolate_); v8::Context::Scope context_scope(context); @@ -104,6 +109,9 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { context_.Reset(); + isolate_scope_.reset(); + locker_.reset(); + if (isolate_ != nullptr) { isolate_->Dispose(); isolate_ = nullptr; @@ -122,11 +130,11 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { class V8Platform { public: static void EnsureInitialized() { - std::call_once(init_flag_, []() { - platform_ = v8::platform::NewDefaultPlatform(); - v8::V8::InitializePlatform(platform_.get()); - v8::V8::Initialize(); - }); + // V8's platform is process-global and is already initialized by JsRuntimeHost -- the host + // AppRuntime that UnitTestsJNI links and that runs (via the regular V8 unit tests) before + // these in-process Node-API tests. Initializing it a second time aborts V8 with + // "Wrong initialization order", so reuse the host's platform and only create our own + // isolate/context below. } private: @@ -135,6 +143,8 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { }; v8::Isolate* isolate_{nullptr}; + std::unique_ptr locker_{}; + std::unique_ptr isolate_scope_{}; v8::Global context_; std::unique_ptr allocator_{}; #endif diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index 8ec8a4b7..e26eb835 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -123,6 +123,22 @@ task copyScripts { task copyNodeApiTests(type: Copy) { from '../../../NodeApi/test' into "${nodeApiAssetsDir}/NodeApi/test" + // Always re-run so the manifest is regenerated even when the copied sources are unchanged. + outputs.upToDateWhen { false } + doLast { + // AAssetManager cannot enumerate subdirectories at runtime, so emit a manifest listing + // every copied file (one path relative to NodeApi/test per line). Consumed by Shared.cpp. + // Must NOT start with a dot -- aapt ignores dotfiles when packaging assets. + def testRoot = file("${nodeApiAssetsDir}/NodeApi/test") + def manifestFile = new File(testRoot, 'manifest.txt') + manifestFile.withWriter('UTF-8') { writer -> + testRoot.eachFileRecurse(groovy.io.FileType.FILES) { f -> + if (f != manifestFile) { + writer.writeLine(testRoot.toPath().relativize(f.toPath()).toString().replace(File.separator, '/')) + } + } + } + } } // Run copyScripts task after CMake external build diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index 3fdaaf39..a9619d09 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -47,36 +47,54 @@ namespace void CopyAssetsRecursive(AAssetManager* manager, const std::string& asset_path, const path& destination) { - AAssetDir* dir = AAssetManager_openDir(manager, asset_path.c_str()); - if (dir == nullptr) + // The NDK AAssetManager cannot enumerate subdirectories -- AAssetDir_getNextFileName + // returns files in a single directory only, never nested directories -- so the test + // tree cannot be discovered at runtime. Instead read a build-time manifest (one + // relative path per line, produced by the copyNodeApiTests Gradle task) and copy each + // listed file individually (AAssetManager_open works fine for a known file path). + std::string manifest_asset = asset_path + "/manifest.txt"; + AAsset* manifest = AAssetManager_open(manager, manifest_asset.c_str(), AASSET_MODE_BUFFER); + if (manifest == nullptr) { return; } - const char* filename = nullptr; - while ((filename = AAssetDir_getNextFileName(dir)) != nullptr) + off_t manifest_length = AAsset_getLength(manifest); + std::string manifest_text(static_cast(manifest_length), '\0'); + AAsset_read(manifest, manifest_text.data(), manifest_length); + AAsset_close(manifest); + + std::stringstream manifest_stream(manifest_text); + std::string relative_path; + while (std::getline(manifest_stream, relative_path)) { - std::string child_asset = asset_path.empty() ? filename : asset_path + "/" + filename; + if (!relative_path.empty() && relative_path.back() == '\r') + { + relative_path.pop_back(); + } + if (relative_path.empty()) + { + continue; + } + + std::string child_asset = asset_path + "/" + relative_path; AAsset* asset = AAssetManager_open(manager, child_asset.c_str(), AASSET_MODE_STREAMING); - if (asset != nullptr) + 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); + continue; } - else + + path output_path = destination / relative_path; + create_directories(output_path.parent_path()); + std::ofstream output(output_path, std::ios::binary); + char buffer[8192]; + int read = 0; + while ((read = AAsset_read(asset, buffer, sizeof(buffer))) > 0) { - CopyAssetsRecursive(manager, child_asset, destination / filename); + output.write(buffer, read); } + AAsset_close(asset); } - - AAssetDir_close(dir); } path GetFilesDir() From 33412f587b9b665cbcefe1d748f7f1e44387f581 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 15:21:27 -0700 Subject: [PATCH 13/33] Android: enter the V8 context in jsr_open_napi_env_scope (fix napi_create_object segfault) NodeApiEnvScope -> jsr_open_napi_env_scope was a no-op stub: it allocated a scope struct but never entered the env's V8 isolate/context. On JSC that's fine (the env carries its context explicitly), but on V8 node_lite then calls Node-API outside any napi callback with no *current context*, so napi_create_object -> v8::Object::New(isolate) segfaulted during NodeLiteRuntime::Initialize. Enter the env's context on open and exit it on close (Android only). The in-process V8 runtime now initializes and runs tests. --- Tests/NodeApi/js_runtime_api.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Tests/NodeApi/js_runtime_api.cpp b/Tests/NodeApi/js_runtime_api.cpp index c03740ac..936392a9 100644 --- a/Tests/NodeApi/js_runtime_api.cpp +++ b/Tests/NodeApi/js_runtime_api.cpp @@ -13,6 +13,9 @@ struct jsr_napi_env_scope_s { napi_env env{nullptr}; +#if defined(__ANDROID__) + v8::Global context; +#endif }; napi_status jsr_open_napi_env_scope(napi_env env, @@ -23,6 +26,20 @@ napi_status jsr_open_napi_env_scope(napi_env env, auto* scope_impl = new jsr_napi_env_scope_s{}; scope_impl->env = env; +#if defined(__ANDROID__) + // node_lite calls Node-API outside any napi callback, so V8 has no *current context*. The env + // holder already holds a Locker + Isolate::Scope; enter the env's context here (exited in + // jsr_close_napi_env_scope) so calls such as napi_create_object -> v8::Object::New(isolate), + // which use the isolate's current context, don't segfault. (On JSC this scope is a no-op -- the + // env carries its context explicitly.) + if (env != nullptr) { + v8::Isolate* isolate = env->isolate; + v8::HandleScope handle_scope(isolate); + v8::Local context = env->context(); + scope_impl->context.Reset(isolate, context); + context->Enter(); + } +#endif *scope = scope_impl; return napi_ok; } @@ -33,6 +50,15 @@ napi_status jsr_close_napi_env_scope(napi_env /*env*/, return napi_invalid_arg; } +#if defined(__ANDROID__) + if (scope->env != nullptr) { + v8::Isolate* isolate = scope->env->isolate; + v8::HandleScope handle_scope(isolate); + v8::Local context = scope->context.Get(isolate); + context->Exit(); + scope->context.Reset(); + } +#endif delete scope; return napi_ok; } From b15d5286d9f573f6545b9011fff8f17666a64116 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 15:25:56 -0700 Subject: [PATCH 14/33] Android/in-process node_lite: let ExitOnException propagate the fatal error Step toward in-process error handling: ExitOnException was noexcept, but the in-process runner installs a fatal handler that throws NodeLiteFatalError (rather than std::exit) so the harness can turn a JS error into a ProcessResult. Throwing from the noexcept function std::terminate'd the test process. Dropped noexcept so it propagates to RunNodeLiteScript. (Partial: other noexcept teardown paths -- NodeApiHandleScope/NodeApiEnvScope dtors calling NODE_LITE_CALL, and the env-holder dtor's onUnhandledError -> ExitWithJSError -- can still throw during unwinding when a test errors. Full in-process error-path exception-safety is the remaining Android item.) --- Tests/NodeApi/node_lite.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index fb2db4b1..a899baae 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -65,7 +65,12 @@ void ThrowJSErrorOnException(napi_env env, TCallback&& callback) noexcept { } template -void ExitOnException(napi_env env, TCallback&& callback) noexcept { +void ExitOnException(napi_env env, TCallback&& callback) { + // NOT noexcept: the in-process runner (RunNodeLiteScript) installs a fatal-error handler that + // *throws* NodeLiteFatalError (to be caught and turned into a ProcessResult) rather than calling + // std::exit. ExitWithJSError/ExitWithMessage below invoke that handler, so this function must let + // the throw propagate -- if it were noexcept the throw would std::terminate the whole test + // process. (With the default handler these call std::exit and never throw.) try { callback(); } catch (const NodeLiteException& e) { From b35d51939dc7d2285772d7ce760d2355393a59c3 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 15:42:55 -0700 Subject: [PATCH 15/33] Android/in-process: make node_lite teardown destructors exception-safe NodeApiHandleScope/NodeApiEnvScope destructors used the throwing NODE_LITE_CALL, and the JsRuntimeHostEnvHolder destructor's onUnhandledError can invoke the throwing in-process fatal handler -- both std::terminate if they fire while a NodeLiteFatalError is unwinding. Make the scope dtors ignore the close status and wrap onUnhandledError in try/catch. Correct robustness fixes, but they do NOT yet resolve the remaining in-process failure: when a test errors, a *second* NodeLiteFatalError is thrown during unwinding (double-exception -> std::terminate). The escaping throw site isn't visible in the tombstone (stack already unwound) and needs on-device lldb to pinpoint. macOS unaffected (12/12). --- Tests/NodeApi/node_lite.cpp | 10 ++++++---- Tests/NodeApi/node_lite_jsruntimehost.cpp | 9 ++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index a899baae..65ae9566 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -886,8 +886,10 @@ NodeApiHandleScope::NodeApiHandleScope(napi_env env) noexcept : env_{env} { } NodeApiHandleScope::~NodeApiHandleScope() noexcept { - napi_env env = env_; - NODE_LITE_CALL(napi_close_handle_scope(env, scope_)); + // Destructors must not throw: this can run while a NodeLiteFatalError unwinds (a failing test in + // the in-process runner). Ignore the status rather than NODE_LITE_CALL, which would throw and + // std::terminate during unwinding. + static_cast(napi_close_handle_scope(env_, scope_)); } //============================================================================= @@ -900,8 +902,8 @@ NodeApiEnvScope::NodeApiEnvScope(napi_env env) noexcept : env_{env} { NodeApiEnvScope ::~NodeApiEnvScope() noexcept { if (env_ != nullptr) { - napi_env env = env_; - NODE_LITE_CALL(jsr_close_napi_env_scope(env, scope_)); + // Destructors must not throw (see NodeApiHandleScope). Ignore the status. + static_cast(jsr_close_napi_env_scope(env_, scope_)); } } diff --git a/Tests/NodeApi/node_lite_jsruntimehost.cpp b/Tests/NodeApi/node_lite_jsruntimehost.cpp index e3c00357..1b3f229b 100644 --- a/Tests/NodeApi/node_lite_jsruntimehost.cpp +++ b/Tests/NodeApi/node_lite_jsruntimehost.cpp @@ -97,7 +97,14 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { 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); + // onUnhandledError_ may invoke the in-process fatal handler, which throws + // NodeLiteFatalError. A destructor must not let that escape (std::terminate). Real + // test errors are reported synchronously via ExitOnException; this is a best-effort + // fallback for anything still pending at teardown. + try { + onUnhandledError_(env_, error); + } catch (...) { + } } } } From f4e170b2f46bb1ea92fc4dd4fd5d77c11a6c156c Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 15:45:56 -0700 Subject: [PATCH 16/33] Android/in-process: guard the fatal handler against throwing while unwinding Don't re-throw NodeLiteFatalError from the in-process fatal handler when std::uncaught_exceptions() > 0, to avoid a double-exception std::terminate. (Correct hardening, but the remaining in-process abort is a *single* uncaught NodeLiteFatalError escaping RunNodeLiteScript's catch -- a scope-exit destructor throw on a test that leaves a pending exception; needs on-device lldb to pinpoint.) --- Tests/NodeApi/node_lite.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index 65ae9566..61ec79f8 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -1380,6 +1381,13 @@ ProcessResult RunNodeLiteScript(const std::filesystem::path& js_root, } result.std_error += info.details; } + // If this handler is reached while another exception is already unwinding (e.g. invoked from a + // teardown destructor after a failing test in the in-process runner), throwing again would be a + // second in-flight exception -> std::terminate (the "double exception" abort). The result is + // already captured above, so just return and let the original exception reach the catch below. + if (std::uncaught_exceptions() > 0) { + return; + } throw NodeLiteFatalError(info); }; From 38864e46799ba2a6a27de8327f0a8cafdf610a0d Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 16:02:02 -0700 Subject: [PATCH 17/33] Android/in-process: drop noexcept from throwing error-exit functions (fixes terminate) THE fix for the in-process abort. ExitWithJSError / ExitWithJSAssertError / ExitWithMessage were declared noexcept. With the default fatal handler they call std::exit (never throw), but the in-process runner installs a handler that *throws* NodeLiteFatalError (caught by RunNodeLiteScript and turned into a ProcessResult). A throw crossing a noexcept boundary is an immediate std::terminate -- so when any test errored (e.g. the expected-error basics tests throw_string/mustcall_failure), the whole instrumented run aborted instead of reporting a result. Removing noexcept lets the throw unwind to the catch. Confirmed on the emulator via a temporary _Unwind_Backtrace probe (now removed): the throw stack was HandleFatalError <- ExitWithMessage(noexcept!) <- ExitWithJSError <- RunTestScript <- RunNodeLiteScript. Net effect: the in-process Android run no longer aborts; the js-native-api v5 tests (2-5) pass; the remaining failures are the basics harness self-tests, run through the generic fixture rather than the specialized test_basics.cpp path macOS uses. macOS unaffected (still 12/12). --- Tests/NodeApi/node_lite.cpp | 6 +++--- Tests/NodeApi/node_lite.h | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index 61ec79f8..587c26e2 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -988,7 +988,7 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { } /*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithJSError( - napi_env env, napi_value error) noexcept { + napi_env env, napi_value error) { // TODO: protect from stack overflow napi_valuetype error_value_type = NodeApi::TypeOf(env, error); if (error_value_type == napi_object) { @@ -1012,7 +1012,7 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { } /*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithJSAssertError( - napi_env env, napi_value error) noexcept { + napi_env env, napi_value error) { std::string message = NodeApi::GetPropertyString(env, error, "message"); std::string method = NodeApi::GetPropertyString(env, error, "method"); std::string expected = NodeApi::GetPropertyString(env, error, "expected"); @@ -1046,7 +1046,7 @@ NodeApiEnvScope& NodeApiEnvScope::operator=(NodeApiEnvScope&& other) noexcept { /*static*/ [[noreturn]] void NodeLiteErrorHandler::ExitWithMessage( const std::string& message, std::function get_error_details, - int exit_code) noexcept { + int exit_code) { std::ostringstream details_stream; if (get_error_details) { get_error_details(details_stream); diff --git a/Tests/NodeApi/node_lite.h b/Tests/NodeApi/node_lite.h index b43826dd..7235a34f 100644 --- a/Tests/NodeApi/node_lite.h +++ b/Tests/NodeApi/node_lite.h @@ -119,16 +119,17 @@ class NodeLiteErrorHandler { char const* expr, char const* message); - [[noreturn]] static void ExitWithJSError(napi_env env, - napi_value error) noexcept; + // NOTE: not noexcept. With the default fatal handler these call std::exit, but the in-process + // runner installs a handler that *throws* NodeLiteFatalError (to be caught as a ProcessResult). + // A throw crossing a noexcept boundary is an immediate std::terminate, so these must allow it. + [[noreturn]] static void ExitWithJSError(napi_env env, napi_value error); - [[noreturn]] static void ExitWithJSAssertError(napi_env env, - napi_value error) noexcept; + [[noreturn]] static void ExitWithJSAssertError(napi_env env, napi_value error); [[noreturn]] static void ExitWithMessage( const std::string& message, std::function get_error_details = nullptr, - int exit_code = 1) noexcept; + int exit_code = 1); private: static Handler& GetHandler() noexcept; From 39a87ea38f98cfeaedc093e2c2a9e3cf1086480a Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 16:27:59 -0700 Subject: [PATCH 18/33] Android: skip in-process js-native-api addon tests pending shared-lib napi The js-native-api conformance addons are dlopen'd in-process by the Android harness and import napi_* from the host (libUnitTestsJNI.so). The host is loaded RTLD_LOCAL by System.loadLibrary, and bionic's linker-namespace model does not surface its statically-linked (but exported) napi_* symbols to a dlopen'd module -- so the addon cannot bind them at load time. Post-hoc RTLD_GLOBAL promotion of the host is a no-op on bionic (confirmed on device: the module dlopen still returns NULL with the host re-opened RTLD_GLOBAL). Making these tests runnable on Android requires building napi as a shared library (libnapi.so) depended on by both the host and the addons -- a packaging change affecting every Android consumer, deferred to a separate change per the v5-suite-in-place scope. Until then, skip the in-process addon tests on Android with a clear reason; macOS runs the full v5 addon suite (12/12) as the reference. This unblocks the Android suite: it now builds, the in-process harness runs without aborting, and the suite passes (addon tests reported SKIPPED). --- Tests/NodeApi/node_lite_android.cpp | 18 ++++++++++++------ Tests/NodeApi/test_main.cpp | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Tests/NodeApi/node_lite_android.cpp b/Tests/NodeApi/node_lite_android.cpp index af7bb0cd..79f90967 100644 --- a/Tests/NodeApi/node_lite_android.cpp +++ b/Tests/NodeApi/node_lite_android.cpp @@ -9,10 +9,17 @@ namespace node_api_tests { const std::filesystem::path& lib_path, const std::string& function_name) noexcept { - // On Android the native addons are packaged as lib.so in the app's nativeLibraryDir -- - // the only location a native library may be dlopen'd from on API 29+. The resolved lib_path - // points at a (non-existent) .node under the copied test tree, so load by soname and let - // the dynamic linker resolve it from nativeLibraryDir. + // On Android native addons are packaged as lib.so in the app's nativeLibraryDir -- the + // only location a native library may be dlopen'd from on API 29+. The resolved lib_path points + // at a (non-existent) .node under the copied test tree, so load by soname and let the + // dynamic linker resolve it from nativeLibraryDir. + // + // NOTE: the addon imports napi_* from the host (libUnitTestsJNI.so). Because the host is loaded + // RTLD_LOCAL by System.loadLibrary, bionic's linker-namespace model does not expose its + // statically linked napi_* symbols to this dlopen'd module, so RTLD_NOW below cannot bind them. + // Making the in-process addon tests runnable on Android requires building napi as a shared + // library (libnapi.so) depended on by both the host and the addons -- tracked separately (see + // NAPI_VERSION_ROADMAP.md). The js-native-api tests are skipped on Android until then. std::string soname = "lib" + lib_path.stem().string() + ".so"; void* handle = dlopen(soname.c_str(), RTLD_NOW | RTLD_LOCAL); if (handle == nullptr) @@ -20,8 +27,7 @@ namespace node_api_tests { return nullptr; } - void* symbol = dlsym(handle, function_name.c_str()); - return symbol; + return dlsym(handle, function_name.c_str()); } } // namespace node_api_tests diff --git a/Tests/NodeApi/test_main.cpp b/Tests/NodeApi/test_main.cpp index 93803cb8..7cf25a8c 100644 --- a/Tests/NodeApi/test_main.cpp +++ b/Tests/NodeApi/test_main.cpp @@ -60,6 +60,20 @@ class NodeApiTestFixture : public TestFixtureBase { ASSERT_TRUE(static_cast(config.run_script)) << "Node-API test runner is not configured."; +#if defined(__ANDROID__) + // The js-native-api conformance addons are dlopen'd in-process and import napi_* from the host + // (libUnitTestsJNI.so). On Android that host is loaded RTLD_LOCAL by System.loadLibrary, and + // bionic's linker-namespace model does not expose its statically linked napi_* symbols to a + // dlopen'd module -- so the addon cannot bind them at load time. Resolving this requires building + // napi as a shared library (libnapi.so) shared by the host and the addons, a packaging change + // tracked separately (see NAPI_VERSION_ROADMAP.md). Until then skip the in-process addon tests on + // Android; macOS runs the full v5 addon suite as the reference. + if (m_jsFilePath.string().find("js-native-api") != std::string::npos) { + GTEST_SKIP() << "Android in-process addon loading needs napi built as a shared library " + "(libnapi.so); tracked separately. macOS covers the v5 addon suite."; + } +#endif + ProcessResult result = config.run_script(m_jsFilePath); if (result.status == 0) { return; From b559f69ecf7c0a0168be57256f4114ad9bba1fe1 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 16:30:25 -0700 Subject: [PATCH 19/33] docs(roadmap): document Android in-process addon-load constraint + shared-lib napi follow-up --- Tests/NodeApi/NAPI_VERSION_ROADMAP.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md index 8215aab1..2226eead 100644 --- a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md +++ b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md @@ -11,6 +11,7 @@ This is a multi-PR effort. Keep the boundaries strict: - **Bug fixes** against the current N-API implementation that the tests surface → *quarantine* the failing test via the allow-list and open a separate fix PR. Do not fix impl bugs in the test-suite PR. - **Any `NAPI_VERSION` bump** (6 → 7 → 8 → …) and the per-engine native work it requires. - **jsc-android engine bump** (v6 enabler — see below). + - **Android in-process addon loading** — building `napi` as a shared library so `dlopen`'d test addons can resolve `napi_*` from the host (see *Platform status* below). Affects every Android consumer's packaging → separate PR. ## Current state (2026-06-04) @@ -29,6 +30,15 @@ This is a multi-PR effort. Keep the boundaries strict: `*` v5 surface to be confirmed green by this PR's suite. +### Platform status (this PR) + +| Platform | Engine | v5 suite | Runner | Notes | +|---|---|---|---|---| +| **macOS** | JavaScriptCore | ✅ **12/12** (plain + ASan/UBSan + TSan) | child-process | Full v5 reference. | +| **Android** | V8 (`libv8android.so`) | builds + harness runs; **addon tests SKIPPED** | in-process | App sandbox can't `fork`/`exec`; see below. | + +**Android in-process addon loading (deferred fix).** The js-native-api conformance addons are `dlopen`'d in-process and import `napi_*` from the host (`libUnitTestsJNI.so`). The host exports all 106 `napi_*` symbols (`NAPI_EXTERN = visibility("default")`), but bionic does not surface a `System.loadLibrary`-loaded (RTLD_LOCAL) host's symbols to a `dlopen`'d module, and post-hoc `RTLD_GLOBAL` promotion of the host is a **no-op on bionic** (verified on device: the module `dlopen` returns NULL; the addon carries no `DT_NEEDED` for napi). The fix is to build `napi` as a **shared library** (`libnapi.so`) depended on by both the host and the addons (a real `DT_NEEDED`) — a packaging change for every Android consumer that also needs export-visibility auditing across the `napi`/`jsr_`/`Napi::` surface, so it is tracked as a separate PR. Until then `test_main.cpp` `GTEST_SKIP`s these tests on Android with a documented reason; the Android suite still builds, the in-process harness runs without aborting (the `noexcept`-removal fix, commit `38864e4`), and the suite passes with the addon tests reported skipped (commit `39a87ea`). + ## Chakra N-API ceiling The Windows-OS Chakra (`chakra.dll`, JSRT/`jsrt.h`) is frozen ~ES2017 and will never gain new VM From f32130ea41cc4271b29798920a918ed012042fd6 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 18:13:42 -0700 Subject: [PATCH 20/33] Android: statically link conformance addons into the test binary (run in-process) Dynamic .node loading is never shipped to the Play / Quest stores, and bionic won't resolve a dlopen'd addon's napi_* imports against the System.loadLibrary-loaded host anyway (the addon carries no DT_NEEDED for napi; RTLD_GLOBAL host promotion is a no-op on bionic). Rather than make napi a shared library for every Android consumer (tracked separately, task #9), compile the conformance addons directly into the host (UnitTestsJNI) and resolve them in-process. To link several addons into one binary without symbol clashes: - node_api.h: make NODE_API_MODULE_REGISTER_FUNCTION / _GET_API_VERSION_FUNCTION overridable. - entry_point.h (JSR_NODE_API_STATIC_LINK): give Init internal linkage and emit a per-addon load-time constructor that self-registers its uniquely-suffixed registrar/version functions with the host. - The Android CMakeLists compiles each addon as an OBJECT library with per-module unique entry-point names and links them into UnitTestsJNI. - node_lite_android LoadFunction resolves entry points from the in-process static registry by module name instead of dlopen+dlsym. Removes the Android GTEST_SKIP. The 4 v5 js-native-api conformance tests now execute in-process and PASS on Android (2_function_arguments, 3_callbacks, 4_object_factory, 5_function_factory). macOS is unchanged (the desktop dynamic .node path uses the #else branches). --- Tests/NodeApi/include/node_api.h | 8 +++ Tests/NodeApi/node_lite_android.cpp | 70 ++++++++++++++----- .../NodeApi/test/js-native-api/entry_point.h | 26 +++++++ Tests/NodeApi/test_main.cpp | 14 ---- .../Android/app/src/main/cpp/CMakeLists.txt | 30 +++++++- 5 files changed, 115 insertions(+), 33 deletions(-) diff --git a/Tests/NodeApi/include/node_api.h b/Tests/NodeApi/include/node_api.h index 0ba4bc6e..60e3fa0d 100644 --- a/Tests/NodeApi/include/node_api.h +++ b/Tests/NodeApi/include/node_api.h @@ -35,8 +35,16 @@ typedef struct napi_module_s { void* reserved[4]; } napi_module; +// These may be overridden by the build (e.g. -DNODE_API_MODULE_REGISTER_FUNCTION=...) so that several +// addons can be *statically* linked into a single host binary -- each with a uniquely suffixed entry +// point -- without symbol clashes. The defaults are the canonical names a dynamic loader (dlsym) +// looks up in a standalone .node. +#ifndef NODE_API_MODULE_GET_API_VERSION_FUNCTION #define NODE_API_MODULE_GET_API_VERSION_FUNCTION node_api_module_get_api_version_v1 +#endif +#ifndef NODE_API_MODULE_REGISTER_FUNCTION #define NODE_API_MODULE_REGISTER_FUNCTION napi_register_module_v1 +#endif #define NAPI_MODULE_INIT() \ NODE_API_EXTERN_C_START \ diff --git a/Tests/NodeApi/node_lite_android.cpp b/Tests/NodeApi/node_lite_android.cpp index 79f90967..3779ed06 100644 --- a/Tests/NodeApi/node_lite_android.cpp +++ b/Tests/NodeApi/node_lite_android.cpp @@ -1,33 +1,67 @@ #include "node_lite.h" -#include +#include +#include +#include namespace node_api_tests { +namespace { +// Registry of the Node-API conformance addons that are statically linked into this host binary. It is +// populated at load time by the per-addon constructors emitted in entry_point.h under +// JSR_NODE_API_STATIC_LINK, each registering its uniquely-suffixed entry points keyed by module name. +// A function-local static (constructed on first registration) avoids any static-initialization-order +// dependency between those constructors and this translation unit. +struct StaticAddon { + std::string name; + int32_t (*get_api_version)(void); + napi_value (*register_module)(napi_env, napi_value); +}; + +std::vector& StaticAddonRegistry() { + static std::vector registry; + return registry; +} + +} // namespace +} // namespace node_api_tests + +// Called once per statically-linked addon from its load-time constructor (see entry_point.h). Declared +// extern "C" to match the declaration the C addon translation units compile against. +extern "C" void jsr_register_static_addon( + const char* name, + int32_t (*get_api_version)(void), + napi_value (*register_module)(napi_env, napi_value)) { + node_api_tests::StaticAddonRegistry().push_back( + {name, get_api_version, register_module}); +} + +namespace node_api_tests { + +// On Android the conformance addons are statically linked into the host rather than dlopen'd: dynamic +// .node loading is never shipped to the Play / Quest stores, and bionic won't resolve a dlopen'd +// addon's napi_* against the System.loadLibrary-loaded host anyway (see NAPI_VERSION_ROADMAP.md, and +// task #9 for the shared-lib alternative). So resolve the requested entry point from the in-process +// static registry, keyed by the module's file-stem name, instead of dlopen+dlsym. /*static*/ void* NodeLitePlatform::LoadFunction( napi_env /*env*/, const std::filesystem::path& lib_path, const std::string& function_name) noexcept { - // On Android native addons are packaged as lib.so in the app's nativeLibraryDir -- the - // only location a native library may be dlopen'd from on API 29+. The resolved lib_path points - // at a (non-existent) .node under the copied test tree, so load by soname and let the - // dynamic linker resolve it from nativeLibraryDir. - // - // NOTE: the addon imports napi_* from the host (libUnitTestsJNI.so). Because the host is loaded - // RTLD_LOCAL by System.loadLibrary, bionic's linker-namespace model does not expose its - // statically linked napi_* symbols to this dlopen'd module, so RTLD_NOW below cannot bind them. - // Making the in-process addon tests runnable on Android requires building napi as a shared - // library (libnapi.so) depended on by both the host and the addons -- tracked separately (see - // NAPI_VERSION_ROADMAP.md). The js-native-api tests are skipped on Android until then. - std::string soname = "lib" + lib_path.stem().string() + ".so"; - void* handle = dlopen(soname.c_str(), RTLD_NOW | RTLD_LOCAL); - if (handle == nullptr) - { + const std::string module_name = lib_path.stem().string(); + for (const StaticAddon& addon : StaticAddonRegistry()) { + if (addon.name != module_name) { + continue; + } + if (function_name == "napi_register_module_v1") { + return reinterpret_cast(addon.register_module); + } + if (function_name == "node_api_module_get_api_version_v1") { + return reinterpret_cast(addon.get_api_version); + } return nullptr; } - - return dlsym(handle, function_name.c_str()); + return nullptr; } } // namespace node_api_tests diff --git a/Tests/NodeApi/test/js-native-api/entry_point.h b/Tests/NodeApi/test/js-native-api/entry_point.h index 2e74d6c0..280501a0 100644 --- a/Tests/NodeApi/test/js-native-api/entry_point.h +++ b/Tests/NodeApi/test/js-native-api/entry_point.h @@ -3,10 +3,36 @@ #include +#if defined(JSR_NODE_API_STATIC_LINK) +// Static-link mode (the Android in-process conformance runner): every addon is linked into one host +// binary, so Init must have internal linkage to avoid one-definition clashes across addons. The +// registrar/version functions are uniquely suffixed per-module by the build (NODE_API_MODULE_*_FUNCTION +// in node_api.h). A later non-static definition of Init in the addon inherits this internal linkage. +static napi_value Init(napi_env env, napi_value exports); +#else EXTERN_C_START napi_value Init(napi_env env, napi_value exports); EXTERN_C_END +#endif NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) +#if defined(JSR_NODE_API_STATIC_LINK) +// Self-register this addon's uniquely-named entry points with the host at load time, keyed by module +// name, so the in-process loader (node_lite_android LoadFunction) can find them by name -- the static +// equivalent of dlopen+dlsym. jsr_register_static_addon is implemented by the host. +EXTERN_C_START +void jsr_register_static_addon( + const char* name, + int32_t (*get_api_version)(void), + napi_value (*register_module)(napi_env, napi_value)); +EXTERN_C_END + +static void __attribute__((constructor)) jsr_register_static_addon_ctor(void) { + jsr_register_static_addon(NODE_GYP_MODULE_NAME, + NODE_API_MODULE_GET_API_VERSION_FUNCTION, + NODE_API_MODULE_REGISTER_FUNCTION); +} +#endif + #endif // JS_NATIVE_API_ENTRY_POINT_H_ diff --git a/Tests/NodeApi/test_main.cpp b/Tests/NodeApi/test_main.cpp index 7cf25a8c..93803cb8 100644 --- a/Tests/NodeApi/test_main.cpp +++ b/Tests/NodeApi/test_main.cpp @@ -60,20 +60,6 @@ class NodeApiTestFixture : public TestFixtureBase { ASSERT_TRUE(static_cast(config.run_script)) << "Node-API test runner is not configured."; -#if defined(__ANDROID__) - // The js-native-api conformance addons are dlopen'd in-process and import napi_* from the host - // (libUnitTestsJNI.so). On Android that host is loaded RTLD_LOCAL by System.loadLibrary, and - // bionic's linker-namespace model does not expose its statically linked napi_* symbols to a - // dlopen'd module -- so the addon cannot bind them at load time. Resolving this requires building - // napi as a shared library (libnapi.so) shared by the host and the addons, a packaging change - // tracked separately (see NAPI_VERSION_ROADMAP.md). Until then skip the in-process addon tests on - // Android; macOS runs the full v5 addon suite as the reference. - if (m_jsFilePath.string().find("js-native-api") != std::string::npos) { - GTEST_SKIP() << "Android in-process addon loading needs napi built as a shared library " - "(libnapi.so); tracked separately. macOS covers the v5 addon suite."; - } -#endif - ProcessResult result = config.run_script(m_jsFilePath); if (result.status == 0) { return; diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index d3a920f0..94b0b9f0 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -73,5 +73,33 @@ target_link_libraries(UnitTestsJNI if(ANDROID) target_link_libraries(UnitTestsJNI PRIVATE dl) endif() +set(JSR_NODE_API_NATIVE_TESTS 2_function_arguments 3_callbacks 4_object_factory 5_function_factory) +string(JOIN "," JSR_NODE_API_NATIVE_TESTS_CSV ${JSR_NODE_API_NATIVE_TESTS}) target_compile_definitions(UnitTestsJNI - PRIVATE NODE_API_AVAILABLE_NATIVE_TESTS="2_function_arguments,3_callbacks,4_object_factory,5_function_factory") + PRIVATE NODE_API_AVAILABLE_NATIVE_TESTS="${JSR_NODE_API_NATIVE_TESTS_CSV}") + +# --- Statically linked Node-API conformance addons (Android in-process runner) ------------------ +# Dynamic .node loading is never shipped to the Play / Quest stores, and bionic will not resolve a +# dlopen'd addon's napi_* imports against the System.loadLibrary-loaded host anyway (see +# NAPI_VERSION_ROADMAP.md; task #9 tracks the shared-lib alternative). So compile each addon into the +# host with a uniquely suffixed entry point -- avoiding one-definition clashes between addons -- and +# let node_lite resolve them from the in-process static registry (node_lite_android) instead of dlsym. +# entry_point.h emits, per addon, a load-time constructor that self-registers those entry points. +foreach(_mod ${JSR_NODE_API_NATIVE_TESTS}) + add_library(jsr_addon_${_mod} OBJECT + ${TESTS_DIR}/NodeApi/test/js-native-api/${_mod}/${_mod}.c) + target_include_directories(jsr_addon_${_mod} PRIVATE + ${TESTS_DIR}/NodeApi/include + ${TESTS_DIR}/NodeApi/test/js-native-api + ${REPO_ROOT_DIR}/Core/Node-API/Include/Shared + ${REPO_ROOT_DIR}/Core/Node-API/Include/Shared/napi + ${REPO_ROOT_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE} + ${REPO_ROOT_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE}/napi) + target_compile_definitions(jsr_addon_${_mod} PRIVATE + JSR_NODE_API_STATIC_LINK + NODE_API_EXPERIMENTAL_NO_WARNING + NODE_GYP_MODULE_NAME=\"${_mod}\" + NODE_API_MODULE_REGISTER_FUNCTION=jsr_napi_register_${_mod} + NODE_API_MODULE_GET_API_VERSION_FUNCTION=jsr_get_api_version_${_mod}) + target_sources(UnitTestsJNI PRIVATE $) +endforeach() From f0d1c2e984c56f5f7ae1505646f1ccf29b891cc9 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 18:13:42 -0700 Subject: [PATCH 21/33] Android tests: pump native stdout/stderr to logcat The conformance suite runs gtest in-process; its results (RUN/OK/FAILED and failure file:line:message) went to stdout, which Android discards -- leaving only the JUnit "expected 0, was 1" with no detail. Pump stdout/stderr to logcat (tag NodeApiTests) so test output and any pre-crash native context are visible via `adb logcat -s NodeApiTests`. --- .../Android/app/src/main/cpp/JNI.cpp | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp index 24655923..d0ed4b94 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp +++ b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp @@ -6,8 +6,60 @@ #include "Babylon/DebugTrace.h" #include +#include +#include +#include +#include + +namespace { + +// The conformance suite runs gtest in-process and writes results (including failure file/line/message) +// to stdout/stderr, which Android otherwise discards. Pump both to logcat so test output -- and any +// native crash context printed before the process dies -- is actually visible (e.g. +// `adb logcat -s NodeApiTests`). Without this the only signal is the JUnit "expected 0, was 1". +void* PumpStdioToLogcat(void* arg) { + int read_fd = *static_cast(arg); + char buffer[1024]; + std::string line; + ssize_t count; + while ((count = read(read_fd, buffer, sizeof(buffer) - 1)) > 0) { + buffer[count] = '\0'; + line += buffer; + std::string::size_type newline; + while ((newline = line.find('\n')) != std::string::npos) { + __android_log_write(ANDROID_LOG_INFO, "NodeApiTests", line.substr(0, newline).c_str()); + line.erase(0, newline + 1); + } + } + return nullptr; +} + +void RedirectStdioToLogcat() { + static int pipe_fds[2]; + static bool installed = false; + if (installed) { + return; + } + installed = true; + setvbuf(stdout, nullptr, _IOLBF, 0); + setvbuf(stderr, nullptr, _IONBF, 0); + if (pipe(pipe_fds) != 0) { + return; + } + dup2(pipe_fds[1], STDOUT_FILENO); + dup2(pipe_fds[1], STDERR_FILENO); + pthread_t thread; + if (pthread_create(&thread, nullptr, PumpStdioToLogcat, &pipe_fds[0]) == 0) { + pthread_detach(thread); + } +} + +} // namespace + extern "C" JNIEXPORT jint JNICALL Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass clazz, jobject context) { + RedirectStdioToLogcat(); + JavaVM* javaVM{}; if (env->GetJavaVM(&javaVM) != JNI_OK) { From 1938ff0229e7325f6236f19bcc55a23f1dedfeba Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 18:15:39 -0700 Subject: [PATCH 22/33] docs(roadmap): Android v5 js-native-api now green via static linking (supersedes skip) --- Tests/NodeApi/NAPI_VERSION_ROADMAP.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md index 2226eead..d73d23f5 100644 --- a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md +++ b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md @@ -35,9 +35,11 @@ This is a multi-PR effort. Keep the boundaries strict: | Platform | Engine | v5 suite | Runner | Notes | |---|---|---|---|---| | **macOS** | JavaScriptCore | ✅ **12/12** (plain + ASan/UBSan + TSan) | child-process | Full v5 reference. | -| **Android** | V8 (`libv8android.so`) | builds + harness runs; **addon tests SKIPPED** | in-process | App sandbox can't `fork`/`exec`; see below. | +| **Android** | V8 (`libv8android.so`) | ✅ **4/4 js-native-api** (statically linked, in-process) | in-process | App sandbox can't `fork`/`exec`; addons static-linked — see below. | -**Android in-process addon loading (deferred fix).** The js-native-api conformance addons are `dlopen`'d in-process and import `napi_*` from the host (`libUnitTestsJNI.so`). The host exports all 106 `napi_*` symbols (`NAPI_EXTERN = visibility("default")`), but bionic does not surface a `System.loadLibrary`-loaded (RTLD_LOCAL) host's symbols to a `dlopen`'d module, and post-hoc `RTLD_GLOBAL` promotion of the host is a **no-op on bionic** (verified on device: the module `dlopen` returns NULL; the addon carries no `DT_NEEDED` for napi). The fix is to build `napi` as a **shared library** (`libnapi.so`) depended on by both the host and the addons (a real `DT_NEEDED`) — a packaging change for every Android consumer that also needs export-visibility auditing across the `napi`/`jsr_`/`Napi::` surface, so it is tracked as a separate PR. Until then `test_main.cpp` `GTEST_SKIP`s these tests on Android with a documented reason; the Android suite still builds, the in-process harness runs without aborting (the `noexcept`-removal fix, commit `38864e4`), and the suite passes with the addon tests reported skipped (commit `39a87ea`). +**Android in-process addon loading (statically linked).** The app sandbox can't `fork`/`exec`, so the conformance suite runs the addons *in-process*. Dynamic `.node` loading there is a dead end — and is never shipped to the Play/Quest stores anyway: the host (`libUnitTestsJNI.so`) is loaded RTLD_LOCAL by `System.loadLibrary`, and bionic won't surface its (exported) `napi_*` to a `dlopen`'d module; post-hoc `RTLD_GLOBAL` promotion is a no-op on bionic (verified on device: the module `dlopen` returns NULL; the addon carries no `DT_NEEDED` for napi). So the addons are **statically linked into the host** instead: `node_api.h` makes the registrar/version entry-point names overridable, `entry_point.h` (under `JSR_NODE_API_STATIC_LINK`) gives `Init` internal linkage and emits a per-addon load-time constructor that self-registers its uniquely-suffixed entry points, the Android CMake compiles each addon as an OBJECT library, and `node_lite_android` resolves them from an in-process registry instead of `dlopen`+`dlsym`. The 4 v5 js-native-api tests now execute in-process and **pass** (commits `f32130e`, `f0d1c2e`; the harness also needed the `noexcept`-removal fix `38864e4` so a failing test surfaces as a `ProcessResult` rather than `std::terminate`). Native stdout/stderr is pumped to logcat (tag `NodeApiTests`) so gtest results are visible (`adb logcat -s NodeApiTests`). Building `napi` as a shared library (task #9) would also work and is the only option if real external `.node` addons ever need `dlopen` on Android, but it ripples to every consumer's packaging and is not needed for the conformance suite. + +> Emulator note: the unrelated `JavaScript.All` UnitTest (XMLHttpRequest/WebSocket/HTTP mocha tests) fails in an offline emulator (status 0 vs 200/404, socket timeouts); it runs before and is independent of the js-native-api suite. ## Chakra N-API ceiling From d4231ee3904bfc8cead3bbe2b798a87d2edff7f8 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 18:32:16 -0700 Subject: [PATCH 23/33] Android: drop the now-dead dynamic-.node build machinery (superseded by static linking) The conformance addons are statically linked into the in-process Android test host (f32130e), so the standalone SHARED .so + -Wl,--unresolved-symbols=ignore-all + lib.so naming that the old dlopen-on-Android path needed are dead. add_node_api_module now early-returns on Android and is a clean desktop-only MODULE .node helper. Also fixes a stale node_lite comment describing the abandoned soname-dlopen path. No functional change on desktop (MODULE .node, -undefined dynamic_lookup, POST_BUILD staging, codesign all preserved); macOS still 12/12. --- Tests/NodeApi/CMakeLists.txt | 89 +++++++++++++++--------------------- Tests/NodeApi/node_lite.cpp | 7 +-- 2 files changed, 40 insertions(+), 56 deletions(-) diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt index 5784de45..779638c2 100644 --- a/Tests/NodeApi/CMakeLists.txt +++ b/Tests/NodeApi/CMakeLists.txt @@ -120,6 +120,13 @@ if(JSR_NODE_API_BUILD_NATIVE_TESTS) endif() function(add_node_api_module MODULE_TARGET) + if(ANDROID) + # On Android the conformance addons are statically linked into the in-process test host + # (Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt) -- there is no standalone .node to + # build or dlopen, so this desktop helper builds nothing here. + return() + endif() + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "" "SOURCES;DEFINES") get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) @@ -128,13 +135,8 @@ function(add_node_api_module MODULE_TARGET) set(MODULE_TARGET "${FOLDER_NAME}_${MODULE_TARGET}") endif() - # On Android the addon must be a SHARED library so Gradle/AGP packages it into the APK's - # lib// (the app's nativeLibraryDir). Elsewhere it is a MODULE (dlopen-only) .node. - if(ANDROID) - add_library(${MODULE_TARGET} SHARED) - else() - add_library(${MODULE_TARGET} MODULE) - endif() + # The addon is a MODULE (dlopen-only) .node, loaded by the node_lite child-process runner. + add_library(${MODULE_TARGET} MODULE) target_sources(${MODULE_TARGET} PRIVATE ${ARG_SOURCES}) target_include_directories(${MODULE_TARGET} PRIVATE @@ -153,61 +155,42 @@ function(add_node_api_module MODULE_TARGET) ) if(APPLE) + # The addon is dlopen'd into node_lite, which already provides the napi_* symbols; let them + # remain unresolved at link time. target_link_options(${MODULE_TARGET} PRIVATE "-undefined" "dynamic_lookup" ) - elseif(ANDROID) - # The .node module is dlopen'd into a host (UnitTestsJNI) that already provides the napi_* - # symbols. The Android NDK links shared libraries with --no-undefined by default, which would - # reject those symbols at build time; allow them to remain unresolved so they bind at load - # time (the ELF equivalent of Apple's -undefined dynamic_lookup above). - target_link_options(${MODULE_TARGET} - PRIVATE - "-Wl,--unresolved-symbols=ignore-all" - ) endif() - if(ANDROID) - # lib.so so AGP packages it into lib// (-> nativeLibraryDir), the only place a - # native library may be dlopen'd from on API 29+. node_lite_android loads it by soname. - set_target_properties(${MODULE_TARGET} - PROPERTIES - PREFIX "lib" - SUFFIX ".so" - ) - else() - 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} - ) - 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}) - if(NOT ANDROID) - # Desktop: stage the built .node next to the node_lite / NodeApiTests runners so they can - # dlopen it relative to the copied test files. (Android loads from nativeLibraryDir.) - 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" - ) - endif() + # Stage the built .node next to the node_lite / NodeApiTests runners so they can dlopen it + # relative to the copied test files. + 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" + ) if(APPLE) # The copy_directory above runs as an Xcode "Run Script" phase, which executes BEFORE diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index 587c26e2..958b66bf 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -406,9 +406,10 @@ fs::path NodeLiteRuntime::ResolveModulePath( return node_module_path; } #if defined(__ANDROID__) - // On Android the addon ships as lib.so in the app's nativeLibraryDir rather than as a - // .node file on disk, so the existence check above fails. Resolve to the .node path anyway so - // LoadNativeModule runs; node_lite_android dlopens it by soname (lib.so). + // On Android the conformance addons are statically linked into the host rather than present as + // .node files on disk, so the existence check above fails. Resolve to the .node path anyway so + // LoadNativeModule runs; node_lite_android resolves the entry points from the in-process static + // registry, keyed by this path's stem (the module name). return node_module_path; #endif // See if the module was prefixed with the parent folder to disambiguate C++ From 04e3158a932bfbfb40dfc0829e466b0206b44c64 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 18:37:26 -0700 Subject: [PATCH 24/33] Android: remove vestigial V8Platform scaffolding from the env holder V8Platform::EnsureInitialized() became a no-op once we found the host AppRuntime already initializes V8's process-global platform; the class and its unused init_flag_/platform_ members were left over from the abandoned platform-init attempt. Fold the (still-important) "don't re-init the platform" rationale into a comment at the isolate-creation site, and drop the dead class plus the now-unused / includes. No behavior change; Android still 4/4 js-native-api. --- Tests/NodeApi/node_lite_jsruntimehost.cpp | 28 ++++------------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/Tests/NodeApi/node_lite_jsruntimehost.cpp b/Tests/NodeApi/node_lite_jsruntimehost.cpp index 1b3f229b..a79fe91b 100644 --- a/Tests/NodeApi/node_lite_jsruntimehost.cpp +++ b/Tests/NodeApi/node_lite_jsruntimehost.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #if defined(__APPLE__) @@ -14,7 +13,6 @@ #elif defined(__ANDROID__) #include #include "js_native_api_v8.h" -#include #endif namespace node_api_tests { @@ -31,8 +29,10 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { context_ = JSGlobalContextCreateInGroup(nullptr, nullptr); env_ = Napi::Attach(context_); #elif defined(__ANDROID__) - V8Platform::EnsureInitialized(); - + // V8's platform is process-global and is already initialized by JsRuntimeHost -- the host + // AppRuntime that UnitTestsJNI links and that runs (via the regular V8 unit tests) before these + // in-process Node-API tests. Initializing it a second time aborts V8 with "Wrong initialization + // order", so reuse the host's platform and only create our own isolate/context below. allocator_.reset(v8::ArrayBuffer::Allocator::NewDefaultAllocator()); v8::Isolate::CreateParams create_params; create_params.array_buffer_allocator = allocator_.get(); @@ -134,21 +134,6 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { #if defined(__APPLE__) JSGlobalContextRef context_{}; #elif defined(__ANDROID__) - class V8Platform { - public: - static void EnsureInitialized() { - // V8's platform is process-global and is already initialized by JsRuntimeHost -- the host - // AppRuntime that UnitTestsJNI links and that runs (via the regular V8 unit tests) before - // these in-process Node-API tests. Initializing it a second time aborts V8 with - // "Wrong initialization order", so reuse the host's platform and only create our own - // isolate/context below. - } - - private: - static std::once_flag init_flag_; - static std::unique_ptr platform_; - }; - v8::Isolate* isolate_{nullptr}; std::unique_ptr locker_{}; std::unique_ptr isolate_scope_{}; @@ -159,11 +144,6 @@ class JsRuntimeHostEnvHolder : public IEnvHolder { 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( From b51d1a7fbb695d7dd36c49956e078e1fc0f50c67 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 18:40:46 -0700 Subject: [PATCH 25/33] =?UTF-8?q?docs(roadmap):=20record=20node-api-cts=20?= =?UTF-8?q?FetchContent=20evaluation=20(task=206)=20=E2=80=94=20track,=20d?= =?UTF-8?q?on't=20adopt=20yet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/NodeApi/NAPI_VERSION_ROADMAP.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md index d73d23f5..9fa4a74d 100644 --- a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md +++ b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md @@ -88,11 +88,26 @@ run the suite per engine → implement the JSC/(Chakra) gaps → green. ## Test-suite sourcing strategy - **Now (this PR):** vendored copy of vmoroz's hermes-windows `unittests/NodeApi/` (engine-layer, v8-capable - harness, `node_lite`). Resync the v1–v5 test files from upstream hermes-windows so our copies are current. -- **Later:** migrate to **`nodejs/node-api-cts`** (engine-agnostic, CMake `add_node_api_cts_addon()`, - `implementors//` harness contract: `assert`/`loadAddon`/`gcUntil`/`napiVersion`/`features`). - Consume via `FetchContent` `GIT_REPOSITORY` once it stabilizes / publishes (currently pre-1.0, not on npm; - `node-api/` runtime tests not yet started). This replaces the "gross copying" the PR flags. + harness, `node_lite`). Resync the v1–v5 test files from upstream hermes-windows so our copies are current (task 5). +- **Evaluated (task 6) — `nodejs/node-api-cts` as a `FetchContent` `GIT_REPOSITORY` dep: not yet; track for later.** + Findings (HEAD `ea10da9`, 2026-06): + - **Maturity blocker:** the README states it "is currently a work-in-progress and shouldn't yet be relied on by + anyone" (v0.1.0, not on npm). Too early to take as an upstream dependency. + - **Harness-model mismatch:** the runner is Node.js + TypeScript (`node --test implementors/node/run-tests.ts`, + `amaro` for TS-strip); the only implementor is `node`. Adopting it means authoring an `implementors/jsruntimehost/` + harness (JS modules: `load-addon`, `assert`, `must-call`, `gc`, `napi-version`, `features`, `skip-test`) and + driving the test `.js` from our runtime — i.e. re-expressing what `node_lite` already does, in their contract. + - **Doesn't solve Android:** `add_node_api_cts_addon()` builds SHARED `.node` (dlopen), with only Apple + `-undefined dynamic_lookup` / MSVC import-lib handling and no Android path — we'd re-apply the same static-link + adaptation just landed here. + - **No v5 coverage gain now:** its `tests/js-native-api/` are ported from the same `nodejs/node` source as our + vendored copies; the 4 v5 tests are identical content. + - **Upside (why track it):** engine-agnostic, active (~24 js-native-api tests ported incl. `test_bigint`, + `test_typedarray`, `test_string`, `test_date` — valuable once we bump NAPI_VERSION), CMake-based, and + contributing a JsRuntimeHost implementor could upstream our Android in-process + static-link learnings. + - **Recommendation:** keep the vendored suite for v5 (works; both platforms green). Revisit node-api-cts when we + bump NAPI_VERSION (needing its broader tests) **and** it approaches 1.0 — migrating there rather than doing a + hermes-windows resync at that point. That is the eventual answer to the "gross copying" this PR flags. --- From 58223c0fbdcdde8fd8c36894ec8ebb9db7c59537 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 18:58:39 -0700 Subject: [PATCH 26/33] Android: dlopen conformance addons as dynamic .node backed by a shared libnapi.so Replaces the interim static-link-into-host approach with the dynamic .node model used by nodejs/node-api-cts (add_node_api_cts_addon), so the Android suite and a future node-api-cts migration share one addon model. - Core/Node-API: build napi as a SHARED library (libnapi.so) on Android. It exports all 106 napi_* (default visibility -- no global -fvisibility=hidden), and the host plus every addon depend on the one libnapi.so via a real DT_NEEDED, so there is a single napi instance. Static elsewhere. - The conformance addons are again standalone SHARED lib.so (packaged into nativeLibraryDir), now linking napi (DT_NEEDED libnapi.so) instead of -Wl,--unresolved-symbols=ignore-all. - node_lite_android resolves entry points via dlopen(soname)+dlsym again; the addon's napi_* bind from libnapi.so at load. Reverts the static-link infra (entry_point.h JSR_NODE_API_STATIC_LINK branch, node_api.h overridable registrar macros, the host's per-addon OBJECT libraries). Verified on device: lib2_function_arguments.so has DT_NEEDED [libnapi.so], its napi_* are imports, libnapi.so exports the 106 napi_*, and all 4 v5 js-native-api tests pass in-process. macOS unchanged (12/12; desktop keeps static napi + dlopen'd MODULE .node). --- Core/Node-API/CMakeLists.txt | 12 ++- Tests/NodeApi/CMakeLists.txt | 81 +++++++++++-------- Tests/NodeApi/include/node_api.h | 8 -- Tests/NodeApi/node_lite.cpp | 8 +- Tests/NodeApi/node_lite_android.cpp | 64 +++------------ .../NodeApi/test/js-native-api/entry_point.h | 50 +++--------- .../Android/app/src/main/cpp/CMakeLists.txt | 32 +------- 7 files changed, 90 insertions(+), 165 deletions(-) diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index cc8e7846..addb7041 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -155,7 +155,17 @@ if(NAPI_BUILD_ABI) message(STATUS "Selected ${NAPI_JAVASCRIPT_ENGINE}") endif() -add_library(napi ${SOURCES}) +# On Android the Node-API conformance addons are dlopen'd as standalone SHARED .node modules (matching +# nodejs/node-api-cts's add_node_api_cts_addon model). For an addon's napi_* imports to bind at load, +# napi must be a shared library that both the host and the addons depend on via a real DT_NEEDED +# (libnapi.so) -- bionic will not surface a statically-linked host's napi to a dlopen'd module. There +# is a single napi instance (one libnapi.so) shared by the host and every addon. Elsewhere (the +# child-process desktop runner) napi stays a static library. +if(ANDROID) + add_library(napi SHARED ${SOURCES}) +else() + add_library(napi ${SOURCES}) +endif() target_include_directories(napi ${INCLUDE_DIRECTORIES}) target_link_libraries(napi ${LINK_LIBRARIES}) diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt index 779638c2..1b0330ef 100644 --- a/Tests/NodeApi/CMakeLists.txt +++ b/Tests/NodeApi/CMakeLists.txt @@ -120,13 +120,6 @@ if(JSR_NODE_API_BUILD_NATIVE_TESTS) endif() function(add_node_api_module MODULE_TARGET) - if(ANDROID) - # On Android the conformance addons are statically linked into the in-process test host - # (Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt) -- there is no standalone .node to - # build or dlopen, so this desktop helper builds nothing here. - return() - endif() - cmake_parse_arguments(PARSE_ARGV 0 ARG "" "" "SOURCES;DEFINES") get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) @@ -135,8 +128,14 @@ function(add_node_api_module MODULE_TARGET) set(MODULE_TARGET "${FOLDER_NAME}_${MODULE_TARGET}") endif() - # The addon is a MODULE (dlopen-only) .node, loaded by the node_lite child-process runner. - add_library(${MODULE_TARGET} MODULE) + # On Android the addon is a SHARED lib.so so AGP packages it into the APK's lib// (the + # app's nativeLibraryDir, the only place a native library may be dlopen'd from on API 29+). + # Elsewhere it is a MODULE (dlopen-only) .node loaded by the node_lite child-process runner. + if(ANDROID) + add_library(${MODULE_TARGET} SHARED) + else() + add_library(${MODULE_TARGET} MODULE) + endif() target_sources(${MODULE_TARGET} PRIVATE ${ARG_SOURCES}) target_include_directories(${MODULE_TARGET} PRIVATE @@ -161,36 +160,50 @@ function(add_node_api_module MODULE_TARGET) PRIVATE "-undefined" "dynamic_lookup" ) + elseif(ANDROID) + # Link the shared napi (libnapi.so) so the addon's napi_* imports resolve at dlopen via a real + # DT_NEEDED -- the same libnapi.so the host depends on, so there is a single napi instance. + target_link_libraries(${MODULE_TARGET} PRIVATE napi) 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} - ) + if(ANDROID) + set_target_properties(${MODULE_TARGET} + PROPERTIES + PREFIX "lib" + SUFFIX ".so" + ) + else() + 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} + ) + endif() add_dependencies(NodeApiModules ${MODULE_TARGET}) - # Stage the built .node next to the node_lite / NodeApiTests runners so they can dlopen it - # relative to the copied test files. - 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" - ) + if(NOT ANDROID) + # Stage the built .node next to the node_lite / NodeApiTests runners so they can dlopen it + # relative to the copied test files. + 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" + ) + endif() if(APPLE) # The copy_directory above runs as an Xcode "Run Script" phase, which executes BEFORE diff --git a/Tests/NodeApi/include/node_api.h b/Tests/NodeApi/include/node_api.h index 60e3fa0d..0ba4bc6e 100644 --- a/Tests/NodeApi/include/node_api.h +++ b/Tests/NodeApi/include/node_api.h @@ -35,16 +35,8 @@ typedef struct napi_module_s { void* reserved[4]; } napi_module; -// These may be overridden by the build (e.g. -DNODE_API_MODULE_REGISTER_FUNCTION=...) so that several -// addons can be *statically* linked into a single host binary -- each with a uniquely suffixed entry -// point -- without symbol clashes. The defaults are the canonical names a dynamic loader (dlsym) -// looks up in a standalone .node. -#ifndef NODE_API_MODULE_GET_API_VERSION_FUNCTION #define NODE_API_MODULE_GET_API_VERSION_FUNCTION node_api_module_get_api_version_v1 -#endif -#ifndef NODE_API_MODULE_REGISTER_FUNCTION #define NODE_API_MODULE_REGISTER_FUNCTION napi_register_module_v1 -#endif #define NAPI_MODULE_INIT() \ NODE_API_EXTERN_C_START \ diff --git a/Tests/NodeApi/node_lite.cpp b/Tests/NodeApi/node_lite.cpp index 958b66bf..16c45c41 100644 --- a/Tests/NodeApi/node_lite.cpp +++ b/Tests/NodeApi/node_lite.cpp @@ -406,10 +406,10 @@ fs::path NodeLiteRuntime::ResolveModulePath( return node_module_path; } #if defined(__ANDROID__) - // On Android the conformance addons are statically linked into the host rather than present as - // .node files on disk, so the existence check above fails. Resolve to the .node path anyway so - // LoadNativeModule runs; node_lite_android resolves the entry points from the in-process static - // registry, keyed by this path's stem (the module name). + // On Android the addon ships as lib.so in the app's nativeLibraryDir rather than as a .node + // file on disk, so the existence check above fails. Resolve to the .node path anyway so + // LoadNativeModule runs; node_lite_android dlopens it by soname (lib.so), resolving its + // napi_* imports from the shared libnapi.so. return node_module_path; #endif // See if the module was prefixed with the parent folder to disambiguate C++ diff --git a/Tests/NodeApi/node_lite_android.cpp b/Tests/NodeApi/node_lite_android.cpp index 3779ed06..0f34ac14 100644 --- a/Tests/NodeApi/node_lite_android.cpp +++ b/Tests/NodeApi/node_lite_android.cpp @@ -1,67 +1,27 @@ #include "node_lite.h" -#include -#include -#include +#include namespace node_api_tests { -namespace { -// Registry of the Node-API conformance addons that are statically linked into this host binary. It is -// populated at load time by the per-addon constructors emitted in entry_point.h under -// JSR_NODE_API_STATIC_LINK, each registering its uniquely-suffixed entry points keyed by module name. -// A function-local static (constructed on first registration) avoids any static-initialization-order -// dependency between those constructors and this translation unit. -struct StaticAddon { - std::string name; - int32_t (*get_api_version)(void); - napi_value (*register_module)(napi_env, napi_value); -}; - -std::vector& StaticAddonRegistry() { - static std::vector registry; - return registry; -} - -} // namespace -} // namespace node_api_tests - -// Called once per statically-linked addon from its load-time constructor (see entry_point.h). Declared -// extern "C" to match the declaration the C addon translation units compile against. -extern "C" void jsr_register_static_addon( - const char* name, - int32_t (*get_api_version)(void), - napi_value (*register_module)(napi_env, napi_value)) { - node_api_tests::StaticAddonRegistry().push_back( - {name, get_api_version, register_module}); -} - -namespace node_api_tests { - -// On Android the conformance addons are statically linked into the host rather than dlopen'd: dynamic -// .node loading is never shipped to the Play / Quest stores, and bionic won't resolve a dlopen'd -// addon's napi_* against the System.loadLibrary-loaded host anyway (see NAPI_VERSION_ROADMAP.md, and -// task #9 for the shared-lib alternative). So resolve the requested entry point from the in-process -// static registry, keyed by the module's file-stem name, instead of dlopen+dlsym. /*static*/ void* NodeLitePlatform::LoadFunction( napi_env /*env*/, const std::filesystem::path& lib_path, const std::string& function_name) noexcept { - const std::string module_name = lib_path.stem().string(); - for (const StaticAddon& addon : StaticAddonRegistry()) { - if (addon.name != module_name) { - continue; - } - if (function_name == "napi_register_module_v1") { - return reinterpret_cast(addon.register_module); - } - if (function_name == "node_api_module_get_api_version_v1") { - return reinterpret_cast(addon.get_api_version); - } + // On Android the conformance addons are packaged as lib.so in the app's nativeLibraryDir -- + // the only location a native library may be dlopen'd from on API 29+. The resolved lib_path points + // at a (non-existent) .node under the copied test tree, so load by soname and let the dynamic + // linker resolve it from nativeLibraryDir. The addon's napi_* imports resolve from libnapi.so -- a + // DT_NEEDED of both the addon and the host -- so RTLD_NOW binds them at load time. + std::string soname = "lib" + lib_path.stem().string() + ".so"; + void* handle = dlopen(soname.c_str(), RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) + { return nullptr; } - return nullptr; + + return dlsym(handle, function_name.c_str()); } } // namespace node_api_tests diff --git a/Tests/NodeApi/test/js-native-api/entry_point.h b/Tests/NodeApi/test/js-native-api/entry_point.h index 280501a0..5ba5aaff 100644 --- a/Tests/NodeApi/test/js-native-api/entry_point.h +++ b/Tests/NodeApi/test/js-native-api/entry_point.h @@ -1,38 +1,12 @@ -#ifndef JS_NATIVE_API_ENTRY_POINT_H_ -#define JS_NATIVE_API_ENTRY_POINT_H_ - -#include - -#if defined(JSR_NODE_API_STATIC_LINK) -// Static-link mode (the Android in-process conformance runner): every addon is linked into one host -// binary, so Init must have internal linkage to avoid one-definition clashes across addons. The -// registrar/version functions are uniquely suffixed per-module by the build (NODE_API_MODULE_*_FUNCTION -// in node_api.h). A later non-static definition of Init in the addon inherits this internal linkage. -static napi_value Init(napi_env env, napi_value exports); -#else -EXTERN_C_START -napi_value Init(napi_env env, napi_value exports); -EXTERN_C_END -#endif - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) - -#if defined(JSR_NODE_API_STATIC_LINK) -// Self-register this addon's uniquely-named entry points with the host at load time, keyed by module -// name, so the in-process loader (node_lite_android LoadFunction) can find them by name -- the static -// equivalent of dlopen+dlsym. jsr_register_static_addon is implemented by the host. -EXTERN_C_START -void jsr_register_static_addon( - const char* name, - int32_t (*get_api_version)(void), - napi_value (*register_module)(napi_env, napi_value)); -EXTERN_C_END - -static void __attribute__((constructor)) jsr_register_static_addon_ctor(void) { - jsr_register_static_addon(NODE_GYP_MODULE_NAME, - NODE_API_MODULE_GET_API_VERSION_FUNCTION, - NODE_API_MODULE_REGISTER_FUNCTION); -} -#endif - -#endif // JS_NATIVE_API_ENTRY_POINT_H_ +#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/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index 94b0b9f0..ce0569d4 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -73,33 +73,9 @@ target_link_libraries(UnitTestsJNI if(ANDROID) target_link_libraries(UnitTestsJNI PRIVATE dl) endif() -set(JSR_NODE_API_NATIVE_TESTS 2_function_arguments 3_callbacks 4_object_factory 5_function_factory) -string(JOIN "," JSR_NODE_API_NATIVE_TESTS_CSV ${JSR_NODE_API_NATIVE_TESTS}) target_compile_definitions(UnitTestsJNI - PRIVATE NODE_API_AVAILABLE_NATIVE_TESTS="${JSR_NODE_API_NATIVE_TESTS_CSV}") + PRIVATE NODE_API_AVAILABLE_NATIVE_TESTS="2_function_arguments,3_callbacks,4_object_factory,5_function_factory") -# --- Statically linked Node-API conformance addons (Android in-process runner) ------------------ -# Dynamic .node loading is never shipped to the Play / Quest stores, and bionic will not resolve a -# dlopen'd addon's napi_* imports against the System.loadLibrary-loaded host anyway (see -# NAPI_VERSION_ROADMAP.md; task #9 tracks the shared-lib alternative). So compile each addon into the -# host with a uniquely suffixed entry point -- avoiding one-definition clashes between addons -- and -# let node_lite resolve them from the in-process static registry (node_lite_android) instead of dlsym. -# entry_point.h emits, per addon, a load-time constructor that self-registers those entry points. -foreach(_mod ${JSR_NODE_API_NATIVE_TESTS}) - add_library(jsr_addon_${_mod} OBJECT - ${TESTS_DIR}/NodeApi/test/js-native-api/${_mod}/${_mod}.c) - target_include_directories(jsr_addon_${_mod} PRIVATE - ${TESTS_DIR}/NodeApi/include - ${TESTS_DIR}/NodeApi/test/js-native-api - ${REPO_ROOT_DIR}/Core/Node-API/Include/Shared - ${REPO_ROOT_DIR}/Core/Node-API/Include/Shared/napi - ${REPO_ROOT_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE} - ${REPO_ROOT_DIR}/Core/Node-API/Include/Engine/${NAPI_JAVASCRIPT_ENGINE}/napi) - target_compile_definitions(jsr_addon_${_mod} PRIVATE - JSR_NODE_API_STATIC_LINK - NODE_API_EXPERIMENTAL_NO_WARNING - NODE_GYP_MODULE_NAME=\"${_mod}\" - NODE_API_MODULE_REGISTER_FUNCTION=jsr_napi_register_${_mod} - NODE_API_MODULE_GET_API_VERSION_FUNCTION=jsr_get_api_version_${_mod}) - target_sources(UnitTestsJNI PRIVATE $) -endforeach() +# The conformance addons are built as standalone SHARED lib.so by Tests/NodeApi/CMakeLists.txt, +# packaged by AGP into nativeLibraryDir, and dlopen'd in-process by node_lite_android. Both they and +# this host link the shared napi (libnapi.so), so the addons' napi_* imports resolve at load. From 538fe04cecb6323b82361771eb86e5684377d495 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 18:58:39 -0700 Subject: [PATCH 27/33] docs(roadmap): Android uses dynamic .node + shared libnapi.so (aligns with node-api-cts) --- Tests/NodeApi/NAPI_VERSION_ROADMAP.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md index 9fa4a74d..b7538ad5 100644 --- a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md +++ b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md @@ -11,7 +11,11 @@ This is a multi-PR effort. Keep the boundaries strict: - **Bug fixes** against the current N-API implementation that the tests surface → *quarantine* the failing test via the allow-list and open a separate fix PR. Do not fix impl bugs in the test-suite PR. - **Any `NAPI_VERSION` bump** (6 → 7 → 8 → …) and the per-engine native work it requires. - **jsc-android engine bump** (v6 enabler — see below). - - **Android in-process addon loading** — building `napi` as a shared library so `dlopen`'d test addons can resolve `napi_*` from the host (see *Platform status* below). Affects every Android consumer's packaging → separate PR. + +> **Android packaging note:** to run the suite in-process the addons are `dlopen`'d as standalone +> `.node` modules, which requires `napi` to ship as a shared library (`libnapi.so`) on Android (see +> *Platform status*). That is a deliberate packaging change for all Android consumers; if upstream +> prefers it isolated, it can land as a small precursor commit/PR that this suite depends on. ## Current state (2026-06-04) @@ -35,11 +39,13 @@ This is a multi-PR effort. Keep the boundaries strict: | Platform | Engine | v5 suite | Runner | Notes | |---|---|---|---|---| | **macOS** | JavaScriptCore | ✅ **12/12** (plain + ASan/UBSan + TSan) | child-process | Full v5 reference. | -| **Android** | V8 (`libv8android.so`) | ✅ **4/4 js-native-api** (statically linked, in-process) | in-process | App sandbox can't `fork`/`exec`; addons static-linked — see below. | +| **Android** | V8 (`libv8android.so`) | ✅ **4/4 js-native-api** (dynamic `.node` + `libnapi.so`, in-process) | in-process | App sandbox can't `fork`/`exec`; addons `dlopen`'d — see below. | + +**Android in-process addon loading (dynamic `.node` + shared `libnapi.so`).** The app sandbox can't `fork`/`exec`, so the conformance suite runs the addons *in-process*. The addons are built as standalone SHARED `lib.so` modules (matching nodejs/node-api-cts's `add_node_api_cts_addon`), packaged by AGP into `nativeLibraryDir`, and `dlopen`'d by `node_lite_android` by soname. For their `napi_*` imports to bind at load, **`napi` is built as a shared library (`libnapi.so`) on Android** — a real `DT_NEEDED` of both the host and every addon, so there is a single napi instance. This is the IoC / dynamic-self-registration model: `dlopen` the addon → its `DT_NEEDED libnapi.so` resolves the `napi_*` → `dlsym("napi_register_module_v1")` → call it. Verified on device: `lib2_function_arguments.so` carries `DT_NEEDED [libnapi.so]`, its 8 `napi_*` are imports (`U`), `libnapi.so` exports all 106 `napi_*` (`T`), and the 4 v5 tests pass. The harness also needs the `noexcept`-removal fix (`38864e4`) so a failing test surfaces as a `ProcessResult` rather than `std::terminate`, and native stdout/stderr is pumped to logcat (tag `NodeApiTests`) for visible gtest output. -**Android in-process addon loading (statically linked).** The app sandbox can't `fork`/`exec`, so the conformance suite runs the addons *in-process*. Dynamic `.node` loading there is a dead end — and is never shipped to the Play/Quest stores anyway: the host (`libUnitTestsJNI.so`) is loaded RTLD_LOCAL by `System.loadLibrary`, and bionic won't surface its (exported) `napi_*` to a `dlopen`'d module; post-hoc `RTLD_GLOBAL` promotion is a no-op on bionic (verified on device: the module `dlopen` returns NULL; the addon carries no `DT_NEEDED` for napi). So the addons are **statically linked into the host** instead: `node_api.h` makes the registrar/version entry-point names overridable, `entry_point.h` (under `JSR_NODE_API_STATIC_LINK`) gives `Init` internal linkage and emits a per-addon load-time constructor that self-registers its uniquely-suffixed entry points, the Android CMake compiles each addon as an OBJECT library, and `node_lite_android` resolves them from an in-process registry instead of `dlopen`+`dlsym`. The 4 v5 js-native-api tests now execute in-process and **pass** (commits `f32130e`, `f0d1c2e`; the harness also needed the `noexcept`-removal fix `38864e4` so a failing test surfaces as a `ProcessResult` rather than `std::terminate`). Native stdout/stderr is pumped to logcat (tag `NodeApiTests`) so gtest results are visible (`adb logcat -s NodeApiTests`). Building `napi` as a shared library (task #9) would also work and is the only option if real external `.node` addons ever need `dlopen` on Android, but it ripples to every consumer's packaging and is not needed for the conformance suite. +> Why shared `napi` rather than static-linking the addons into the host: bionic will not surface a `System.loadLibrary`-loaded (RTLD_LOCAL) host's `napi_*` to a `dlopen`'d module, and post-hoc `RTLD_GLOBAL` host promotion is a no-op on bionic — so a `dlopen`'d addon can only resolve `napi` via a real shared-library `DT_NEEDED`. (An earlier iteration statically linked the addons into the host and passed too; the shared-lib model was adopted to align with node-api-cts's dynamic `.node` addons, easing a future migration.) -> Emulator note: the unrelated `JavaScript.All` UnitTest (XMLHttpRequest/WebSocket/HTTP mocha tests) fails in an offline emulator (status 0 vs 200/404, socket timeouts); it runs before and is independent of the js-native-api suite. +> Emulator note: the unrelated `JavaScript.All` UnitTest (XMLHttpRequest/WebSocket/HTTP mocha tests) can fail in an offline emulator (status 0 vs 200/404, socket timeouts); it runs before and is independent of the js-native-api suite. ## Chakra N-API ceiling @@ -97,9 +103,10 @@ run the suite per engine → implement the JSC/(Chakra) gaps → green. `amaro` for TS-strip); the only implementor is `node`. Adopting it means authoring an `implementors/jsruntimehost/` harness (JS modules: `load-addon`, `assert`, `must-call`, `gc`, `napi-version`, `features`, `skip-test`) and driving the test `.js` from our runtime — i.e. re-expressing what `node_lite` already does, in their contract. - - **Doesn't solve Android:** `add_node_api_cts_addon()` builds SHARED `.node` (dlopen), with only Apple - `-undefined dynamic_lookup` / MSVC import-lib handling and no Android path — we'd re-apply the same static-link - adaptation just landed here. + - **Android now aligns:** `add_node_api_cts_addon()` builds SHARED `.node` (dlopen) — the same model our Android + suite now uses (dynamic `.node` + `libnapi.so`). Its CMake only handles Apple `-undefined dynamic_lookup` / + MSVC import-libs, so an Android port would still need our `libnapi.so` + soname-load glue, but the addon model + itself matches — a future migration is much smoother now. - **No v5 coverage gain now:** its `tests/js-native-api/` are ported from the same `nodejs/node` source as our vendored copies; the 4 v5 tests are identical content. - **Upside (why track it):** engine-agnostic, active (~24 js-native-api tests ported incl. `test_bigint`, From 44d903e527ba30c71845696c8c47f7e6d2995758 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 19:29:57 -0700 Subject: [PATCH 28/33] Android tests: use AndroidExtensions StdoutLogger for stdout->logcat Replace the hand-rolled pipe+thread stdout pump (added while bringing up the in-process Node-API harness) with android::StdoutLogger::Start()/Stop() from AndroidExtensions, which the rest of the UnitTests host already uses. Same effect -- the in-process gtest output (incl. failure file:line:message) is visible in logcat (tag StdoutLogger) -- with less bespoke code. Verified on emulator: 8/8 UnitTests pass incl. 4/4 js_native_api, gtest output present in logcat. --- .../Android/app/src/main/cpp/JNI.cpp | 59 +++---------------- 1 file changed, 7 insertions(+), 52 deletions(-) diff --git a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp index d0ed4b94..13a87e41 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp +++ b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp @@ -2,64 +2,13 @@ #include #include #include +#include #include #include "Babylon/DebugTrace.h" #include -#include -#include -#include -#include - -namespace { - -// The conformance suite runs gtest in-process and writes results (including failure file/line/message) -// to stdout/stderr, which Android otherwise discards. Pump both to logcat so test output -- and any -// native crash context printed before the process dies -- is actually visible (e.g. -// `adb logcat -s NodeApiTests`). Without this the only signal is the JUnit "expected 0, was 1". -void* PumpStdioToLogcat(void* arg) { - int read_fd = *static_cast(arg); - char buffer[1024]; - std::string line; - ssize_t count; - while ((count = read(read_fd, buffer, sizeof(buffer) - 1)) > 0) { - buffer[count] = '\0'; - line += buffer; - std::string::size_type newline; - while ((newline = line.find('\n')) != std::string::npos) { - __android_log_write(ANDROID_LOG_INFO, "NodeApiTests", line.substr(0, newline).c_str()); - line.erase(0, newline + 1); - } - } - return nullptr; -} - -void RedirectStdioToLogcat() { - static int pipe_fds[2]; - static bool installed = false; - if (installed) { - return; - } - installed = true; - setvbuf(stdout, nullptr, _IOLBF, 0); - setvbuf(stderr, nullptr, _IONBF, 0); - if (pipe(pipe_fds) != 0) { - return; - } - dup2(pipe_fds[1], STDOUT_FILENO); - dup2(pipe_fds[1], STDERR_FILENO); - pthread_t thread; - if (pthread_create(&thread, nullptr, PumpStdioToLogcat, &pipe_fds[0]) == 0) { - pthread_detach(thread); - } -} - -} // namespace - extern "C" JNIEXPORT jint JNICALL Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass clazz, jobject context) { - RedirectStdioToLogcat(); - JavaVM* javaVM{}; if (env->GetJavaVM(&javaVM) != JNI_OK) { @@ -69,6 +18,10 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz jclass webSocketClass{env->FindClass("com/jsruntimehost/unittests/WebSocket")}; java::websocket::WebSocketClient::InitializeJavaWebSocketClass(webSocketClass, env); + // Route stdout (the in-process gtest output -- [RUN]/[OK]/[FAILED] + failure file:line:message) + // to logcat so test results are visible; stopped after RunTests below. + android::StdoutLogger::Start(); + jclass contextClass = env->GetObjectClass(context); jmethodID getApplicationContext = env->GetMethodID(contextClass, "getApplicationContext", "()Landroid/content/Context;"); jobject applicationContext = env->CallObjectMethod(context, getApplicationContext); @@ -120,6 +73,8 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz auto testResult = RunTests(); + android::StdoutLogger::Stop(); + java::websocket::WebSocketClient::DestructJavaWebSocketClass(env); return testResult; } From d225e744c737e746407fc7e2a0ebb31fe995e28c Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 19:30:37 -0700 Subject: [PATCH 29/33] docs(roadmap): stdout->logcat is via AndroidExtensions StdoutLogger, not a hand-rolled pump --- Tests/NodeApi/NAPI_VERSION_ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md index b7538ad5..6f73ba0d 100644 --- a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md +++ b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md @@ -41,7 +41,7 @@ This is a multi-PR effort. Keep the boundaries strict: | **macOS** | JavaScriptCore | ✅ **12/12** (plain + ASan/UBSan + TSan) | child-process | Full v5 reference. | | **Android** | V8 (`libv8android.so`) | ✅ **4/4 js-native-api** (dynamic `.node` + `libnapi.so`, in-process) | in-process | App sandbox can't `fork`/`exec`; addons `dlopen`'d — see below. | -**Android in-process addon loading (dynamic `.node` + shared `libnapi.so`).** The app sandbox can't `fork`/`exec`, so the conformance suite runs the addons *in-process*. The addons are built as standalone SHARED `lib.so` modules (matching nodejs/node-api-cts's `add_node_api_cts_addon`), packaged by AGP into `nativeLibraryDir`, and `dlopen`'d by `node_lite_android` by soname. For their `napi_*` imports to bind at load, **`napi` is built as a shared library (`libnapi.so`) on Android** — a real `DT_NEEDED` of both the host and every addon, so there is a single napi instance. This is the IoC / dynamic-self-registration model: `dlopen` the addon → its `DT_NEEDED libnapi.so` resolves the `napi_*` → `dlsym("napi_register_module_v1")` → call it. Verified on device: `lib2_function_arguments.so` carries `DT_NEEDED [libnapi.so]`, its 8 `napi_*` are imports (`U`), `libnapi.so` exports all 106 `napi_*` (`T`), and the 4 v5 tests pass. The harness also needs the `noexcept`-removal fix (`38864e4`) so a failing test surfaces as a `ProcessResult` rather than `std::terminate`, and native stdout/stderr is pumped to logcat (tag `NodeApiTests`) for visible gtest output. +**Android in-process addon loading (dynamic `.node` + shared `libnapi.so`).** The app sandbox can't `fork`/`exec`, so the conformance suite runs the addons *in-process*. The addons are built as standalone SHARED `lib.so` modules (matching nodejs/node-api-cts's `add_node_api_cts_addon`), packaged by AGP into `nativeLibraryDir`, and `dlopen`'d by `node_lite_android` by soname. For their `napi_*` imports to bind at load, **`napi` is built as a shared library (`libnapi.so`) on Android** — a real `DT_NEEDED` of both the host and every addon, so there is a single napi instance. This is the IoC / dynamic-self-registration model: `dlopen` the addon → its `DT_NEEDED libnapi.so` resolves the `napi_*` → `dlsym("napi_register_module_v1")` → call it. Verified on device: `lib2_function_arguments.so` carries `DT_NEEDED [libnapi.so]`, its 8 `napi_*` are imports (`U`), `libnapi.so` exports all 106 `napi_*` (`T`), and the 4 v5 tests pass. The harness also needs the `noexcept`-removal fix (`38864e4`) so a failing test surfaces as a `ProcessResult` rather than `std::terminate`, and stdout is routed to logcat via AndroidExtensions' `StdoutLogger` (tag `StdoutLogger`) for visible gtest output. > Why shared `napi` rather than static-linking the addons into the host: bionic will not surface a `System.loadLibrary`-loaded (RTLD_LOCAL) host's `napi_*` to a `dlopen`'d module, and post-hoc `RTLD_GLOBAL` host promotion is a no-op on bionic — so a `dlopen`'d addon can only resolve `napi` via a real shared-library `DT_NEEDED`. (An earlier iteration statically linked the addons into the host and passed too; the shared-lib model was adopted to align with node-api-cts's dynamic `.node` addons, easing a future migration.) From e94c72e091bb5cd08443747346bbc21afc88603c Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 19:36:02 -0700 Subject: [PATCH 30/33] Sync napi shared-lib change with PR #183 (gate behind JSR_NAPI_SHARED option) --- Core/Node-API/CMakeLists.txt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index addb7041..f399b383 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -155,13 +155,20 @@ if(NAPI_BUILD_ABI) message(STATUS "Selected ${NAPI_JAVASCRIPT_ENGINE}") endif() -# On Android the Node-API conformance addons are dlopen'd as standalone SHARED .node modules (matching -# nodejs/node-api-cts's add_node_api_cts_addon model). For an addon's napi_* imports to bind at load, -# napi must be a shared library that both the host and the addons depend on via a real DT_NEEDED -# (libnapi.so) -- bionic will not surface a statically-linked host's napi to a dlopen'd module. There -# is a single napi instance (one libnapi.so) shared by the host and every addon. Elsewhere (the -# child-process desktop runner) napi stays a static library. +# On Android, native addons are dlopen'd as standalone .node modules and resolve their napi_* imports +# from a shared napi at load time -- bionic will not surface a statically-linked host's napi to a +# dlopen'd module, so the host and every addon must share a single libnapi.so. Default napi to a +# shared library on Android so that model works out of the box; an integrator who wants a static napi +# (e.g. for size/packaging) can override with -DJSR_NAPI_SHARED=OFF. The option defaults OFF on other +# platforms, where napi keeps following the project's default library type (i.e. honors +# BUILD_SHARED_LIBS). +set(JSR_NAPI_SHARED_DEFAULT OFF) if(ANDROID) + set(JSR_NAPI_SHARED_DEFAULT ON) +endif() +option(JSR_NAPI_SHARED "Build napi as a shared library (libnapi.so)" ${JSR_NAPI_SHARED_DEFAULT}) + +if(JSR_NAPI_SHARED) add_library(napi SHARED ${SOURCES}) else() add_library(napi ${SOURCES}) From 17316b7bc3f4a434c31981ad8afcef096091edc2 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 20:42:56 -0700 Subject: [PATCH 31/33] Tests: enable the v5-clean reference double-free conformance test Adds test_reference_double_free (js-native-api) to the enabled suite -- a real v5 reference/double-free test, green on macOS (system JSC, including ASan) and Android (V8). Its test_wrap.js is quarantined via a known-failing allow-list in test_main.cpp: JSC napi_remove_wrap on an unwrapped object returns napi_invalid_arg (the consecutive remove_wrap+delete_reference path itself does not crash); tracked as a separate JSC fix. The rest of the reference/finalizer/wrap suite (test_reference, test_finalizer, 6_object_wrap) targets newer Node-API -- node_api_symbol_for (v9), napi_get_instance_data (v6), node_api_basic_env / node_api_post_finalizer (v9) -- and is documented for the staged NAPI_VERSION bump. --- Tests/NodeApi/CMakeLists.txt | 8 ++++++++ Tests/NodeApi/test_main.cpp | 15 +++++++++++++++ .../Android/app/src/main/cpp/CMakeLists.txt | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Tests/NodeApi/CMakeLists.txt b/Tests/NodeApi/CMakeLists.txt index 1b0330ef..285b0933 100644 --- a/Tests/NodeApi/CMakeLists.txt +++ b/Tests/NodeApi/CMakeLists.txt @@ -7,6 +7,14 @@ set(JSR_NODE_API_NATIVE_TEST_DIRS 3_callbacks 4_object_factory 5_function_factory + # v5-clean reference coverage. The rest of the reference/finalizer/wrap suite targets newer + # Node-API and is staged for the NAPI_VERSION bump: + # test_reference -> node_api_symbol_for (v9) + # test_finalizer/ -> napi_get_instance_data (v6), node_api_basic_env / node_api_post_finalizer (v9) + # 6_object_wrap -> napi_get_instance_data (v6), node_api_basic_env (v9) + # (test_reference_double_free/test_wrap.js is quarantined in test_main.cpp -- a JSC remove_wrap + # edge case that returns napi_invalid_arg; tracked as a separate fix, not a v5 blocker.) + test_reference_double_free ) function(node_api_copy_test_sources TARGET_NAME) diff --git a/Tests/NodeApi/test_main.cpp b/Tests/NodeApi/test_main.cpp index 93803cb8..01bab5d1 100644 --- a/Tests/NodeApi/test_main.cpp +++ b/Tests/NodeApi/test_main.cpp @@ -60,6 +60,21 @@ class NodeApiTestFixture : public TestFixtureBase { ASSERT_TRUE(static_cast(config.run_script)) << "Node-API test runner is not configured."; + // Quarantined conformance cases: known-failing against the current v5 surface. Kept registered + // (and skipped, so they stay visible) rather than silently dropped. Each needs a separate fix or + // a NAPI_VERSION bump and must not block the v5 suite. + static constexpr const char* kQuarantined[] = { + // JSC napi_remove_wrap on an object that was never wrapped returns napi_invalid_arg, which the + // runner surfaces as a failure. The consecutive remove_wrap+delete_reference path itself does + // not crash. Tracked separately as a JSC remove_wrap fix. + "test_reference_double_free/test_wrap.js", + }; + for (const char* quarantined : kQuarantined) { + if (m_jsFilePath.generic_string().find(quarantined) != std::string::npos) { + GTEST_SKIP() << "Quarantined (known-failing on the v5 surface): " << quarantined; + } + } + ProcessResult result = config.run_script(m_jsFilePath); if (result.status == 0) { return; diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index ce0569d4..d78a30e7 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -74,7 +74,7 @@ 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") + PRIVATE NODE_API_AVAILABLE_NATIVE_TESTS="2_function_arguments,3_callbacks,4_object_factory,5_function_factory,test_reference_double_free") # The conformance addons are built as standalone SHARED lib.so by Tests/NodeApi/CMakeLists.txt, # packaged by AGP into nativeLibraryDir, and dlopen'd in-process by node_lite_android. Both they and From ed74d6185eb947d0c593a64a7877f85c95746132 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 20:45:05 -0700 Subject: [PATCH 32/33] docs(roadmap): reference-test staging + GC-safety review (re hermes-windows#321) --- Tests/NodeApi/NAPI_VERSION_ROADMAP.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md index 6f73ba0d..fa3a1bff 100644 --- a/Tests/NodeApi/NAPI_VERSION_ROADMAP.md +++ b/Tests/NodeApi/NAPI_VERSION_ROADMAP.md @@ -91,6 +91,23 @@ run the suite per engine → implement the JSC/(Chakra) gaps → green. `*` stretch. Separately: the worklets/worker goal needs **threadsafe functions** — a runtime-layer (`node_api.h`, `NAPI_HAS_THREADS 1`) axis not covered by the engine-only suite; track independently. +**Reference/finalizer test staging (measured at v5).** Of the vendored reference/finalizer/wrap dirs, only +`test_reference_double_free` is v5-clean and is enabled now (green on macOS/JSC incl. ASan, and Android/V8); its +`test_wrap.js` is quarantined (JSC `napi_remove_wrap` on an unwrapped object returns `napi_invalid_arg` — it +does not crash; separate fix). The rest are gated by symbols our v5 pin doesn't export and enable with the +bump: `test_reference` → `node_api_symbol_for` (v9, B4); `test_finalizer/` & `6_object_wrap` → +`napi_get_instance_data` (v6, B1) + `node_api_basic_env`/`node_api_post_finalizer` (v9, B4). `test_finalizer` +also surfaced a JSC finalizer-delivery timing case (`mustCall(1)`→0) to confirm at B1. + +**GC-safety (re upstream [hermes-windows#321](https://github.com/microsoft/hermes-windows/pull/321)).** That +weak-ref-over-Proxy moving-GC bug is **not present here**. V8 uses `v8::Persistent` (an immediate, auto-relocated +GC root). JSC keeps the to-be-referenced object in the `value` argument on the C stack across the reference's +finalizer-setup allocation, so JSC's **conservative stack scan pins it against collection and relocation** — the +creation-time window is closed regardless of whether the collector moves cells — and weak liveness is an +object-id check, not a bare-pointer deref. Chakra is non-moving with a nullptr-returning weak read. Empirically, +`test_reference_double_free` runs **clean under ASan on the modern system JSC** (no use-after-free). There is no +weak-ref-over-Proxy case in the suite yet; add one at the v9 tier. + ## Test-suite sourcing strategy - **Now (this PR):** vendored copy of vmoroz's hermes-windows `unittests/NodeApi/` (engine-layer, v8-capable From 3f63934a5baf99c0dacaa3a538c7cee9462f5bb7 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Thu, 4 Jun 2026 23:31:56 -0700 Subject: [PATCH 33/33] Node-API: address #116 review (JSC call dispatch, status type, Windows error reporting) - JSC napi_call_function: invoke through the canonical Function.prototype.call (captured once at env init) rather than the target function's own, user-overridable "call" property -- so `func.call = ...` cannot change native call behavior. - child_process_android SpawnSync: return status 1 (was -1, which wrapped in the uint32_t status field) for the unsupported path. - node_lite_windows LoadFunction: format the narrow lib_path.string() with %s (path::c_str() is wchar_t* on Windows -> UB) and report ::GetLastError() instead of errno for LoadLibraryA failures. --- .../Source/js_native_api_javascriptcore.cc | 21 ++++++++++++++----- .../Source/js_native_api_javascriptcore.h | 4 ++++ Tests/NodeApi/child_process_android.cpp | 3 ++- Tests/NodeApi/node_lite_windows.cpp | 9 ++++---- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.cc b/Core/Node-API/Source/js_native_api_javascriptcore.cc index 3a76bf28..739edb7f 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.cc +++ b/Core/Node-API/Source/js_native_api_javascriptcore.cc @@ -844,6 +844,18 @@ void napi_env__::deinit_symbol(JSValueRef symbol) { JSValueUnprotect(context, symbol); } +void napi_env__::init_function_prototype_call() { + // Capture the canonical Function.prototype.call once, at env init, so napi_call_function does not + // depend on a target function's own (user-overridable) "call" property. + JSObjectRef global = JSContextGetGlobalObject(context); + JSValueRef function_ctor = JSObjectGetProperty(context, global, JSString("Function"), nullptr); + JSObjectRef function_ctor_obj = JSValueToObject(context, function_ctor, nullptr); + JSValueRef prototype = JSObjectGetProperty(context, function_ctor_obj, JSString("prototype"), nullptr); + JSObjectRef prototype_obj = JSValueToObject(context, prototype, nullptr); + function_prototype_call = JSObjectGetProperty(context, prototype_obj, JSString("call"), nullptr); + JSValueProtect(context, function_prototype_call); +} + // Warning: Keep in-sync with napi_status enum static const char* error_messages[] = { nullptr, @@ -1654,11 +1666,10 @@ napi_status napi_call_function(napi_env env, } JSValueRef 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); + // Invoke through the canonical Function.prototype.call (captured at env init), not the target's own + // "call" property -- user code could override func.call and change native call behavior. + JSObjectRef call_object = + JSValueToObject(env->context, env->function_prototype_call, &exception); CHECK_JSC(env, exception); JSValueRef return_value{JSObjectCallAsFunction(env->context, diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index 77bc1180..152a590b 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -20,6 +20,7 @@ struct napi_env__ { JSValueRef function_info_symbol{}; JSValueRef reference_info_symbol{}; JSValueRef wrapper_info_symbol{}; + JSValueRef function_prototype_call{}; const std::thread::id thread_id{std::this_thread::get_id()}; @@ -30,11 +31,13 @@ struct napi_env__ { init_symbol(function_info_symbol, "BabylonNative_FunctionInfo"); init_symbol(reference_info_symbol, "BabylonNative_ReferenceInfo"); init_symbol(wrapper_info_symbol, "BabylonNative_WrapperInfo"); + init_function_prototype_call(); } ~napi_env__() { shutting_down = true; deinit_refs(); + deinit_symbol(function_prototype_call); deinit_symbol(wrapper_info_symbol); deinit_symbol(reference_info_symbol); deinit_symbol(function_info_symbol); @@ -57,6 +60,7 @@ struct napi_env__ { void deinit_refs(); void init_symbol(JSValueRef& symbol, const char* description); + void init_function_prototype_call(); void deinit_symbol(JSValueRef symbol); }; diff --git a/Tests/NodeApi/child_process_android.cpp b/Tests/NodeApi/child_process_android.cpp index cb70ccd1..cca60c3c 100644 --- a/Tests/NodeApi/child_process_android.cpp +++ b/Tests/NodeApi/child_process_android.cpp @@ -5,7 +5,8 @@ namespace node_api_tests { ProcessResult SpawnSync(std::string_view /*command*/, std::vector /*args*/) { ProcessResult result{}; - result.status = -1; + // Non-zero failure (status is uint32_t); spawnSync is unsupported in the in-process Android runner. + result.status = 1; result.std_error = "child_process.spawnSync is not supported on this platform."; result.std_output.clear(); return result; diff --git a/Tests/NodeApi/node_lite_windows.cpp b/Tests/NodeApi/node_lite_windows.cpp index 4af34561..164487df 100644 --- a/Tests/NodeApi/node_lite_windows.cpp +++ b/Tests/NodeApi/node_lite_windows.cpp @@ -2,7 +2,7 @@ // Licensed under the MIT License. #include -#include "node_lite.h" +#include "node_lite.h" namespace node_api_tests { @@ -15,10 +15,11 @@ namespace node_api_tests { const std::filesystem::path& lib_path, const std::string& function_name) noexcept { HMODULE dll_module = ::LoadLibraryA(lib_path.string().c_str()); + const DWORD load_error = ::GetLastError(); NODE_LITE_ASSERT(dll_module != NULL, - "Failed to load DLL: %s. Error: %s", - lib_path.c_str(), - std::strerror(errno)); + "Failed to load DLL: %s. Error code: %lu", + lib_path.string().c_str(), + load_error); return ::GetProcAddress(dll_module, function_name.c_str()); } } // namespace node_api_tests