UNPKG

config-sets

Version:
509 lines (433 loc) 23.8 kB
/** Copyright (c) Manuel Lõhmus (MIT). */ 'use strict'; // 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); } }