Skip to content

Commit 14557cc

Browse files
committed
assert: make partialDeepStrictEqual work with ArrayBuffers
Fixes: nodejs#56097
1 parent 3f9c6c0 commit 14557cc

File tree

3 files changed

+215
-77
lines changed

3 files changed

+215
-77
lines changed

lib/assert.js

+155-74
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@
2121
'use strict';
2222

2323
const {
24+
ArrayBufferIsView,
25+
ArrayBufferPrototypeGetByteLength,
2426
ArrayFrom,
2527
ArrayIsArray,
2628
ArrayPrototypeIndexOf,
2729
ArrayPrototypeJoin,
2830
ArrayPrototypePush,
2931
ArrayPrototypeSlice,
32+
DataViewPrototypeGetBuffer,
33+
DataViewPrototypeGetByteLength,
34+
DataViewPrototypeGetByteOffset,
3035
Error,
3136
FunctionPrototypeCall,
3237
MapPrototypeDelete,
@@ -38,6 +43,7 @@ const {
3843
ObjectIs,
3944
ObjectKeys,
4045
ObjectPrototypeIsPrototypeOf,
46+
ObjectPrototypeToString,
4147
ReflectApply,
4248
ReflectHas,
4349
ReflectOwnKeys,
@@ -50,6 +56,8 @@ const {
5056
StringPrototypeSlice,
5157
StringPrototypeSplit,
5258
SymbolIterator,
59+
TypedArrayPrototypeGetLength,
60+
Uint8Array,
5361
} = primordials;
5462

5563
const {
@@ -65,6 +73,8 @@ const AssertionError = require('internal/assert/assertion_error');
6573
const { inspect } = require('internal/util/inspect');
6674
const { Buffer } = require('buffer');
6775
const {
76+
isArrayBuffer,
77+
isDataView,
6878
isKeyObject,
6979
isPromise,
7080
isRegExp,
@@ -73,6 +83,7 @@ const {
7383
isDate,
7484
isWeakSet,
7585
isWeakMap,
86+
isSharedArrayBuffer,
7687
} = require('internal/util/types');
7788
const { isError, deprecate, emitExperimentalWarning } = require('internal/util');
7889
const { innerOk } = require('internal/assert/utils');
@@ -369,9 +380,143 @@ function isSpecial(obj) {
369380
}
370381

371382
const typesToCallDeepStrictEqualWith = [
372-
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer,
383+
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer, isSharedArrayBuffer,
373384
];
374385

386+
function compareMaps(actual, expected, comparedObjects) {
387+
if (actual.size !== expected.size) {
388+
return false;
389+
}
390+
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
391+
392+
comparedObjects ??= new SafeWeakSet();
393+
394+
for (const { 0: key, 1: val } of safeIterator) {
395+
if (!MapPrototypeHas(expected, key)) {
396+
return false;
397+
}
398+
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {
399+
return false;
400+
}
401+
}
402+
return true;
403+
}
404+
405+
function compareArrayBuffers(actual, expected) {
406+
let actualView, expectedView, expectedViewLength;
407+
408+
if (!ArrayBufferIsView(actual)) {
409+
expectedViewLength = ArrayBufferPrototypeGetByteLength(expected);
410+
411+
if (expectedViewLength > ArrayBufferPrototypeGetByteLength(actual)) {
412+
return false;
413+
}
414+
actualView = new Uint8Array(actual);
415+
expectedView = new Uint8Array(expected);
416+
417+
} else if (isDataView(actual)) {
418+
const actualByteLength = DataViewPrototypeGetByteLength(actual);
419+
expectedViewLength = DataViewPrototypeGetByteLength(expected);
420+
if (expectedViewLength > actualByteLength) {
421+
return false;
422+
}
423+
424+
actualView = new Uint8Array(
425+
DataViewPrototypeGetBuffer(actual),
426+
DataViewPrototypeGetByteOffset(actual),
427+
actualByteLength,
428+
);
429+
expectedView = new Uint8Array(
430+
DataViewPrototypeGetBuffer(expected),
431+
DataViewPrototypeGetByteOffset(expected),
432+
expectedViewLength,
433+
);
434+
} else {
435+
if (ObjectPrototypeToString(actual) !== ObjectPrototypeToString(expected)) {
436+
return false;
437+
}
438+
actualView = actual;
439+
expectedView = expected;
440+
expectedViewLength = TypedArrayPrototypeGetLength(expected);
441+
442+
if (expectedViewLength > TypedArrayPrototypeGetLength(actual)) {
443+
return false;
444+
}
445+
}
446+
447+
for (let i = 0; i < expectedViewLength; i++) {
448+
if (actualView[i] !== expectedView[i]) {
449+
return false;
450+
}
451+
}
452+
453+
return true;
454+
}
455+
456+
function compareSets(actual, expected, comparedObjects) {
457+
if (expected.size > actual.size) {
458+
return false; // `expected` can't be a subset if it has more elements
459+
}
460+
461+
if (isDeepEqual === undefined) lazyLoadComparison();
462+
463+
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
464+
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
465+
const usedIndices = new SafeSet();
466+
467+
expectedIteration: for (const expectedItem of expectedIterator) {
468+
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
469+
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
470+
usedIndices.add(actualIdx);
471+
continue expectedIteration;
472+
}
473+
}
474+
return false;
475+
}
476+
477+
return true;
478+
}
479+
480+
function compareArrays(actual, expected, comparedObjects) {
481+
if (expected.length > actual.length) {
482+
return false;
483+
}
484+
485+
if (isDeepEqual === undefined) lazyLoadComparison();
486+
487+
// Create a map to count occurrences of each element in the expected array
488+
const expectedCounts = new SafeMap();
489+
for (const expectedItem of expected) {
490+
let found = false;
491+
for (const { 0: key, 1: count } of expectedCounts) {
492+
if (isDeepStrictEqual(key, expectedItem)) {
493+
MapPrototypeSet(expectedCounts, key, count + 1);
494+
found = true;
495+
break;
496+
}
497+
}
498+
if (!found) {
499+
MapPrototypeSet(expectedCounts, expectedItem, 1);
500+
}
501+
}
502+
503+
// Create a map to count occurrences of relevant elements in the actual array
504+
for (const actualItem of actual) {
505+
for (const { 0: key, 1: count } of expectedCounts) {
506+
if (isDeepStrictEqual(key, actualItem)) {
507+
if (count === 1) {
508+
MapPrototypeDelete(expectedCounts, key);
509+
} else {
510+
MapPrototypeSet(expectedCounts, key, count - 1);
511+
}
512+
break;
513+
}
514+
}
515+
}
516+
517+
return !expectedCounts.size;
518+
}
519+
375520
/**
376521
* Compares two objects or values recursively to check if they are equal.
377522
* @param {any} actual - The actual value to compare.
@@ -388,22 +533,14 @@ function compareBranch(
388533
) {
389534
// Check for Map object equality
390535
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();
536+
return compareMaps(actual, expected, comparedObjects);
537+
}
397538

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;
539+
if (
540+
(ArrayBufferIsView(actual) && ArrayBufferIsView(expected)) ||
541+
(isArrayBuffer(actual) && isArrayBuffer(expected))
542+
) {
543+
return compareArrayBuffers(actual, expected);
407544
}
408545

409546
for (const type of typesToCallDeepStrictEqualWith) {
@@ -415,68 +552,12 @@ function compareBranch(
415552

416553
// Check for Set object equality
417554
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;
555+
return compareSets(actual, expected, comparedObjects);
439556
}
440557

441558
// Check if expected array is a subset of actual array
442559
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;
560+
return compareArrays(actual, expected, comparedObjects);
480561
}
481562

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

test/parallel/test-assert-objects.js

+56-3
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,15 @@ describe('Object Comparison Tests', () => {
3939
describe('throws an error', () => {
4040
const tests = [
4141
{
42-
description: 'throws when only one argument is provided',
42+
description: 'throws when only actual is provided',
4343
actual: { a: 1 },
4444
expected: undefined,
4545
},
46+
{
47+
description: 'throws when only expected is provided',
48+
actual: undefined,
49+
expected: { a: 1 },
50+
},
4651
{
4752
description: 'throws when expected has more properties than actual',
4853
actual: [1, 'two'],
@@ -207,6 +212,21 @@ describe('Object Comparison Tests', () => {
207212
actual: [1, 2, 3],
208213
expected: ['2'],
209214
},
215+
{
216+
description: 'throws when comparing a ArrayBuffer with a SharedArrayBuffer',
217+
actual: new ArrayBuffer(3),
218+
expected: new SharedArrayBuffer(3),
219+
},
220+
{
221+
description: 'throws when comparing an Int16Array with a Uint16Array',
222+
actual: new Int16Array(3),
223+
expected: new Uint16Array(3),
224+
},
225+
{
226+
description: 'throws when comparing two dataviews with different buffers',
227+
actual: { dataView: new DataView(new ArrayBuffer(3)) },
228+
expected: { dataView: new DataView(new ArrayBuffer(4)) },
229+
},
210230
];
211231

212232
if (common.hasCrypto) {
@@ -343,10 +363,30 @@ describe('Object Comparison Tests', () => {
343363
expected: { error: new Error('Test error') },
344364
},
345365
{
346-
description: 'compares two objects with TypedArray instances with the same content',
347-
actual: { typedArray: new Uint8Array([1, 2, 3]) },
366+
description: 'compares two Uint8Array objects',
367+
actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) },
348368
expected: { typedArray: new Uint8Array([1, 2, 3]) },
349369
},
370+
{
371+
description: 'compares two Int16Array objects',
372+
actual: { typedArray: new Int16Array([1, 2, 3, 4, 5]) },
373+
expected: { typedArray: new Int16Array([1, 2, 3]) },
374+
},
375+
{
376+
description: 'compares two DataView objects with the same buffer and different views',
377+
actual: { dataView: new DataView(new ArrayBuffer(8), 0, 4) },
378+
expected: { dataView: new DataView(new ArrayBuffer(8), 4, 4) },
379+
},
380+
{
381+
description: 'compares two DataView objects with different buffers',
382+
actual: { dataView: new DataView(new ArrayBuffer(8)) },
383+
expected: { dataView: new DataView(new ArrayBuffer(8)) },
384+
},
385+
{
386+
description: 'compares two DataView objects with the same buffer and same views',
387+
actual: { dataView: new DataView(new ArrayBuffer(8), 0, 8) },
388+
expected: { dataView: new DataView(new ArrayBuffer(8), 0, 8) },
389+
},
350390
{
351391
description: 'compares two Map objects with identical entries',
352392
actual: new Map([
@@ -358,6 +398,19 @@ describe('Object Comparison Tests', () => {
358398
['key2', 'value2'],
359399
]),
360400
},
401+
{
402+
description: 'compares two Map where one is a subset of the other',
403+
actual: new Map([
404+
['key1', { nested: { property: true } }],
405+
['key2', new Set([1, 2, 3])],
406+
['key3', new Uint8Array([1, 2, 3])],
407+
]),
408+
expected: new Map([
409+
['key1', { nested: { property: true } }],
410+
['key2', new Set([1, 2, 3])],
411+
['key3', new Uint8Array([1, 2, 3])],
412+
])
413+
},
361414
{
362415
describe: 'compares two array of objects',
363416
actual: [{ a: 5 }],

0 commit comments

Comments
 (0)