This commit is contained in:
2026-03-25 14:14:07 +01:00
parent d6b31e2ef7
commit a0073b4fb1
10368 changed files with 2214340 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
# Enable running tests with specific filter conditions:
### Example:
- compile and run only tests on objectwrap.cc and objectwrap.js
```
npm run test --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 test --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 test --filter='*function objectwrap'
```

View File

@@ -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());
};

View File

@@ -0,0 +1,72 @@
{
'target_defaults': {
'includes': ['../common.gypi'],
'include_dirs': ['../test/common', "./generated"],
'variables': {
'setup': ["<!@(node -p \"require('./setup')\")"],
'build_sources': [
"<!@(node -p \"require('./injectTestParams').filesToCompile()\")",
]
},
},
'targets': [
{
"target_name": "generateBindingCC",
"type": "none",
"actions": [ {
"action_name": "generateBindingCC",
"message": "Generating binding cc file",
"outputs": ["generated/binding.cc"],
"conditions": [
[ "'true'=='true'", {
"inputs": [""],
"action": [
"node",
"generate-binding-cc.js",
"<!@(node -p \"require('./injectTestParams').filesForBinding()\" )"
]
} ]
]
} ]
},
{
'target_name': 'binding',
'includes': ['../except.gypi'],
'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' ]
},
{
'target_name': 'binding_custom_namespace',
'includes': ['../noexcept.gypi'],
'sources': ['>@(build_sources)'],
'defines': ['NAPI_CPP_CUSTOM_NAMESPACE=cstm'],
'dependencies': [ 'generateBindingCC' ]
},
],
}

View File

@@ -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'
]
};

View File

@@ -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);
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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');
}

View File

@@ -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
}

View File

@@ -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);
});
});
};

View File

@@ -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();