163 lines
5 KiB
JavaScript
163 lines
5 KiB
JavaScript
import kleur from 'kleur';
|
|
import { compare } from 'uvu/diff';
|
|
|
|
let isCLI = false, isNode = false;
|
|
let hrtime = (now = Date.now()) => () => (Date.now() - now).toFixed(2) + 'ms';
|
|
let write = console.log;
|
|
|
|
const into = (ctx, key) => (name, handler) => ctx[key].push({ name, handler });
|
|
const context = (state) => ({ tests:[], before:[], after:[], bEach:[], aEach:[], only:[], skips:0, state });
|
|
const milli = arr => (arr[0]*1e3 + arr[1]/1e6).toFixed(2) + 'ms';
|
|
const hook = (ctx, key) => handler => ctx[key].push(handler);
|
|
|
|
if (isNode = typeof process < 'u' && typeof process.stdout < 'u') {
|
|
// globalThis polyfill; Node < 12
|
|
if (typeof globalThis !== 'object') {
|
|
Object.defineProperty(global, 'globalThis', {
|
|
get: function () { return this }
|
|
});
|
|
}
|
|
|
|
let rgx = /(\.bin[\\+\/]uvu$|uvu[\\+\/]bin\.js)/i;
|
|
isCLI = process.argv.some(x => rgx.test(x));
|
|
|
|
// attach node-specific utils
|
|
write = x => process.stdout.write(x);
|
|
hrtime = (now = process.hrtime()) => () => milli(process.hrtime(now));
|
|
} else if (typeof performance < 'u') {
|
|
hrtime = (now = performance.now()) => () => (performance.now() - now).toFixed(2) + 'ms';
|
|
}
|
|
|
|
globalThis.UVU_QUEUE = globalThis.UVU_QUEUE || [];
|
|
isCLI = isCLI || !!globalThis.UVU_DEFER;
|
|
isCLI || UVU_QUEUE.push([null]);
|
|
|
|
const QUOTE = kleur.dim('"'), GUTTER = '\n ';
|
|
const FAIL = kleur.red('✘ '), PASS = kleur.gray('• ');
|
|
const IGNORE = /^\s*at.*(?:\(|\s)(?:node|(internal\/[\w/]*))/;
|
|
const FAILURE = kleur.bold().bgRed(' FAIL ');
|
|
const FILE = kleur.bold().underline().white;
|
|
const SUITE = kleur.bgWhite().bold;
|
|
|
|
function stack(stack, idx) {
|
|
let i=0, line, out='';
|
|
let arr = stack.substring(idx).replace(/\\/g, '/').split('\n');
|
|
for (; i < arr.length; i++) {
|
|
line = arr[i].trim();
|
|
if (line.length && !IGNORE.test(line)) {
|
|
out += '\n ' + line;
|
|
}
|
|
}
|
|
return kleur.grey(out) + '\n';
|
|
}
|
|
|
|
function format(name, err, suite = '') {
|
|
let { details, operator='' } = err;
|
|
let idx = err.stack && err.stack.indexOf('\n');
|
|
if (err.name.startsWith('AssertionError') && !operator.includes('not')) details = compare(err.actual, err.expected); // TODO?
|
|
let str = ' ' + FAILURE + (suite ? kleur.red(SUITE(` ${suite} `)) : '') + ' ' + QUOTE + kleur.red().bold(name) + QUOTE;
|
|
str += '\n ' + err.message + (operator ? kleur.italic().dim(` (${operator})`) : '') + '\n';
|
|
if (details) str += GUTTER + details.split('\n').join(GUTTER);
|
|
if (!!~idx) str += stack(err.stack, idx);
|
|
return str + '\n';
|
|
}
|
|
|
|
async function runner(ctx, name) {
|
|
let { only, tests, before, after, bEach, aEach, state } = ctx;
|
|
let hook, test, arr = only.length ? only : tests;
|
|
let num=0, errors='', total=arr.length;
|
|
|
|
try {
|
|
if (name) write(SUITE(kleur.black(` ${name} `)) + ' ');
|
|
for (hook of before) await hook(state);
|
|
|
|
for (test of arr) {
|
|
state.__test__ = test.name;
|
|
try {
|
|
for (hook of bEach) await hook(state);
|
|
await test.handler(state);
|
|
for (hook of aEach) await hook(state);
|
|
write(PASS);
|
|
num++;
|
|
} catch (err) {
|
|
for (hook of aEach) await hook(state);
|
|
if (errors.length) errors += '\n';
|
|
errors += format(test.name, err, name);
|
|
write(FAIL);
|
|
}
|
|
}
|
|
} finally {
|
|
state.__test__ = '';
|
|
for (hook of after) await hook(state);
|
|
let msg = ` (${num} / ${total})\n`;
|
|
let skipped = (only.length ? tests.length : 0) + ctx.skips;
|
|
write(errors.length ? kleur.red(msg) : kleur.green(msg));
|
|
return [errors || true, num, skipped, total];
|
|
}
|
|
}
|
|
|
|
let timer;
|
|
function defer() {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(exec);
|
|
}
|
|
|
|
function setup(ctx, name = '') {
|
|
ctx.state.__test__ = '';
|
|
ctx.state.__suite__ = name;
|
|
const test = into(ctx, 'tests');
|
|
test.before = hook(ctx, 'before');
|
|
test.before.each = hook(ctx, 'bEach');
|
|
test.after = hook(ctx, 'after');
|
|
test.after.each = hook(ctx, 'aEach');
|
|
test.only = into(ctx, 'only');
|
|
test.skip = () => { ctx.skips++ };
|
|
test.run = () => {
|
|
let copy = { ...ctx };
|
|
let run = runner.bind(0, copy, name);
|
|
Object.assign(ctx, context(copy.state));
|
|
UVU_QUEUE[globalThis.UVU_INDEX || 0].push(run);
|
|
isCLI || defer();
|
|
};
|
|
return test;
|
|
}
|
|
|
|
export const suite = (name = '', state = {}) => setup(context(state), name);
|
|
export const test = suite();
|
|
|
|
let isRunning = false;
|
|
export async function exec(bail) {
|
|
let timer = hrtime();
|
|
let done=0, total=0, skips=0, code=0;
|
|
|
|
isRunning = true;
|
|
for (let group of UVU_QUEUE) {
|
|
if (total) write('\n');
|
|
|
|
let name = group.shift();
|
|
if (name != null) write(FILE(name) + '\n');
|
|
|
|
for (let test of group) {
|
|
let [errs, ran, skip, max] = await test();
|
|
total += max; done += ran; skips += skip;
|
|
if (errs.length) {
|
|
write('\n' + errs + '\n'); code=1;
|
|
if (bail) return isNode && process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
isRunning = false;
|
|
write('\n Total: ' + total);
|
|
write((code ? kleur.red : kleur.green)('\n Passed: ' + done));
|
|
write('\n Skipped: ' + (skips ? kleur.yellow(skips) : skips));
|
|
write('\n Duration: ' + timer() + '\n\n');
|
|
|
|
if (isNode) process.exitCode = code;
|
|
}
|
|
|
|
if (isNode) process.on('exit', () => {
|
|
if (!isRunning) return; // okay to exit
|
|
process.exitCode = process.exitCode || 1;
|
|
console.error('Exiting early before testing is finished.');
|
|
});
|