config-sets
Version:
Easily configure the app in real-time.
509 lines (433 loc) • 23.8 kB
JavaScript
/** Copyright (c) Manuel Lõhmus (MIT). */
;
// Use local entry during development to avoid resolution issues
var configSets = require('./index.js');
configSets.enableFileReadWrite = false;
/***** Init TESTS *********************************************************/
testRunner('TESTS for config-sets', { skip: false }, (test) => {
test('assign should merge source into target ', { skip: false }, (check) => {
const target = { a: 1, c: { d: 3 } };
const source = { b: 2, c: { d: 0 } };
const result = configSets.assign(target, source);
return check(result.a).mustBe(1).
check(result.b).mustBe(2).
check(result.c.d).mustBe(3);
});
test('assign should overwrite target properties', { skip: false }, (check) => {
const target = { a: 1, c: { d: 3 } };
const source = { b: 2, c: { d: 0 } };
const result = configSets.assign(target, source, true);
return check(result.a).mustBe(1).
check(result.b).mustBe(2).
check(result.c.d).mustBe(0);
});
test('arg_options ', { skip: false }, (check) => {
const args = ['--key1=value1', '--key2=value2'];
process.argv = [process.argv[0], process.argv[1], ...args];
const result = configSets.arg_options();
return check(result.key1).mustBe('value1').
check(result.key2).mustBe('value2');
});
test('configSets module settings ', { skip: false }, (check) => {
configSets.isProduction = true;
configSets.enableFileReadWrite = false;
const result = configSets('moduleName', { key: 'value' });
return check(result.key).mustBe('value')
.check(configSets.production.key).mustBe(undefined);
});
test('configSets production settings ', { skip: false }, (check) => {
configSets.isProduction = true;
configSets.enableFileReadWrite = false;
const result = configSets({ key: 'value' });
return check(result.key).mustBe('value');
});
test('configSets development settings ', { skip: false }, (check) => {
configSets.isProduction = false;
configSets.enableFileReadWrite = false;
configSets.development.key = 'dev';
const result = configSets({ key: 'value' });
return check(result.key).mustBe('dev');
});
}).then((summary) => {
//console.log('Test runner summary:', summary);
process.exit(summary.ok ? 0 : 1);
});
// 'test-runner-lite' — simple test execution framework for Node.js and the browser
/** Copyright (c) Manuel Lõhmus (MIT License). */
/**
* @typedef Options - Options object for the test runner.
* @type {object}
* @property {string} [controlServerSocketPath="/tmp/test-runner-lite.sock"] - Path to the control server socket. Used for inter-process communication between primary and worker processes (on Node.js).
* @property {boolean} [workers=false] - Allows tests to be run in a worker process. Does not create in a worker process.
* @property {number} [timeout=5000] - Default timeout for tests in milliseconds.
* @property {boolean} [json=false] - Whether to output results in JSON format.
* @property {boolean} [raw=false] - Whether to disable colored output.
* @property {boolean} [silent=false] - Whether to suppress console output.
* @property {boolean} [bail=false] - Whether to stop on the first test failure.
* @property {boolean} [noExit=false] - Whether to prevent exiting the process on completion.
* @property {string[]} [testIDs] - Array of test IDs to run. If provided, only these tests will be executed.
* @property {function} [onComplete] - Callback function to be called when all tests are complete. Receives the summary object as an argument.
*/
/**
* @typedef Done - Function to call when the test is complete. Can be called with an error message to indicate failure.
* @type {function}
* @param {string|Error} label - Optional label for the error message.
* @param {string|Error} err - Optional error message. If provided, the test is considered failed.
* @return {void}
*/
/**
* @typedef Check - Object with assertion methods.
* @type {object}
* @property {function} mustBe - Asserts that the value is equal to one of the provided arguments.
* @property {function} mustNotBe - Asserts that the value is not equal to any of the provided arguments.
* @property {function} mustInclude - Asserts that the value includes the provided argument.
* @property {function} mustBeDefined - Asserts that the value is defined.
* @property {function} mustBeUndefined - Asserts that the value is undefined.
* @property {function} mustBeNull - Asserts that the value is null.
* @property {function} mustBeNotNull - Asserts that the value is not null.
* @property {function} mustBeTrue - Asserts that the value is true.
* @property {function} mustBeFalse - Asserts that the value is false.
* @property {function} mustBeObject - Asserts that the value is an object.
* @property {function} mustBeArray - Asserts that the value is an array.
* @property {function} mustBeString - Asserts that the value is a string.
* @property {function} mustBeNumber - Asserts that the value is a number.
* @property {function} mustBeFunction - Asserts that the value is a function.
* @property {function} mustBeGreaterThan - Asserts that the value is greater than the provided argument.
* @property {function} mustBeLessThan - Asserts that the value is less than the provided argument.
* @property {function} mustBeGreaterOrEqual - Asserts that the value is greater than or equal to the provided argument.
* @property {function} mustBeLessOrEqual - Asserts that the value is less than or equal to the provided argument.
* @property {function} mustMatch - Asserts that the value matches the provided regular expression.
* @property {function} mustBeInstanceOf - Asserts that the value is an instance of the provided type.
* @property {function} toBe - Alias for mustBe.
* @property {function} notToBe - Alias for mustNotBe.
* @property {function} toInclude - Alias for mustInclude.
* @property {function} toBeDefined - Alias for mustBeDefined.
* @property {function} toBeUndefined - Alias for mustBeUndefined.
* @property {function} toBeNull - Alias for mustBeNull.
* @property {function} toBeNotNull - Alias for mustBeNotNull.
* @property {function} toBeTrue - Alias for mustBeTrue.
* @property {function} toBeFalse - Alias for mustBeFalse.
* @property {function} toBeObject - Alias for mustBeObject.
* @property {function} toBeArray - Alias for mustBeArray.
* @property {function} toBeString - Alias for mustBeString.
* @property {function} toBeNumber - Alias for mustBeNumber.
* @property {function} toBeFunction - Alias for mustBeFunction.
* @property {function} toBeGreaterThan - Alias for mustBeGreaterThan.
* @property {function} toBeLessThan - Alias for mustBeLessThan.
* @property {function} toBeGreaterOrEqual - Alias for mustBeGreaterOrEqual.
* @property {function} toBeLessOrEqual - Alias for mustBeLessOrEqual.
* @property {function} toMatch - Alias for mustMatch.
* @property {function} toBeInstanceOf - Alias for mustBeInstanceOf.
* @property {function} truthy - Asserts that the value is truthy.
* @property {function} falsy - Asserts that the value is falsy.
* @property {Done} done - Function to call when the test is complete or to indicate failure.
*/
/**
* @typedef TestCallback - Callback function for defining a test.
* @type {function}
* @param {Check} check - Object with assertion methods.
* @param {function} done - Function to call when the test is complete. Can be called with an error message to indicate failure.
* @param {boolean} isPrimary - Whether the current process is the primary process.
*/
/**
* @typedef TestFunction - Function to define a test.
* @type {function}
* @param {string} name - Name of the test.
* @param {Options|function} [opts] - Options object or test function if no options are provided.
* @param {TestCallback} fn - Test callback function.
*/
/**
* @typedef SuiteCallback - Callback function for defining tests.
* @type {function}
* @param {TestFunction} test - Function to define a test.
* @param {boolean} isPrimary - Whether the current process is the primary process.
* @param {boolean} isWorker - Whether the current process is a worker process.
*/
/**
* Test runner. Simple test execution framework for Node.js and the browser.
* Can run tests in the primary process and worker processes (if workers are supported).
* Collects and reports test results.
* Uses a control server to collect results from worker processes.
* @module test-runner-lite
* @param {string} runnerName - Name of the test runner.
* @param {Options|function} options - Options object or suite function if no options are provided.
* @param {SuiteCallback} [suite] - Suite callback function to define tests.
* @returns {Promise<object>} Promise that resolves with the test summary or rejects if any test fails.
* @example
* const testRunner = require('test-runner-lite');
*
* testRunner("My Test Suite", { workers: true, timeout: 2000 }, (test, isPrimary, isWorker) => {
* test("Primary process test", { off: !isPrimary }, (check, done) => {
* check("typeof", typeof someModule).mustBe("object").done();
* });
* test("Worker process test", { off: !isWorker }, (check, done) => {
* check("isWorker", isWorker).mustBe(true).done('stop');
* });
* test("Universal test", (check, done) => {
* check("platform", process.platform).mustInclude("linux").done();
* });
* });
* .then((summary) => {
* console.log("All tests completed", summary);
* });
* @example
* // Minimal example if testRunner function is included directly
* testRunner("My Test Suite", (test) => {
* test("Simple test", (check) => {
* return check(42).mustBe(42)
* .check(true).mustBeTrue();
* });
*/
function testRunner(runnerName, options, suite) {
if (typeof options === 'function') { suite = options; options = {}; }
let numberOfTests = 0, ok = true, endTimer = null, resolveRunnerPromise;
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined',
color = makeColors(isBrowser || process.stdout.isTTY && !options.raw),
isPrimary = typeof process === 'undefined' || !process.send, // process is undefined in the browser, process.send is undefined in the primary thread
primaryOnly = !options.workers,
printToConsole = !options.silent && !options.json,
startTime = performance.now(),
tests = [],
summary = {
runner: runnerName,
ok,
total: 0,
passed: 0,
failed: 0,
skipped: 0,
turned_off: 0,
time_ms: NaN,
results: [],
},
controlServer = createControlServer(startTesting);
// if controlServer is created, startTesting() will be called when all workers are connected
// if controlServer is not created (e.g. in the browser or if workers are not used), startTesting() is called immediately
if (!controlServer || isBrowser) { startTesting(); }
return new Promise((resolve) => { resolveRunnerPromise = resolve; });
function startTesting() {
// START print
if (isPrimary) {
if (printToConsole) console.log(`START > ${runnerName}`);
}
suite(testFn, isPrimary, !isPrimary);
finish();
}
async function testFn(name, opts, fn) {
if (!ok && options.bail) { return; }
if (typeof opts === 'function') { fn = opts; opts = {}; }
const test = {
id: ++numberOfTests,
name,
status: 'OFF',
time_ms: NaN,
};
if (!isPrimary && !isBrowser) { test.worker_pid = process.pid; }
if (opts.off || options.testIDs && !options.testIDs.includes(String(test.id))) {
test.status = 'OFF';
return setTimeout(finish, 1, test);
}
if (opts.skip) {
test.status = 'SKIP'
return setTimeout(finish, 1, test);
}
const { check, done } = makeCheckAndDone(test);
test.startTime = performance.now();
try {
const maybePromise = fn(check, done, isPrimary);
if (maybePromise && typeof maybePromise.then === 'function') {
// Promise returned
return maybePromise.then(() => done()).catch(done);
}
if (maybePromise.done) { maybePromise.done(); }
if (maybePromise === true) { done(); }
throw new Error('Test function did not call done()');
}
catch (err) {
done(err); // done called with error
}
function makeCheckAndDone() {
const done = makeDone();
return { check, done };
function check(label, value) {
if (value === undefined) { value = label; label = `${test.id}. ${test.name} returned '${value}'`; }
return {
mustBe(...args) { if (!args.includes(value)) { done(`${label} must be '${args}'`); } return this; },
mustNotBe(...args) { if (args.includes(value)) { done(`${label} must not be '${args}'`); } return this; },
mustInclude(v) { if (!value?.includes || !value.includes(v)) { done(`${label} must include '${v}'`); } return this; },
mustBeDefined() { if (value === undefined) { done(`${label} must be defined`); } return this; },
mustBeUndefined() { if (value !== undefined) { done(`${label} must be undefined`); } return this; },
mustBeNull() { if (value !== null) { done(`${label} must be null`); } return this; },
mustBeNotNull() { if (value === null) { done(`${label} must not be null`); } return this; },
mustBeTrue() { if (value !== true) { done(`${label} must be true`); } return this; },
mustBeFalse() { if (value !== false) { done(`${label} must be false`); } return this; },
mustBeObject() { if (typeof value !== 'object' || value === null || Array.isArray(value)) { done(`${label} must be an object`); } return this; },
mustBeArray() { if (!Array.isArray(value)) { done(`${label} must be an array`); } return this; },
mustBeString() { if (typeof value !== 'string') { done(`${label} must be a string`); } return this; },
mustBeNumber() { if (typeof value !== 'number' || isNaN(value)) { done(`${label} must be a number`); } return this; },
mustBeFunction() { if (typeof value !== 'function') { done(`${label} must be a function`); } return this; },
mustBeGreaterThan(v) { if (typeof value !== 'number' || value <= v) { done(`${label} must be greater than ${v}`); } return this; },
mustBeLessThan(v) { if (typeof value !== 'number' || value >= v) { done(`${label} must be less than ${v}`); } return this; },
mustBeGreaterOrEqual(v) { if (typeof value !== 'number' || value < v) { done(`${label} must be greater or equal to ${v}`); } return this; },
mustBeLessOrEqual(v) { if (typeof value !== 'number' || value > v) { done(`${label} must be less or equal to ${v}`); } return this; },
mustMatch(regex) { if (typeof value !== 'string' || !value.match(regex)) { done(`${label} must match ${regex}`); } return this; },
mustBeInstanceOf(type) { if (!(value instanceof type)) { done(`${label} must be an instance of ${type.name || type}`); } return this; },
toBe(...args) { return this.mustBe(...args); },
notToBe(...args) { return this.mustNotBe(...args); },
toInclude(v) { return this.mustInclude(v); },
toBeDefined() { return this.mustBeDefined(); },
toBeUndefined() { return this.mustBeUndefined(); },
toBeNull() { return this.mustBeNull(); },
toBeNotNull() { return this.mustBeNotNull(); },
toBeTrue() { return this.mustBeTrue(); },
toBeFalse() { return this.mustBeFalse(); },
toBeObject() { return this.mustBeObject(); },
toBeArray() { return this.mustBeArray(); },
toBeString() { return this.mustBeString(); },
toBeNumber() { return this.mustBeNumber(); },
toBeFunction() { return this.mustBeFunction(); },
toBeGreaterThan(v) { return this.mustBeGreaterThan(v); },
toBeLessThan(v) { return this.mustBeLessThan(v); },
toBeGreaterOrEqual(v) { return this.mustBeGreaterOrEqual(v); },
toBeLessOrEqual(v) { return this.mustBeLessOrEqual(v); },
toMatch(regex) { return this.mustMatch(regex); },
toBeInstanceOf(type) { return this.mustBeInstanceOf(type); },
truthy() { if (!value) { done(`${label} must be truthy`); } return this; },
falsy() { if (value) { done(`${label} must be falsy`); } return this; },
check,
done
};
}
function makeDone() {
test.timer = setTimeout(function () { done('timeout') }, opts.timeout || options.timeout);
return done;
function done(label, err) {
// done called
if (!test.timer) return;
if (label && !err) { err = label; label = ''; }
clearTimeout(test.timer);
delete test.timer;
test.time_ms = +(performance.now() - test.startTime).toFixed(2);
delete test.startTime;
test.status = err ? 'FAIL' : 'OK';
if (err) { ok = false; }
if (!label) { label = `${test.id}. ${test.name} - `; }
err = err ? label + (err.message || err) : null;
setTimeout(finish, 1, test, err);
}
}
}
}
function finish(test, err) {
// continue
if (!test && numberOfTests) { return; }
// add test
if (test) {
tests.push(test);
// TEST print
if (printToConsole && (primaryOnly && isPrimary || !primaryOnly)) {
if (test.status !== 'OFF') {
const status = test.status === 'SKIP'
? `${color.bgGray} SKIP ${color.reset}`
: test.status === 'OK'
? `${color.bgGreen} OK ${color.reset}`
: `${color.bgRed} FAILED ${color.reset}`;
console.log(`${startLengthening(test.id, 3)}. ${primaryOnly ? '' : isPrimary ? 'p' : 'w'}TEST > ${endLengthening(test.name, 50)} ${startLengthening(test.time_ms, 8)}ms ${status} ${err ? '-> ' + err : ''}`);
}
}
}
// continue
if (numberOfTests > tests.length) { return; }
report(tests);
}
function report(results) {
// Is Worker
if (!isPrimary) { return workerEnd(); }
for (let t of results) {
summary.total++;
if (t.status === 'OK') { summary.passed++; }
if (t.status === 'FAIL') { summary.failed++; ok = false; }
if (t.status === 'SKIP') { summary.skipped++; }
if (t.status === 'OFF') { summary.turned_off++ }
if (t.status !== 'OFF') { summary.results.push(t); }
}
// Only Primary Thread
if (primaryOnly) { return end(); }
// Timeout Exit
if (endTimer === null && !isBrowser) {
process.on('beforeExit', function () { end(); });
}
clearTimeout(endTimer);
endTimer = setTimeout(end, options.timeout);
return;
function workerEnd() {
postMessageToPrimary({ type: 'tests-request', tests }, function () {
if (!isBrowser) { process.exit(ok ? 0 : 1); }
});
}
function end() {
// totalTime
summary.time_ms = +(performance.now() - startTime).toFixed(2);
// END print
if (options.json) {
console.log(JSON.stringify(summary, null, 2));
}
else if (printToConsole && isPrimary) {
console.log(`END > ${runnerName}\t ${ok ? color.bgGreen + ' DONE ' + color.reset : color.bgRed + ' FAIL ' + color.reset}`);
}
// Integration
if (typeof options.onComplete === 'function') { options.onComplete(summary); }
if (controlServer) {
controlServer.close(exit)
}
else { exit(); }
function exit() {
// Exit
if (ok && typeof resolveRunnerPromise === 'function') { resolveRunnerPromise(summary); }
if (!ok && !options.noExit && !isBrowser) { setTimeout(() => process.exit(1), 100); }
}
}
}
function createControlServer(callback) {
if (!isPrimary || typeof net === 'undefined') { return null; }
const server = net.createServer(function (socket) {
socket.on('data', function onMsg(msg) {
msg = JSON.parse(msg.toString());
if (msg.type === 'tests-request') {
report(msg.tests);
}
});
});
server.on('error', function (error) { console.error(error); });
server.listen(options.controlServerSocketPath, callback);
return server;
}
function postMessageToPrimary(msg, callback) {
if (isPrimary) { return null; }
const client = net.createConnection({ path: options.controlServerSocketPath }, function () {
client.write(JSON.stringify(msg));
client.end();
});
client.on('error', function (error) { console.error(error); });
client.on('close', function () { if (typeof callback === 'function') { callback(); } });
return client;
}
function makeColors(enable) {
if (!enable) return new Proxy({}, { get: function () { return ''; } });
return {
reset: '\x1b[0m',
bgGreen: '\x1b[7m\x1b[1m\x1b[32m',
bgRed: '\x1b[7m\x1b[1m\x1b[31m',
bgGray: '\x1b[7m\x1b[1m\x1b[90m',
};
}
function startLengthening(str, toLength) {
if (options.raw) { return str; }
str = str + '';
return ' '.repeat(toLength - str.length > 0 ? toLength - str.length : 0) + str;
}
function endLengthening(str, toLength) {
if (options.raw) { return str; }
str = str + '';
return str + ' '.repeat(toLength - str.length > 0 ? toLength - str.length : 0);
}
}