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/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/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/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..ab97fcf52 --- /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();