Skip to content

Commit

Permalink
assert: Add support for Map and Set in deepEqual
Browse files Browse the repository at this point in the history
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: nodejs#2309
Refs: tape-testing/tape#342
Refs: nodejs#2315
Refs: nodejs/CTC#63
  • Loading branch information
josephg committed Mar 31, 2017
1 parent b084907 commit 561561a
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 1 deletion.
56 changes: 55 additions & 1 deletion lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand All @@ -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))
Expand All @@ -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--) {
Expand Down
101 changes: 101 additions & 0 deletions test/parallel/test-assert-deep.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

0 comments on commit 561561a

Please sign in to comment.