| // 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; |
| var mustBeTrue = assert.deepEqual._compare(actual, expected); |
| |
| // format can be slow when `actual` or `expected` are large objects, like for |
| // example the global object, so only call it when the assertion will fail. |
| if (mustBeTrue !== true) { |
| message = `Expected ${format(actual)} to be structurally equal to ${format(expected)}. ${(message || '')}`; |
| } |
| |
| assert(mustBeTrue, message); |
| }; |
| |
| (function() { |
| 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' || typeof value === 'function'; |
| } |
| |
| 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; |
| })(); |