Skip to content

Commit 2b419d7

Browse files
BridgeARRafaelGSS
authored andcommitted
assert: implement partial error comparison
assert.partialDeepStrictEqual now also handled error properties as expected. On top of that, the main implementation also handles non-string `name` and `message` properties and the comparison is a tad faster by removing duplicated comparison steps. As a drive-by fix this also cleans up some code by abstracting code and renaming variables for clarity. PR-URL: #57370 Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Vinícius Lourenço Claro Cardoso <contact@viniciusl.com.br>
1 parent 01cf5fb commit 2b419d7

File tree

3 files changed

+94
-62
lines changed

3 files changed

+94
-62
lines changed

lib/internal/util/comparisons.js

+44-61
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ const {
1010
Error,
1111
NumberIsNaN,
1212
NumberPrototypeValueOf,
13-
ObjectGetOwnPropertySymbols,
13+
ObjectGetOwnPropertySymbols: getOwnSymbols,
1414
ObjectGetPrototypeOf,
1515
ObjectIs,
1616
ObjectKeys,
17-
ObjectPrototypeHasOwnProperty,
18-
ObjectPrototypePropertyIsEnumerable,
17+
ObjectPrototypeHasOwnProperty: hasOwn,
18+
ObjectPrototypePropertyIsEnumerable: hasEnumerable,
1919
ObjectPrototypeToString,
2020
SafeSet,
2121
StringPrototypeValueOf,
2222
SymbolPrototypeValueOf,
23-
TypedArrayPrototypeGetByteLength,
23+
TypedArrayPrototypeGetByteLength: getByteLength,
2424
TypedArrayPrototypeGetSymbolToStringTag,
2525
Uint8Array,
2626
} = primordials;
@@ -76,8 +76,8 @@ function areSimilarRegExps(a, b) {
7676
}
7777

7878
function isPartialUint8Array(a, b) {
79-
const lenA = TypedArrayPrototypeGetByteLength(a);
80-
const lenB = TypedArrayPrototypeGetByteLength(b);
79+
const lenA = getByteLength(a);
80+
const lenB = getByteLength(b);
8181
if (lenA < lenB) {
8282
return false;
8383
}
@@ -105,10 +105,10 @@ function isPartialArrayBufferView(a, b) {
105105
}
106106

107107
function areSimilarFloatArrays(a, b) {
108-
if (a.byteLength !== b.byteLength) {
108+
const len = getByteLength(a);
109+
if (len !== getByteLength(b)) {
109110
return false;
110111
}
111-
const len = TypedArrayPrototypeGetByteLength(a);
112112
for (let offset = 0; offset < len; offset++) {
113113
if (a[offset] !== b[offset]) {
114114
return false;
@@ -156,6 +156,12 @@ function isEqualBoxedPrimitive(val1, val2) {
156156
assert.fail(`Unknown boxed type ${val1}`);
157157
}
158158

159+
function isEnumerableOrIdentical(val1, val2, prop, mode, memos, method) {
160+
return hasEnumerable(val2, prop) || // This is handled by Object.keys()
161+
(mode === kPartial && (val2[prop] === undefined || (prop === 'message' && val2[prop] === ''))) ||
162+
innerDeepEqual(val1[prop], val2[prop], mode, memos);
163+
}
164+
159165
// Notes: Type tags are historical [[Class]] properties that can be set by
160166
// FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS
161167
// and retrieved using Object.prototype.toString.call(obj) in JS
@@ -261,8 +267,7 @@ function innerDeepEqual(val1, val2, mode, memos) {
261267
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) {
262268
return false;
263269
}
264-
const result = keyCheck(val1, val2, mode, memos, kIsSet);
265-
return result;
270+
return keyCheck(val1, val2, mode, memos, kIsSet);
266271
} else if (isMap(val1)) {
267272
if (!isMap(val2) ||
268273
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) {
@@ -283,26 +288,15 @@ function innerDeepEqual(val1, val2, mode, memos) {
283288
} else if (isError(val1)) {
284289
// Do not compare the stack as it might differ even though the error itself
285290
// is otherwise identical.
286-
if (!isError(val2)) {
291+
if (!isError(val2) ||
292+
!isEnumerableOrIdentical(val1, val2, 'message', mode, memos) ||
293+
!isEnumerableOrIdentical(val1, val2, 'name', mode, memos) ||
294+
!isEnumerableOrIdentical(val1, val2, 'cause', mode, memos) ||
295+
!isEnumerableOrIdentical(val1, val2, 'errors', mode, memos)) {
287296
return false;
288297
}
289-
290-
const message1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'message');
291-
const name1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'name');
292-
// TODO(BridgeAR): Adjust cause and errors properties for partial mode.
293-
const cause1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'cause');
294-
const errors1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'errors');
295-
296-
if ((message1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'message') ||
297-
(!message1Enumerable && val1.message !== val2.message)) ||
298-
(name1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'name') ||
299-
(!name1Enumerable && val1.name !== val2.name)) ||
300-
(cause1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'cause') ||
301-
(!cause1Enumerable && (
302-
ObjectPrototypeHasOwnProperty(val1, 'cause') !== ObjectPrototypeHasOwnProperty(val2, 'cause') ||
303-
!innerDeepEqual(val1.cause, val2.cause, mode, memos)))) ||
304-
(errors1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'errors') ||
305-
(!errors1Enumerable && !innerDeepEqual(val1.errors, val2.errors, mode, memos)))) {
298+
const hasOwnVal2Cause = hasOwn(val2, 'cause');
299+
if ((hasOwnVal2Cause !== hasOwn(val1, 'cause') && (mode !== kPartial || hasOwnVal2Cause))) {
306300
return false;
307301
}
308302
} else if (isBoxedPrimitive(val1)) {
@@ -344,10 +338,7 @@ function innerDeepEqual(val1, val2, mode, memos) {
344338
}
345339

346340
function getEnumerables(val, keys) {
347-
return ArrayPrototypeFilter(
348-
keys,
349-
(k) => ObjectPrototypePropertyIsEnumerable(val, k),
350-
);
341+
return ArrayPrototypeFilter(keys, (key) => hasEnumerable(val, key));
351342
}
352343

353344
function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
@@ -367,7 +358,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
367358
// Cheap key test
368359
if (keys2.length > 0) {
369360
for (const key of keys2) {
370-
if (!ObjectPrototypePropertyIsEnumerable(val1, key)) {
361+
if (!hasEnumerable(val1, key)) {
371362
return false;
372363
}
373364
}
@@ -376,11 +367,11 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
376367
if (!isArrayLikeObject) {
377368
// The pair must have the same number of owned properties.
378369
if (mode === kPartial) {
379-
const symbolKeys = ObjectGetOwnPropertySymbols(val2);
370+
const symbolKeys = getOwnSymbols(val2);
380371
if (symbolKeys.length !== 0) {
381372
for (const key of symbolKeys) {
382-
if (ObjectPrototypePropertyIsEnumerable(val2, key)) {
383-
if (!ObjectPrototypePropertyIsEnumerable(val1, key)) {
373+
if (hasEnumerable(val2, key)) {
374+
if (!hasEnumerable(val1, key)) {
384375
return false;
385376
}
386377
ArrayPrototypePush(keys2, key);
@@ -392,27 +383,27 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
392383
}
393384

394385
if (mode === kStrict) {
395-
const symbolKeysA = ObjectGetOwnPropertySymbols(val1);
386+
const symbolKeysA = getOwnSymbols(val1);
396387
if (symbolKeysA.length !== 0) {
397388
let count = 0;
398389
for (const key of symbolKeysA) {
399-
if (ObjectPrototypePropertyIsEnumerable(val1, key)) {
400-
if (!ObjectPrototypePropertyIsEnumerable(val2, key)) {
390+
if (hasEnumerable(val1, key)) {
391+
if (!hasEnumerable(val2, key)) {
401392
return false;
402393
}
403394
ArrayPrototypePush(keys2, key);
404395
count++;
405-
} else if (ObjectPrototypePropertyIsEnumerable(val2, key)) {
396+
} else if (hasEnumerable(val2, key)) {
406397
return false;
407398
}
408399
}
409-
const symbolKeysB = ObjectGetOwnPropertySymbols(val2);
400+
const symbolKeysB = getOwnSymbols(val2);
410401
if (symbolKeysA.length !== symbolKeysB.length &&
411402
getEnumerables(val2, symbolKeysB).length !== count) {
412403
return false;
413404
}
414405
} else {
415-
const symbolKeysB = ObjectGetOwnPropertySymbols(val2);
406+
const symbolKeysB = getOwnSymbols(val2);
416407
if (symbolKeysB.length !== 0 &&
417408
getEnumerables(val2, symbolKeysB).length !== 0) {
418409
return false;
@@ -437,7 +428,6 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
437428
c: undefined,
438429
d: undefined,
439430
deep: false,
440-
deleteFailures: false,
441431
};
442432
return objEquiv(val1, val2, mode, keys2, memos, iterationType);
443433
}
@@ -472,27 +462,21 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
472462

473463
const areEq = objEquiv(val1, val2, mode, keys2, memos, iterationType);
474464

475-
if (areEq || memos.deleteFailures) {
476-
set.delete(val1);
477-
set.delete(val2);
478-
}
465+
set.delete(val1);
466+
set.delete(val2);
479467

480468
return areEq;
481469
}
482470

483471
function setHasEqualElement(set, val1, mode, memo) {
484-
const { deleteFailures } = memo;
485-
memo.deleteFailures = true;
486472
for (const val2 of set) {
487473
if (innerDeepEqual(val1, val2, mode, memo)) {
488474
// Remove the matching element to make sure we do not check that again.
489475
set.delete(val2);
490-
memo.deleteFailures = deleteFailures;
491476
return true;
492477
}
493478
}
494479

495-
memo.deleteFailures = deleteFailures;
496480
return false;
497481
}
498482

@@ -553,6 +537,8 @@ function partialObjectSetEquiv(a, b, mode, set, memo) {
553537
return false;
554538
}
555539
}
540+
/* c8 ignore next */
541+
assert.fail('Unreachable code');
556542
}
557543

558544
function setObjectEquiv(a, b, mode, set, memo) {
@@ -619,18 +605,14 @@ function mapHasEqualEntry(set, map, key1, item1, mode, memo) {
619605
// To be able to handle cases like:
620606
// Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']])
621607
// ... we need to consider *all* matching keys, not just the first we find.
622-
const { deleteFailures } = memo;
623-
memo.deleteFailures = true;
624608
for (const key2 of set) {
625609
if (innerDeepEqual(key1, key2, mode, memo) &&
626610
innerDeepEqual(item1, map.get(key2), mode, memo)) {
627611
set.delete(key2);
628-
memo.deleteFailures = deleteFailures;
629612
return true;
630613
}
631614
}
632615

633-
memo.deleteFailures = deleteFailures;
634616
return false;
635617
}
636618

@@ -648,6 +630,8 @@ function partialObjectMapEquiv(a, b, mode, set, memo) {
648630
return false;
649631
}
650632
}
633+
/* c8 ignore next */
634+
assert.fail('Unreachable code');
651635
}
652636

653637
function mapObjectEquivalence(a, b, mode, set, memo) {
@@ -743,11 +727,11 @@ function partialSparseArrayEquiv(a, b, mode, memos, startA, startB) {
743727
function partialArrayEquiv(a, b, mode, memos) {
744728
let aPos = 0;
745729
for (let i = 0; i < b.length; i++) {
746-
let isSparse = b[i] === undefined && !ObjectPrototypeHasOwnProperty(b, i);
730+
let isSparse = b[i] === undefined && !hasOwn(b, i);
747731
if (isSparse) {
748732
return partialSparseArrayEquiv(a, b, mode, memos, aPos, i);
749733
}
750-
while (!(isSparse = a[aPos] === undefined && !ObjectPrototypeHasOwnProperty(a, aPos)) &&
734+
while (!(isSparse = a[aPos] === undefined && !hasOwn(a, aPos)) &&
751735
!innerDeepEqual(a[aPos], b[i], mode, memos)) {
752736
aPos++;
753737
if (aPos > a.length - b.length + i) {
@@ -772,8 +756,7 @@ function sparseArrayEquiv(a, b, mode, memos, i) {
772756
}
773757
for (; i < keysA.length; i++) {
774758
const key = keysA[i];
775-
if (!ObjectPrototypeHasOwnProperty(b, key) ||
776-
!innerDeepEqual(a[key], b[key], mode, memos)) {
759+
if (!hasOwn(b, key) || !innerDeepEqual(a[key], b[key], mode, memos)) {
777760
return false;
778761
}
779762
}
@@ -798,8 +781,8 @@ function objEquiv(a, b, mode, keys2, memos, iterationType) {
798781
if (!innerDeepEqual(a[i], b[i], mode, memos)) {
799782
return false;
800783
}
801-
const isSparseA = a[i] === undefined && !ObjectPrototypeHasOwnProperty(a, i);
802-
const isSparseB = b[i] === undefined && !ObjectPrototypeHasOwnProperty(b, i);
784+
const isSparseA = a[i] === undefined && !hasOwn(a, i);
785+
const isSparseB = b[i] === undefined && !hasOwn(b, i);
803786
if (isSparseA !== isSparseB) {
804787
return false;
805788
}

test/parallel/test-assert-deep-with-error.js

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ require('../common');
33
const assert = require('assert');
44
const { test } = require('node:test');
55

6+
// Disable colored output to prevent color codes from breaking assertion
7+
// message comparisons. This should only be an issue when process.stdout
8+
// is a TTY.
9+
if (process.stdout.isTTY) {
10+
process.env.NODE_DISABLE_COLORS = '1';
11+
}
12+
613
const defaultStartMessage = 'Expected values to be strictly deep-equal:\n' +
714
'+ actual - expected\n' +
815
'\n';

test/parallel/test-assert-partial-deep-equal.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,32 @@ describe('Object Comparison Tests', () => {
166166
},
167167
{
168168
description:
169-
'throws when comparing two objects with different Error instances',
169+
'throws when comparing two objects with different Error message',
170170
actual: { error: new Error('Test error 1') },
171171
expected: { error: new Error('Test error 2') },
172172
},
173+
{
174+
description:
175+
'throws when comparing two objects with missing cause on the actual Error',
176+
actual: { error: new Error('Test error 1') },
177+
expected: { error: new Error('Test error 1', { cause: 42 }) },
178+
},
179+
{
180+
description:
181+
'throws when comparing two objects with missing message on the actual Error',
182+
actual: { error: new Error() },
183+
expected: { error: new Error('Test error 1') },
184+
},
185+
{
186+
description: 'throws when comparing two Errors with missing cause on the actual Error',
187+
actual: { error: new Error('Test error 1') },
188+
expected: { error: new Error('Test error 1', { cause: undefined }) },
189+
},
190+
{
191+
description: 'throws when comparing two AggregateErrors with missing message on the actual Error',
192+
actual: { error: new AggregateError([], 'Test error 1') },
193+
expected: { error: new AggregateError([new Error()], 'Test error 1') },
194+
},
173195
{
174196
description:
175197
'throws when comparing two objects with different TypedArray instances and content',
@@ -1095,6 +1117,26 @@ describe('Object Comparison Tests', () => {
10951117
}
10961118
}]
10971119
},
1120+
{
1121+
description: 'comparing two Errors with missing cause on the expected Error',
1122+
actual: { error: new Error('Test error 1', { cause: 42 }) },
1123+
expected: { error: new Error('Test error 1') },
1124+
},
1125+
{
1126+
description: 'comparing two Errors with cause set to undefined on the actual Error',
1127+
actual: { error: new Error('Test error 1', { cause: undefined }) },
1128+
expected: { error: new Error('Test error 1') },
1129+
},
1130+
{
1131+
description: 'comparing two Errors with missing message on the expected Error',
1132+
actual: { error: new Error('Test error 1') },
1133+
expected: { error: new Error() },
1134+
},
1135+
{
1136+
description: 'comparing two AggregateErrors with no message or errors on the expected Error',
1137+
actual: { error: new AggregateError([new Error(), 123]) },
1138+
expected: { error: new AggregateError([]) },
1139+
},
10981140
].forEach(({ description, actual, expected }) => {
10991141
it(description, () => {
11001142
assert.partialDeepStrictEqual(actual, expected);

0 commit comments

Comments
 (0)