bmocha
Version:
Alternative implementation of mocha
1,069 lines (862 loc) • 23.9 kB
JavaScript
/*!
* util.js - utils for bmocha
* Copyright (c) 2018-2019, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bmocha
*
* Parts of this software are based on nodejs/node:
* Copyright Node.js contributors. All rights reserved.
* https://github.com/nodejs/node
*/
/* eslint no-control-regex: "off" */
;
const inspect = require('./inspect');
const error = require('./error');
const {hasLine, getLine, cleanStack, why} = error;
/*
* Globals
*/
const {
Array,
Error,
Math,
Object,
Promise,
RegExp,
String,
TypeError
} = global;
/*
* Constants
*/
const MAX_SHORT_LENGTH = 10;
const errorStrings = {
__proto__: null,
bufferEqual: 'Expected buffers to be strictly equal:',
deepStrictEqual: 'Expected values to be strictly deep-equal:',
strictEqual: 'Expected values to be strictly equal:',
strictEqualObject: 'Expected "actual" to be reference-equal to "expected":',
deepEqual: 'Expected values to be loosely deep-equal:',
equal: 'Expected values to be loosely equal:',
notBufferEqual: 'Expected "actual" to be strictly unequal to:',
notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:',
notStrictEqual: 'Expected "actual" to be strictly unequal to:',
notStrictEqualObject:
'Expected "actual" not to be reference-equal to "expected":',
notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:',
notEqual: 'Expected "actual" to be loosely unequal to:',
notIdentical: 'Values identical but not reference-equal:'
};
const RED = '\x1b[31m';
const GREEN = '\x1b[32m';
const BLUE = '\x1b[34m';
const WHITE = '\x1b[39m';
/*
* Utils
*/
function assert(ok, msg) {
if (!ok) {
const err = new Error(msg || 'Assertion failure');
if (Error.captureStackTrace)
Error.captureStackTrace(err, assert);
throw err;
}
}
function get(obj, prop) {
try {
return obj[prop];
} catch (e) {
return undefined;
}
}
function has(obj, prop) {
try {
return Object.prototype.hasOwnProperty.call(obj, prop);
} catch (e) {
return false;
}
}
function toString(obj) {
try {
return String(obj);
} catch (e) {
return 'Object';
}
}
function getJSON(obj, prop) {
try {
const value = obj[prop];
JSON.stringify(value);
return value;
} catch (e) {
return undefined;
}
}
async function nextTick() {
return new Promise(r => setImmediate(r));
}
function isObject(error) {
if (error == null)
return false;
return typeof error === 'object'
|| typeof error === 'function';
}
function isPromise(value) {
if (!isObject(value))
return false;
const then = get(value, 'then');
return typeof then === 'function';
}
function inject(target, values) {
assert(target && typeof target === 'object');
assert(values && typeof values === 'object');
const snapshot = [];
for (const key of Object.keys(values)) {
const desc = Object.getOwnPropertyDescriptor(target, key);
Object.defineProperty(target, key, {
configurable: true,
enumerable: true,
writable: true,
value: values[key]
});
snapshot.push([key, desc]);
}
return snapshot;
}
function restore(target, snapshot) {
assert(target && typeof target === 'object');
assert(Array.isArray(snapshot));
for (const [key, desc] of snapshot) {
if (!desc) {
delete target[key];
continue;
}
Object.defineProperty(target, key, desc);
}
}
function stackError(start, msg) {
const err = new Error(msg);
if (Error.captureStackTrace) {
if (typeof start !== 'function')
start = stackError;
Error.captureStackTrace(err, start);
}
return err;
}
function noop() {}
function getOperator(error) {
if (!isObject(error))
return null;
const name = get(error, 'name');
const code = get(error, 'code');
const message = get(error, 'message');
const operator = get(error, 'operator');
const expected = get(error, 'expected');
if (typeof operator !== 'string') {
const isAssertion = name === 'AssertionError' // browserify
|| code === 'ERR_ASSERTION'; // node.js
if (isAssertion && operator === undefined) {
// The node ~8.0.0 throws and doesNotThrow
// calls set an undefined operator.
if (typeof message === 'string') {
if (message.startsWith('Missing expected exception'))
return 'throws';
if (message.startsWith('Got unwanted exception'))
return 'doesNotThrow';
}
// So does the fail call.
return 'fail';
}
// Not an assertion error.
return null;
}
// Normalize loose equal calls.
if (operator === '==') {
// Direct assert() call.
if (expected === true)
return 'ok';
return 'equal';
}
if (operator === '!=')
return 'notEqual';
// Normalize strict equal calls (browserify still does this).
if (operator === '===')
return 'strictEqual';
if (operator === '!==')
return 'notStrictEqual';
return operator;
}
function castArray(funcs, file = null) {
if (funcs == null)
return [];
if (!Array.isArray(funcs))
funcs = [funcs];
for (const func of funcs) {
if (typeof func !== 'function') {
let msg = 'Hook must be a function';
if (file != null)
msg += ` (${file})`;
throw new TypeError(msg);
}
}
return funcs;
}
/*
* Text Processing
*/
function indent(str, depth) {
if (depth == null)
depth = 0;
assert(typeof str === 'string');
assert((depth >>> 0) === depth);
if (depth === 0)
return str;
return str.replace(/^/gm, ' '.repeat(depth * 2));
}
function sanitize(str) {
str = toString(str);
str = str.replace(/^\ufeff/, '');
str = str.replace(/\r\n/g, '\n');
str = str.replace(/[\r\u2028\u2029]/g, '\n');
str = str.replace(/\t/g, ' ');
str = str.replace(/\x1b\[[\?\d;]*[a-zA-Z]/g, '');
str = str.replace(/[\x00-\x09\x0b-\x1f\x7f]/g, '');
return str;
}
function strip(str) {
str = toString(str);
str = str.replace(/\x1b\[[\?\d;]*[a-zA-Z]/g, '');
return str;
}
function singlify(str) {
str = toString(str);
const index = str.indexOf('\n');
if (index !== -1)
str = str.substring(0, index) + '...';
return str;
}
function trim(str) {
str = toString(str);
str = str.replace(/^\n+/, '');
str = str.replace(/\s+$/, '');
return str;
}
function escape(str) {
str = toString(str);
str = str.replace(/&/g, '&');
str = str.replace(/</g, '<');
str = str.replace(/>/g, '>');
str = str.replace(/"/g, '"');
str = str.replace(/'/g, ''');
return str;
}
function clean(func) {
assert(typeof func === 'function');
let str = sanitize(func);
let braceless = false;
let state = 0;
if (str.length < 4)
return '';
let i = 0;
let j = str.length;
outer:
for (; i < str.length; i++) {
const ch = str.charCodeAt(i);
switch (state) {
case 0:
switch (ch) {
case 0x3d: // '='
state = 1;
break;
case 0x7b: // '{'
i += 1;
break outer;
}
break;
case 1:
switch (ch) {
case 0x3e: // '>'
state = 2;
break;
default:
i = 0;
break outer;
}
break;
case 2:
switch (ch) {
case 0x7b: // '{'
i += 1;
break outer;
default:
if (ch > 0x20) { // ' '
braceless = true;
break outer;
}
break;
}
break;
}
}
if (!braceless) {
for (j -= 1; j > i; j--) {
if (str.charCodeAt(j) === 0x7d) // '}'
break;
}
}
if (i >= j)
return '';
str = str.substring(i, j);
str = trim(str);
if (braceless)
return `${str.trim()};`;
let tab = false;
let sp = 0;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
if (ch !== 0x09 && ch !== 0x20) // '\t', ' '
break;
tab = ch === 0x09; // '\t'
sp += 1;
}
if (sp === 0)
return str;
const ch = tab ? '\t' : ' ';
const re = new RegExp(`^${ch}{${sp}}`, 'gm');
return str.replace(re, '');
}
/*
* Error Processing
*/
function isError(error) {
try {
return error instanceof Error;
} catch (e) {
return false;
}
}
function toError(error) {
if (isError(error))
return error;
if (isObject(error)) {
const message = get(error, 'message');
if (typeof message === 'string')
return new Error(message);
}
const type = inspect.type(error);
const data = inspect.single(error);
return new Error(`the ${type} ${data} was thrown, throw an Error :)`);
}
function toMessage(error) {
assert(isObject(error));
let name = get(error, 'name');
let message = get(error, 'message');
name = sanitize(name || 'Error');
message = sanitize(message);
if (get(error, 'uncaught') === true) {
if (get(error, 'rejection') === true)
name = `Unhandled ${name}`;
else
name = `Uncaught ${name}`;
}
name = singlify(name);
if (get(error, 'generatedMessage') === true) {
const operator = getOperator(error);
if (operator == null) {
message = singlify(message);
} else if (operator === 'ok') {
// Better message for `assert(false)`.
message = 'Assertion failed.';
} else if (message.includes('\n')) {
// Drop all the nonsense that node.js adds now.
message = `${operator} failed.`;
}
} else {
message = singlify(message);
}
return `${name}: ${message}`;
}
function toStack(error, fullTrace) {
if (fullTrace == null)
fullTrace = false;
assert(isObject(error));
assert(typeof fullTrace === 'boolean');
let stack = get(error, 'stack');
if (typeof stack !== 'string' || stack === '')
return '';
const message = get(error, 'message');
let index = -1;
// Remove the message from the stack trace.
if (typeof message === 'string' && message.length > 0) {
index = stack.indexOf(message);
if (index !== -1) {
index += message.length;
stack = stack.substring(index + 1);
}
}
stack = sanitize(stack);
if (!fullTrace) {
if (index === -1)
stack = stack.substring(stack.indexOf('\n') + 1);
stack = cleanStack(stack);
}
stack = stack.replace(/^ +/gm, '');
stack = trim(stack);
return stack;
}
function errorify(error) {
if (typeof error === 'string')
error = new Error(error);
return toError(error);
}
function stackify(error, fullTrace) {
const err = toError(error);
const msg = toMessage(err);
const stack = toStack(err, fullTrace);
return msg + '\n' + indent(stack, 2);
}
function jsonify(error, fullTrace) {
const stack = toStack(error, fullTrace);
const message = get(error, 'message');
const generatedMessage = getJSON(error, 'generatedMessage');
const name = get(error, 'name');
const type = getJSON(error, 'type');
const code = getJSON(error, 'code');
const actual = getJSON(error, 'actual');
const expected = getJSON(error, 'expected');
const operator = getJSON(error, 'operator');
return {
stack,
message: sanitize(message),
generatedMessage,
name: sanitize(name || 'Error'),
type,
code,
actual,
expected,
operator
};
}
/*
* Display
*/
function isDisplayable(error) {
if (!isObject(error))
return false;
return get(error, 'display') === true
&& has(error, 'value');
}
function display(value, colors, fullTrace) {
if (colors == null)
colors = false;
if (fullTrace == null)
fullTrace = false;
if (isError(value))
return stackify(value, fullTrace);
const options = { colors };
const lines = inspect(value, options).split('\n');
let blue = '';
let white = '';
if (colors) {
blue = BLUE;
white = WHITE;
}
if (lines.length > 30) {
lines[26] = `${blue}...${white}`;
lines.length = 27;
}
return lines.join('\n');
}
/*
* Diff
*/
function isDiffable(error) {
if (!isObject(error))
return false;
if (get(error, 'showDiff') === false)
return false;
if (!has(error, 'actual')
&& !has(error, 'expected')) {
return false;
}
let operator = getOperator(error);
if (operator == null)
operator = 'strictEqual';
return errorStrings[operator] != null;
}
function diff(actual, expected, operator, colors) {
if (typeof operator !== 'string')
operator = 'strictEqual';
if (errorStrings[operator] == null)
operator = 'strictEqual';
if (colors == null)
colors = false;
const options = { sort: true, colors };
const actualInspected = inspect(actual, options);
const actualLines = actualInspected.split('\n');
const expectedLines = inspect(expected, options).split('\n');
let red = '';
let green = '';
let blue = '';
let white = '';
if (colors) {
red = RED;
green = GREEN;
blue = BLUE;
white = WHITE;
}
let other = '';
let res = '';
let lastPos = 0;
let end = '';
let skipped = false;
let i = 0;
let indicator = '';
// In case both values are objects explicitly
// mark them as not reference equal for the
// `strictEqual` operator.
if (operator === 'strictEqual'
&& typeof actual === 'object'
&& typeof expected === 'object'
&& actual !== null
&& expected !== null) {
operator = 'strictEqualObject';
}
// If "actual" and "expected" fit on a single
// line and they are not strictly equal, check
// further special handling.
if (actualLines.length === 1
&& expectedLines.length === 1
&& actualLines[0] !== expectedLines[0]) {
const actualLine = strip(actualLines[0]);
const expectedLine = strip(expectedLines[0]);
const inputLength = actualLine.length + expectedLine.length;
// If the character length of "actual" and
// "expected" together is less than
// MAX_SHORT_LENGTH and if neither is an
// object and at least one of them is not
// `zero`, use the strict equal comparison
// to visualize the output.
if (inputLength <= MAX_SHORT_LENGTH) {
if ((typeof actual !== 'object' || actual === null)
&& (typeof expected !== 'object' || expected === null)
&& (actual !== 0 || expected !== 0)) {
return `${errorStrings[operator]}\n\n`
+ `${actualLines[0]} !== ${expectedLines[0]}`;
}
} else if (operator !== 'strictEqualObject') {
// If the stderr is a tty and the input
// length is lower than the current
// columns per line, add a mismatch
// indicator below the output. If it is
// not a tty, use a default value of 80
// characters.
if (inputLength < 80) {
while (actualLine[i] === expectedLine[i])
i += 1;
// Ignore the first characters.
if (i > 2) {
// Add position indicator for the
// first mismatch in case it is a
// single line and the input length
// is less than the column length.
indicator = `\n ${' '.repeat(i)}^`;
i = 0;
}
}
}
}
// Remove all ending lines that match (this
// optimizes the output for readability by
// reducing the number of total changed
// lines).
let a = actualLines[actualLines.length - 1];
let b = expectedLines[expectedLines.length - 1];
while (a === b) {
if (i < 2)
end = `\n ${a}${end}`;
else
other = a;
i += 1;
actualLines.pop();
expectedLines.pop();
if (actualLines.length === 0
|| expectedLines.length === 0) {
break;
}
a = actualLines[actualLines.length - 1];
b = expectedLines[expectedLines.length - 1];
}
const maxLines = Math.max(actualLines.length,
expectedLines.length);
// Strict equal with identical objects that
// are not identical by reference. e.g.
// assert.deepStrictEqual({ a: Symbol() },
// { a: Symbol() })
if (maxLines === 0) {
// We have to get the result again.
// The lines were all removed before.
const actualLines = actualInspected.split('\n');
// Only remove lines in case it makes sense
// to collapse those.
if (actualLines.length > 30) {
actualLines[26] = `${blue}...${white}`;
actualLines.length = 27;
}
return `${errorStrings.notIdentical}\n\n`
+ `${actualLines.join('\n')}`;
}
if (i > 3) {
end = `\n${blue}...${white}${end}`;
skipped = true;
}
if (other !== '') {
end = `\n ${other}${end}`;
other = '';
}
const msg = `${errorStrings[operator]}\n\n`
+ `${green}+ actual${white} `
+ `${red}- expected${white}`;
const skippedMsg = ` ${blue}...${white} Lines skipped`;
let printedLines = 0;
for (i = 0; i < maxLines; i++) {
// Only extra expected lines exist.
const cur = i - lastPos;
if (actualLines.length < i + 1) {
// If the last diverging line is more
// than one line above and the current
// line is at least line three, add some
// of the former lines and also add dots
// to indicate skipped entries.
if (cur > 1 && i > 2) {
if (cur > 4) {
res += `\n${blue}...${white}`;
skipped = true;
} else if (cur > 3) {
res += `\n ${expectedLines[i - 2]}`;
printedLines += 1;
}
res += `\n ${expectedLines[i - 1]}`;
printedLines += 1;
}
// Mark the current line as the last diverging one.
lastPos = i;
// Add the expected line to the cache.
other += `\n${red}- ${strip(expectedLines[i])}${white}`;
printedLines += 1;
} else if (expectedLines.length < i + 1) {
// Only extra actual lines exist.
// If the last diverging line is more
// than one line above and the current
// line is at least line three, add
// some of the former lines and also
// add dots to indicate skipped entries.
if (cur > 1 && i > 2) {
if (cur > 4) {
res += `\n${blue}...${white}`;
skipped = true;
} else if (cur > 3) {
res += `\n ${actualLines[i - 2]}`;
printedLines += 1;
}
res += `\n ${actualLines[i - 1]}`;
printedLines += 1;
}
// Mark the current line as the last
// diverging one.
lastPos = i;
// Add the actual line to the result.
res += `\n${green}+ ${strip(actualLines[i])}${white}`;
printedLines += 1;
} else {
// Lines diverge.
const expectedLine = expectedLines[i];
let actualLine = actualLines[i];
// If the lines diverge, specifically
// check for lines that only diverge by
// a trailing comma. In that case it
// is actually identical and we should
// mark it as such.
let divergingLines = false;
if (actualLine !== expectedLine) {
divergingLines = !actualLine.endsWith(',')
|| actualLine.slice(0, -1) !== expectedLine;
}
// If the expected line has a trailing
// comma but is otherwise identical,
// add a comma at the end of the actual
// line. Otherwise the output could
// look weird as in:
//
// [
// 1 // No comma at the end!
// + 2
// ]
//
if (divergingLines
&& expectedLine.endsWith(',')
&& expectedLine.slice(0, -1) === actualLine) {
divergingLines = false;
actualLine += ',';
}
if (divergingLines) {
// If the last diverging line is more
// than one line above and the current
// line is at least line three, add
// some of the former lines and also
// add dots to indicate skipped entries.
if (cur > 1 && i > 2) {
if (cur > 4) {
res += `\n${blue}...${white}`;
skipped = true;
} else if (cur > 3) {
res += `\n ${actualLines[i - 2]}`;
printedLines += 1;
}
res += `\n ${actualLines[i - 1]}`;
printedLines += 1;
}
// Mark the current line as the last
// diverging one.
lastPos = i;
// Add the actual line to the result
// and cache the expected diverging
// line so consecutive diverging lines
// show up as +++--- and not +-+-+-.
res += `\n${green}+ ${strip(actualLine)}${white}`;
other += `\n${red}- ${strip(expectedLine)}${white}`;
printedLines += 2;
} else {
// Lines are identical.
// Add all cached information to the
// result before adding other things
// and reset the cache.
res += other;
other = '';
// If the last diverging line is
// exactly one line above or if it
// is the very first line, add the
// line to the result.
if (cur === 1 || i === 0) {
res += `\n ${actualLine}`;
printedLines += 1;
}
}
}
// Inspected object to big.
// Show ~20 rows max.
if (printedLines > 20 && i < maxLines - 2) {
return `${msg}${skippedMsg}\n`
+ `${res}\n`
+ `${blue}...${white}${other}\n`
+ `${blue}...${white}`;
}
}
return `${msg}${skipped ? skippedMsg : ''}\n`
+ `${res}${other}${end}${indicator}`;
}
/*
* Find
*/
function isFindable(error) {
if (!isObject(error))
return false;
if (get(error, 'showCode') === false)
return false;
if (get(error, 'multiple') === true) {
const value = get(error, 'value');
if (!isError(value))
return false;
error = value;
}
return hasLine(error);
}
function find(error) {
let line;
try {
line = getLine(error);
} catch (e) {
return null;
}
if (!line || !line.code)
return null;
let code = line.code;
if (line.column !== 0) {
code += '\n';
code += ' '.repeat(line.column);
code += '^';
}
return code;
}
/*
* Show
*/
function isShowable(error) {
return isDisplayable(error)
|| isDiffable(error)
|| isFindable(error);
}
function show(error, colors, fullTrace) {
const out = [];
if (isDisplayable(error)) {
const value = get(error, 'value');
const text = display(value, colors, fullTrace);
out.push(text);
}
if (isDiffable(error)) {
const actual = get(error, 'actual');
const expected = get(error, 'expected');
const operator = getOperator(error);
const text = diff(actual, expected, operator, colors);
out.push(text);
}
if (isFindable(error)) {
const err = get(error, 'multiple') === true
? get(error, 'value')
: error;
const text = find(err);
if (text)
out.push(text);
}
return out.join('\n\n');
}
/*
* Expose
*/
exports.assert = assert;
exports.get = get;
exports.has = has;
exports.toString = toString;
exports.getJSON = getJSON;
exports.nextTick = nextTick;
exports.isObject = isObject;
exports.isPromise = isPromise;
exports.inject = inject;
exports.restore = restore;
exports.stackError = stackError;
exports.noop = noop;
exports.getOperator = getOperator;
exports.castArray = castArray;
exports.indent = indent;
exports.sanitize = sanitize;
exports.strip = strip;
exports.singlify = singlify;
exports.trim = trim;
exports.escape = escape;
exports.clean = clean;
exports.isError = isError;
exports.toError = toError;
exports.toMessage = toMessage;
exports.toStack = toStack;
exports.errorify = errorify;
exports.stackify = stackify;
exports.jsonify = jsonify;
exports.isDisplayable = isDisplayable;
exports.display = display;
exports.isDiffable = isDiffable;
exports.diff = diff;
exports.isFindable = isFindable;
exports.find = find;
exports.isShowable = isShowable;
exports.show = show;
exports.inspect = inspect;
exports.why = why;