diff --git a/PORTING.md b/PORTING.md index 5a63cf6..cd9f2a2 100644 --- a/PORTING.md +++ b/PORTING.md @@ -55,7 +55,7 @@ Tests covering the engine-specific part of Node-API, defined in `js_native_api.h | `test_dataview` | Ported ✅ | Medium | | `test_date` | Ported ✅ | Easy | | `test_error` | Ported ✅ | Medium | -| `test_exception` | Not ported | Medium | +| `test_exception` | Ported ✅ | Medium | | `test_finalizer` | Not ported | Medium | | `test_function` | Ported ✅ | Medium | | `test_general` | Not ported | Hard | diff --git a/tests/js-native-api/test_exception/CMakeLists.txt b/tests/js-native-api/test_exception/CMakeLists.txt new file mode 100644 index 0000000..5ecd96a --- /dev/null +++ b/tests/js-native-api/test_exception/CMakeLists.txt @@ -0,0 +1 @@ +add_node_api_cts_addon(test_exception test_exception.c) diff --git a/tests/js-native-api/test_exception/test.js b/tests/js-native-api/test_exception/test.js new file mode 100644 index 0000000..2fc728e --- /dev/null +++ b/tests/js-native-api/test_exception/test.js @@ -0,0 +1,146 @@ +'use strict'; +// Flags: --expose-gc + +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 { + loadAddon('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(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(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(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(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/js-native-api/test_exception/testFinalizerException.js b/tests/js-native-api/test_exception/testFinalizerException.js new file mode 100644 index 0000000..5bfe43c --- /dev/null +++ b/tests/js-native-api/test_exception/testFinalizerException.js @@ -0,0 +1,9 @@ +// An exception thrown from a C finalizer during GC surfaces as an uncaught +// exception on stderr; it runs in a spawned child so the crash doesn't take +// down the test runner. Skipped where the runtime can't spawn (WASM, RN). +if (!runtimeFeatures.spawn) { + skipTest(); +} + +const result = await spawnTest('testFinalizerException_child.mjs'); +assert.match(result.stderr, /Error during Finalize/); diff --git a/tests/js-native-api/test_exception/testFinalizerException_child.mjs b/tests/js-native-api/test_exception/testFinalizerException_child.mjs new file mode 100644 index 0000000..bf03347 --- /dev/null +++ b/tests/js-native-api/test_exception/testFinalizerException_child.mjs @@ -0,0 +1,12 @@ +// Child of testFinalizerException.js. The addon throws during Init with its +// exports on `.binding`; createExternal installs a finalizer that throws. +try { + loadAddon('test_exception'); +} catch (anException) { + anException.binding.createExternal(); +} + +// GC until the finalizer fires; its throw crashes the process, printing to +// stderr for the parent to match. The counter just bounds the loop. +let gcCount = 10; +await gcUntil('test', () => --gcCount <= 0); diff --git a/tests/js-native-api/test_exception/test_exception.c b/tests/js-native-api/test_exception/test_exception.c new file mode 100644 index 0000000..84b9919 --- /dev/null +++ b/tests/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