From 561561adab1506807eef01d20f2597e18478171d Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Fri, 31 Mar 2017 14:46:07 +1100 Subject: [PATCH 01/13] assert: Add support for Map and Set in deepEqual assert.deepEqual and assert.deepStrictEqual currently return true for any pair of Maps and Sets regardless of content. This patch adds support in deepEqual and deepStrictEqual to verify the contents of Maps and Sets. Unfortunately because there's no way to pairwise fetch set values or map values which are equivalent but not reference-equal, this change currently only supports reference equality checking in set values and map key values. Equivalence checking could be done, but it would be an O(n^2) operation, and worse, it would get slower exponentially if maps and sets were nested. Note that this change breaks compatibility with previous versions of deepEqual and deepStrictEqual if consumers were depending on all maps and sets to be seen as equivalent. The old behaviour was never documented, but nevertheless there are certainly some tests out there which depend on it. Support has stalled because the assert API was frozen, but was recently unfrozen in CTC#63 Fixes: https://github.com/nodejs/node/issues/2309 Refs: https://github.com/substack/tape/issues/342 Refs: https://github.com/nodejs/node/pull/2315 Refs: https://github.com/nodejs/CTC/issues/63 --- lib/assert.js | 56 ++++++++++++++++- test/parallel/test-assert-deep.js | 101 ++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/lib/assert.js b/lib/assert.js index df575c0f169b22..2eac9f73987f25 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -161,6 +161,14 @@ function isArguments(tag) { return tag === '[object Arguments]'; } +function isMap(object) { + return object.constructor === Map; +} + +function isSet(object) { + return object.constructor === Set; +} + function _deepEqual(actual, expected, strict, memos) { // All identical values are equivalent, as determined by ===. if (actual === expected) { @@ -262,11 +270,18 @@ function _deepEqual(actual, expected, strict, memos) { } } - // For all other Object pairs, including Array objects, + if (isSet(actual)) { + return isSet(expected) && setEquiv(actual, expected); + } else if (isSet(expected)) { + return false; + } + + // For all other Object pairs, including Array objects and Maps, // equivalence is determined by having: // a) The same number of owned enumerable properties // b) The same set of keys/indexes (although not necessarily the same order) // c) Equivalent values for every corresponding key/index + // d) For Maps, strict-equal keys mapping to deep-equal values // Note: this accounts for both named and indexed properties on Arrays. // Use memos to handle cycles. @@ -283,6 +298,26 @@ function _deepEqual(actual, expected, strict, memos) { return objEquiv(actual, expected, strict, memos); } +function setEquiv(a, b) { + // This behaviour will work for any sets with contents that have + // strict-equality. That is, it will return false if the two sets contain + // equivalent objects that aren't reference-equal. We could support that, but + // it would require scanning each pairwise set of not strict-equal items, + // which is O(n^2), and would get exponentially worse if sets are nested. So + // for now we simply won't support deep equality checking set items or map + // keys. + if (a.size !== b.size) + return false; + + var val; + for (val of a) { + if (!b.has(val)) + return false; + } + + return true; +} + function objEquiv(a, b, strict, actualVisitedObjects) { // If one of them is a primitive, the other must be the same. if (util.isPrimitive(a) || util.isPrimitive(b)) @@ -307,6 +342,25 @@ function objEquiv(a, b, strict, actualVisitedObjects) { return false; } + if (isMap(a)) { + if (!isMap(b)) + return false; + + if (a.size !== b.size) + return false; + + var item; + for ([key, item] of a) { + if (!b.has(key)) + return false; + + if (!_deepEqual(item, b.get(key), strict, actualVisitedObjects)) + return false; + } + } else if (isMap(b)) { + return false; + } + // The pair must have equivalent values for every corresponding key. // Possibly expensive deep test: for (i = aKeys.length - 1; i >= 0; i--) { diff --git a/test/parallel/test-assert-deep.js b/test/parallel/test-assert-deep.js index 395f9c36f7c688..2ae6469fe5179f 100644 --- a/test/parallel/test-assert-deep.js +++ b/test/parallel/test-assert-deep.js @@ -107,4 +107,105 @@ for (const a of similar) { } } +function assertDeepAndStrictEqual(a, b) { + assert.doesNotThrow(() => assert.deepEqual(a, b)); + assert.doesNotThrow(() => assert.deepStrictEqual(a, b)); + + assert.doesNotThrow(() => assert.deepEqual(b, a)); + assert.doesNotThrow(() => assert.deepStrictEqual(b, a)); +} + +function assertNotDeepOrStrict(a, b) { + assert.throws(() => assert.deepEqual(a, b)); + assert.throws(() => assert.deepStrictEqual(a, b)); + + assert.throws(() => assert.deepEqual(b, a)); + assert.throws(() => assert.deepStrictEqual(b, a)); +} + +// es6 Maps and Sets +assertDeepAndStrictEqual(new Set(), new Set()); +assertDeepAndStrictEqual(new Map(), new Map()); + +// deepEqual only works with primitive key values and reference-equal values in +// sets and map keys avoid O(n^d) complexity (where d is depth) +assertDeepAndStrictEqual(new Set([1, 2, 3]), new Set([1, 2, 3])); +assertNotDeepOrStrict(new Set([1, 2, 3]), new Set([1, 2, 3, 4])); +assertNotDeepOrStrict(new Set([1, 2, 3, 4]), new Set([1, 2, 3])); +assertDeepAndStrictEqual(new Set(['1', '2', '3']), new Set(['1', '2', '3'])); + +assertDeepAndStrictEqual(new Map([[1, 1], [2, 2]]), new Map([[1, 1], [2, 2]])); +assertDeepAndStrictEqual(new Map([[1, 1], [2, 2]]), new Map([[2, 2], [1, 1]])); +assertNotDeepOrStrict(new Map([[1, 1], [2, 2]]), new Map([[1, 2], [2, 1]])); + +assertNotDeepOrStrict(new Set([1]), [1]); +assertNotDeepOrStrict(new Set(), []); +assertNotDeepOrStrict(new Set(), {}); + +assertNotDeepOrStrict(new Map([['a', 1]]), {a: 1}); +assertNotDeepOrStrict(new Map(), []); +assertNotDeepOrStrict(new Map(), {}); + +{ + const values = [ + 123, + Infinity, + 0, + null, + undefined, + false, + true, + {}, // Objects, lists and functions are ok if they're in by reference. + [], + () => {}, + ]; + assertDeepAndStrictEqual(new Set(values), new Set(values)); + assertDeepAndStrictEqual(new Set(values), new Set(values.reverse())); + + const mapValues = values.map((v) => [v, {a: 5}]); + assertDeepAndStrictEqual(new Map(mapValues), new Map(mapValues)); + assertDeepAndStrictEqual(new Map(mapValues), new Map(mapValues.reverse())); +} + +{ + const s1 = new Set(); + const s2 = new Set(); + s1.add(1); + s1.add(2); + s2.add(2); + s2.add(1); + assertDeepAndStrictEqual(s1, s2); +} + +{ + const m1 = new Map(); + const m2 = new Map(); + const obj = {a: 5, b: 6}; + m1.set(1, obj); + m1.set(2, 'hi'); + m1.set(3, [1, 2, 3]); + + m2.set(2, 'hi'); // different order + m2.set(1, obj); + m2.set(3, [1, 2, 3]); // deep equal, but not reference equal. + + assertDeepAndStrictEqual(m1, m2); +} + +{ + const m1 = new Map(); + const m2 = new Map(); + + // m1 contains itself. + m1.set(1, m1); + m2.set(1, new Map()); + + assertNotDeepOrStrict(m1, m2); +} + +assert.deepEqual(new Map([[1, 1]]), new Map([[1, '1']])); +assert.throws(() => + assert.deepStrictEqual(new Map([[1, 1]]), new Map([[1, '1']])) +); + /* eslint-enable */ From f0518408bf63390732382a3a4a0de3dee79c8308 Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Fri, 31 Mar 2017 15:22:04 +1100 Subject: [PATCH 02/13] assert: Use isSet and isMap from process.binding Updated PR based on comments Ref: https://github.com/nodejs/node/pull/12142 --- lib/assert.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 2eac9f73987f25..76fecc77532826 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -23,6 +23,7 @@ // UTILITY const compare = process.binding('buffer').compare; const util = require('util'); +const { isSet, isMap } = process.binding('util'); const objectToString = require('internal/util').objectToString; const Buffer = require('buffer').Buffer; @@ -161,14 +162,6 @@ function isArguments(tag) { return tag === '[object Arguments]'; } -function isMap(object) { - return object.constructor === Map; -} - -function isSet(object) { - return object.constructor === Set; -} - function _deepEqual(actual, expected, strict, memos) { // All identical values are equivalent, as determined by ===. if (actual === expected) { From 1d6cda68c5e57306699dce53bcb750ae1ff641ad Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Fri, 31 Mar 2017 19:09:48 +1100 Subject: [PATCH 03/13] assert: Added deeper equality checking for Map,Set This change updates the checks for deep equality checking on Map and Set to check all set values / all map keys to see if any of them match the expected result. This change is much slower, but based on the conversation in the pull request its probably the right approach. Ref: https://github.com/nodejs/node/pull/12142 --- lib/assert.js | 107 ++++++++++++++++++++++-------- test/parallel/test-assert-deep.js | 45 +++++++++++-- 2 files changed, 119 insertions(+), 33 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 76fecc77532826..5ac84b6a474920 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -263,12 +263,6 @@ function _deepEqual(actual, expected, strict, memos) { } } - if (isSet(actual)) { - return isSet(expected) && setEquiv(actual, expected); - } else if (isSet(expected)) { - return false; - } - // For all other Object pairs, including Array objects and Maps, // equivalence is determined by having: // a) The same number of owned enumerable properties @@ -288,24 +282,82 @@ function _deepEqual(actual, expected, strict, memos) { memos.actual.push(actual); memos.expected.push(expected); + return objEquiv(actual, expected, strict, memos); } -function setEquiv(a, b) { - // This behaviour will work for any sets with contents that have - // strict-equality. That is, it will return false if the two sets contain - // equivalent objects that aren't reference-equal. We could support that, but - // it would require scanning each pairwise set of not strict-equal items, - // which is O(n^2), and would get exponentially worse if sets are nested. So - // for now we simply won't support deep equality checking set items or map - // keys. +function setEquiv(a, b, strict, actualVisitedObjects) { + // This code currently returns false for this pair of sets: + // assert.deepEqual(new Set(['1', 1]), new Set([1])) + // + // In theory, all the items in the first set have a corresponding == value in + // the second set, but the sets have different sizes. Should they be + // considered to be non-strict deep equal to one another? Its a silly case, + // and more evidence that deepStrictEqual should always be preferred over + // deepEqual. The implementation currently returns false, which is a simpler + // and faster implementation. if (a.size !== b.size) return false; - var val; - for (val of a) { - if (!b.has(val)) + var val1, val2; + outer: for (val1 of a) { + if (!b.has(val1)) { + // The value doesn't exist in the second set by reference, so we'll go + // hunting for something thats deep-equal to it. Note that this is O(n^2) + // complexity, and will get slower if large, very similar sets / maps are + // nested inside. Unfortunately there's no real way around this. + for (val2 of b) { + if (_deepEqual(val1, val2, strict, actualVisitedObjects)) { + continue outer; + } + } + + // Not found! return false; + } + } + + return true; +} + +function mapEquiv(a, b, strict, actualVisitedObjects) { + // Caveat: In non-strict mode, this implementation does not handle cases + // where maps contain two equivalent-but-not-reference-equal keys. + // + // For example, maps like this are currently considered not equivalent: + if (a.size !== b.size) + return false; + + var key1, key2, item1, item2; + outer: for ([key1, item1] of a) { + // To be able to handle cases like: + // Map([[1, 'a'], ['1', 'b']]) vs Map([['1', 'a'], [1, 'b']]) + // or: + // Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']]) + // ... we need to consider *all* matching keys, not just the first we find. + + // This check is not strictly necessary, but its here to improve + // performance of the common case when reference-equal keys exist (which + // includes all primitive valued keys). + if (b.has(key1)) { + if (_deepEqual(item1, b.get(key1), strict, actualVisitedObjects)) + continue outer; + } + + // Hunt for keys which are deep-equal to key1 in b. Just like setEquiv + // above, this hunt makes this function O(n^2). + for ([key2, item2] of b) { + // Just for performance. We already checked these keys above. + if (key2 === key1) + continue; + + if (_deepEqual(key1, key2, strict, actualVisitedObjects) && + _deepEqual(item1, item2, strict, actualVisitedObjects)) { + continue outer; + } + } + + return false; } return true; @@ -335,21 +387,18 @@ function objEquiv(a, b, strict, actualVisitedObjects) { return false; } - if (isMap(a)) { - if (!isMap(b)) + // Sets and maps don't have their entries accessable via normal object + // properties. + if (isSet(a)) { + if (!isSet(b) || !setEquiv(a, b, strict, actualVisitedObjects)) return false; + } else if (isSet(b)) { + return false; + } - if (a.size !== b.size) + if (isMap(a)) { + if (!isMap(b) || !mapEquiv(a, b, strict, actualVisitedObjects)) return false; - - var item; - for ([key, item] of a) { - if (!b.has(key)) - return false; - - if (!_deepEqual(item, b.get(key), strict, actualVisitedObjects)) - return false; - } } else if (isMap(b)) { return false; } diff --git a/test/parallel/test-assert-deep.js b/test/parallel/test-assert-deep.js index 2ae6469fe5179f..9f4beef19c3834 100644 --- a/test/parallel/test-assert-deep.js +++ b/test/parallel/test-assert-deep.js @@ -100,7 +100,7 @@ const similar = new Set([ for (const a of similar) { for (const b of similar) { if (a !== b) { - assert.doesNotThrow(() => assert.deepEqual(a, b)); + assert.deepEqual(a, b); assert.throws(() => assert.deepStrictEqual(a, b), re`${a} deepStrictEqual ${b}`); } @@ -123,16 +123,23 @@ function assertNotDeepOrStrict(a, b) { assert.throws(() => assert.deepStrictEqual(b, a)); } +function assertOnlyDeepEqual(a, b) { + assert.doesNotThrow(() => assert.deepEqual(a, b)); + assert.throws(() => assert.deepStrictEqual(a, b)); + + assert.doesNotThrow(() => assert.deepEqual(b, a)); + assert.throws(() => assert.deepStrictEqual(b, a)); +} + // es6 Maps and Sets assertDeepAndStrictEqual(new Set(), new Set()); assertDeepAndStrictEqual(new Map(), new Map()); -// deepEqual only works with primitive key values and reference-equal values in -// sets and map keys avoid O(n^d) complexity (where d is depth) assertDeepAndStrictEqual(new Set([1, 2, 3]), new Set([1, 2, 3])); assertNotDeepOrStrict(new Set([1, 2, 3]), new Set([1, 2, 3, 4])); assertNotDeepOrStrict(new Set([1, 2, 3, 4]), new Set([1, 2, 3])); assertDeepAndStrictEqual(new Set(['1', '2', '3']), new Set(['1', '2', '3'])); +assertDeepAndStrictEqual(new Set([[1, 2], [3, 4]]), new Set([[3, 4], [1, 2]])); assertDeepAndStrictEqual(new Map([[1, 1], [2, 2]]), new Map([[1, 1], [2, 2]])); assertDeepAndStrictEqual(new Map([[1, 1], [2, 2]]), new Map([[2, 2], [1, 1]])); @@ -146,6 +153,21 @@ assertNotDeepOrStrict(new Map([['a', 1]]), {a: 1}); assertNotDeepOrStrict(new Map(), []); assertNotDeepOrStrict(new Map(), {}); +assertOnlyDeepEqual(new Set(['1']), new Set([1])); + +assertOnlyDeepEqual(new Map([['1', 'a']]), new Map([[1, 'a']])); +assertOnlyDeepEqual(new Map([['a', '1']]), new Map([['a', 1]])); + +// This is an awful case, where a map contains multiple equivalent keys: +assertOnlyDeepEqual( + new Map([[1, 'a'], ['1', 'b']]), + new Map([['1', 'a'], [1, 'b']]) +); +assertDeepAndStrictEqual( + new Map([[{}, 'a'], [{}, 'b']]), + new Map([[{}, 'b'], [{}, 'a']]) +); + { const values = [ 123, @@ -155,7 +177,7 @@ assertNotDeepOrStrict(new Map(), {}); undefined, false, true, - {}, // Objects, lists and functions are ok if they're in by reference. + {}, [], () => {}, ]; @@ -208,4 +230,19 @@ assert.throws(() => assert.deepStrictEqual(new Map([[1, 1]]), new Map([[1, '1']])) ); +{ + // Two equivalent sets / maps with different key/values applied shouldn't be + // the same. This is a terrible idea to do in practice, but deepEqual should + // still check for it. + const s1 = new Set(); + const s2 = new Set(); + s1.x = 5; + assertNotDeepOrStrict(s1, s2); + + const m1 = new Map(); + const m2 = new Map(); + m1.x = 5; + assertNotDeepOrStrict(m1, m2); +} + /* eslint-enable */ From 800ae463ed0c9aa6ab410de8fa93c3a6ffb08b01 Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Fri, 31 Mar 2017 19:29:06 +1100 Subject: [PATCH 04/13] assert: Fixed nit in code review --- lib/assert.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/assert.js b/lib/assert.js index 5ac84b6a474920..219613c065d1b4 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -338,7 +338,7 @@ function mapEquiv(a, b, strict, actualVisitedObjects) { // This check is not strictly necessary, but its here to improve // performance of the common case when reference-equal keys exist (which - // includes all primitive valued keys). + // includes all primitive-valued keys). if (b.has(key1)) { if (_deepEqual(item1, b.get(key1), strict, actualVisitedObjects)) continue outer; From 031f6f3235e524ef3f3af3cc0e05aa6beb723e55 Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Fri, 31 Mar 2017 20:01:14 +1100 Subject: [PATCH 05/13] assert: Fixed typo in comment --- lib/assert.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/assert.js b/lib/assert.js index 219613c065d1b4..b353e63b2ea400 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -387,7 +387,7 @@ function objEquiv(a, b, strict, actualVisitedObjects) { return false; } - // Sets and maps don't have their entries accessable via normal object + // Sets and maps don't have their entries accessible via normal object // properties. if (isSet(a)) { if (!isSet(b) || !setEquiv(a, b, strict, actualVisitedObjects)) From d6baaeeab73ee88184ba47a947156c0d59120f63 Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Sat, 1 Apr 2017 08:19:22 +1100 Subject: [PATCH 06/13] assert: var -> const and added tests Cleaned up as per comments in issue Ref: https://github.com/nodejs/node/issues/6416 --- lib/assert.js | 19 ++++++++----------- test/parallel/test-assert-deep.js | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index b353e63b2ea400..9d0e18d677b6bc 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -268,7 +268,7 @@ function _deepEqual(actual, expected, strict, memos) { // a) The same number of owned enumerable properties // b) The same set of keys/indexes (although not necessarily the same order) // c) Equivalent values for every corresponding key/index - // d) For Maps, strict-equal keys mapping to deep-equal values + // d) For Sets and Maps, equal contents // Note: this accounts for both named and indexed properties on Arrays. // Use memos to handle cycles. @@ -291,22 +291,20 @@ function setEquiv(a, b, strict, actualVisitedObjects) { // assert.deepEqual(new Set(['1', 1]), new Set([1])) // // In theory, all the items in the first set have a corresponding == value in - // the second set, but the sets have different sizes. Should they be - // considered to be non-strict deep equal to one another? Its a silly case, + // the second set, but the sets have different sizes. Its a silly case, // and more evidence that deepStrictEqual should always be preferred over - // deepEqual. The implementation currently returns false, which is a simpler - // and faster implementation. + // deepEqual. The implementation currently returns false, which is simpler + // and slightly faster. if (a.size !== b.size) return false; - var val1, val2; - outer: for (val1 of a) { + outer: for (const val1 of a) { if (!b.has(val1)) { // The value doesn't exist in the second set by reference, so we'll go // hunting for something thats deep-equal to it. Note that this is O(n^2) // complexity, and will get slower if large, very similar sets / maps are // nested inside. Unfortunately there's no real way around this. - for (val2 of b) { + for (const val2 of b) { if (_deepEqual(val1, val2, strict, actualVisitedObjects)) { continue outer; } @@ -328,8 +326,7 @@ function mapEquiv(a, b, strict, actualVisitedObjects) { if (a.size !== b.size) return false; - var key1, key2, item1, item2; - outer: for ([key1, item1] of a) { + outer: for (const [key1, item1] of a) { // To be able to handle cases like: // Map([[1, 'a'], ['1', 'b']]) vs Map([['1', 'a'], [1, 'b']]) // or: @@ -346,7 +343,7 @@ function mapEquiv(a, b, strict, actualVisitedObjects) { // Hunt for keys which are deep-equal to key1 in b. Just like setEquiv // above, this hunt makes this function O(n^2). - for ([key2, item2] of b) { + for (const [key2, item2] of b) { // Just for performance. We already checked these keys above. if (key2 === key1) continue; diff --git a/test/parallel/test-assert-deep.js b/test/parallel/test-assert-deep.js index 9f4beef19c3834..2c3285e4229a38 100644 --- a/test/parallel/test-assert-deep.js +++ b/test/parallel/test-assert-deep.js @@ -245,4 +245,25 @@ assert.throws(() => assertNotDeepOrStrict(m1, m2); } +{ + // Circular references. + const s1 = new Set(); + s1.add(s1); + const s2 = new Set(); + s2.add(s2); + assertDeepAndStrictEqual(s1, s2); + + const m1 = new Map(); + m1.set(2, m1); + const m2 = new Map(); + m2.set(2, m2); + assertDeepAndStrictEqual(m1, m2); + + const m3 = new Map(); + m3.set(m3, 2); + const m4 = new Map(); + m4.set(m4, 2); + assertDeepAndStrictEqual(m3, m4); +} + /* eslint-enable */ From 8fb6ebf9745badfb2f3f6a72cef211ff8bfbf6ba Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Sat, 1 Apr 2017 10:36:24 +1100 Subject: [PATCH 07/13] assert: restrict O(n^2) search Based on comments in the PR, this change restricts an O(n^2) to only happen when your set contains object-like objects, your map contains object-like keys or you're not in strict mode. ref: https://github.com/nodejs/node/pull/12142#pullrequestreview-30263194 --- lib/assert.js | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 9d0e18d677b6bc..bfee68fd616b53 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -300,13 +300,15 @@ function setEquiv(a, b, strict, actualVisitedObjects) { outer: for (const val1 of a) { if (!b.has(val1)) { - // The value doesn't exist in the second set by reference, so we'll go - // hunting for something thats deep-equal to it. Note that this is O(n^2) - // complexity, and will get slower if large, very similar sets / maps are - // nested inside. Unfortunately there's no real way around this. - for (const val2 of b) { - if (_deepEqual(val1, val2, strict, actualVisitedObjects)) { - continue outer; + if (!strict || typeof val1 === 'object') { + // The value doesn't exist in the second set by reference, so we'll go + // hunting for something thats deep-equal to it. Note that this is + // O(n^2) complexity, and will get slower if large, very similar sets / + // maps are nested inside. Unfortunately there's no real way around + // this. + for (const val2 of b) { + if (_deepEqual(val1, val2, strict, actualVisitedObjects)) + continue outer; } } @@ -333,24 +335,27 @@ function mapEquiv(a, b, strict, actualVisitedObjects) { // Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']]) // ... we need to consider *all* matching keys, not just the first we find. - // This check is not strictly necessary, but its here to improve - // performance of the common case when reference-equal keys exist (which - // includes all primitive-valued keys). + // This check is not strictly necessary if we run the loop below, but it + // improves performance of the common case when reference-equal keys exist + // (which includes all primitive-valued keys). if (b.has(key1)) { if (_deepEqual(item1, b.get(key1), strict, actualVisitedObjects)) continue outer; } // Hunt for keys which are deep-equal to key1 in b. Just like setEquiv - // above, this hunt makes this function O(n^2). - for (const [key2, item2] of b) { - // Just for performance. We already checked these keys above. - if (key2 === key1) - continue; - - if (_deepEqual(key1, key2, strict, actualVisitedObjects) && - _deepEqual(item1, item2, strict, actualVisitedObjects)) { - continue outer; + // above, this hunt makes this function O(n^2) when using objects and lists + // as keys + if (!strict || typeof key1 === 'object') { + for (const [key2, item2] of b) { + // Just for performance. We already checked these keys above. + if (key2 === key1) + continue; + + if (_deepEqual(key1, key2, strict, actualVisitedObjects) && + _deepEqual(item1, item2, strict, actualVisitedObjects)) { + continue outer; + } } } From acef7011338f756734762fdf59e8e6a00d6bb608 Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Sat, 1 Apr 2017 11:09:55 +1100 Subject: [PATCH 08/13] assert: added null check in set/map short circuit ref: https://github.com/nodejs/node/pull/12142/files/8fb6ebf9745badfb2f3f6a72cef211ff8bfbf6ba --- lib/assert.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index bfee68fd616b53..41fe52b78e6f9a 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -300,7 +300,7 @@ function setEquiv(a, b, strict, actualVisitedObjects) { outer: for (const val1 of a) { if (!b.has(val1)) { - if (!strict || typeof val1 === 'object') { + if (!strict || (typeof val1 === 'object' && val1 !== null)) { // The value doesn't exist in the second set by reference, so we'll go // hunting for something thats deep-equal to it. Note that this is // O(n^2) complexity, and will get slower if large, very similar sets / @@ -346,7 +346,7 @@ function mapEquiv(a, b, strict, actualVisitedObjects) { // Hunt for keys which are deep-equal to key1 in b. Just like setEquiv // above, this hunt makes this function O(n^2) when using objects and lists // as keys - if (!strict || typeof key1 === 'object') { + if (!strict || (typeof key1 === 'object' && key1 !== null)) { for (const [key2, item2] of b) { // Just for performance. We already checked these keys above. if (key2 === key1) From ee131e877c897134a4c64d2b38fe95b4e5b67a96 Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Sun, 2 Apr 2017 01:18:47 +1100 Subject: [PATCH 09/13] assert: update documentation to reflect changes Ref: https://github.com/nodejs/node/pull/12142 --- doc/api/assert.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/doc/api/assert.md b/doc/api/assert.md index 868b90445031c5..0528efda8ffb8d 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -40,7 +40,7 @@ Only [enumerable "own" properties][] are considered. The [`assert.deepEqual()`][] implementation does not test the [`[[Prototype]]`][prototype-spec] of objects, attached symbols, or non-enumerable properties — for such checks, consider using -[assert.deepStrictEqual()][] instead. This can lead to some +[`assert.deepStrictEqual()`][] instead. This can lead to some potentially surprising results. For example, the following example does not throw an `AssertionError` because the properties on the [`Error`][] object are not enumerable: @@ -50,6 +50,9 @@ not enumerable: assert.deepEqual(Error('a'), Error('b')); ``` +An exception is made for [`Map`][] and [`Set`][]. Maps and Sets have their +contained items compared too, as expected. + "Deep" equality means that the enumerable "own" properties of child objects are evaluated also: @@ -148,6 +151,21 @@ assert.deepStrictEqual(date, fakeDate); // Different type tags ``` +An exception is made to the strict equality comparison rule for [`Map`][] keys +and [`Set`][] items. These tests also inherit the rules of `Set.prototype.has` +and `Map.prototype.has`, which differs from strict equality comparison in +also allowing NaN to be considered equal to itself: + +```js +assert.deepStrictEqual(NaN, NaN); +// AssertionError: NaN deepStrictEqual NaN +// because NaN !== NaN + +// But this is ok: +assert.deepStrictEqual(new Set([NaN]), new Set([NaN])); +assert.deepStrictEqual(new Map([[NaN, 1]]), new Map([[NaN, 1]])); +``` + If the values are not equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a default error message is assigned. @@ -580,6 +598,8 @@ For more information, see [`TypeError`]: errors.html#errors_class_typeerror [Abstract Equality Comparison]: https://tc39.github.io/ecma262/#sec-abstract-equality-comparison [Strict Equality Comparison]: https://tc39.github.io/ecma262/#sec-strict-equality-comparison +[`Map`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map +[`Set`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Set [`Object.is()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is [SameValueZero]: https://tc39.github.io/ecma262/#sec-samevaluezero [prototype-spec]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots From 7bc29b0b4a3febeab6314e0a772f8b65e85ded67 Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Sun, 2 Apr 2017 02:52:18 +1100 Subject: [PATCH 10/13] assert: Updated based on PR - Added changes: entries in assert API documentation - Refactored setEquiv based on @joyeecheung's comments ref: https://github.com/nodejs/node/pull/12142 --- doc/api/assert.md | 6 ++++++ lib/assert.js | 36 +++++++++++++++---------------- test/parallel/test-assert-deep.js | 8 +++---- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/doc/api/assert.md b/doc/api/assert.md index 0528efda8ffb8d..1ca03e02e6ffca 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -18,6 +18,9 @@ An alias of [`assert.ok()`][].