diff --git a/README.md b/README.md index b7fdaeb..5230831 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,14 @@ Throws an `AssertionError` unless `actual` and `expected` are the same value, as Throws an `AssertionError` unless `actual` and `expected` are not the same value, as determined by `Object.is()`. +#### `assert.deepStrictEqual(actual, expected[, message])` + +Throws an `AssertionError` unless `actual` and `expected` are the same value, recursively. + +#### `assert.notDeepStrictEqual(actual, expected[, message])` + +Throws an `AssertionError` unless `actual` and `expected` are not the same value, recursively. + #### `assert.match(actual, regexp[, message])` Throws an `AssertionError` unless `actual` is a string that matches `regexp`. diff --git a/index.d.ts b/index.d.ts index aa4349c..e677b88 100644 --- a/index.d.ts +++ b/index.d.ts @@ -19,6 +19,10 @@ declare namespace assert { export function notStrictEqual(actual: any, expected: any, message?: string | Error): void + export function deepStrictEqual(actual: any, expected: any, message?: string | Error): void + + export function notDeepStrictEqual(actual: any, expected: any, message?: string | Error): void + export function match(actual: string, expected: RegExp, message?: string | Error): void export function doesNotMatch(actual: string, expected: RegExp, message?: string | Error): void diff --git a/index.js b/index.js index fe83c74..36d117e 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,6 @@ const inspect = require('bare-inspect') +const getType = require('bare-type') +const Memoization = require('./lib/memoization') class AssertionError extends Error { constructor(opts = {}) { @@ -107,3 +109,210 @@ exports.ifError = function ifError(actual) { assertFail({ message, actual, operator: 'ifError' }, ifError) } + +exports.deepStrictEqual = function deepStrictEqual(actual, expected, message) { + if (deepStrictEqualValue(actual, expected)) return + + assertFail({ message, actual, expected, operator: 'deepStrictEqual' }, deepStrictEqual) +} + +exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, message) { + if (!deepStrictEqualValue(actual, expected)) return + + assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) +} + +function deepStrictEqualValue(a, b, memo = new Memoization()) { + const type = getType(a) + + if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b) + + const prototype = Object.getPrototypeOf(a) + + if (prototype !== Object.getPrototypeOf(b)) return false + + if (type.isWeakMap() || type.isWeakSet() || type.isPromise()) return a === b + + if (Buffer.isBuffer(a)) return deepStrictEqualBuffer(a, b) + if (type.isArrayBuffer()) return deepStrictEqualBuffer(new Uint8Array(a), new Uint8Array(b)) + if (type.isDataView()) { + return deepStrictEqualBuffer( + new Uint8Array(a.buffer, a.byteOffset, a.byteLength), + new Uint8Array(b.buffer, b.byteOffset, b.byteLength) + ) + } + + // Anything that can be settled without descending into the values is settled + // first. A pair that already differs in its own right is unequal whatever the + // surrounding structures do. + if (!deepStrictEqualShallow(a, b, type, prototype)) return false + + if (memo.has(a, b)) { + return memo.compare(a, b) + } else { + memo.add(a, b) + } + + let result + + if (type.isError()) result = deepStrictEqualError(a, b, memo) + else if (type.isMap()) result = deepStrictEqualMap(a, b, memo) + else if (type.isSet()) result = deepStrictEqualSet(a, b, memo) + else result = deepStrictEqualObject(a, b, memo) + + memo.remove(a, b) + + return result +} + +// Compares everything about a pair that can be decided on the spot, leaving +// only the values reachable from it for the caller to walk. +function deepStrictEqualShallow(a, b, type, prototype) { + if ( + prototype === BigInt.prototype || + prototype === Boolean.prototype || + prototype === Number.prototype || + prototype === String.prototype || + prototype === Symbol.prototype + ) { + if (!Object.is(a.valueOf(), b.valueOf())) return false + } else if (type.isRegExp()) { + if (a.lastIndex !== b.lastIndex || a.flags !== b.flags || a.source !== b.source) return false + } else if (type.isTypedArray()) { + if (!deepStrictEqualBuffer(a, b)) return false + } else if (type.isDate()) { + if (!Object.is(a.getTime(), b.getTime())) return false + } else if (type.isArguments() || type.isArray()) { + if (a.length !== b.length) return false + } else if (type.isMap() || type.isSet()) { + if (a.size !== b.size) return false + } + + return getEnumerableKeys(a).length === getEnumerableKeys(b).length +} + +function deepStrictEqualBuffer(a, b) { + return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0 +} + +function deepStrictEqualError(a, b, memo) { + return ( + deepStrictEqualValue(a.name, b.name, memo) && + deepStrictEqualValue(a.message, b.message, memo) && + deepStrictEqualObjectKeys(a, b, ['cause', 'errors'], memo) && + deepStrictEqualObject(a, b, memo) + ) +} + +function deepStrictEqualArrayUnordered(a, b, memo) { + if (a.length !== b.length) return false + + for (let i = 0; i < a.length; i++) { + let found = false + const itemA = a[i] + + for (let j = 0; j < b.length; j++) { + const itemB = b[j] + + if (deepStrictEqualValue(itemA, itemB, memo)) { + found = true + + b.splice(j, 1) + + break + } + } + + if (found === false) return false + } + + return true +} + +// A key can be matched through a native `Map`/`Set` lookup only when it is a +// primitive whose deep equality collapses to the `SameValueZero` relation those +// lookups use. Objects and functions must be matched by deep comparison, and so +// must `+0` and `-0`: `SameValueZero` treats them as equal, but +// `deepStrictEqual` keeps their signs distinct. +function requiresDeepKeyMatch(key) { + if (key === 0) return true // Covers both `+0` and `-0`. + + const type = typeof key + + return (type === 'object' && key !== null) || type === 'function' +} + +function deepStrictEqualMap(a, b, memo) { + if (!deepStrictEqualObject(a, b, memo)) return false + + // Match entries with primitive keys directly through `b` in linear time and + // leave only the object-keyed entries for the quadratic fallback. + const restA = [] + const restB = [] + + for (const [key, value] of a) { + if (requiresDeepKeyMatch(key)) { + restA.push([key, value]) + } else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), memo)) { + return false + } + } + + for (const entry of b) { + if (requiresDeepKeyMatch(entry[0])) restB.push(entry) + } + + return deepStrictEqualArrayUnordered(restA, restB, memo) +} + +function deepStrictEqualSet(a, b, memo) { + if (!deepStrictEqualObject(a, b, memo)) return false + + // Match primitive members directly through `b` in linear time and leave only + // the object members for the quadratic fallback. + const restA = [] + const restB = [] + + for (const value of a) { + if (requiresDeepKeyMatch(value)) restA.push(value) + else if (!b.has(value)) return false + } + + for (const value of b) { + if (requiresDeepKeyMatch(value)) restB.push(value) + } + + return deepStrictEqualArrayUnordered(restA, restB, memo) +} + +function deepStrictEqualObjectKeys(a, b, keys, memo) { + for (const key of keys) { + const hasA = key in a + const hasB = key in b + + if ((hasA ^ hasB) === 1) return false + if (hasA && hasB && !deepStrictEqualValue(a[key], b[key], memo)) return false + } + + return true +} + +// The key counts have already been compared, so only the values are left. +function deepStrictEqualObject(a, b, memo) { + for (const key of getEnumerableKeys(a)) { + if (!(key in b) || !deepStrictEqualValue(a[key], b[key], memo)) return false + } + + return true +} + +function getEnumerableKeys(obj) { + const keys = Object.keys(obj) + + for (const symbolKey of Object.getOwnPropertySymbols(obj)) { + const { enumerable } = Object.getOwnPropertyDescriptor(obj, symbolKey) + if (enumerable) keys.push(symbolKey) + } + + return keys +} diff --git a/lib/memoization.js b/lib/memoization.js new file mode 100644 index 0000000..1ade1a7 --- /dev/null +++ b/lib/memoization.js @@ -0,0 +1,29 @@ +// Tracks the values currently being compared so that a comparison which +// revisits a pair can be settled without recursing forever. +// +// Reaching a pair where both values are already being compared means the two +// structures have looped back in step, so they agree. Reaching a pair where +// only one of them has looped means one structure repeated where the other did +// not, so they differ. +module.exports = class Memoization { + constructor() { + this._nodes = new Set() + } + + has(a, b) { + return this._nodes.has(a) || this._nodes.has(b) + } + + compare(a, b) { + return this._nodes.has(a) && this._nodes.has(b) + } + + add(a, b) { + this._nodes.add(a).add(b) + } + + remove(a, b) { + this._nodes.delete(a) + this._nodes.delete(b) + } +} diff --git a/package.json b/package.json index e7d451a..0c77306 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "files": [ "index.js", - "index.d.ts" + "index.d.ts", + "lib" ], "scripts": { "format": "prettier --write . && lunte --fix", @@ -29,7 +30,8 @@ }, "homepage": "https://github.com/holepunchto/bare-assert#readme", "dependencies": { - "bare-inspect": "^3.1.2" + "bare-inspect": "^3.1.5", + "bare-type": "^1.1.0" }, "devDependencies": { "brittle": "^4.1.0", diff --git a/test.js b/test.js index cd5705f..6b58d17 100644 --- a/test.js +++ b/test.js @@ -59,3 +59,1137 @@ test('ifError', (t) => { t.execution(() => assert.ifError(undefined)) t.exception(() => assert.ifError('error')) }) + +test('deepStrictEqual, basic', (t) => { + t.execution(() => assert.deepStrictEqual(NaN, NaN)) + t.execution(() => assert.deepStrictEqual(1, 1)) + t.execution(() => assert.deepStrictEqual('foo', 'foo')) + t.exception(() => assert.deepStrictEqual(1, new Date(), 'should fail'), /should fail/) +}) + +test('deepStrictEqual, negative zero', (t) => { + t.exception(() => assert.deepStrictEqual(-0, 0, 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual([-0], [0], 'should fail'), /should fail/) + + t.execution(() => assert.deepStrictEqual(new Set([-0]), new Set([0]))) + t.execution(() => assert.deepStrictEqual(new Map([[-0, 1]]), new Map([[0, 1]]))) +}) + +test('deepStrictEqual, array', (t) => { + t.execution(() => assert.deepStrictEqual([1, 'foo'], [1, 'foo'])) + t.execution(() => assert.deepStrictEqual([1, , , 3], [1, , , 3])) + t.exception(() => assert.deepStrictEqual([1, 'foo'], [1], 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual([1, 'foo'], [1, 'bar'], 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual([1, , , 3], [1, , , 3, ,], 'should fail'), /should fail/) + t.exception( + () => assert.deepStrictEqual([1, , 3], [1, undefined, 3], 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual([1, , undefined, 3], [1, undefined, , 3], 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, array, additional property', (t) => { + const a = [1, 2] + const b = [1, 2] + + a.foo = 'x' + b.foo = 'y' + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + + const c = [1, 2] + c.foo = 'x' + + t.exception(() => assert.deepStrictEqual(c, [1, 2], 'should fail'), /should fail/) +}) + +test('deepStrictEqual, array, symbol property', (t) => { + const symbol = Symbol.for('symbol') + + const a = [1] + const b = [1] + + a[symbol] = 1 + b[symbol] = 2 + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual(a, b), assert.AssertionError) +}) + +test('deepStrictEqual, arguments vs array', (t) => { + function make() { + return arguments + } + + t.exception(() => assert.deepStrictEqual(make(1, 2, 3), [1, 2, 3], 'should fail'), /should fail/) +}) + +test('deepStrictEqual, object', (t) => { + t.execution(() => assert.deepStrictEqual({}, {})) + t.execution(() => assert.deepStrictEqual({ a: { b: 1 } }, { a: { b: 1 } })) + t.execution(() => assert.deepStrictEqual({ a: [1, 2] }, { a: [1, 2] })) + t.exception( + () => assert.deepStrictEqual({ a: { b: 1 } }, { a: { b: '1' } }, 'should fail'), + /should fail/ + ) + t.exception(() => assert.deepStrictEqual({ a: [1, 2] }, { a: [1] }, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, object, key order', (t) => { + t.execution(() => assert.deepStrictEqual({ a: 1, b: 2 }, { b: 2, a: 1 })) +}) + +test('deepStrictEqual, object, non-enumerable property', (t) => { + const a = {} + const b = {} + + Object.defineProperty(a, 'x', { value: 1, enumerable: false }) + Object.defineProperty(b, 'x', { value: 2, enumerable: false }) + + t.execution(() => assert.deepStrictEqual(a, b)) +}) + +test('deepStrictEqual, object, non-enumerable symbol', (t) => { + const symbol = Symbol('symbol') + + const a = {} + const b = {} + + Object.defineProperty(a, symbol, { value: 1, enumerable: false }) + Object.defineProperty(b, symbol, { value: 2, enumerable: false }) + + t.execution(() => assert.deepStrictEqual(a, b)) +}) + +test('deepStrictEqual, class', (t) => { + class MyClass { + constructor(value) { + this.value = value + } + } + + t.execution(() => assert.deepStrictEqual(new MyClass('foo'), new MyClass('foo'))) + t.exception( + () => assert.deepStrictEqual(new MyClass('foo'), new MyClass('bar'), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, object, getter', (t) => { + const obj = { + get foo() { + return 'bar' + } + } + + t.execution(() => assert.deepStrictEqual(obj, { foo: 'bar' })) + t.exception(() => assert.deepStrictEqual(obj, { foo: 'baz' }, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, object, prototype', (t) => { + const prototype = { __proto__: null } + const a = { constructor: 42, foo: 'bar' } + const b = { constructor: 42, foo: 'bar' } + + Object.setPrototypeOf(a, prototype) + Object.setPrototypeOf(b, prototype) + + t.execution(() => assert.deepStrictEqual(a, b)) + + Object.setPrototypeOf(b, { __proto__: null }) + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, null prototype', (t) => { + t.exception(() => assert.deepStrictEqual({}, Object.create(null), 'should fail'), /should fail/) +}) + +test('deepStrictEqual, regexp', (t) => { + t.execution(() => assert.deepStrictEqual(/abc/, /abc/)) + t.exception(() => assert.deepStrictEqual(/abc/, /abc/g, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, regexp, additional property', (t) => { + const a = /x/ + const b = /x/ + + a.foo = 1 + b.foo = 2 + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, map', (t) => { + t.execution(() => + assert.deepStrictEqual( + new Map([ + [{}, null], + [true, 2], + [undefined, {}] + ]), + new Map([ + [undefined, {}], + [true, 2], + [{}, null] + ]) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new Map([ + [{}, null], + [true, 2], + [undefined, {}] + ]), + new Map([ + [{}, null], + [true, 2], + [null, {}] // different key + ]), + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, map, additional property', (t) => { + const map1 = new Map([['a', 1]]) + const map2 = new Map([['a', 1]]) + + map1.foo = true + map2.foo = true + + t.execution(() => assert.deepStrictEqual(map1, map2)) + + map2.foo = false + + t.exception(() => assert.deepStrictEqual(map1, map2, 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual(map1, new Map([['a', 1]]), 'should fail'), /should fail/) +}) + +test('deepStrictEqual, map, object keys', (t) => { + t.execution(() => + assert.deepStrictEqual( + new Map([ + [{ x: 1 }, 'a'], + [{ x: 2 }, 'b'] + ]), + new Map([ + [{ x: 2 }, 'b'], + [{ x: 1 }, 'a'] + ]) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new Map([ + [{ x: 1 }, 'a'], + [{ x: 9 }, 'zzz'] + ]), + new Map([ + [{ x: 2 }, 'b'], + [{ x: 3 }, 'c'] + ]), + 'should fail' + ), + /should fail/ + ) + t.exception( + () => + assert.deepStrictEqual( + new Map([ + [{ x: 1 }, 'a'], + [{ x: 2 }, 'b'] + ]), + new Map([ + [{ x: 1 }, 'a'], + [{ x: 2 }, 'c'] + ]), + 'should fail' + ), + /should fail/ + ) + t.exception( + () => + assert.deepStrictEqual( + { + map: new Map([ + [{ k: 1 }, [1, 2]], + [{ k: 2 }, [3, 4]] + ]) + }, + { + map: new Map([ + [{ k: 1 }, [9, 9]], + [{ k: 2 }, [8, 8]] + ]) + }, + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, set', (t) => { + t.execution(() => assert.deepStrictEqual(new Set(['a', 1, 'b', 2]), new Set(['b', 2, 'a', 1]))) + t.execution(() => + assert.deepStrictEqual(new Set([{ a: 1 }, 1, {}, 2]), new Set([{}, 2, 1, { a: 1 }])) + ) + t.exception( + () => + assert.deepStrictEqual(new Set(['a', 1, 'b', 2]), new Set(['b', 2, 'a', 42]), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, set, additional property', (t) => { + const set1 = new Set([1]) + const set2 = new Set([1]) + + set1.foo = true + set2.foo = true + + t.execution(() => assert.deepStrictEqual(set1, set2)) + + set2.foo = false + + t.exception(() => assert.deepStrictEqual(set1, set2, 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual(set1, new Set([1]), 'should fail'), /should fail/) +}) + +test('deepStrictEqual, set, object members', (t) => { + t.execution(() => + assert.deepStrictEqual(new Set([{ x: 1 }, { x: 2 }]), new Set([{ x: 2 }, { x: 1 }])) + ) + t.exception( + () => + assert.deepStrictEqual( + new Set([{ x: 1 }, { x: 9 }]), + new Set([{ x: 2 }, { x: 3 }]), + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, weak map', (t) => { + const map1 = new WeakMap([[Object, true]]) + const map2 = new WeakMap([[Object, true]]) + + t.execution(() => assert.deepStrictEqual(map1, map1)) + t.exception(() => assert.deepStrictEqual(map1, map2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, weak set', (t) => { + const obj = {} + + const set1 = new WeakSet([obj]) + const set2 = new WeakSet([obj]) + + t.execution(() => assert.deepStrictEqual(set1, set1)) + t.exception(() => assert.deepStrictEqual(set1, set2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, symbol', (t) => { + t.execution(() => assert.deepStrictEqual(Symbol.for('foo'), Symbol.for('foo'))) + t.exception( + () => assert.deepStrictEqual(Symbol.for('foo'), Symbol.for('bar'), 'should fail'), + /should fail/ + ) + + const sym1 = Symbol() + const sym2 = Symbol() + + t.execution(() => assert.deepStrictEqual({ [sym1]: 1 }, { [sym1]: 1 })) + t.exception( + () => assert.deepStrictEqual({ [sym1]: 1 }, { [sym2]: 1 }, 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, boxed value', (t) => { + const boxedSymbol = Object(Symbol()) + + t.execution(() => assert.deepStrictEqual(new Number(1), new Number(1))) + t.exception( + () => assert.deepStrictEqual(new Number(1), new Number(2), 'should fail'), + /should fail/ + ) + + t.execution(() => assert.deepStrictEqual(new String('foo'), Object('foo'))) + t.exception( + () => assert.deepStrictEqual(new Boolean(true), Object(false), 'should fail'), + /should fail/ + ) + + t.execution(() => assert.deepStrictEqual(Object(1n), Object(1n))) + t.exception(() => assert.deepStrictEqual(Object(1n), Object(2n), 'should fail'), /should fail/) + + t.execution(() => assert.deepStrictEqual(boxedSymbol, boxedSymbol)) + t.exception( + () => assert.deepStrictEqual(boxedSymbol, Object(Symbol()), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, boxed value, additional property', (t) => { + const a = new Number(1) + const b = new Number(1) + + a.foo = 'x' + b.foo = 'y' + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, boxed value vs primitive', (t) => { + t.exception(() => assert.deepStrictEqual(new Number(1), 1, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, date', (t) => { + t.execution(() => assert.deepStrictEqual(new Date(2000, 3, 14), new Date(2000, 3, 14))) + t.exception( + () => assert.deepStrictEqual(new Date(), new Date(2000, 3, 14), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, date, additional property', (t) => { + const date1 = new Date('foo') + const date2 = new Date('bar') + + date1.foo = true + date2.foo = true + + t.execution(() => assert.deepStrictEqual(date1, date2)) + + date2.foo = false + + t.exception(() => assert.deepStrictEqual(date1, date2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, date, invalid', (t) => { + t.execution(() => assert.deepStrictEqual(new Date(NaN), new Date(NaN))) +}) + +test('deepStrictEqual, error', (t) => { + t.execution(() => assert.deepStrictEqual(new Error('foo'), new Error('foo'))) + t.exception( + () => assert.deepStrictEqual(new Error('foo'), new Error('bar'), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(new Error('foo'), new TypeError('foo'), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, error, cause property', (t) => { + t.execution(() => + assert.deepStrictEqual( + new Error('err', { cause: new Error('foo') }), + new Error('err', { cause: new Error('foo') }) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new Error('err', { cause: new Error('foo') }), + new Error('err', { cause: new Error('bar') }), + 'should fail' + ), + /should fail/ + ) + t.exception( + () => + assert.deepStrictEqual( + new Error('err', { cause: new Error('foo') }), + new Error('err'), + 'should fail' + ), + /should fail/ + ) + t.execution(() => + assert.deepStrictEqual( + new Error('err', { cause: undefined }), + new Error('err', { cause: undefined }) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new Error('err'), + new Error('err', { cause: undefined }), + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, error, aggregate error', (t) => { + t.execution(() => + assert.deepStrictEqual( + new AggregateError([new Error('foo'), new Error('bar')]), + new AggregateError([new Error('foo'), new Error('bar')]) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new AggregateError([new Error('foo'), new Error('bar')]), + new AggregateError([new Error('foo'), new Error('baz')]), + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, error, additional property', (t) => { + const error1 = new Error('foo') + const error2 = new Error('foo') + + error1.foo = true + error2.foo = true + + t.execution(() => assert.deepStrictEqual(error1, error2)) + + error2.foo = false + + t.exception(() => assert.deepStrictEqual(error1, error2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, error, custom toStringTag', (t) => { + const error = new Error('foo') + + error[Symbol.toStringTag] = 'CustomTag' + + t.exception(() => assert.deepStrictEqual(error, new Error('foo'), 'should fail'), /should fail/) +}) + +test('deepStrictEqual, buffer', (t) => { + t.execution(() => assert.deepStrictEqual(Buffer.from('foo'), Buffer.from('foo'))) + t.exception( + () => assert.deepStrictEqual(Buffer.from('foo'), Buffer.from('bar'), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, arraybuffer', (t) => { + t.execution(() => assert.deepStrictEqual(new ArrayBuffer(8), new ArrayBuffer(8))) + t.exception( + () => assert.deepStrictEqual(new ArrayBuffer(10), new ArrayBuffer(12), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, typed array', (t) => { + t.execution(() => assert.deepStrictEqual(new Uint16Array([21, 31]), new Uint16Array([21, 31]))) + t.exception( + () => + assert.deepStrictEqual(new Uint16Array([21, 31]), new Uint16Array([31, 21]), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, typed array, additional property', (t) => { + const a = new Uint8Array([1]) + const b = new Uint8Array([1]) + + a.foo = 1 + b.foo = 2 + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, typed array, differing type', (t) => { + t.exception( + () => assert.deepStrictEqual(new Uint8Array([1, 2]), new Int8Array([1, 2]), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(new Uint8Array([1, 2, 3]), Buffer.from([1, 2, 3]), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, dataview', (t) => { + t.execution(() => + assert.deepStrictEqual(new DataView(new ArrayBuffer(10)), new DataView(new ArrayBuffer(10))) + ) + t.exception( + () => + assert.deepStrictEqual( + new DataView(new ArrayBuffer(10)), + new DataView(new ArrayBuffer(12)), + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, promise', (t) => { + const promise1 = Promise.resolve(1) + const promise2 = Promise.resolve(1) + + t.execution(() => assert.deepStrictEqual(promise1, promise1)) + t.exception(() => assert.deepStrictEqual(promise1, promise2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, function', (t) => { + const fn = () => {} + + t.execution(() => assert.deepStrictEqual(fn, fn)) + t.exception( + () => + assert.deepStrictEqual( + () => {}, + () => {}, + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, proxy', (t) => { + const proxy = new Proxy([1, 2], {}) + + t.execution(() => assert.deepStrictEqual(proxy, [1, 2])) + t.exception(() => assert.deepStrictEqual(proxy, [1, 1], 'should fail'), /should fail/) +}) + +test('deepStrictEqual, url', (t) => { + t.execution(() => assert.deepStrictEqual(new URL('http://foo'), new URL('http://foo'))) + t.exception( + () => assert.deepStrictEqual(new URL('http://foo'), new URL('http://bar'), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, url, additional property', (t) => { + const url1 = new URL('http://foo') + const url2 = new URL('http://foo') + + url1.foo = true + url2.foo = true + + t.execution(() => assert.deepStrictEqual(url1, url2)) + + url2.foo = false + + t.exception(() => assert.deepStrictEqual(url1, url2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, recursive object', (t) => { + { + const a = {} + a.prop = a + + const b = {} + b.prop = b + + t.execution(() => assert.deepStrictEqual(a, b)) + } + + { + const a = { prop: null } + const b = { prop: a } + a.prop = b + + t.execution(() => assert.deepStrictEqual(a, b)) + } + + { + const a = {} + a.prop = {} + a.prop.prop = a.prop + + const b = {} + b.prop = {} + b.prop.prop = a.prop + + t.execution(() => assert.deepStrictEqual(a, b)) + } + + { + const a = {} + a.prop = 'foo' + + const b = {} + b.prop = b + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + } +}) + +test('deepStrictEqual, recursive object, cycle shape', (t) => { + // Builds a chain of `tail` objects leading into a cycle of `cycle` objects, + // every node linked to the next through a single `prop` property. Every such + // graph unfolds to the same infinite chain, so the shape of the cycle alone + // does not decide equality; what matters is how many objects are reachable + // from the root before one repeats. + function cyclic(tail, cycle) { + const nodes = [] + + for (let i = 0; i < tail + cycle; i++) nodes.push({}) + for (let i = 0; i < nodes.length - 1; i++) nodes[i].prop = nodes[i + 1] + + nodes[nodes.length - 1].prop = nodes[tail] + + return nodes[0] + } + + t.execution(() => assert.deepStrictEqual(cyclic(0, 1), cyclic(0, 1))) + t.execution(() => assert.deepStrictEqual(cyclic(2, 2), cyclic(2, 2))) + + t.execution(() => assert.deepStrictEqual(cyclic(0, 2), cyclic(1, 1))) + t.execution(() => assert.deepStrictEqual(cyclic(1, 3), cyclic(3, 1))) + + t.execution(() => assert.deepStrictEqual(cyclic(0, 3), cyclic(1, 2))) + t.execution(() => assert.deepStrictEqual(cyclic(1, 2), cyclic(2, 1))) + t.execution(() => assert.deepStrictEqual(cyclic(0, 3), cyclic(2, 1))) + + t.exception( + () => assert.deepStrictEqual(cyclic(0, 1), cyclic(0, 2), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(cyclic(0, 1), cyclic(1, 1), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(cyclic(0, 2), cyclic(1, 2), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(cyclic(1, 2), cyclic(2, 2), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(cyclic(0, 3), cyclic(1, 3), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, recursive object, cycle position', (t) => { + // A self-edge under a different property, or at a different depth, describes a + // different structure. None of these are equal to each other, so treating any + // pair as equal would also make equality intransitive. + const selfThenLeaf = () => { + const a = {} + a.foo = a + a.bar = { value: 1 } + return a + } + + const onwardThenSelf = () => { + const a = {} + a.foo = { foo: { value: 1 } } + a.bar = a + return a + } + + const selfThenChain = () => { + const a = {} + const b = {} + a.foo = a + a.bar = b + b.foo = { value: 1 } + return a + } + + t.exception( + () => assert.deepStrictEqual(selfThenLeaf(), onwardThenSelf(), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(onwardThenSelf(), selfThenChain(), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(selfThenLeaf(), selfThenChain(), 'should fail'), + /should fail/ + ) + + // The same shape with no cycle at all is different again. + t.exception( + () => + assert.deepStrictEqual( + selfThenLeaf(), + { foo: { value: 9, other: 9 }, bar: { value: 1 } }, + 'should fail' + ), + /should fail/ + ) + + t.execution(() => assert.deepStrictEqual(selfThenLeaf(), selfThenLeaf())) + t.execution(() => assert.deepStrictEqual(onwardThenSelf(), onwardThenSelf())) + t.execution(() => assert.deepStrictEqual(selfThenChain(), selfThenChain())) +}) + +test('deepStrictEqual, recursive object, sibling cycles', (t) => { + // Cycles found while comparing one property must not affect the comparison of + // the next. Here the first property holds a cycle on both sides, and the + // second holds a cycle on one side only, which is a difference in its own + // right regardless of what the first property established. + { + const a1 = {} + a1.foo = a1 + + const a2 = {} + a2.foo = a2 + + const b1 = {} + b1.foo = b1 + + const b2 = {} + b2.foo = { value: 1 } + + t.exception( + () => + assert.deepStrictEqual({ first: a1, second: a2 }, { first: b1, second: b2 }, 'should fail'), + /should fail/ + ) + + // The same difference on its own, and with the properties swapped. + t.exception( + () => assert.deepStrictEqual({ second: a2 }, { second: b2 }, 'should fail'), + /should fail/ + ) + t.exception( + () => + assert.deepStrictEqual({ first: a2, second: a1 }, { first: b2, second: b1 }, 'should fail'), + /should fail/ + ) + } + + { + const a1 = {} + a1.foo = a1 + a1.bar = a1 + + const a2 = {} + a2.foo = a2 + a2.bar = { value: 1 } + + const b1 = {} + b1.foo = b1 + b1.bar = b1 + + const b2 = {} + b2.foo = { value: 1, extra: 2 } + b2.bar = { value: 1 } + + t.exception( + () => + assert.deepStrictEqual({ first: a1, second: a2 }, { first: b1, second: b2 }, 'should fail'), + /should fail/ + ) + } + + const build = () => { + const first = {} + first.foo = first + + const second = {} + second.foo = second + + return { first, second } + } + + t.execution(() => assert.deepStrictEqual(build(), build())) +}) + +test('deepStrictEqual, recursive object, nested up-reference', (t) => { + // Both children point back at the root on one side, while on the other the + // second child points at its sibling instead. + const a = { foo: {}, bar: {} } + a.foo.up = a + a.bar.up = a + + const b = { foo: {}, bar: {} } + b.foo.up = b + b.bar.up = b.foo + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + + const build = () => { + const value = { foo: {}, bar: {} } + value.foo.up = value + value.bar.up = value + return value + } + + t.execution(() => assert.deepStrictEqual(build(), build())) +}) + +test('deepStrictEqual, recursive object, self vs sibling', (t) => { + // The second property points back at the root on one side and at its sibling + // on the other, which are different structures. + const buildSelf = () => { + const first = {} + const second = {} + first.foo = second + first.bar = first + second.foo = first + second.bar = first + return first + } + + const buildSibling = () => { + const first = {} + const second = {} + first.foo = second + first.bar = second + second.foo = first + second.bar = first + return first + } + + // The answer must not depend on how deeply the comparison is nested. + t.exception( + () => assert.deepStrictEqual(buildSelf(), buildSibling(), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual({ value: buildSelf() }, { value: buildSibling() }, 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual([buildSelf()], [buildSibling()], 'should fail'), + /should fail/ + ) + + t.execution(() => assert.deepStrictEqual({ value: buildSelf() }, { value: buildSelf() })) + t.execution(() => assert.deepStrictEqual({ value: buildSibling() }, { value: buildSibling() })) +}) + +test('deepStrictEqual, recursive object, differing node count', (t) => { + // Three objects on one side against two on the other, describing the same + // structure once the cycles are followed. How many objects a structure is + // built from does not decide the answer. + const buildThree = () => { + const first = {} + const second = {} + const third = {} + first.foo = third + first.bar = second + second.foo = first + second.bar = second + third.foo = first + third.bar = first + return first + } + + const buildTwo = () => { + const first = {} + const second = {} + first.foo = second + first.bar = second + second.foo = first + second.bar = first + return first + } + + t.execution(() => assert.deepStrictEqual({ value: buildThree() }, { value: buildTwo() })) + t.execution(() => assert.deepStrictEqual([buildThree()], [buildTwo()])) + t.execution(() => assert.deepStrictEqual({ value: buildThree() }, { value: buildThree() })) + + const buildThreeWithLeaves = () => { + const first = {} + const second = {} + const third = {} + first.foo = third + first.bar = second + second.foo = { leaf: 1 } + second.bar = first + third.foo = { leaf: 1 } + third.bar = third + return first + } + + const buildTwoWithLeaves = () => { + const first = {} + const second = {} + first.foo = second + first.bar = second + second.foo = { leaf: 1 } + second.bar = first + return first + } + + t.execution(() => + assert.deepStrictEqual({ value: buildThreeWithLeaves() }, { value: buildTwoWithLeaves() }) + ) +}) + +test('deepStrictEqual, recursive object, repeated edges', (t) => { + // The same object reached through more than one property is still a single + // cycle, so revisiting it must not restart the traversal. + t.execution(() => { + const build = () => { + const a = {} + a.foo = a + a.bar = a + return a + } + + return assert.deepStrictEqual(build(), build()) + }) + + t.execution(() => { + const build = () => { + const a = {} + a.foo = a + a.bar = a + a.baz = a + return a + } + + return assert.deepStrictEqual(build(), build()) + }) + + t.execution(() => { + const build = () => { + const a = {} + const b = {} + + a.foo = b + a.bar = b + b.foo = a + b.bar = a + + return a + } + + return assert.deepStrictEqual(build(), build()) + }) + + const a = { value: 1 } + a.foo = a + a.bar = a + + const b = { value: 2 } + b.foo = b + b.bar = b + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, shared reference', (t) => { + // One object referenced from several properties is not a cycle: it must + // compare equal to a structure that repeats the value instead of sharing it. + const shared = { value: 1 } + + t.execution(() => + assert.deepStrictEqual({ foo: shared, bar: shared }, { foo: { value: 1 }, bar: { value: 1 } }) + ) + t.execution(() => + assert.deepStrictEqual( + { foo: shared, bar: shared, baz: shared }, + { foo: { value: 1 }, bar: { value: 1 }, baz: { value: 1 } } + ) + ) + t.execution(() => assert.deepStrictEqual([shared, shared], [{ value: 1 }, { value: 1 }])) + t.execution(() => + assert.deepStrictEqual({ foo: shared, bar: shared }, { foo: shared, bar: shared }) + ) + + t.exception( + () => + assert.deepStrictEqual( + { foo: shared, bar: shared }, + { foo: { value: 1 }, bar: { value: 2 } }, + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, recursive array', (t) => { + const a = [] + const b = [a] + a[0] = b + + t.execution(() => assert.deepStrictEqual(a, b)) +}) + +test('deepStrictEqual, recursive map', (t) => { + { + const a = new Map() + a.set('prop', a) + + const b = new Map() + b.set('prop', b) + + t.execution(() => assert.deepStrictEqual(a, b)) + } + + { + const a = new Map() + a.set(a, 'value') + + const b = new Map() + b.set(b, 'value') + + t.execution(() => assert.deepStrictEqual(a, b)) + } +}) + +test('deepStrictEqual, recursive set', (t) => { + const a = new Set() + a.add(a) + + const b = new Set() + b.add(b) + + t.execution(() => assert.deepStrictEqual(a, b)) +}) + +test('notDeepStrictEqual', (t) => { + t.execution(() => assert.notDeepStrictEqual({ foo: 1 }, { foo: 2 })) + t.execution(() => assert.notDeepStrictEqual([1, 2], [1, 2, 3])) + t.exception(() => assert.notDeepStrictEqual({ foo: 1 }, { foo: 1 }, 'should fail'), /should fail/) + t.exception(() => assert.notDeepStrictEqual([1, 2], [1, 2], 'should fail'), /should fail/) +}) + +test('notDeepStrictEqual, recursive object', (t) => { + // A two object cycle against the same cycle behind one extra object. + const twoCycle = () => { + const first = {} + const second = {} + first.prop = second + second.prop = first + return first + } + + const tailIntoTwoCycle = () => ({ prop: twoCycle() }) + + t.execution(() => assert.notDeepStrictEqual(twoCycle(), tailIntoTwoCycle())) + t.exception(() => assert.notDeepStrictEqual(twoCycle(), twoCycle(), 'should fail'), /should fail/) +}) + +test('notDeepStrictEqual, shared reference', (t) => { + const shared = { value: 1 } + + t.execution(() => + assert.notDeepStrictEqual( + { foo: shared, bar: shared, baz: 1 }, + { foo: { value: 1 }, bar: { value: 1 }, baz: 2 } + ) + ) + t.exception( + () => + assert.notDeepStrictEqual( + { foo: shared, bar: shared }, + { foo: { value: 1 }, bar: { value: 1 } }, + 'should fail' + ), + /should fail/ + ) +})