diff --git a/harness/assert.js b/harness/assert.js index bb2db89011..9dfbd40911 100644 --- a/harness/assert.js +++ b/harness/assert.js @@ -94,58 +94,6 @@ assert.throws = function (expectedErrorConstructor, func, message) { $ERROR(message); }; -assert._formatValue = (value, seen) => { - switch (typeof value) { - case 'string': - return typeof JSON !== "undefined" ? JSON.stringify(value) : `"${value}"`; - case 'number': - case 'boolean': - case 'symbol': - case 'bigint': - return value.toString(); - case 'undefined': - return 'undefined'; - case 'function': - return `[Function${value.name ? `: ${value.name}` : ''}]`; - case 'object': - if (value === null) return 'null'; - if (value instanceof Date) return `Date "${value.toISOString()}"`; - if (value instanceof RegExp) return value.toString(); - if (!seen) { - seen = { - counter: 0, - map: new Map() - }; - } - - let usage = seen.map.get(value); - if (usage) { - usage.used = true; - return `[Ref: #${usage.id}]`; - } - - usage = { id: ++seen.counter, used: false }; - seen.map.set(value, usage); - - if (typeof Set !== "undefined" && value instanceof Set) { - return `Set {${Array.from(value).map(value => assert._formatValue(value, seen)).join(', ')}}${usage.used ? ` as #${usage.id}` : ''}`; - } - if (typeof Map !== "undefined" && value instanceof Map) { - return `Map {${Array.from(value).map(pair => `${assert._formatValue(pair[0], seen)} => ${assert._formatValue(pair[1], seen)}}`).join(', ')}}${usage.used ? ` as #${usage.id}` : ''}`; - } - if (Array.isArray ? Array.isArray(value) : value instanceof Array) { - return `[${value.map(value => assert._formatValue(value, seen)).join(', ')}]${usage.used ? ` as #${usage.id}` : ''}`; - } - let tag = Symbol.toStringTag in value ? value[Symbol.toStringTag] : 'Object'; - if (tag === 'Object' && Object.getPrototypeOf(value) === null) { - tag = '[Object: null prototype]'; - } - return `${tag ? `${tag} ` : ''}{ ${Object.keys(value).map(key => `${key.toString()}: ${assert._formatValue(value[key], seen)}`).join(', ')} }${usage.used ? ` as #${usage.id}` : ''}`; - default: - return typeof value; - } -}; - assert._toString = function (value) { try { return String(value); diff --git a/harness/compareArray.js b/harness/compareArray.js index 53feee10c1..0e84f55e51 100644 --- a/harness/compareArray.js +++ b/harness/compareArray.js @@ -6,40 +6,34 @@ description: | defines: [compareArray] ---*/ -// @ts-check - -function isSameValue(a, b) { - if (a === 0 && b === 0) return 1 / a === 1 / b; - if (a !== a && b !== b) return true; - - return a === b; -} - -/** - * @template T - * @param {T[]} a - * @param {T[]} b - */ function compareArray(a, b) { if (b.length !== a.length) { return false; } for (var i = 0; i < a.length; i++) { - if (!isSameValue(b[i], a[i])) { + if (!compareArray.isSameValue(b[i], a[i])) { return false; } } return true; } -/** - * @template T - * @param {T[]} actual - * @param {T[]} expected - * @param {string} [message] - */ -assert.compareArray = function(actual, expected, message) { - assert(compareArray(actual, expected), - 'Expected ' + assert._formatValue(actual) + ' and ' + assert._formatValue(expected) + ' to have the same contents. ' + message); +compareArray.isSameValue = function(a, b) { + if (a === 0 && b === 0) return 1 / a === 1 / b; + if (a !== a && b !== b) return true; + + return a === b; +}; + +compareArray.format = function(array) { + return `[${array.map(String).join(', ')}]`; +}; + +assert.compareArray = function(actual, expected, message) { + var format = compareArray.format; + assert( + compareArray(actual, expected), + `Expected ${format(actual)} and ${format(expected)} to have the same contents. ${(message || '')}` + ); }; diff --git a/harness/deepEqual.js b/harness/deepEqual.js index 2287d5f793..e5ca1011da 100644 --- a/harness/deepEqual.js +++ b/harness/deepEqual.js @@ -6,268 +6,322 @@ description: > defines: [assert.deepEqual] ---*/ -var deepEqual = (function () { - var EQUAL = 1; - var NOT_EQUAL = -1; - var UNKNOWN = 0; - - function deepEqual(a, b) { - return compareEquality(a, b) === EQUAL; - } - - function compareEquality(a, b, cache) { - return compareIf(a, b, isOptional, compareOptionality) - || compareIf(a, b, isPrimitiveEquatable, comparePrimitiveEquality) - || compareIf(a, b, isObjectEquatable, compareObjectEquality, cache) - || NOT_EQUAL; - } - - function compareIf(a, b, test, compare, cache) { - return !test(a) - ? !test(b) ? UNKNOWN : NOT_EQUAL - : !test(b) ? NOT_EQUAL : cacheComparison(a, b, compare, cache); - } - - function tryCompareStrictEquality(a, b) { - return a === b ? EQUAL : UNKNOWN; - } - - function tryCompareTypeOfEquality(a, b) { - return typeof a !== typeof b ? NOT_EQUAL : UNKNOWN; - } - - function tryCompareToStringTagEquality(a, b) { - var aTag = Symbol.toStringTag in a ? a[Symbol.toStringTag] : undefined; - var bTag = Symbol.toStringTag in b ? b[Symbol.toStringTag] : undefined; - return aTag !== bTag ? NOT_EQUAL : UNKNOWN; - } - - function isOptional(value) { - return value === undefined - || value === null; - } - - function compareOptionality(a, b) { - return tryCompareStrictEquality(a, b) - || NOT_EQUAL; - } - - function isPrimitiveEquatable(value) { - switch (typeof value) { - case 'string': - case 'number': - case 'bigint': - case 'boolean': - case 'symbol': - return true; - default: - return isBoxed(value); - } - } - - function comparePrimitiveEquality(a, b) { - if (isBoxed(a)) a = a.valueOf(); - if (isBoxed(b)) b = b.valueOf(); - return tryCompareStrictEquality(a, b) - || tryCompareTypeOfEquality(a, b) - || compareIf(a, b, isNaNEquatable, compareNaNEquality) - || NOT_EQUAL; - } - - function isNaNEquatable(value) { - return typeof value === 'number'; - } - - function compareNaNEquality(a, b) { - return isNaN(a) && isNaN(b) ? EQUAL : NOT_EQUAL; - } - - function isObjectEquatable(value) { - return typeof value === 'object'; - } - - function compareObjectEquality(a, b, cache) { - if (!cache) cache = new Map(); - return getCache(cache, a, b) - || setCache(cache, a, b, EQUAL) // consider equal for now - || cacheComparison(a, b, tryCompareStrictEquality, cache) - || cacheComparison(a, b, tryCompareToStringTagEquality, cache) - || compareIf(a, b, isValueOfEquatable, compareValueOfEquality) - || compareIf(a, b, isToStringEquatable, compareToStringEquality) - || compareIf(a, b, isArrayLikeEquatable, compareArrayLikeEquality, cache) - || compareIf(a, b, isStructurallyEquatable, compareStructuralEquality, cache) - || compareIf(a, b, isIterableEquatable, compareIterableEquality, cache) - || cacheComparison(a, b, fail, cache); - } - - function isBoxed(value) { - return value instanceof String - || value instanceof Number - || value instanceof Boolean - || typeof Symbol === 'function' && value instanceof Symbol - || typeof BigInt === 'function' && value instanceof BigInt; - } - - function isValueOfEquatable(value) { - return value instanceof Date; - } - - function compareValueOfEquality(a, b) { - return compareIf(a.valueOf(), b.valueOf(), isPrimitiveEquatable, comparePrimitiveEquality) - || NOT_EQUAL; - } - - function isToStringEquatable(value) { - return value instanceof RegExp; - } - - function compareToStringEquality(a, b) { - return compareIf(a.toString(), b.toString(), isPrimitiveEquatable, comparePrimitiveEquality) - || NOT_EQUAL; - } - - function isArrayLikeEquatable(value) { - return (Array.isArray ? Array.isArray(value) : value instanceof Array) - || (typeof Uint8Array === 'function' && value instanceof Uint8Array) - || (typeof Uint8ClampedArray === 'function' && value instanceof Uint8ClampedArray) - || (typeof Uint16Array === 'function' && value instanceof Uint16Array) - || (typeof Uint32Array === 'function' && value instanceof Uint32Array) - || (typeof Int8Array === 'function' && value instanceof Int8Array) - || (typeof Int16Array === 'function' && value instanceof Int16Array) - || (typeof Int32Array === 'function' && value instanceof Int32Array) - || (typeof Float32Array === 'function' && value instanceof Float32Array) - || (typeof Float64Array === 'function' && value instanceof Float64Array) - || (typeof BigUint64Array === 'function' && value instanceof BigUint64Array) - || (typeof BigInt64Array === 'function' && value instanceof BigInt64Array); - } - - function compareArrayLikeEquality(a, b, cache) { - if (a.length !== b.length) return NOT_EQUAL; - for (var i = 0; i < a.length; i++) { - if (compareEquality(a[i], b[i], cache) === NOT_EQUAL) { - return NOT_EQUAL; - } - } - return EQUAL; - } - - function isStructurallyEquatable(value) { - return !(typeof Promise === 'function' && value instanceof Promise // only comparable by reference - || typeof WeakMap === 'function' && value instanceof WeakMap // only comparable by reference - || typeof WeakSet === 'function' && value instanceof WeakSet // only comparable by reference - || typeof Map === 'function' && value instanceof Map // comparable via @@iterator - || typeof Set === 'function' && value instanceof Set); // comparable via @@iterator - } - - function compareStructuralEquality(a, b, cache) { - var aKeys = []; - for (var key in a) aKeys.push(key); - - var bKeys = []; - for (var key in b) bKeys.push(key); - - if (aKeys.length !== bKeys.length) { - return NOT_EQUAL; - } - - aKeys.sort(); - bKeys.sort(); - - for (var i = 0; i < aKeys.length; i++) { - var aKey = aKeys[i]; - var bKey = bKeys[i]; - if (compareEquality(aKey, bKey, cache) === NOT_EQUAL) { - return NOT_EQUAL; - } - if (compareEquality(a[aKey], b[bKey], cache) === NOT_EQUAL) { - return NOT_EQUAL; - } - } - - return compareIf(a, b, isIterableEquatable, compareIterableEquality, cache) - || EQUAL; - } - - function isIterableEquatable(value) { - return typeof Symbol === 'function' - && typeof value[Symbol.iterator] === 'function'; - } - - function compareIteratorEquality(a, b, cache) { - if (typeof Map === 'function' && a instanceof Map && b instanceof Map || - typeof Set === 'function' && a instanceof Set && b instanceof Set) { - if (a.size !== b.size) return NOT_EQUAL; // exit early if we detect a difference in size - } - - var ar, br; - while (true) { - ar = a.next(); - br = b.next(); - if (ar.done) { - if (br.done) return EQUAL; - if (b.return) b.return(); - return NOT_EQUAL; - } - if (br.done) { - if (a.return) a.return(); - return NOT_EQUAL; - } - if (compareEquality(ar.value, br.value, cache) === NOT_EQUAL) { - if (a.return) a.return(); - if (b.return) b.return(); - return NOT_EQUAL; - } - } - } - - function compareIterableEquality(a, b, cache) { - return compareIteratorEquality(a[Symbol.iterator](), b[Symbol.iterator](), cache); - } - - function cacheComparison(a, b, compare, cache) { - var result = compare(a, b, cache); - if (cache && (result === EQUAL || result === NOT_EQUAL)) { - setCache(cache, a, b, /** @type {EQUAL | NOT_EQUAL} */(result)); - } - return result; - } - - function fail() { - return NOT_EQUAL; - } - - function setCache(cache, left, right, result) { - var otherCache; - - otherCache = cache.get(left); - if (!otherCache) cache.set(left, otherCache = new Map()); - otherCache.set(right, result); - - otherCache = cache.get(right); - if (!otherCache) cache.set(right, otherCache = new Map()); - otherCache.set(left, result); - } - - function getCache(cache, left, right) { - var otherCache; - /** @type {EQUAL | NOT_EQUAL | UNKNOWN | undefined} */ - var result; - - otherCache = cache.get(left); - result = otherCache && otherCache.get(right); - if (result) return result; - - otherCache = cache.get(right); - result = otherCache && otherCache.get(left); - if (result) return result; - - return UNKNOWN; - } - - return deepEqual; -})(); - -assert.deepEqual = function (actual, expected, message) { - assert(deepEqual(actual, expected), - 'Expected ' + assert._formatValue(actual) + ' to be structurally equal to ' + assert._formatValue(expected) + '. ' + (message || '')); +assert.deepEqual = function(actual, expected, message) { + var format = assert.deepEqual.format; + assert( + assert.deepEqual._compare(actual, expected), + `Expected ${format(actual)} to be structurally equal to ${format(expected)}. ${(message || '')}` + ); }; + +assert.deepEqual.format = function(value, seen) { + switch (typeof value) { + case 'string': + return typeof JSON !== "undefined" ? JSON.stringify(value) : `"${value}"`; + case 'number': + case 'boolean': + case 'symbol': + case 'bigint': + return value.toString(); + case 'undefined': + return 'undefined'; + case 'function': + return `[Function${value.name ? `: ${value.name}` : ''}]`; + case 'object': + if (value === null) return 'null'; + if (value instanceof Date) return `Date "${value.toISOString()}"`; + if (value instanceof RegExp) return value.toString(); + if (!seen) { + seen = { + counter: 0, + map: new Map() + }; + } + + let usage = seen.map.get(value); + if (usage) { + usage.used = true; + return `[Ref: #${usage.id}]`; + } + + usage = { id: ++seen.counter, used: false }; + seen.map.set(value, usage); + + if (typeof Set !== "undefined" && value instanceof Set) { + return `Set {${Array.from(value).map(value => assert.deepEqual.format(value, seen)).join(', ')}}${usage.used ? ` as #${usage.id}` : ''}`; + } + if (typeof Map !== "undefined" && value instanceof Map) { + return `Map {${Array.from(value).map(pair => `${assert.deepEqual.format(pair[0], seen)} => ${assert.deepEqual.format(pair[1], seen)}}`).join(', ')}}${usage.used ? ` as #${usage.id}` : ''}`; + } + if (Array.isArray ? Array.isArray(value) : value instanceof Array) { + return `[${value.map(value => assert.deepEqual.format(value, seen)).join(', ')}]${usage.used ? ` as #${usage.id}` : ''}`; + } + let tag = Symbol.toStringTag in value ? value[Symbol.toStringTag] : 'Object'; + if (tag === 'Object' && Object.getPrototypeOf(value) === null) { + tag = '[Object: null prototype]'; + } + return `${tag ? `${tag} ` : ''}{ ${Object.keys(value).map(key => `${key.toString()}: ${assert.deepEqual.format(value[key], seen)}`).join(', ')} }${usage.used ? ` as #${usage.id}` : ''}`; + default: + return typeof value; + } +}; + +assert.deepEqual._compare = (function () { + var EQUAL = 1; + var NOT_EQUAL = -1; + var UNKNOWN = 0; + + function deepEqual(a, b) { + return compareEquality(a, b) === EQUAL; + } + + function compareEquality(a, b, cache) { + return compareIf(a, b, isOptional, compareOptionality) + || compareIf(a, b, isPrimitiveEquatable, comparePrimitiveEquality) + || compareIf(a, b, isObjectEquatable, compareObjectEquality, cache) + || NOT_EQUAL; + } + + function compareIf(a, b, test, compare, cache) { + return !test(a) + ? !test(b) ? UNKNOWN : NOT_EQUAL + : !test(b) ? NOT_EQUAL : cacheComparison(a, b, compare, cache); + } + + function tryCompareStrictEquality(a, b) { + return a === b ? EQUAL : UNKNOWN; + } + + function tryCompareTypeOfEquality(a, b) { + return typeof a !== typeof b ? NOT_EQUAL : UNKNOWN; + } + + function tryCompareToStringTagEquality(a, b) { + var aTag = Symbol.toStringTag in a ? a[Symbol.toStringTag] : undefined; + var bTag = Symbol.toStringTag in b ? b[Symbol.toStringTag] : undefined; + return aTag !== bTag ? NOT_EQUAL : UNKNOWN; + } + + function isOptional(value) { + return value === undefined + || value === null; + } + + function compareOptionality(a, b) { + return tryCompareStrictEquality(a, b) + || NOT_EQUAL; + } + + function isPrimitiveEquatable(value) { + switch (typeof value) { + case 'string': + case 'number': + case 'bigint': + case 'boolean': + case 'symbol': + return true; + default: + return isBoxed(value); + } + } + + function comparePrimitiveEquality(a, b) { + if (isBoxed(a)) a = a.valueOf(); + if (isBoxed(b)) b = b.valueOf(); + return tryCompareStrictEquality(a, b) + || tryCompareTypeOfEquality(a, b) + || compareIf(a, b, isNaNEquatable, compareNaNEquality) + || NOT_EQUAL; + } + + function isNaNEquatable(value) { + return typeof value === 'number'; + } + + function compareNaNEquality(a, b) { + return isNaN(a) && isNaN(b) ? EQUAL : NOT_EQUAL; + } + + function isObjectEquatable(value) { + return typeof value === 'object'; + } + + function compareObjectEquality(a, b, cache) { + if (!cache) cache = new Map(); + return getCache(cache, a, b) + || setCache(cache, a, b, EQUAL) // consider equal for now + || cacheComparison(a, b, tryCompareStrictEquality, cache) + || cacheComparison(a, b, tryCompareToStringTagEquality, cache) + || compareIf(a, b, isValueOfEquatable, compareValueOfEquality) + || compareIf(a, b, isToStringEquatable, compareToStringEquality) + || compareIf(a, b, isArrayLikeEquatable, compareArrayLikeEquality, cache) + || compareIf(a, b, isStructurallyEquatable, compareStructuralEquality, cache) + || compareIf(a, b, isIterableEquatable, compareIterableEquality, cache) + || cacheComparison(a, b, fail, cache); + } + + function isBoxed(value) { + return value instanceof String + || value instanceof Number + || value instanceof Boolean + || typeof Symbol === 'function' && value instanceof Symbol + || typeof BigInt === 'function' && value instanceof BigInt; + } + + function isValueOfEquatable(value) { + return value instanceof Date; + } + + function compareValueOfEquality(a, b) { + return compareIf(a.valueOf(), b.valueOf(), isPrimitiveEquatable, comparePrimitiveEquality) + || NOT_EQUAL; + } + + function isToStringEquatable(value) { + return value instanceof RegExp; + } + + function compareToStringEquality(a, b) { + return compareIf(a.toString(), b.toString(), isPrimitiveEquatable, comparePrimitiveEquality) + || NOT_EQUAL; + } + + function isArrayLikeEquatable(value) { + return (Array.isArray ? Array.isArray(value) : value instanceof Array) + || (typeof Uint8Array === 'function' && value instanceof Uint8Array) + || (typeof Uint8ClampedArray === 'function' && value instanceof Uint8ClampedArray) + || (typeof Uint16Array === 'function' && value instanceof Uint16Array) + || (typeof Uint32Array === 'function' && value instanceof Uint32Array) + || (typeof Int8Array === 'function' && value instanceof Int8Array) + || (typeof Int16Array === 'function' && value instanceof Int16Array) + || (typeof Int32Array === 'function' && value instanceof Int32Array) + || (typeof Float32Array === 'function' && value instanceof Float32Array) + || (typeof Float64Array === 'function' && value instanceof Float64Array) + || (typeof BigUint64Array === 'function' && value instanceof BigUint64Array) + || (typeof BigInt64Array === 'function' && value instanceof BigInt64Array); + } + + function compareArrayLikeEquality(a, b, cache) { + if (a.length !== b.length) return NOT_EQUAL; + for (var i = 0; i < a.length; i++) { + if (compareEquality(a[i], b[i], cache) === NOT_EQUAL) { + return NOT_EQUAL; + } + } + return EQUAL; + } + + function isStructurallyEquatable(value) { + return !(typeof Promise === 'function' && value instanceof Promise // only comparable by reference + || typeof WeakMap === 'function' && value instanceof WeakMap // only comparable by reference + || typeof WeakSet === 'function' && value instanceof WeakSet // only comparable by reference + || typeof Map === 'function' && value instanceof Map // comparable via @@iterator + || typeof Set === 'function' && value instanceof Set); // comparable via @@iterator + } + + function compareStructuralEquality(a, b, cache) { + var aKeys = []; + for (var key in a) aKeys.push(key); + + var bKeys = []; + for (var key in b) bKeys.push(key); + + if (aKeys.length !== bKeys.length) { + return NOT_EQUAL; + } + + aKeys.sort(); + bKeys.sort(); + + for (var i = 0; i < aKeys.length; i++) { + var aKey = aKeys[i]; + var bKey = bKeys[i]; + if (compareEquality(aKey, bKey, cache) === NOT_EQUAL) { + return NOT_EQUAL; + } + if (compareEquality(a[aKey], b[bKey], cache) === NOT_EQUAL) { + return NOT_EQUAL; + } + } + + return compareIf(a, b, isIterableEquatable, compareIterableEquality, cache) + || EQUAL; + } + + function isIterableEquatable(value) { + return typeof Symbol === 'function' + && typeof value[Symbol.iterator] === 'function'; + } + + function compareIteratorEquality(a, b, cache) { + if (typeof Map === 'function' && a instanceof Map && b instanceof Map || + typeof Set === 'function' && a instanceof Set && b instanceof Set) { + if (a.size !== b.size) return NOT_EQUAL; // exit early if we detect a difference in size + } + + var ar, br; + while (true) { + ar = a.next(); + br = b.next(); + if (ar.done) { + if (br.done) return EQUAL; + if (b.return) b.return(); + return NOT_EQUAL; + } + if (br.done) { + if (a.return) a.return(); + return NOT_EQUAL; + } + if (compareEquality(ar.value, br.value, cache) === NOT_EQUAL) { + if (a.return) a.return(); + if (b.return) b.return(); + return NOT_EQUAL; + } + } + } + + function compareIterableEquality(a, b, cache) { + return compareIteratorEquality(a[Symbol.iterator](), b[Symbol.iterator](), cache); + } + + function cacheComparison(a, b, compare, cache) { + var result = compare(a, b, cache); + if (cache && (result === EQUAL || result === NOT_EQUAL)) { + setCache(cache, a, b, /** @type {EQUAL | NOT_EQUAL} */(result)); + } + return result; + } + + function fail() { + return NOT_EQUAL; + } + + function setCache(cache, left, right, result) { + var otherCache; + + otherCache = cache.get(left); + if (!otherCache) cache.set(left, otherCache = new Map()); + otherCache.set(right, result); + + otherCache = cache.get(right); + if (!otherCache) cache.set(right, otherCache = new Map()); + otherCache.set(left, result); + } + + function getCache(cache, left, right) { + var otherCache; + var result; + + otherCache = cache.get(left); + result = otherCache && otherCache.get(right); + if (result) return result; + + otherCache = cache.get(right); + result = otherCache && otherCache.get(left); + if (result) return result; + + return UNKNOWN; + } + + return deepEqual; +})();