Skip to content

Commit 561561a

Browse files
committed
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: nodejs#2309 Refs: tape-testing/tape#342 Refs: nodejs#2315 Refs: nodejs/CTC#63
1 parent b084907 commit 561561a

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

lib/assert.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ function isArguments(tag) {
161161
return tag === '[object Arguments]';
162162
}
163163

164+
function isMap(object) {
165+
return object.constructor === Map;
166+
}
167+
168+
function isSet(object) {
169+
return object.constructor === Set;
170+
}
171+
164172
function _deepEqual(actual, expected, strict, memos) {
165173
// All identical values are equivalent, as determined by ===.
166174
if (actual === expected) {
@@ -262,11 +270,18 @@ function _deepEqual(actual, expected, strict, memos) {
262270
}
263271
}
264272

265-
// For all other Object pairs, including Array objects,
273+
if (isSet(actual)) {
274+
return isSet(expected) && setEquiv(actual, expected);
275+
} else if (isSet(expected)) {
276+
return false;
277+
}
278+
279+
// For all other Object pairs, including Array objects and Maps,
266280
// equivalence is determined by having:
267281
// a) The same number of owned enumerable properties
268282
// b) The same set of keys/indexes (although not necessarily the same order)
269283
// c) Equivalent values for every corresponding key/index
284+
// d) For Maps, strict-equal keys mapping to deep-equal values
270285
// Note: this accounts for both named and indexed properties on Arrays.
271286

272287
// Use memos to handle cycles.
@@ -283,6 +298,26 @@ function _deepEqual(actual, expected, strict, memos) {
283298
return objEquiv(actual, expected, strict, memos);
284299
}
285300

301+
function setEquiv(a, b) {
302+
// This behaviour will work for any sets with contents that have
303+
// strict-equality. That is, it will return false if the two sets contain
304+
// equivalent objects that aren't reference-equal. We could support that, but
305+
// it would require scanning each pairwise set of not strict-equal items,
306+
// which is O(n^2), and would get exponentially worse if sets are nested. So
307+
// for now we simply won't support deep equality checking set items or map
308+
// keys.
309+
if (a.size !== b.size)
310+
return false;
311+
312+
var val;
313+
for (val of a) {
314+
if (!b.has(val))
315+
return false;
316+
}
317+
318+
return true;
319+
}
320+
286321
function objEquiv(a, b, strict, actualVisitedObjects) {
287322
// If one of them is a primitive, the other must be the same.
288323
if (util.isPrimitive(a) || util.isPrimitive(b))
@@ -307,6 +342,25 @@ function objEquiv(a, b, strict, actualVisitedObjects) {
307342
return false;
308343
}
309344

345+
if (isMap(a)) {
346+
if (!isMap(b))
347+
return false;
348+
349+
if (a.size !== b.size)
350+
return false;
351+
352+
var item;
353+
for ([key, item] of a) {
354+
if (!b.has(key))
355+
return false;
356+
357+
if (!_deepEqual(item, b.get(key), strict, actualVisitedObjects))
358+
return false;
359+
}
360+
} else if (isMap(b)) {
361+
return false;
362+
}
363+
310364
// The pair must have equivalent values for every corresponding key.
311365
// Possibly expensive deep test:
312366
for (i = aKeys.length - 1; i >= 0; i--) {

test/parallel/test-assert-deep.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,105 @@ for (const a of similar) {
107107
}
108108
}
109109

110+
function assertDeepAndStrictEqual(a, b) {
111+
assert.doesNotThrow(() => assert.deepEqual(a, b));
112+
assert.doesNotThrow(() => assert.deepStrictEqual(a, b));
113+
114+
assert.doesNotThrow(() => assert.deepEqual(b, a));
115+
assert.doesNotThrow(() => assert.deepStrictEqual(b, a));
116+
}
117+
118+
function assertNotDeepOrStrict(a, b) {
119+
assert.throws(() => assert.deepEqual(a, b));
120+
assert.throws(() => assert.deepStrictEqual(a, b));
121+
122+
assert.throws(() => assert.deepEqual(b, a));
123+
assert.throws(() => assert.deepStrictEqual(b, a));
124+
}
125+
126+
// es6 Maps and Sets
127+
assertDeepAndStrictEqual(new Set(), new Set());
128+
assertDeepAndStrictEqual(new Map(), new Map());
129+
130+
// deepEqual only works with primitive key values and reference-equal values in
131+
// sets and map keys avoid O(n^d) complexity (where d is depth)
132+
assertDeepAndStrictEqual(new Set([1, 2, 3]), new Set([1, 2, 3]));
133+
assertNotDeepOrStrict(new Set([1, 2, 3]), new Set([1, 2, 3, 4]));
134+
assertNotDeepOrStrict(new Set([1, 2, 3, 4]), new Set([1, 2, 3]));
135+
assertDeepAndStrictEqual(new Set(['1', '2', '3']), new Set(['1', '2', '3']));
136+
137+
assertDeepAndStrictEqual(new Map([[1, 1], [2, 2]]), new Map([[1, 1], [2, 2]]));
138+
assertDeepAndStrictEqual(new Map([[1, 1], [2, 2]]), new Map([[2, 2], [1, 1]]));
139+
assertNotDeepOrStrict(new Map([[1, 1], [2, 2]]), new Map([[1, 2], [2, 1]]));
140+
141+
assertNotDeepOrStrict(new Set([1]), [1]);
142+
assertNotDeepOrStrict(new Set(), []);
143+
assertNotDeepOrStrict(new Set(), {});
144+
145+
assertNotDeepOrStrict(new Map([['a', 1]]), {a: 1});
146+
assertNotDeepOrStrict(new Map(), []);
147+
assertNotDeepOrStrict(new Map(), {});
148+
149+
{
150+
const values = [
151+
123,
152+
Infinity,
153+
0,
154+
null,
155+
undefined,
156+
false,
157+
true,
158+
{}, // Objects, lists and functions are ok if they're in by reference.
159+
[],
160+
() => {},
161+
];
162+
assertDeepAndStrictEqual(new Set(values), new Set(values));
163+
assertDeepAndStrictEqual(new Set(values), new Set(values.reverse()));
164+
165+
const mapValues = values.map((v) => [v, {a: 5}]);
166+
assertDeepAndStrictEqual(new Map(mapValues), new Map(mapValues));
167+
assertDeepAndStrictEqual(new Map(mapValues), new Map(mapValues.reverse()));
168+
}
169+
170+
{
171+
const s1 = new Set();
172+
const s2 = new Set();
173+
s1.add(1);
174+
s1.add(2);
175+
s2.add(2);
176+
s2.add(1);
177+
assertDeepAndStrictEqual(s1, s2);
178+
}
179+
180+
{
181+
const m1 = new Map();
182+
const m2 = new Map();
183+
const obj = {a: 5, b: 6};
184+
m1.set(1, obj);
185+
m1.set(2, 'hi');
186+
m1.set(3, [1, 2, 3]);
187+
188+
m2.set(2, 'hi'); // different order
189+
m2.set(1, obj);
190+
m2.set(3, [1, 2, 3]); // deep equal, but not reference equal.
191+
192+
assertDeepAndStrictEqual(m1, m2);
193+
}
194+
195+
{
196+
const m1 = new Map();
197+
const m2 = new Map();
198+
199+
// m1 contains itself.
200+
m1.set(1, m1);
201+
m2.set(1, new Map());
202+
203+
assertNotDeepOrStrict(m1, m2);
204+
}
205+
206+
assert.deepEqual(new Map([[1, 1]]), new Map([[1, '1']]));
207+
assert.throws(() =>
208+
assert.deepStrictEqual(new Map([[1, 1]]), new Map([[1, '1']]))
209+
);
210+
110211
/* eslint-enable */

0 commit comments

Comments
 (0)