diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index ebe1748ad..a19982969 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -21,4 +21,4 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm install - - run: CLANG_FORMAT_START=refs/remotes/origin/main npm run lint + - run: FORMAT_START=refs/remotes/origin/main npm run lint diff --git a/doc/addon.md b/doc/addon.md index 96dae718d..7e5ec4791 100644 --- a/doc/addon.md +++ b/doc/addon.md @@ -90,6 +90,12 @@ to either attach methods, accessors, and/or values to the `exports` object or to create its own `exports` object and attach methods, accessors, and/or values to it. +**Note:** `Napi::Addon` uses `Napi::Env::SetInstanceData()` internally. This +means that the add-on should only use `Napi::Env::GetInstanceData` explicitly to +retrieve the instance of the `Napi::Addon` class. Variables whose scope would +otherwise be global should be stored as instance variables in the +`Napi::Addon` class. + Functions created with `Napi::Function::New()`, accessors created with `PropertyDescriptor::Accessor()`, and values can also be attached. If their implementation requires the `ExampleAddon` instance, it can be retrieved from diff --git a/doc/object.md b/doc/object.md index 8aa872dc3..677ff5532 100644 --- a/doc/object.md +++ b/doc/object.md @@ -288,6 +288,34 @@ Napi::Value Napi::Object::operator[] (uint32_t index) const; Returns an indexed property or array element as a [`Napi::Value`](value.md). +### begin() + +```cpp +Napi::Object::iterator Napi::Object::begin() const; +``` + +Returns a constant iterator to the beginning of the object. + +```cpp +Napi::Object::iterator Napi::Object::begin(); +``` + +Returns a non constant iterator to the beginning of the object. + +### end() + +```cpp +Napi::Object::iterator Napi::Object::end() const; +``` + +Returns a constant iterator to the end of the object. + +```cpp +Napi::Object::iterator Napi::Object::end(); +``` + +Returns a non constant iterator to the end of the object. + ## Iterator Iterators expose an `std::pair<...>`, where the `first` property is a @@ -300,6 +328,41 @@ exceptions are enabled (by defining `NAPI_CPP_EXCEPTIONS` during the build). In constant iterators, the iterated values are immutable. +#### operator++() + +```cpp +inline Napi::Object::const_iterator& Napi::Object::const_iterator::operator++(); +``` + +Moves the iterator one step forward. + +#### operator== + +```cpp +inline bool Napi::Object::const_iterator::operator==(const Napi::Object::const_iterator& other) const; +``` +- `[in] other`: Another iterator to compare the current iterator to. + +Returns whether both iterators are at the same index. + +#### operator!= + +```cpp +inline bool Napi::Object::const_iterator::operator!=(const Napi::Object::const_iterator& other) const; +``` +- `[in] other`: Another iterator to compare the current iterator to. + +Returns whether both iterators are at different indices. + +#### operator*() + +```cpp +inline const std::pair> Napi::Object::const_iterator::operator*() const; +``` + +Returns the currently iterated key and value. + +#### Example ```cpp Value Sum(const CallbackInfo& info) { Object object = info[0].As(); @@ -317,6 +380,41 @@ Value Sum(const CallbackInfo& info) { In non constant iterators, the iterated values are mutable. +#### operator++() + +```cpp +inline Napi::Object::iterator& Napi::Object::iterator::operator++(); +``` + +Moves the iterator one step forward. + +#### operator== + +```cpp +inline bool Napi::Object::iterator::operator==(const Napi::Object::iterator& other) const; +``` +- `[in] other`: Another iterator to compare the current iterator to. + +Returns whether both iterators are at the same index. + +#### operator!= + +```cpp +inline bool Napi::Object::iterator::operator!=(const Napi::Object::iterator& other) const; +``` +- `[in] other`: Another iterator to compare the current iterator to. + +Returns whether both iterators are at different indices. + +#### operator*() + +```cpp +inline std::pair> Napi::Object::iterator::operator*(); +``` + +Returns the currently iterated key and value. + +#### Example ```cpp void Increment(const CallbackInfo& info) { Env env = info.Env(); diff --git a/doc/object_wrap.md b/doc/object_wrap.md index d90da42c4..0d3ef9856 100644 --- a/doc/object_wrap.md +++ b/doc/object_wrap.md @@ -36,9 +36,9 @@ class Example : public Napi::ObjectWrap { Napi::Object Example::Init(Napi::Env env, Napi::Object exports) { // This method is used to hook the accessor and method callbacks Napi::Function func = DefineClass(env, "Example", { - InstanceMethod<&Example::GetValue>("GetValue"), - InstanceMethod<&Example::SetValue>("SetValue"), - StaticMethod<&Example::CreateNewItem>("CreateNewItem"), + InstanceMethod<&Example::GetValue>("GetValue", static_cast(napi_writable | napi_configurable)), + InstanceMethod<&Example::SetValue>("SetValue", static_cast(napi_writable | napi_configurable)), + StaticMethod<&Example::CreateNewItem>("CreateNewItem", static_cast(napi_writable | napi_configurable)), }); Napi::FunctionReference* constructor = new Napi::FunctionReference(); diff --git a/napi-inl.h b/napi-inl.h index 19aaab447..5eff0b915 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -2520,12 +2520,23 @@ inline Error Error::New(napi_env env) { napi_status status; napi_value error = nullptr; bool is_exception_pending; - const napi_extended_error_info* info; + napi_extended_error_info last_error_info_copy; - // We must retrieve the last error info before doing anything else, because - // doing anything else will replace the last error info. - status = napi_get_last_error_info(env, &info); - NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_get_last_error_info"); + { + // We must retrieve the last error info before doing anything else because + // doing anything else will replace the last error info. + const napi_extended_error_info* last_error_info; + status = napi_get_last_error_info(env, &last_error_info); + NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_get_last_error_info"); + + // All fields of the `napi_extended_error_info` structure gets reset in + // subsequent Node-API function calls on the same `env`. This includes a + // call to `napi_is_exception_pending()`. So here it is necessary to make a + // copy of the information as the `error_code` field is used later on. + memcpy(&last_error_info_copy, + last_error_info, + sizeof(napi_extended_error_info)); + } status = napi_is_exception_pending(env, &is_exception_pending); NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_is_exception_pending"); @@ -2536,8 +2547,9 @@ inline Error Error::New(napi_env env) { NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_get_and_clear_last_exception"); } else { - const char* error_message = info->error_message != nullptr ? - info->error_message : "Error in native callback"; + const char* error_message = last_error_info_copy.error_message != nullptr + ? last_error_info_copy.error_message + : "Error in native callback"; napi_value message; status = napi_create_string_utf8( @@ -2547,16 +2559,16 @@ inline Error Error::New(napi_env env) { &message); NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_create_string_utf8"); - switch (info->error_code) { - case napi_object_expected: - case napi_string_expected: - case napi_boolean_expected: - case napi_number_expected: - status = napi_create_type_error(env, nullptr, message, &error); - break; - default: - status = napi_create_error(env, nullptr, message, &error); - break; + switch (last_error_info_copy.error_code) { + case napi_object_expected: + case napi_string_expected: + case napi_boolean_expected: + case napi_number_expected: + status = napi_create_type_error(env, nullptr, message, &error); + break; + default: + status = napi_create_error(env, nullptr, message, &error); + break; } NAPI_FATAL_IF_FAILED(status, "Error::New", "napi_create_error"); } diff --git a/package.json b/package.json index 03519e9c9..cddfda18b 100644 --- a/package.json +++ b/package.json @@ -378,7 +378,9 @@ "dev:incremental": "node test", "doc": "doxygen doc/Doxyfile", "lint": "eslint $(git diff --name-only refs/remotes/origin/main '**/*.js' | xargs) && node tools/clang-format", - "lint:fix": "node tools/clang-format --fix && eslint --fix $(git diff --cached --name-only '**/*.js' | xargs && git diff --name-only '**/*.js' | xargs)" + "lint:fix": "node tools/clang-format --fix && eslint --fix $(git diff --name-only refs/remotes/origin/main '**/*.js' | xargs && git diff --name-only refs/remotes/origin/main '**/*.js' | xargs)", + "preunit": "filter=\"$npm_config_filter\" node-gyp rebuild -C unit-test", + "unit": "filter=\"$npm_config_filter\" node unit-test/test" }, "pre-commit": "lint", "version": "4.2.0", diff --git a/test/bigint.cc b/test/bigint.cc index 300e5a498..e8e6e9cb4 100644 --- a/test/bigint.cc +++ b/test/bigint.cc @@ -25,6 +25,14 @@ Value IsLossless(const CallbackInfo& info) { return Boolean::New(env, lossless); } +Value IsBigInt(const CallbackInfo& info) { + Env env = info.Env(); + + BigInt big = info[0].As(); + + return Boolean::New(env, big.IsBigInt()); +} + Value TestInt64(const CallbackInfo& info) { bool lossless; int64_t input = info[0].As().Int64Value(&lossless); @@ -71,6 +79,7 @@ Value TestTooBigBigInt(const CallbackInfo& info) { Object InitBigInt(Env env) { Object exports = Object::New(env); exports["IsLossless"] = Function::New(env, IsLossless); + exports["IsBigInt"] = Function::New(env, IsBigInt); exports["TestInt64"] = Function::New(env, TestInt64); exports["TestUint64"] = Function::New(env, TestUint64); exports["TestWords"] = Function::New(env, TestWords); diff --git a/test/bigint.js b/test/bigint.js index bc27d9501..c30e525d4 100644 --- a/test/bigint.js +++ b/test/bigint.js @@ -4,13 +4,14 @@ const assert = require('assert'); module.exports = require('./common').runTest(test); -function test(binding) { +function test (binding) { const { TestInt64, TestUint64, TestWords, IsLossless, - TestTooBigBigInt, + IsBigInt, + TestTooBigBigInt } = binding.bigint; [ @@ -24,7 +25,7 @@ function test(binding) { 986583n, -976675n, 98765432213456789876546896323445679887645323232436587988766545658n, - -4350987086545760976737453646576078997096876957864353245245769809n, + -4350987086545760976737453646576078997096876957864353245245769809n ].forEach((num) => { if (num > -(2n ** 63n) && num < 2n ** 63n) { assert.strictEqual(TestInt64(num), num); @@ -40,11 +41,13 @@ function test(binding) { assert.strictEqual(IsLossless(num, false), false); } + assert.strictEqual(IsBigInt(num), true); + assert.strictEqual(num, TestWords(num)); }); assert.throws(TestTooBigBigInt, { name: /^(RangeError|Error)$/, - message: /^(Maximum BigInt size exceeded|Invalid argument)$/, + message: /^(Maximum BigInt size exceeded|Invalid argument)$/ }); } diff --git a/test/common/index.js b/test/common/index.js index 110979917..90d02b108 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -7,10 +7,10 @@ const noop = () => {}; const mustCallChecks = []; -function runCallChecks(exitCode) { +function runCallChecks (exitCode) { if (exitCode !== 0) return; - const failed = mustCallChecks.filter(function(context) { + const failed = mustCallChecks.filter(function (context) { if ('minimum' in context) { context.messageSegment = `at least ${context.minimum}`; return context.actual < context.minimum; @@ -20,25 +20,25 @@ function runCallChecks(exitCode) { } }); - failed.forEach(function(context) { + failed.forEach(function (context) { console.log('Mismatched %s function calls. Expected %s, actual %d.', - context.name, - context.messageSegment, - context.actual); + context.name, + context.messageSegment, + context.actual); console.log(context.stack.split('\n').slice(2).join('\n')); }); if (failed.length) process.exit(1); } -exports.mustCall = function(fn, exact) { +exports.mustCall = function (fn, exact) { return _mustCallInner(fn, exact, 'exact'); }; -exports.mustCallAtLeast = function(fn, minimum) { +exports.mustCallAtLeast = function (fn, minimum) { return _mustCallInner(fn, minimum, 'minimum'); }; -function _mustCallInner(fn, criteria, field) { +function _mustCallInner (fn, criteria, field) { if (typeof fn === 'number') { criteria = fn; fn = noop; @@ -49,8 +49,7 @@ function _mustCallInner(fn, criteria, field) { criteria = 1; } - if (typeof criteria !== 'number') - throw new TypeError(`Invalid ${field} value: ${criteria}`); + if (typeof criteria !== 'number') { throw new TypeError(`Invalid ${field} value: ${criteria}`); } const context = { [field]: criteria, @@ -64,50 +63,50 @@ function _mustCallInner(fn, criteria, field) { mustCallChecks.push(context); - return function() { + return function () { context.actual++; return fn.apply(this, arguments); }; } -exports.mustNotCall = function(msg) { - return function mustNotCall() { +exports.mustNotCall = function (msg) { + return function mustNotCall () { assert.fail(msg || 'function should not have been called'); }; }; -exports.runTest = async function(test, buildType, buildPathRoot = process.env.REL_BUILD_PATH || '') { +exports.runTest = async function (test, buildType, buildPathRoot = process.env.BUILD_PATH || '') { buildType = buildType || process.config.target_defaults.default_configuration || 'Release'; const bindings = [ path.join(buildPathRoot, `../build/${buildType}/binding.node`), path.join(buildPathRoot, `../build/${buildType}/binding_noexcept.node`), - path.join(buildPathRoot, `../build/${buildType}/binding_noexcept_maybe.node`), + path.join(buildPathRoot, `../build/${buildType}/binding_noexcept_maybe.node`) ].map(it => require.resolve(it)); for (const item of bindings) { await Promise.resolve(test(require(item))) .finally(exports.mustCall()); } -} +}; -exports.runTestWithBindingPath = async function(test, buildType, buildPathRoot = process.env.REL_BUILD_PATH || '') { +exports.runTestWithBindingPath = async function (test, buildType, buildPathRoot = process.env.BUILD_PATH || '') { buildType = buildType || process.config.target_defaults.default_configuration || 'Release'; const bindings = [ path.join(buildPathRoot, `../build/${buildType}/binding.node`), path.join(buildPathRoot, `../build/${buildType}/binding_noexcept.node`), - path.join(buildPathRoot, `../build/${buildType}/binding_noexcept_maybe.node`), + path.join(buildPathRoot, `../build/${buildType}/binding_noexcept_maybe.node`) ].map(it => require.resolve(it)); for (const item of bindings) { await test(item); } -} +}; -exports.runTestWithBuildType = async function(test, buildType) { +exports.runTestWithBuildType = async function (test, buildType) { buildType = buildType || process.config.target_defaults.default_configuration || 'Release'; - await Promise.resolve(test(buildType)) - .finally(exports.mustCall()); -} + await Promise.resolve(test(buildType)) + .finally(exports.mustCall()); +}; diff --git a/test/error.cc b/test/error.cc index 15858182e..5b470a957 100644 --- a/test/error.cc +++ b/test/error.cc @@ -59,6 +59,16 @@ void ThrowApiError(const CallbackInfo& info) { Function(info.Env(), nullptr).Call(std::initializer_list{}); } +void LastExceptionErrorCode(const CallbackInfo& info) { + // Previously, `napi_extended_error_info.error_code` got reset to `napi_ok` in + // subsequent Node-API function calls, so this would have previously thrown an + // `Error` object instead of a `TypeError` object. + Env env = info.Env(); + bool res; + napi_get_value_bool(env, Value::From(env, "asd"), &res); + NAPI_THROW_VOID(Error::New(env)); +} + #ifdef NAPI_CPP_EXCEPTIONS void ThrowJSError(const CallbackInfo& info) { @@ -256,6 +266,8 @@ void ThrowDefaultError(const CallbackInfo& info) { Object InitError(Env env) { Object exports = Object::New(env); exports["throwApiError"] = Function::New(env, ThrowApiError); + exports["lastExceptionErrorCode"] = + Function::New(env, LastExceptionErrorCode); exports["throwJSError"] = Function::New(env, ThrowJSError); exports["throwTypeError"] = Function::New(env, ThrowTypeError); exports["throwRangeError"] = Function::New(env, ThrowRangeError); diff --git a/test/error.js b/test/error.js index 763115c64..d1519ec8e 100644 --- a/test/error.js +++ b/test/error.js @@ -5,27 +5,30 @@ const assert = require('assert'); if (process.argv[2] === 'fatal') { const binding = require(process.argv[3]); binding.error.throwFatalError(); - return; } module.exports = require('./common').runTestWithBindingPath(test); -function test(bindingPath) { +function test (bindingPath) { const binding = require(bindingPath); - assert.throws(() => binding.error.throwApiError('test'), function(err) { + assert.throws(() => binding.error.throwApiError('test'), function (err) { return err instanceof Error && err.message.includes('Invalid'); }); - assert.throws(() => binding.error.throwJSError('test'), function(err) { + assert.throws(() => binding.error.lastExceptionErrorCode(), function (err) { + return err instanceof TypeError && err.message === 'A boolean was expected'; + }); + + assert.throws(() => binding.error.throwJSError('test'), function (err) { return err instanceof Error && err.message === 'test'; }); - assert.throws(() => binding.error.throwTypeError('test'), function(err) { + assert.throws(() => binding.error.throwTypeError('test'), function (err) { return err instanceof TypeError && err.message === 'test'; }); - assert.throws(() => binding.error.throwRangeError('test'), function(err) { + assert.throws(() => binding.error.throwRangeError('test'), function (err) { return err instanceof RangeError && err.message === 'test'; }); @@ -34,7 +37,7 @@ function test(bindingPath) { () => { throw new TypeError('test'); }), - function(err) { + function (err) { return err instanceof TypeError && err.message === 'test' && !err.caught; }); @@ -43,7 +46,7 @@ function test(bindingPath) { () => { throw new TypeError('test'); }), - function(err) { + function (err) { return err instanceof TypeError && err.message === 'test' && err.caught; }); @@ -56,19 +59,19 @@ function test(bindingPath) { () => { throw new TypeError('test'); }); assert.strictEqual(msg, 'test'); - assert.throws(() => binding.error.throwErrorThatEscapesScope('test'), function(err) { + assert.throws(() => binding.error.throwErrorThatEscapesScope('test'), function (err) { return err instanceof Error && err.message === 'test'; }); - assert.throws(() => binding.error.catchAndRethrowErrorThatEscapesScope('test'), function(err) { + assert.throws(() => binding.error.catchAndRethrowErrorThatEscapesScope('test'), function (err) { return err instanceof Error && err.message === 'test' && err.caught; }); const p = require('./napi_child').spawnSync( - process.execPath, [ __filename, 'fatal', bindingPath ]); + process.execPath, [__filename, 'fatal', bindingPath]); assert.ifError(p.error); assert.ok(p.stderr.toString().includes( - 'FATAL ERROR: Error::ThrowFatalError This is a fatal error')); + 'FATAL ERROR: Error::ThrowFatalError This is a fatal error')); assert.throws(() => binding.error.throwDefaultError(false), /Cannot convert undefined or null to object/); diff --git a/test/function_reference.cc b/test/function_reference.cc index aaa899e11..a25119846 100644 --- a/test/function_reference.cc +++ b/test/function_reference.cc @@ -3,7 +3,163 @@ using namespace Napi; +class FuncRefObject : public Napi::ObjectWrap { + public: + FuncRefObject(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(info) { + Napi::Env env = info.Env(); + int argLen = info.Length(); + if (argLen <= 0 || !info[0].IsNumber()) { + Napi::TypeError::New(env, "First param should be a number") + .ThrowAsJavaScriptException(); + return; + } + Napi::Number value = info[0].As(); + this->_value = value.Int32Value(); + } + + Napi::Value GetValue(const Napi::CallbackInfo& info) { + int value = this->_value; + return Napi::Number::New(info.Env(), value); + } + + private: + int _value; +}; + namespace { + +Value ConstructRefFromExisitingRef(const CallbackInfo& info) { + HandleScope scope(info.Env()); + FunctionReference ref; + FunctionReference movedRef; + ref.Reset(info[0].As()); + movedRef = std::move(ref); + + return MaybeUnwrap(movedRef({})); +} + +Value CallWithVectorArgs(const CallbackInfo& info) { + HandleScope scope(info.Env()); + std::vector newVec; + FunctionReference ref; + ref.Reset(info[0].As()); + + for (int i = 1; i < (int)info.Length(); i++) { + newVec.push_back(info[i]); + } + return MaybeUnwrap(ref.Call(newVec)); +} + +Value CallWithInitList(const CallbackInfo& info) { + HandleScope scope(info.Env()); + FunctionReference ref; + ref.Reset(info[0].As()); + + return MaybeUnwrap(ref.Call({info[1], info[2], info[3]})); +} + +Value CallWithRecvInitList(const CallbackInfo& info) { + HandleScope scope(info.Env()); + FunctionReference ref; + ref.Reset(info[0].As()); + + return MaybeUnwrap(ref.Call(info[1], {info[2], info[3], info[4]})); +} + +Value CallWithRecvVector(const CallbackInfo& info) { + HandleScope scope(info.Env()); + FunctionReference ref; + std::vector newVec; + ref.Reset(info[0].As()); + + for (int i = 2; i < (int)info.Length(); i++) { + newVec.push_back(info[i]); + } + return MaybeUnwrap(ref.Call(info[1], newVec)); +} + +Value CallWithRecvArgc(const CallbackInfo& info) { + HandleScope scope(info.Env()); + FunctionReference ref; + int argLength = info.Length() - 2; + napi_value* args = new napi_value[argLength]; + ref.Reset(info[0].As()); + + int argIdx = 0; + for (int i = 2; i < (int)info.Length(); i++, argIdx++) { + args[argIdx] = info[i]; + } + + return MaybeUnwrap(ref.Call(info[1], argLength, args)); +} + +Value MakeAsyncCallbackWithInitList(const Napi::CallbackInfo& info) { + Napi::FunctionReference ref; + ref.Reset(info[0].As()); + + Napi::AsyncContext context(info.Env(), "func_ref_resources", {}); + + return MaybeUnwrap( + ref.MakeCallback(Napi::Object::New(info.Env()), {}, context)); +} + +Value MakeAsyncCallbackWithVector(const Napi::CallbackInfo& info) { + Napi::FunctionReference ref; + ref.Reset(info[0].As()); + std::vector newVec; + Napi::AsyncContext context(info.Env(), "func_ref_resources", {}); + + for (int i = 1; i < (int)info.Length(); i++) { + newVec.push_back(info[i]); + } + + return MaybeUnwrap( + ref.MakeCallback(Napi::Object::New(info.Env()), newVec, context)); +} + +Value MakeAsyncCallbackWithArgv(const Napi::CallbackInfo& info) { + Napi::FunctionReference ref; + ref.Reset(info[0].As()); + int argLength = info.Length() - 1; + napi_value* args = new napi_value[argLength]; + + int argIdx = 0; + for (int i = 1; i < (int)info.Length(); i++, argIdx++) { + args[argIdx] = info[i]; + } + + Napi::AsyncContext context(info.Env(), "func_ref_resources", {}); + return MaybeUnwrap(ref.MakeCallback( + Napi::Object::New(info.Env()), argLength, args, context)); +} + +Value CreateFunctionReferenceUsingNew(const Napi::CallbackInfo& info) { + Napi::Function func = ObjectWrap::DefineClass( + info.Env(), + "MyObject", + {ObjectWrap::InstanceMethod("getValue", + &FuncRefObject::GetValue)}); + Napi::FunctionReference* constructor = new Napi::FunctionReference(); + *constructor = Napi::Persistent(func); + + return MaybeUnwrapOr(constructor->New({info[0].As()}), Object()); +} + +Value CreateFunctionReferenceUsingNewVec(const Napi::CallbackInfo& info) { + Napi::Function func = ObjectWrap::DefineClass( + info.Env(), + "MyObject", + {ObjectWrap::InstanceMethod("getValue", + &FuncRefObject::GetValue)}); + Napi::FunctionReference* constructor = new Napi::FunctionReference(); + *constructor = Napi::Persistent(func); + std::vector newVec; + newVec.push_back(info[0]); + + return MaybeUnwrapOr(constructor->New(newVec), Object()); +} + Value Call(const CallbackInfo& info) { HandleScope scope(info.Env()); FunctionReference ref; @@ -23,7 +179,22 @@ Value Construct(const CallbackInfo& info) { Object InitFunctionReference(Env env) { Object exports = Object::New(env); - + exports["CreateFuncRefWithNew"] = + Function::New(env, CreateFunctionReferenceUsingNew); + exports["CreateFuncRefWithNewVec"] = + Function::New(env, CreateFunctionReferenceUsingNewVec); + exports["CallWithRecvArgc"] = Function::New(env, CallWithRecvArgc); + exports["CallWithRecvVector"] = Function::New(env, CallWithRecvVector); + exports["CallWithRecvInitList"] = Function::New(env, CallWithRecvInitList); + exports["CallWithInitList"] = Function::New(env, CallWithInitList); + exports["CallWithVec"] = Function::New(env, CallWithVectorArgs); + exports["ConstructWithMove"] = + Function::New(env, ConstructRefFromExisitingRef); + exports["AsyncCallWithInitList"] = + Function::New(env, MakeAsyncCallbackWithInitList); + exports["AsyncCallWithVector"] = + Function::New(env, MakeAsyncCallbackWithVector); + exports["AsyncCallWithArgv"] = Function::New(env, MakeAsyncCallbackWithArgv); exports["call"] = Function::New(env, Call); exports["construct"] = Function::New(env, Construct); diff --git a/test/function_reference.js b/test/function_reference.js index 3266f0031..84263fce9 100644 --- a/test/function_reference.js +++ b/test/function_reference.js @@ -1,15 +1,148 @@ 'use strict'; const assert = require('assert'); +const asyncHook = require('async_hooks'); -module.exports = require('./common').runTest(binding => { - test(binding.functionreference); +module.exports = require('./common').runTest(async (binding) => { + await test(binding.functionreference); }); -function test(binding) { +function installAsyncHook () { + let id; + let destroyed; + let hook; + const events = []; + return new Promise((resolve, reject) => { + const interval = setInterval(() => { + if (destroyed) { + hook.disable(); + clearInterval(interval); + resolve(events); + } + }, 10); + + hook = asyncHook + .createHook({ + init (asyncId, type, triggerAsyncId, resource) { + if (id === undefined && type === 'func_ref_resources') { + id = asyncId; + events.push({ eventName: 'init', type, triggerAsyncId, resource }); + } + }, + before (asyncId) { + if (asyncId === id) { + events.push({ eventName: 'before' }); + } + }, + after (asyncId) { + if (asyncId === id) { + events.push({ eventName: 'after' }); + } + }, + destroy (asyncId) { + if (asyncId === id) { + events.push({ eventName: 'destroy' }); + destroyed = true; + } + } + }) + .enable(); + }); +} + +function canConstructRefFromExistingRef (binding) { + const testFunc = () => 240; + assert(binding.ConstructWithMove(testFunc) === 240); +} + +function canCallFunctionWithDifferentOverloads (binding) { + let outsideRef = {}; + const testFunc = (a, b) => a * a - b * b; + const testFuncB = (a, b, c) => a + b - c * c; + const testFuncC = (a, b, c) => { + outsideRef.a = a; + outsideRef.b = b; + outsideRef.c = c; + }; + const testFuncD = (a, b, c, d) => { + outsideRef.result = a + b * c - d; + return outsideRef.result; + }; + + assert(binding.CallWithVec(testFunc, 5, 4) === testFunc(5, 4)); + assert(binding.CallWithInitList(testFuncB, 2, 4, 5) === testFuncB(2, 4, 5)); + + binding.CallWithRecvVector(testFuncC, outsideRef, 1, 2, 4); + assert(outsideRef.a === 1 && outsideRef.b === 2 && outsideRef.c === 4); + + outsideRef = {}; + binding.CallWithRecvInitList(testFuncC, outsideRef, 1, 2, 4); + assert(outsideRef.a === 1 && outsideRef.b === 2 && outsideRef.c === 4); + + outsideRef = {}; + binding.CallWithRecvArgc(testFuncD, outsideRef, 2, 4, 5, 6); + assert(outsideRef.result === testFuncD(2, 4, 5, 6)); +} + +async function canCallAsyncFunctionWithDifferentOverloads (binding) { + const testFunc = () => 2100; + const testFuncB = (a, b, c, d) => a + b + c + d; + let hook = installAsyncHook(); + binding.AsyncCallWithInitList(testFunc); + let triggerAsyncId = asyncHook.executionAsyncId(); + let res = await hook; + assert.deepStrictEqual(res, [ + { + eventName: 'init', + type: 'func_ref_resources', + triggerAsyncId: triggerAsyncId, + resource: {} + }, + { eventName: 'before' }, + { eventName: 'after' }, + { eventName: 'destroy' } + ]); + + hook = installAsyncHook(); + triggerAsyncId = asyncHook.executionAsyncId(); + assert( + binding.AsyncCallWithVector(testFuncB, 2, 4, 5, 6) === testFuncB(2, 4, 5, 6) + ); + res = await hook; + assert.deepStrictEqual(res, [ + { + eventName: 'init', + type: 'func_ref_resources', + triggerAsyncId: triggerAsyncId, + resource: {} + }, + { eventName: 'before' }, + { eventName: 'after' }, + { eventName: 'destroy' } + ]); + + hook = installAsyncHook(); + triggerAsyncId = asyncHook.executionAsyncId(); + assert( + binding.AsyncCallWithArgv(testFuncB, 2, 4, 5, 6) === testFuncB(2, 4, 5, 6) + ); +} +async function test (binding) { const e = new Error('foobar'); - const functionMayThrow = () => { throw e; }; - const classMayThrow = class { constructor() { throw e; } }; + const functionMayThrow = () => { + throw e; + }; + const classMayThrow = class { + constructor () { + throw e; + } + }; + + const newRef = binding.CreateFuncRefWithNew(120); + assert(newRef.getValue() === 120); + + const newRefWithVecArg = binding.CreateFuncRefWithNewVec(80); + assert(newRefWithVecArg.getValue() === 80); assert.throws(() => { binding.call(functionMayThrow); @@ -17,4 +150,8 @@ function test(binding) { assert.throws(() => { binding.construct(classMayThrow); }, /foobar/); + + canConstructRefFromExistingRef(binding); + canCallFunctionWithDifferentOverloads(binding); + await canCallAsyncFunctionWithDifferentOverloads(binding); } diff --git a/test/index.js b/test/index.js index 2fe09ac2b..2f0933658 100644 --- a/test/index.js +++ b/test/index.js @@ -4,7 +4,7 @@ const majorNodeVersion = process.versions.node.split('.')[0]; if (typeof global.gc !== 'function') { // Construct the correct (version-dependent) command-line args. - let args = ['--expose-gc']; + const args = ['--expose-gc']; const majorV8Version = process.versions.v8.split('.')[0]; if (majorV8Version < 9) { args.push('--no-concurrent-array-buffer-freeing'); @@ -15,7 +15,7 @@ if (typeof global.gc !== 'function') { args.push(__filename); const child = require('./napi_child').spawnSync(process.argv[0], args, { - stdio: 'inherit', + stdio: 'inherit' }); if (child.signal) { @@ -27,17 +27,36 @@ if (typeof global.gc !== 'function') { process.exit(process.exitCode); } +const testModules = []; + const fs = require('fs'); const path = require('path'); -let testModules = []; +let filterCondition = process.env.npm_config_filter || ''; +let filterConditionFiles = []; + +if (filterCondition !== '') { + filterCondition = require('../unit-test/matchModules').matchWildCards(process.env.npm_config_filter); + filterConditionFiles = filterCondition.split(' ').length > 0 ? filterCondition.split(' ') : [filterCondition]; +} + +const filterConditionsProvided = filterConditionFiles.length > 0; + +function checkFilterCondition (fileName, parsedFilepath) { + let result = false; + + if (!filterConditionsProvided) return true; + if (filterConditionFiles.includes(parsedFilepath)) result = true; + if (filterConditionFiles.includes(fileName)) result = true; + return result; +} // TODO(RaisinTen): Update this when the test filenames // are changed into test_*.js. -function loadTestModules(currentDirectory = __dirname, pre = '') { +function loadTestModules (currentDirectory = __dirname, pre = '') { fs.readdirSync(currentDirectory).forEach((file) => { if (currentDirectory === __dirname && ( - file === 'binding.cc' || + file === 'binding.cc' || file === 'binding.gyp' || file === 'build' || file === 'common' || @@ -50,15 +69,19 @@ function loadTestModules(currentDirectory = __dirname, pre = '') { return; } const absoluteFilepath = path.join(currentDirectory, file); + const parsedFilepath = path.parse(file); + const parsedPath = path.parse(currentDirectory); + if (fs.statSync(absoluteFilepath).isDirectory()) { if (fs.existsSync(absoluteFilepath + '/index.js')) { - testModules.push(pre + file); + if (checkFilterCondition(parsedFilepath.name, parsedPath.base)) { + testModules.push(pre + file); + } } else { loadTestModules(absoluteFilepath, pre + file + '/'); } } else { - const parsedFilepath = path.parse(file); - if (parsedFilepath.ext === '.js') { + if (parsedFilepath.ext === '.js' && checkFilterCondition(parsedFilepath.name, parsedPath.base)) { testModules.push(pre + parsedFilepath.name); } } @@ -69,7 +92,7 @@ loadTestModules(); process.config.target_defaults.default_configuration = fs - .readdirSync(path.join(__dirname, 'build')) + .readdirSync(path.join(__dirname, process.env.REL_BUILD_PATH || '', 'build')) .filter((item) => (item === 'Debug' || item === 'Release'))[0]; let napiVersion = Number(process.versions.napi); @@ -87,7 +110,7 @@ if (napiVersion < 3) { testModules.splice(testModules.indexOf('version_management'), 1); } -if (napiVersion < 4) { +if (napiVersion < 4 && !filterConditionsProvided) { testModules.splice(testModules.indexOf('asyncprogressqueueworker'), 1); testModules.splice(testModules.indexOf('asyncprogressworker'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_ctx'), 1); @@ -98,36 +121,36 @@ if (napiVersion < 4) { testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function'), 1); } -if (napiVersion < 5) { +if (napiVersion < 5 && !filterConditionsProvided) { testModules.splice(testModules.indexOf('date'), 1); } -if (napiVersion < 6) { +if (napiVersion < 6 && !filterConditionsProvided) { testModules.splice(testModules.indexOf('addon'), 1); testModules.splice(testModules.indexOf('addon_data'), 1); testModules.splice(testModules.indexOf('bigint'), 1); testModules.splice(testModules.indexOf('typedarray-bigint'), 1); } -if (majorNodeVersion < 12) { +if (majorNodeVersion < 12 && !filterConditionsProvided) { testModules.splice(testModules.indexOf('objectwrap_worker_thread'), 1); testModules.splice(testModules.indexOf('error_terminating_environment'), 1); } -if (napiVersion < 8) { +if (napiVersion < 8 && !filterConditionsProvided) { testModules.splice(testModules.indexOf('object/object_freeze_seal'), 1); } -(async function() { +(async function () { console.log(`Testing with Node-API Version '${napiVersion}'.`); - console.log('Starting test suite\n'); + if (filterConditionsProvided) { console.log('Starting test suite\n', testModules); } else { console.log('Starting test suite\n'); } // Requiring each module runs tests in the module. for (const name of testModules) { console.log(`Running test '${name}'`); await require('./' + name); - }; + } console.log('\nAll tests passed!'); })().catch((error) => { diff --git a/test/run_script.js b/test/run_script.js index 711ab1035..81b5879bd 100644 --- a/test/run_script.js +++ b/test/run_script.js @@ -5,7 +5,7 @@ const testUtil = require('./testUtil'); module.exports = require('./common').runTest(test); -function test(binding) { +function test (binding) { return testUtil.runGCTests([ 'Plain C string', () => { @@ -21,7 +21,7 @@ function test(binding) { 'JavaScript string', () => { - const sum = binding.run_script.jsString("1 + 2 + 3"); + const sum = binding.run_script.jsString('1 + 2 + 3'); assert.strictEqual(sum, 1 + 2 + 3); }, @@ -30,15 +30,15 @@ function test(binding) { assert.throws(() => { binding.run_script.jsString(true); }, { - name: 'Error', + name: 'TypeError', message: 'A string was expected' }); }, 'With context', () => { - const a = 1, b = 2, c = 3; - const sum = binding.run_script.withContext("a + b + c", { a, b, c }); + const a = 1; const b = 2; const c = 3; + const sum = binding.run_script.withContext('a + b + c', { a, b, c }); assert.strictEqual(sum, a + b + c); } ]); diff --git a/test/typed_threadsafe_function/typed_threadsafe_function_ctx.js b/test/typed_threadsafe_function/typed_threadsafe_function_ctx.js index 94ea01fb4..b8c842bc6 100644 --- a/test/typed_threadsafe_function/typed_threadsafe_function_ctx.js +++ b/test/typed_threadsafe_function/typed_threadsafe_function_ctx.js @@ -4,9 +4,9 @@ const assert = require('assert'); module.exports = require('../common').runTest(test); -async function test(binding) { +async function test (binding) { const ctx = { }; - const tsfn = new binding.threadsafe_function_ctx.TSFNWrap(ctx); + const tsfn = new binding.typed_threadsafe_function_ctx.TSFNWrap(ctx); assert(tsfn.getContext() === ctx); await tsfn.release(); } diff --git a/tools/clang-format.js b/tools/clang-format.js index 997733e5d..b76d89bda 100644 --- a/tools/clang-format.js +++ b/tools/clang-format.js @@ -4,7 +4,7 @@ const spawn = require('child_process').spawnSync; const path = require('path'); const filesToCheck = ['*.h', '*.cc']; -const CLANG_FORMAT_START = process.env.CLANG_FORMAT_START || 'main'; +const FORMAT_START = process.env.FORMAT_START || 'main'; function main (args) { let fix = false; @@ -22,19 +22,17 @@ function main (args) { const clangFormatPath = path.dirname(require.resolve('clang-format')); const options = ['--binary=node_modules/.bin/clang-format', '--style=file']; if (fix) { - options.push(CLANG_FORMAT_START); + options.push(FORMAT_START); } else { - options.push('--diff', CLANG_FORMAT_START); + options.push('--diff', FORMAT_START); } - const gitClangFormatPath = path.join(clangFormatPath, - 'bin/git-clang-format'); - const result = spawn('python', [ - gitClangFormatPath, - ...options, - '--', - ...filesToCheck - ], { encoding: 'utf-8' }); + const gitClangFormatPath = path.join(clangFormatPath, 'bin/git-clang-format'); + const result = spawn( + 'python', + [gitClangFormatPath, ...options, '--', ...filesToCheck], + { encoding: 'utf-8' } + ); if (result.stderr) { console.error('Error running git-clang-format:', result.stderr); @@ -48,9 +46,11 @@ function main (args) { return 0; } // Detect if there is any complains from clang-format - if (clangFormatOutput !== '' && - clangFormatOutput !== ('no modified files to format') && - clangFormatOutput !== ('clang-format did not modify any files')) { + if ( + clangFormatOutput !== '' && + clangFormatOutput !== 'no modified files to format' && + clangFormatOutput !== 'clang-format did not modify any files' + ) { console.error(clangFormatOutput); const fixCmd = 'npm run lint:fix'; console.error(` @@ -58,7 +58,7 @@ function main (args) { Note that when running the command locally, please keep your local main branch and working branch up to date with nodejs/node-addon-api to exclude un-related complains. - Or you can run "env CLANG_FORMAT_START=upstream/main ${fixCmd}".`); + Or you can run "env FORMAT_START=upstream/main ${fixCmd}".`); return 1; } } diff --git a/tools/eslint-format.js b/tools/eslint-format.js new file mode 100644 index 000000000..5938835d8 --- /dev/null +++ b/tools/eslint-format.js @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +const spawn = require('child_process').spawnSync; + +const filesToCheck = '*.js'; +const FORMAT_START = process.env.FORMAT_START || 'main'; + +function main (args) { + let fix = false; + while (args.length > 0) { + switch (args[0]) { + case '-f': + case '--fix': + fix = true; + break; + default: + } + args.shift(); + } + + // Check js files that change on unstaged file + const fileUnStaged = spawn( + 'git', + ['diff', '--name-only', FORMAT_START, filesToCheck], + { + encoding: 'utf-8' + } + ); + + // Check js files that change on staged file + const fileStaged = spawn( + 'git', + ['diff', '--name-only', '--cached', FORMAT_START, filesToCheck], + { + encoding: 'utf-8' + } + ); + + const options = [ + ...fileStaged.stdout.split('\n').filter((f) => f !== ''), + ...fileUnStaged.stdout.split('\n').filter((f) => f !== '') + ]; + + if (fix) { + options.push('--fix'); + } + const result = spawn('node_modules/.bin/eslint', [...options], { + encoding: 'utf-8' + }); + + if (result.status === 1) { + console.error('Eslint error:', result.stdout); + const fixCmd = 'npm run lint:fix'; + console.error(`ERROR: please run "${fixCmd}" to format changes in your commit + Note that when running the command locally, please keep your local + main branch and working branch up to date with nodejs/node-addon-api + to exclude un-related complains. + Or you can run "env FORMAT_START=upstream/main ${fixCmd}". + Also fix JS files by yourself if necessary.`); + return 1; + } + + if (result.stderr) { + console.error('Error running eslint:', result.stderr); + return 2; + } +} + +if (require.main === module) { + process.exitCode = main(process.argv.slice(2)); +} diff --git a/unit-test/.gitignore b/unit-test/.gitignore new file mode 100644 index 000000000..40a7bd0a1 --- /dev/null +++ b/unit-test/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/build +/generated diff --git a/unit-test/README.md b/unit-test/README.md new file mode 100644 index 000000000..c059b5ddf --- /dev/null +++ b/unit-test/README.md @@ -0,0 +1,28 @@ + +# Enable running tests with specific filter conditions: + +### Example: + + - compile ad run only tests on objectwrap.cc and objectwrap.js +``` + npm run unit --filter=objectwrap +``` + + +# Wildcards are also possible: + +### Example: + + - compile and run all tests files ending with reference -> function_reference.cc object_reference.cc reference.cc +``` + npm run unit --filter=*reference +``` + +# Multiple filter conditions are also allowed + +### Example: + + - compile and run all tests under folders threadsafe_function and typed_threadsafe_function and also the objectwrap.cc file +``` + npm run unit --filter='*function objectwrap' +``` diff --git a/unit-test/binding-file-template.js b/unit-test/binding-file-template.js new file mode 100644 index 000000000..2c7b3aa8b --- /dev/null +++ b/unit-test/binding-file-template.js @@ -0,0 +1,39 @@ +const path = require('path'); +const fs = require('fs'); + +/** + * @param bindingConfigurations + * This method acts as a template to generate the content of binding.cc file + */ +module.exports.generateFileContent = function (bindingConfigurations) { + const content = []; + const inits = []; + const exports = []; + + for (const config of bindingConfigurations) { + inits.push(`Object Init${config.objectName}(Env env);`); + exports.push(`exports.Set("${config.propertyName}", Init${config.objectName}(env));`); + } + + content.push('#include "napi.h"'); + content.push('using namespace Napi;'); + + inits.forEach(init => content.push(init)); + + content.push('Object Init(Env env, Object exports) {'); + + exports.forEach(exp => content.push(exp)); + + content.push('return exports;'); + content.push('}'); + content.push('NODE_API_MODULE(addon, Init);'); + + return Promise.resolve(content.join('\r\n')); +}; + +module.exports.writeToBindingFile = function writeToBindingFile (content) { + const generatedFilePath = path.join(__dirname, 'generated', 'binding.cc'); + fs.writeFileSync(generatedFilePath, ''); + fs.writeFileSync(generatedFilePath, content, { flag: 'a' }); + console.log('generated binding file ', generatedFilePath, new Date()); +}; diff --git a/unit-test/binding.gyp b/unit-test/binding.gyp new file mode 100644 index 000000000..8d4a92cc9 --- /dev/null +++ b/unit-test/binding.gyp @@ -0,0 +1,65 @@ +{ + 'target_defaults': { + 'includes': ['../common.gypi'], + 'include_dirs': ['../test/common', "./generated"], + 'variables': { + 'setup': ["@(build_sources)'], + 'dependencies': [ 'generateBindingCC' ] + }, + { + 'target_name': 'binding_noexcept', + 'includes': ['../noexcept.gypi'], + 'sources': ['>@(build_sources)'], + 'dependencies': [ 'generateBindingCC' ] + }, + { + 'target_name': 'binding_noexcept_maybe', + 'includes': ['../noexcept.gypi'], + 'sources': ['>@(build_sources)'], + 'defines': ['NODE_ADDON_API_ENABLE_MAYBE'] + }, + { + 'target_name': 'binding_swallowexcept', + 'includes': ['../except.gypi'], + 'sources': ['>@(build_sources)'], + 'defines': ['NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS'], + 'dependencies': [ 'generateBindingCC' ] + }, + { + 'target_name': 'binding_swallowexcept_noexcept', + 'includes': ['../noexcept.gypi'], + 'sources': ['>@(build_sources)'], + 'defines': ['NODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS'], + 'dependencies': [ 'generateBindingCC' ] + }, + ], +} diff --git a/unit-test/exceptions.js b/unit-test/exceptions.js new file mode 100644 index 000000000..bf7ed32db --- /dev/null +++ b/unit-test/exceptions.js @@ -0,0 +1,32 @@ +/** + * This file points out anomalies/exceptions in test files when generating the binding.cc file + * + * nouns: words in file names that are misspelled + * *NOTE: a 'constructor' property is explicitly added to override javascript object constructor + * + * exportNames: anomalies in init function names + * + * propertyNames: anomalies in exported property name of init functions + * + * skipBinding: skip including this file in binding.cc + */ +module.exports = { + nouns: { + constructor: 'constructor', + threadsafe: 'threadSafe', + objectwrap: 'objectWrap' + }, + exportNames: { + AsyncWorkerPersistent: 'PersistentAsyncWorker' + }, + propertyNames: { + async_worker_persistent: 'persistentasyncworker', + objectwrap_constructor_exception: 'objectwrapConstructorException' + }, + skipBinding: [ + 'global_object_delete_property', + 'global_object_get_property', + 'global_object_has_own_property', + 'global_object_set_property' + ] +}; diff --git a/unit-test/generate-binding-cc.js b/unit-test/generate-binding-cc.js new file mode 100644 index 000000000..75ef477d2 --- /dev/null +++ b/unit-test/generate-binding-cc.js @@ -0,0 +1,61 @@ +const listOfTestModules = require('./listOfTestModules'); +const exceptions = require('./exceptions'); +const { generateFileContent, writeToBindingFile } = require('./binding-file-template'); + +const buildDirs = listOfTestModules.dirs; +const buildFiles = listOfTestModules.files; + +/** + * @param none + * @requires list of files to bind as command-line argument + * @returns list of binding configurations + */ +function generateBindingConfigurations () { + const testFilesToBind = process.argv.slice(2); + console.log('test modules to bind: ', testFilesToBind); + + const configs = []; + + testFilesToBind.forEach((file) => { + const configName = file.split('.cc')[0]; + + if (buildDirs[configName]) { + for (const file of buildDirs[configName]) { + if (exceptions.skipBinding.includes(file)) continue; + configs.push(buildFiles[file]); + } + } else if (buildFiles[configName]) { + configs.push(buildFiles[configName]); + } else { + console.log('not found', file, configName); + } + }); + + return Promise.resolve(configs); +} + +generateBindingConfigurations().then(generateFileContent).then(writeToBindingFile); + +/** + * Test cases + * @fires only when run directly from terminal with TEST=true + * eg: TEST=true node generate-binding-cc + */ +if (require.main === module && process.env.TEST === 'true') { + const assert = require('assert'); + + const setArgsAndCall = (fn, filterCondition) => { process.argv = [null, null, ...filterCondition.split(' ')]; return fn(); }; + const assertPromise = (promise, expectedVal) => promise.then((val) => assert.deepEqual(val, expectedVal)).catch(console.log); + + const expectedVal = [{ + dir: '', + objectName: 'AsyncProgressWorker', + propertyName: 'async_progress_worker' + }, + { + dir: '', + objectName: 'PersistentAsyncWorker', + propertyName: 'persistentasyncworker' + }]; + assertPromise(setArgsAndCall(generateBindingConfigurations, 'async_progress_worker async_worker_persistent'), expectedVal); +} diff --git a/unit-test/injectTestParams.js b/unit-test/injectTestParams.js new file mode 100644 index 000000000..11a054a13 --- /dev/null +++ b/unit-test/injectTestParams.js @@ -0,0 +1,101 @@ +const fs = require('fs'); +const path = require('path'); + +const listOfTestModules = require('./listOfTestModules'); + +const buildDirs = listOfTestModules.dirs; +const buildFiles = listOfTestModules.files; + +if (!fs.existsSync('./generated')) { + fs.mkdirSync('./generated'); +} + +/** + * @returns : list of files to compile by node-gyp + * @param : none + * @requires : picks `filter` parameter from process.env + * This function is used as an utility method to inject a list of files to compile into binding.gyp + */ +module.exports.filesToCompile = function () { + // match filter argument with available test modules + const matchedModules = require('./matchModules').matchWildCards(process.env.npm_config_filter || ''); + + // standard list of files to compile + const addedFiles = './generated/binding.cc test_helper.h'; + + const filterConditions = matchedModules.split(' ').length ? matchedModules.split(' ') : [matchedModules]; + const files = []; + + // generate a list of all files to compile + for (const matchCondition of filterConditions) { + if (buildDirs[matchCondition.toLowerCase()]) { + for (const file of buildDirs[matchCondition.toLowerCase()]) { + const config = buildFiles[file]; + const separator = config.dir.length ? '/' : ''; + files.push(config.dir + separator + file); + } + } else if (buildFiles[matchCondition.toLowerCase()]) { + const config = buildFiles[matchCondition.toLowerCase()]; + const separator = config.dir.length ? '/' : ''; + files.push(config.dir + separator + matchCondition.toLowerCase()); + } + } + + // generate a string of files to feed to the compiler + let filesToCompile = ''; + files.forEach((file) => { + filesToCompile = `${filesToCompile} ../test/${file}.cc`; + }); + + // log list of compiled files + fs.writeFileSync(path.join(__dirname, '/generated/compilelist'), `${addedFiles} ${filesToCompile}`.split(' ').join('\r\n')); + + // return file list + return `${addedFiles} ${filesToCompile}`; +}; + +/** + * @returns list of test files to bind exported init functions + * @param : none + * @requires : picks `filter` parameter from process.env + * This function is used as an utility method by the generateBindingCC step in binding.gyp + */ +module.exports.filesForBinding = function () { + const filterCondition = require('./matchModules').matchWildCards(process.env.npm_config_filter || ''); + fs.writeFileSync(path.join(__dirname, '/generated/bindingList'), filterCondition.split(' ').join('\r\n')); + return filterCondition; +}; + +/** + * Test cases + * @fires only when run directly from terminal + * eg: node injectTestParams + */ +if (require.main === module) { + const assert = require('assert'); + + const setEnvAndCall = (fn, filterCondition) => { process.env.npm_config_filter = filterCondition; return fn(); }; + + assert.strictEqual(setEnvAndCall(exports.filesToCompile, 'typed*ex*'), './generated/binding.cc test_helper.h ../test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc'); + + const expectedFilesToMatch = [ + './generated/binding.cc test_helper.h ', + '../test/threadsafe_function/threadsafe_function.cc', + '../test/threadsafe_function/threadsafe_function_ctx.cc', + '../test/threadsafe_function/threadsafe_function_existing_tsfn.cc', + '../test/threadsafe_function/threadsafe_function_ptr.cc', + '../test/threadsafe_function/threadsafe_function_sum.cc', + '../test/threadsafe_function/threadsafe_function_unref.cc', + '../test/typed_threadsafe_function/typed_threadsafe_function.cc', + '../test/typed_threadsafe_function/typed_threadsafe_function_ctx.cc', + '../test/typed_threadsafe_function/typed_threadsafe_function_existing_tsfn.cc', + '../test/typed_threadsafe_function/typed_threadsafe_function_ptr.cc', + '../test/typed_threadsafe_function/typed_threadsafe_function_sum.cc', + '../test/typed_threadsafe_function/typed_threadsafe_function_unref.cc' + ]; + assert.strictEqual(setEnvAndCall(exports.filesToCompile, 'threadsafe_function typed_threadsafe_function'), expectedFilesToMatch.join(' ')); + + assert.strictEqual(setEnvAndCall(exports.filesToCompile, 'objectwrap'), './generated/binding.cc test_helper.h ../test/objectwrap.cc'); + + console.log('ALL tests passed'); +} diff --git a/unit-test/listOfTestModules.js b/unit-test/listOfTestModules.js new file mode 100644 index 000000000..13a7e6183 --- /dev/null +++ b/unit-test/listOfTestModules.js @@ -0,0 +1,88 @@ +const fs = require('fs'); +const path = require('path'); +const exceptions = require('./exceptions'); + +const buildFiles = {}; +const buidDirs = {}; + +/** + * @param fileName - expect to be in snake case , eg: this_is_a_test_file.cc + * @returns init function name in the file + * + * general format of init function name is camelCase version of the snake_case file name + */ +function getExportObjectName (fileName) { + fileName = fileName.split('_').map(token => exceptions.nouns[token] ? exceptions.nouns[token] : token).join('_'); + const str = fileName.replace(/(_\w)/g, (k) => k[1].toUpperCase()); + const exportObjectName = str.charAt(0).toUpperCase() + str.substring(1); + if (exceptions.exportNames[exportObjectName]) { + return exceptions.exportNames[exportObjectName]; + } + return exportObjectName; +} + +/** + * @param fileName - expect to be in snake case , eg: this_is_a_test_file.cc + * @returns property name of exported init function + */ +function getExportPropertyName (fileName) { + if (exceptions.propertyNames[fileName.toLowerCase()]) { + return exceptions.propertyNames[fileName.toLowerCase()]; + } + return fileName; +} + +/** + * creates a configuration list for all available test modules + * The configuration object contains the expected init function names and corresponding export property names + */ +function listOfTestModules (currentDirectory = path.join(__dirname, '/../test'), pre = '') { + fs.readdirSync(currentDirectory).forEach((file) => { + if (file === 'binding.cc' || + file === 'binding.gyp' || + file === 'build' || + file === 'common' || + file === 'thunking_manual.cc' || + file === 'addon_build' || + file[0] === '.') { + return; + } + const absoluteFilepath = path.join(currentDirectory, file); + const fileName = file.toLowerCase().replace('.cc', ''); + if (fs.statSync(absoluteFilepath).isDirectory()) { + buidDirs[fileName] = []; + listOfTestModules(absoluteFilepath, pre + file + '/'); + } else { + if (!file.toLowerCase().endsWith('.cc')) return; + if (currentDirectory.trim().split('/test/').length > 1) { + buidDirs[currentDirectory.split('/test/')[1].toLowerCase()].push(fileName); + } + const relativePath = (currentDirectory.split(`${fileName}.cc`)[0]).split('/test/')[1] || ''; + buildFiles[fileName] = { dir: relativePath, propertyName: getExportPropertyName(fileName), objectName: getExportObjectName(fileName) }; + } + }); +} +listOfTestModules(); + +module.exports = { + dirs: buidDirs, + files: buildFiles +}; + +/** + * Test cases + * @fires only when run directly from terminal + * eg: node listOfTestModules + */ +if (require.main === module) { + const assert = require('assert'); + assert.strictEqual(getExportObjectName('objectwrap_constructor_exception'), 'ObjectWrapConstructorException'); + assert.strictEqual(getExportObjectName('typed_threadsafe_function'), 'TypedThreadSafeFunction'); + assert.strictEqual(getExportObjectName('objectwrap_removewrap'), 'ObjectWrapRemovewrap'); + assert.strictEqual(getExportObjectName('function_reference'), 'FunctionReference'); + assert.strictEqual(getExportObjectName('async_worker'), 'AsyncWorker'); + assert.strictEqual(getExportObjectName('async_progress_worker'), 'AsyncProgressWorker'); + assert.strictEqual(getExportObjectName('async_worker_persistent'), 'PersistentAsyncWorker'); + + console.log('ALL tests passed'); +} diff --git a/unit-test/matchModules.js b/unit-test/matchModules.js new file mode 100644 index 000000000..ce8317c91 --- /dev/null +++ b/unit-test/matchModules.js @@ -0,0 +1,65 @@ +const listOfTestModules = require('./listOfTestModules'); +const buildDirs = listOfTestModules.dirs; +const buildFiles = listOfTestModules.files; + +function isWildcard (filter) { + if (filter.includes('*')) return true; + return false; +} + +function filterBy (wildcard, item) { + return new RegExp('^' + wildcard.replace(/\*/g, '.*') + '$').test(item); +} + +/** + * @param filterCondition + * matches all given wildcards with available test modules to generate an elaborate filter condition + */ +function matchWildCards (filterCondition) { + const conditions = filterCondition.split(' ').length ? filterCondition.split(' ') : [filterCondition]; + const matches = []; + + for (const filter of conditions) { + if (isWildcard(filter)) { + const matchedDirs = Object.keys(buildDirs).filter(e => filterBy(filter, e)); + if (matchedDirs.length) { + matches.push(matchedDirs.join(' ')); + } + const matchedModules = Object.keys(buildFiles).filter(e => filterBy(filter, e)); + if (matchedModules.length) { matches.push(matchedModules.join(' ')); } + } else { + matches.push(filter); + } + } + + return matches.join(' '); +} + +module.exports.matchWildCards = matchWildCards; + +/** + * Test cases + * @fires only when run directly from terminal + * eg: node matchModules + */ +if (require.main === module) { + const assert = require('assert'); + + assert.strictEqual(matchWildCards('typed*ex'), 'typed*ex'); + assert.strictEqual(matchWildCards('typed*ex*'), 'typed_threadsafe_function_existing_tsfn'); + assert.strictEqual(matchWildCards('async*'), 'async_context async_progress_queue_worker async_progress_worker async_worker async_worker_persistent'); + assert.strictEqual(matchWildCards('typed*func'), 'typed*func'); + assert.strictEqual(matchWildCards('typed*func*'), 'typed_threadsafe_function'); + assert.strictEqual(matchWildCards('typed*function'), 'typed_threadsafe_function'); + assert.strictEqual(matchWildCards('object*inh'), 'object*inh'); + assert.strictEqual(matchWildCards('object*inh*'), 'objectwrap_multiple_inheritance'); + assert.strictEqual(matchWildCards('*remove*'), 'objectwrap_removewrap'); + assert.strictEqual(matchWildCards('*function'), 'threadsafe_function typed_threadsafe_function'); + assert.strictEqual(matchWildCards('**function'), 'threadsafe_function typed_threadsafe_function'); + assert.strictEqual(matchWildCards('a*w*p*'), 'async_worker_persistent'); + assert.strictEqual(matchWildCards('fun*ref'), 'fun*ref'); + assert.strictEqual(matchWildCards('fun*ref*'), 'function_reference'); + assert.strictEqual(matchWildCards('*reference'), 'function_reference object_reference reference'); + + console.log('ALL tests passed'); +} diff --git a/unit-test/setup.js b/unit-test/setup.js new file mode 100644 index 000000000..2e1a66960 --- /dev/null +++ b/unit-test/setup.js @@ -0,0 +1,13 @@ +const fs = require('fs'); +const { generateFileContent, writeToBindingFile } = require('./binding-file-template'); + +/** + * @summary setup script to execute before node-gyp begins target actions + */ +if (!fs.existsSync('./generated')) { + // create generated folder + fs.mkdirSync('./generated'); + // create empty binding.cc file + generateFileContent([]).then(writeToBindingFile); + // FIX: Its necessary to have an empty bindng.cc file, otherwise build fails first time +} diff --git a/unit-test/spawnTask.js b/unit-test/spawnTask.js new file mode 100644 index 000000000..d932af2cb --- /dev/null +++ b/unit-test/spawnTask.js @@ -0,0 +1,26 @@ +const { spawn } = require('child_process'); + +/** + * spawns a child process to run a given node.js script + */ +module.exports.runChildProcess = function (scriptName, options) { + const childProcess = spawn('node', [scriptName], options); + + childProcess.stdout.on('data', data => { + console.log(`${data}`); + }); + childProcess.stderr.on('data', data => { + console.log(`error: ${data}`); + }); + + return new Promise((resolve, reject) => { + childProcess.on('error', (error) => { + console.log(`error: ${error.message}`); + reject(error); + }); + childProcess.on('close', code => { + console.log(`child process exited with code ${code}`); + resolve(code); + }); + }); +}; diff --git a/unit-test/test.js b/unit-test/test.js new file mode 100644 index 000000000..0fcab6edf --- /dev/null +++ b/unit-test/test.js @@ -0,0 +1,30 @@ +'use strict'; +const path = require('path'); +const runChildProcess = require('./spawnTask').runChildProcess; + +/* +* Execute tests with given filter conditions as a child process +*/ +const executeTests = async function () { + try { + const workingDir = path.join(__dirname, '../'); + const relativeBuildPath = path.join('../', 'unit-test'); + const buildPath = path.join(__dirname, './unit-test'); + const envVars = { ...process.env, REL_BUILD_PATH: relativeBuildPath, BUILD_PATH: buildPath }; + + console.log('Starting to run tests in ', buildPath, new Date()); + + const code = await runChildProcess('test', { cwd: workingDir, env: envVars }); + + if (code !== '0') { + process.exitCode = code; + process.exit(process.exitCode); + } + + console.log('Completed running tests', new Date()); + } catch (e) { + console.log('Error occured running tests', new Date()); + } +}; + +executeTests();