From 2fc9d73164139c6f993ed3ae5b289e212fe9cb4e Mon Sep 17 00:00:00 2001 From: Deepak Rajamohan Date: Thu, 29 Jul 2021 23:40:46 -0700 Subject: [PATCH] enable unit tests --- package.json | 6 +- test/common/index.js | 36 ++++++++++ test/index.js | 41 ++++++++--- .../typed_threadsafe_function_ctx.js | 2 +- unit-test/.gitignore | 3 + unit-test/binding-file-template.js | 27 +++++++ unit-test/binding.gyp | 64 +++++++++++++++++ unit-test/common.gypi | 21 ++++++ unit-test/exceptions.js | 20 ++++++ unit-test/generate-binding-cc.js | 41 +++++++++++ unit-test/injectTestParams.js | 69 ++++++++++++++++++ unit-test/listOfTestModules.js | 70 +++++++++++++++++++ unit-test/matchModules.js | 58 +++++++++++++++ unit-test/spawnTask.js | 23 ++++++ unit-test/test.js | 22 ++++++ 15 files changed, 491 insertions(+), 12 deletions(-) create mode 100644 unit-test/.gitignore create mode 100644 unit-test/binding-file-template.js create mode 100644 unit-test/binding.gyp create mode 100644 unit-test/common.gypi create mode 100644 unit-test/exceptions.js create mode 100644 unit-test/generate-binding-cc.js create mode 100644 unit-test/injectTestParams.js create mode 100644 unit-test/listOfTestModules.js create mode 100644 unit-test/matchModules.js create mode 100644 unit-test/spawnTask.js create mode 100644 unit-test/test.js diff --git a/package.json b/package.json index 7538c84f9..200cfd637 100644 --- a/package.json +++ b/package.json @@ -344,8 +344,10 @@ "predev:incremental": "node-gyp configure build -C test --debug", "dev:incremental": "node test", "doc": "doxygen doc/Doxyfile", - "lint": "node tools/clang-format.js", - "lint:fix": "git-clang-format '*.h', '*.cc'" + "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)", + "preunit": "filter=\"$npm_config_filter\" node-gyp rebuild -C unit-test", + "unit": "filter=\"$npm_config_filter\" node unit-test/test" }, "pre-commit": "lint", "version": "3.1.0", diff --git a/test/common/index.js b/test/common/index.js index 54139bb2d..51ebd3bb7 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -74,3 +74,39 @@ exports.mustNotCall = function(msg) { assert.fail(msg || 'function should not have been called'); }; }; + +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`), + ].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.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`), + ].map(it => require.resolve(it)); + + for (const item of bindings) { + await test(item); + } +} + +exports.runTestWithBuildType = async function(test, buildType) { + buildType = buildType || process.config.target_defaults.default_configuration || 'Release'; + + await Promise.resolve(test(buildType)) + .finally(exports.mustCall()); +} diff --git a/test/index.js b/test/index.js index 35db3187b..e0a95adeb 100644 --- a/test/index.js +++ b/test/index.js @@ -29,9 +29,23 @@ if (typeof global.gc !== 'function') { const fs = require('fs'); const path = require('path'); +process.env.filter = require('../unit-test/matchModules').matchWildCards(process.env.filter); let testModules = []; +const filterCondition = process.env.filter || ''; +const filterConditionFiles = filterCondition.split(' ').length ? filterCondition.split(' ') : [filterCondition]; + + +function checkFilterCondition(fileName, parsedFilepath) { + let result = false; + + if (!filterConditionFiles.length) result = 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 = '') { @@ -50,15 +64,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 +87,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); @@ -86,7 +104,7 @@ if (napiVersion < 3) { testModules.splice(testModules.indexOf('version_management'), 1); } -if (napiVersion < 4) { +if (napiVersion < 4 && !filterConditionFiles.length) { testModules.splice(testModules.indexOf('asyncprogressqueueworker'), 1); testModules.splice(testModules.indexOf('asyncprogressworker'), 1); testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function_ctx'), 1); @@ -97,25 +115,30 @@ if (napiVersion < 4) { testModules.splice(testModules.indexOf('threadsafe_function/threadsafe_function'), 1); } -if (napiVersion < 5) { +if (napiVersion < 5 && !filterConditionFiles.length) { testModules.splice(testModules.indexOf('date'), 1); } -if (napiVersion < 6) { +if (napiVersion < 6 && !filterConditionFiles.length) { 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 && !filterConditionFiles.length) { testModules.splice(testModules.indexOf('objectwrap_worker_thread'), 1); + testModules.splice(testModules.indexOf('error_terminating_environment'), 1); +} + +if (napiVersion < 8 && !filterConditionFiles.length) { + testModules.splice(testModules.indexOf('object/object_freeze_seal'), 1); } (async function() { console.log(`Testing with N-API Version '${napiVersion}'.`); -console.log('Starting test suite\n'); +console.log('Starting test suite\n', testModules); // Requiring each module runs tests in the module. for (const name of testModules) { diff --git a/test/typed_threadsafe_function/typed_threadsafe_function_ctx.js b/test/typed_threadsafe_function/typed_threadsafe_function_ctx.js index 2651586a0..c76c9865d 100644 --- a/test/typed_threadsafe_function/typed_threadsafe_function_ctx.js +++ b/test/typed_threadsafe_function/typed_threadsafe_function_ctx.js @@ -8,7 +8,7 @@ module.exports = test(require(`../build/${buildType}/binding.node`)) 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..3f1f6888d --- /dev/null +++ b/unit-test/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/build +/generated \ No newline at end of file diff --git a/unit-test/binding-file-template.js b/unit-test/binding-file-template.js new file mode 100644 index 000000000..eebcfa642 --- /dev/null +++ b/unit-test/binding-file-template.js @@ -0,0 +1,27 @@ +module.exports.generateFileContent = function(configs) { + const content = []; + const inits = []; + const exports = []; + + for (let config of configs) { + 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;"); + + //content.push("Object InitName(Env env);"); + inits.forEach(init => content.push(init)); + + content.push("Object Init(Env env, Object exports) {"); + + //content.push("exports.Set(\"name\", InitName(env));"); + 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')); +} diff --git a/unit-test/binding.gyp b/unit-test/binding.gyp new file mode 100644 index 000000000..5c33af53b --- /dev/null +++ b/unit-test/binding.gyp @@ -0,0 +1,64 @@ +{ + 'target_defaults': { + 'includes': ['common.gypi'], + 'include_dirs': ['../test/common'], + 'variables': { + 'build_sources': [ + "@(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' ] + }, + ], +} \ No newline at end of file diff --git a/unit-test/common.gypi b/unit-test/common.gypi new file mode 100644 index 000000000..7173deb81 --- /dev/null +++ b/unit-test/common.gypi @@ -0,0 +1,21 @@ +{ + 'variables': { + 'NAPI_VERSION%': " { + const configName = file.split('.cc')[0]; + + if (buildDirs[configName]) { + for (let 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); +} + +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()); +} + +generateBindingConfigurations().then(generateFileContent).then(writeToBindingFile); diff --git a/unit-test/injectTestParams.js b/unit-test/injectTestParams.js new file mode 100644 index 000000000..3e1a36263 --- /dev/null +++ b/unit-test/injectTestParams.js @@ -0,0 +1,69 @@ +const fs = require('fs'); +const listOfTestModules = require('./listOfTestModules'); + +const buildDirs = listOfTestModules.dirs; +const buildFiles = listOfTestModules.files; + +module.exports.filesToCompile = function () { + const filterCondition = require('./matchModules').matchWildCards(process.env.filter || ''); + let files_to_compile = './generated/binding.cc test_helper.h'; + let conditions = filterCondition.split(' ').length ? filterCondition.split(' ') : [filterCondition]; + let files = []; + + for (let matchCondition of conditions) { + if (buildDirs[matchCondition.toLowerCase()]) { + for (let 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()); + } + } + + let addedFiles = ''; + files.forEach((file) => { + addedFiles = `${addedFiles} ../test/${file}.cc`; + }); + fs.writeFileSync(__dirname + '/generated/compilelist', `${files_to_compile} ${addedFiles}`); + return `${files_to_compile} ${addedFiles}`; +}; + +module.exports.filesForBinding = function () { + const filterCondition = require('./matchModules').matchWildCards(process.env.filter || ''); + fs.writeFileSync(__dirname + '/generated/bindingList', filterCondition); + return filterCondition; +}; + + +if (require.main === module) { + const assert = require('assert'); + + const setEnvAndCall = (fn, filterCondition) => { process.env.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..20b712224 --- /dev/null +++ b/unit-test/listOfTestModules.js @@ -0,0 +1,70 @@ +const fs = require('fs'); +const path = require('path'); +const exceptions = require('./exceptions'); + +const buildFiles = {}; +const buidDirs = {}; + +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; +} + +function getExportPropertyName(fileName) { + if (exceptions.propertyNames[fileName.toLowerCase()]) { + return exceptions.propertyNames[fileName.toLowerCase()]; + } + return fileName; +} + +function listOfTestModules(currentDirectory = __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 +}; + + +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..8ce531cba --- /dev/null +++ b/unit-test/matchModules.js @@ -0,0 +1,58 @@ +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) +} + +function matchWildCards(filterCondition) { + let conditions = filterCondition.split(' ').length ? filterCondition.split(' ') : [filterCondition]; + let matches = []; + + for (let filter of conditions) { + if (isWildcard(filter)) { + const matchedDirs = Object.keys(buildDirs).filter(e => filterBy(filter, e)); + if (matchedDirs.length) { + matches.push(matchedDirs.join(' ')); + } else { + 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; + +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/spawnTask.js b/unit-test/spawnTask.js new file mode 100644 index 000000000..b14776481 --- /dev/null +++ b/unit-test/spawnTask.js @@ -0,0 +1,23 @@ +const { spawn } = require("child_process"); + +module.exports.runChildProcess = async function (command, options) { + const childProcess = spawn("node", [command], 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(); + }); + }); +} diff --git a/unit-test/test.js b/unit-test/test.js new file mode 100644 index 000000000..5d5667f04 --- /dev/null +++ b/unit-test/test.js @@ -0,0 +1,22 @@ +'use strict'; +const path = require('path'); +const runChildProcess = require('./spawnTask').runChildProcess; + +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()); + + await runChildProcess('test', {cwd: workingDir, env: envVars}); + + console.log('Completed running tests', new Date()); + } catch(e) { + console.log('Error occured running tests', new Date()); + } +} + +executeTests();