Skip to content

Commit 2cc5ee6

Browse files
committed
assert: partialDeepStrictEqual works with ArrayBuffers
Fixes: nodejs#56097
1 parent 3f9c6c0 commit 2cc5ee6

File tree

3 files changed

+141
-76
lines changed

3 files changed

+141
-76
lines changed

lib/assert.js

+120-74
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
'use strict';
2222

2323
const {
24+
ArrayBuffer,
25+
ArrayBufferIsView,
2426
ArrayFrom,
2527
ArrayIsArray,
2628
ArrayPrototypeIndexOf,
@@ -38,6 +40,7 @@ const {
3840
ObjectIs,
3941
ObjectKeys,
4042
ObjectPrototypeIsPrototypeOf,
43+
ObjectPrototypeToString,
4144
ReflectApply,
4245
ReflectHas,
4346
ReflectOwnKeys,
@@ -50,6 +53,7 @@ const {
5053
StringPrototypeSlice,
5154
StringPrototypeSplit,
5255
SymbolIterator,
56+
Uint8Array,
5357
} = primordials;
5458

5559
const {
@@ -73,6 +77,7 @@ const {
7377
isDate,
7478
isWeakSet,
7579
isWeakMap,
80+
isSharedArrayBuffer,
7681
} = require('internal/util/types');
7782
const { isError, deprecate, emitExperimentalWarning } = require('internal/util');
7883
const { innerOk } = require('internal/assert/utils');
@@ -369,9 +374,114 @@ function isSpecial(obj) {
369374
}
370375

371376
const typesToCallDeepStrictEqualWith = [
372-
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer,
377+
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer, isSharedArrayBuffer,
373378
];
374379

380+
function compareMaps(actual, expected, comparedObjects) {
381+
if (actual.size !== expected.size) {
382+
return false;
383+
}
384+
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
385+
386+
comparedObjects ??= new SafeWeakSet();
387+
388+
for (const { 0: key, 1: val } of safeIterator) {
389+
if (!MapPrototypeHas(expected, key)) {
390+
return false;
391+
}
392+
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {
393+
return false;
394+
}
395+
}
396+
return true;
397+
}
398+
399+
function compareArrayBuffers(actual, expected) {
400+
const actualView = ArrayBufferIsView(actual) ? actual : new Uint8Array(actual);
401+
const expectedView = ArrayBufferIsView(expected) ? expected : new Uint8Array(expected);
402+
403+
if (ObjectPrototypeToString(actualView) !== ObjectPrototypeToString(expectedView)) {
404+
return false;
405+
}
406+
407+
// Compare the lengths of the views (not just byte length, but actual element count)
408+
if (expectedView.length > actualView.length) {
409+
return false;
410+
}
411+
412+
for (let i = 0; i < expectedView.length; i++) {
413+
if (actualView[i] !== expectedView[i]) {
414+
return false;
415+
}
416+
}
417+
418+
return true;
419+
}
420+
421+
function compareSets(actual, expected, comparedObjects) {
422+
if (expected.size > actual.size) {
423+
return false; // `expected` can't be a subset if it has more elements
424+
}
425+
426+
if (isDeepEqual === undefined) lazyLoadComparison();
427+
428+
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
429+
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
430+
const usedIndices = new SafeSet();
431+
432+
expectedIteration: for (const expectedItem of expectedIterator) {
433+
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
434+
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
435+
usedIndices.add(actualIdx);
436+
continue expectedIteration;
437+
}
438+
}
439+
return false;
440+
}
441+
442+
return true;
443+
}
444+
445+
function compareArrays(actual, expected, comparedObjects) {
446+
if (expected.length > actual.length) {
447+
return false;
448+
}
449+
450+
if (isDeepEqual === undefined) lazyLoadComparison();
451+
452+
// Create a map to count occurrences of each element in the expected array
453+
const expectedCounts = new SafeMap();
454+
for (const expectedItem of expected) {
455+
let found = false;
456+
for (const { 0: key, 1: count } of expectedCounts) {
457+
if (isDeepStrictEqual(key, expectedItem)) {
458+
MapPrototypeSet(expectedCounts, key, count + 1);
459+
found = true;
460+
break;
461+
}
462+
}
463+
if (!found) {
464+
MapPrototypeSet(expectedCounts, expectedItem, 1);
465+
}
466+
}
467+
468+
// Create a map to count occurrences of relevant elements in the actual array
469+
for (const actualItem of actual) {
470+
for (const { 0: key, 1: count } of expectedCounts) {
471+
if (isDeepStrictEqual(key, actualItem)) {
472+
if (count === 1) {
473+
MapPrototypeDelete(expectedCounts, key);
474+
} else {
475+
MapPrototypeSet(expectedCounts, key, count - 1);
476+
}
477+
break;
478+
}
479+
}
480+
}
481+
482+
return !expectedCounts.size;
483+
}
484+
375485
/**
376486
* Compares two objects or values recursively to check if they are equal.
377487
* @param {any} actual - The actual value to compare.
@@ -388,22 +498,14 @@ function compareBranch(
388498
) {
389499
// Check for Map object equality
390500
if (isMap(actual) && isMap(expected)) {
391-
if (actual.size !== expected.size) {
392-
return false;
393-
}
394-
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
395-
396-
comparedObjects ??= new SafeWeakSet();
501+
return compareMaps(actual, expected, comparedObjects);
502+
}
397503

398-
for (const { 0: key, 1: val } of safeIterator) {
399-
if (!MapPrototypeHas(expected, key)) {
400-
return false;
401-
}
402-
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {
403-
return false;
404-
}
405-
}
406-
return true;
504+
if (
505+
(ArrayBufferIsView(actual) && ArrayBufferIsView(expected)) ||
506+
(actual instanceof ArrayBuffer && expected instanceof ArrayBuffer)
507+
) {
508+
return compareArrayBuffers(actual, expected);
407509
}
408510

409511
for (const type of typesToCallDeepStrictEqualWith) {
@@ -415,68 +517,12 @@ function compareBranch(
415517

416518
// Check for Set object equality
417519
if (isSet(actual) && isSet(expected)) {
418-
if (expected.size > actual.size) {
419-
return false; // `expected` can't be a subset if it has more elements
420-
}
421-
422-
if (isDeepEqual === undefined) lazyLoadComparison();
423-
424-
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
425-
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
426-
const usedIndices = new SafeSet();
427-
428-
expectedIteration: for (const expectedItem of expectedIterator) {
429-
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
430-
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
431-
usedIndices.add(actualIdx);
432-
continue expectedIteration;
433-
}
434-
}
435-
return false;
436-
}
437-
438-
return true;
520+
return compareSets(actual, expected, comparedObjects);
439521
}
440522

441523
// Check if expected array is a subset of actual array
442524
if (ArrayIsArray(actual) && ArrayIsArray(expected)) {
443-
if (expected.length > actual.length) {
444-
return false;
445-
}
446-
447-
if (isDeepEqual === undefined) lazyLoadComparison();
448-
449-
// Create a map to count occurrences of each element in the expected array
450-
const expectedCounts = new SafeMap();
451-
for (const expectedItem of expected) {
452-
let found = false;
453-
for (const { 0: key, 1: count } of expectedCounts) {
454-
if (isDeepStrictEqual(key, expectedItem)) {
455-
MapPrototypeSet(expectedCounts, key, count + 1);
456-
found = true;
457-
break;
458-
}
459-
}
460-
if (!found) {
461-
MapPrototypeSet(expectedCounts, expectedItem, 1);
462-
}
463-
}
464-
465-
// Create a map to count occurrences of relevant elements in the actual array
466-
for (const actualItem of actual) {
467-
for (const { 0: key, 1: count } of expectedCounts) {
468-
if (isDeepStrictEqual(key, actualItem)) {
469-
if (count === 1) {
470-
MapPrototypeDelete(expectedCounts, key);
471-
} else {
472-
MapPrototypeSet(expectedCounts, key, count - 1);
473-
}
474-
break;
475-
}
476-
}
477-
}
478-
479-
return !expectedCounts.size;
525+
return compareArrays(actual, expected, comparedObjects);
480526
}
481527

482528
// Comparison done when at least one of the values is not an object

test/parallel/test-assert-objects.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@ describe('Object Comparison Tests', () => {
207207
actual: [1, 2, 3],
208208
expected: ['2'],
209209
},
210+
{
211+
description: 'throws when comparing a ArrayBuffer with a SharedArrayBuffer',
212+
actual: new ArrayBuffer(3),
213+
expected: new SharedArrayBuffer(3),
214+
},
215+
{
216+
description: 'throws when comparing an Int16Array with a Uint16Array',
217+
actual: new Int16Array(3),
218+
expected: new Uint16Array(3),
219+
},
210220
];
211221

212222
if (common.hasCrypto) {
@@ -343,10 +353,15 @@ describe('Object Comparison Tests', () => {
343353
expected: { error: new Error('Test error') },
344354
},
345355
{
346-
description: 'compares two objects with TypedArray instances with the same content',
347-
actual: { typedArray: new Uint8Array([1, 2, 3]) },
356+
description: 'compares two Uint8Array objects',
357+
actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) },
348358
expected: { typedArray: new Uint8Array([1, 2, 3]) },
349359
},
360+
{
361+
description: 'compares two Int16Array objects',
362+
actual: { typedArray: new Int16Array([1, 2, 3, 4, 5]) },
363+
expected: { typedArray: new Int16Array([1, 2, 3]) },
364+
},
350365
{
351366
description: 'compares two Map objects with identical entries',
352367
actual: new Map([

test/parallel/test-assert-typedarray-deepequal.js

+4
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ suite('notEqualArrayPairs', () => {
9999
makeBlock(assert.deepStrictEqual, arrayPair[0], arrayPair[1]),
100100
assert.AssertionError
101101
);
102+
assert.throws(
103+
makeBlock(assert.partialDeepStrictEqual, arrayPair[0], arrayPair[1]),
104+
assert.AssertionError
105+
);
102106
});
103107
}
104108
});

0 commit comments

Comments
 (0)