// Copyright 2019 Ron Buckton. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- description: > Compare two values structurally defines: [assert.deepEqual] ---*/ 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 || '')}` ); }; let getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; let join = arr => arr.join(', '); function stringFromTemplate(strings, ...subs) { let parts = strings.map((str, i) => `${i === 0 ? '' : subs[i - 1]}${str}`); return parts.join(''); } function escapeKey(key) { if (typeof key === 'symbol') return `[${String(key)}]`; if (/^[a-zA-Z0-9_$]+$/.test(key)) return key; return assert._formatIdentityFreeValue(key); } assert.deepEqual.format = function(value, seen) { let basic = assert._formatIdentityFreeValue(value); if (basic) return basic; switch (value === null ? 'null' : typeof value) { case 'string': case 'bigint': case 'number': case 'boolean': case 'undefined': case 'null': assert(false, 'values without identity should use basic formatting'); break; case 'symbol': case 'function': case 'object': break; default: return typeof value; } 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); // Properly communicating multiple references requires deferred rendering of // all identity-bearing values until the outermost format call finishes, // because the current value can also in appear in a not-yet-visited part of // the object graph (which, when visited, will update the usage object). // // To preserve readability of the desired output formatting, we accomplish // this deferral using tagged template literals. // Evaluation closes over the usage object and returns a function that accepts // "mapper" arguments for rendering the corresponding substitution values and // returns an object with only a toString method which will itself be invoked // when trying to use the result as a string in assert.deepEqual. // // For convenience, any absent mapper is presumed to be `String`, and the // function itself has a toString method that self-invokes with no mappers // (allowing returning the function directly when every mapper is `String`). function lazyResult(strings, ...subs) { function acceptMappers(...mappers) { function toString() { let renderings = subs.map((sub, i) => (mappers[i] || String)(sub)); let rendered = stringFromTemplate(strings, ...renderings); if (usage.used) rendered += ` as #${usage.id}`; return rendered; } return { toString }; } acceptMappers.toString = () => String(acceptMappers()); return acceptMappers; } let format = assert.deepEqual.format; function lazyString(strings, ...subs) { return { toString: () => stringFromTemplate(strings, ...subs) }; } if (typeof value === 'function') { return lazyResult`function${value.name ? ` ${String(value.name)}` : ''}`; } if (typeof value !== 'object') { // probably a symbol return lazyResult`${value}`; } if (Array.isArray ? Array.isArray(value) : value instanceof Array) { return lazyResult`[${value.map(value => format(value, seen))}]`(join); } if (value instanceof Date) { return lazyResult`Date(${format(value.toISOString(), seen)})`; } if (value instanceof Error) { return lazyResult`error ${value.name || 'Error'}(${format(value.message, seen)})`; } if (value instanceof RegExp) { return lazyResult`${value}`; } if (typeof Map !== "undefined" && value instanceof Map) { let contents = Array.from(value).map(pair => lazyString`${format(pair[0], seen)} => ${format(pair[1], seen)}`); return lazyResult`Map {${contents}}`(join); } if (typeof Set !== "undefined" && value instanceof Set) { let contents = Array.from(value).map(value => format(value, seen)); return lazyResult`Set {${contents}}`(join); } let tag = Symbol.toStringTag && Symbol.toStringTag in value ? value[Symbol.toStringTag] : Object.getPrototypeOf(value) === null ? '[Object: null prototype]' : 'Object'; let keys = Reflect.ownKeys(value).filter(key => getOwnPropertyDescriptor(value, key).enumerable); let contents = keys.map(key => lazyString`${escapeKey(key)}: ${format(value[key], seen)}`); return lazyResult`${tag ? `${tag} ` : ''}{${contents}}`(String, join); }; 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; })();