UNPKG

@j-o-r/sh

Version:

Execute shell commands on Linux-based systems from javascript

239 lines (226 loc) 6.12 kB
import { jsType } from './SH.js' /** * Valid jsTypes for test methods */ const FNC = ['Function', 'AsyncFunction']; // Settle async calls in a SYNC function const SETTLE_ASYNC = 50; /** * @typedef {(function(): Promise<any>)} AsyncFunction */ /** * @typedef {Object} testDefinition * @prop {string} description * @prop {Function|AsyncFunction} callback - syc/ async function */ /** * @typedef {Object} testReport * @prop {string} description * @prop {number} duration - start time in MS * @prop {boolean} executed - Has it been called? */ /** * @typedef {Object} Report * @prop {number} tests * @prop {number} duration - start time in MS * @prop {number} errors - number of errors * @prop {number} executed - number of tests executed */ /** * Get the current time * used for calculating a duration * @returns {number} */ function getNow() { return new Date().getTime(); } /** * Get the duration based on a previous gathered start time * @param {number} start - start time * @returns {number} */ function getDuration(start) { return new Date().getTime() - start; } class Test { #catchErrors = false; #currentTest = -1; /** @type {testDefinition[]} */ #tests = []; /** @type {testReport[]} */ #reports = []; /** * @type {Error[]} */ #errors = []; /** verbosed **/ #quite = false; /** Timeout in ms to settle async code blocks called from sync methods */ #TO = SETTLE_ASYNC; /** * @param {boolean} [quiet] - does not output a report when true, default `false` */ constructor(quiet = false) { if (quiet) { this.#quite = true; } } /** * Set the timeout when a synced function is called. * This settles async code used in a sync function * and give some time to catch errors (#HACK) * @param {number} timeout - in MS, default 50 */ syncTimeout(timeout) { this.#TO = timeout; } /** * Activate a listener on errors outside the call stack scope * @param {boolean} active - register / unregister */ #detectErrors(active = true) { const errListener = (err) => { this.#handleError(err, true); } if (active) { if (!this.#catchErrors) { process.on('uncaughtException', errListener); this.#catchErrors = true; } } else { if (this.#catchErrors) { process.removeListener('uncaughtException', errListener); this.#catchErrors = false; } } } /** * * @param {string} description * @param {Function|AsyncFunction} callback - sync / async function * @throws Error when conditions are not met * @returns {Test} */ add(description, callback) { if (typeof(description) !== 'string') { throw new Error(`'description' should be a string`) } if (!FNC.includes(jsType(callback))) { throw new Error(`'callback' should be a (async) Function`) } this.#tests.push({ description, callback }); return this; } /** * Execute tests * @param {number[]} [execute] - limit the execution tests * @returns {Promise<Report>} */ async run(execute) { // Detect errors outside the call stack this.#detectErrors(true); let errors = false; let i = 0; const len = this.#tests.length; for (; i < len; i++) { this.#currentTest = i; if (execute && !execute.includes(i)) continue; let duration = 0; let start = getNow(); let executed = false; const cb = this.#tests[i]; const type = jsType(cb.callback); this.#reports[i] = { description: cb.description, duration, executed }; let error; try { if (!this.#quite) process.stdout.write(`${i}. ${cb.description} `); await Promise.resolve(cb.callback()); executed = true; if (type === 'Function') { // To settle async calls in sync functions // (catching errors outside this call stack, that may throw later (=== bad practise)) await new Promise(resolve => setTimeout(resolve, this.#TO)); // This will pause the the current loop. // add timeout for an honest execution time start = start + this.#TO; } } catch (e) { executed = true; if (!errors) { errors = true } error = e; } duration = getDuration(start); if (!this.#quite && !error) process.stdout.write(`(duration: ${duration} ms)\n`); this.#reports[i].duration = duration; this.#reports[i].executed = executed; if (error) { this.#handleError(error); } } this.#currentTest = -1; this.#detectErrors(false); return this.#report(); } /** * @returns {Report} */ #report() { let duration = 0; let executed = 0; const tests = this.#tests.length; let i = 0; const len = this.#reports.length; for (; i < len; i++) { const r = this.#reports[i]; if (r) { duration = r.duration + duration; if (r.executed) { executed = 1 + executed; } } } const errors = this.#errors.length; if (tests !== executed) { if (!this.#quite) console.log('** Not all tests have been executed **'); } if (!this.#quite) { console.log('--------------------------------------------------'); console.log(`Total: ${tests} tests, executed: ${executed} in ${duration} ms - errors: ${errors}`); } return { tests, executed, duration, errors }; } /** * Empty tests */ reset() { this.#tests = []; this.#reports = []; this.#errors = []; } /** * Handle an error for the current test * @param {Error} err * @param {boolean} [outside] default false, Error is catched ouside the callscope of the test */ #handleError(err, outside = false) { // A global error is an error catched outside the callstack of the test const ERR = outside? 'GLOBAL_ERROR' : 'ERROR' if (this.#currentTest > -1) { if (!this.#quite) process.stdout.write(`\n`); // Register this error // Always print out errors despite #quite const description = this.#reports[this.#currentTest].description; const executed = this.#reports[this.#currentTest].executed; if (executed) { process.stdout.write(`\x1b[31m-- ${ERR} Test: ${this.#currentTest}. ${description} --\x1b[0m\n`); } else { // Can been thrown from an other test process.stdout.write(`\x1b[31m-- ${ERR} --\x1b[0m\n`); } console.error(err); this.#errors.push(err); } } } export default Test;