From 561561adab1506807eef01d20f2597e18478171d Mon Sep 17 00:00:00 2001 From: Joseph Gentle Date: Fri, 31 Mar 2017 14:46:07 +1100 Subject: [PATCH] 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 */