assert: make partialDeepStrictEqual work with ArrayBuffers

Fixes: https://github.com/nodejs/node/issues/56097
PR-URL: https://github.com/nodejs/node/pull/56098
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Giovanni Bucci
2024-12-08 23:41:26 +01:00
committed by GitHub
parent 4f51d461d1
commit dbfcbe371c
3 changed files with 353 additions and 79 deletions

View File

@@ -21,35 +21,44 @@
'use strict'; 'use strict';
const { const {
ArrayBufferIsView,
ArrayBufferPrototypeGetByteLength,
ArrayFrom, ArrayFrom,
ArrayIsArray, ArrayIsArray,
ArrayPrototypeIndexOf, ArrayPrototypeIndexOf,
ArrayPrototypeJoin, ArrayPrototypeJoin,
ArrayPrototypePush, ArrayPrototypePush,
ArrayPrototypeSlice, ArrayPrototypeSlice,
DataViewPrototypeGetBuffer,
DataViewPrototypeGetByteLength,
DataViewPrototypeGetByteOffset,
Error, Error,
FunctionPrototypeCall, FunctionPrototypeCall,
MapPrototypeDelete,
MapPrototypeGet, MapPrototypeGet,
MapPrototypeGetSize,
MapPrototypeHas, MapPrototypeHas,
MapPrototypeSet,
NumberIsNaN, NumberIsNaN,
ObjectAssign, ObjectAssign,
ObjectIs, ObjectIs,
ObjectKeys, ObjectKeys,
ObjectPrototypeIsPrototypeOf, ObjectPrototypeIsPrototypeOf,
ObjectPrototypeToString,
ReflectApply, ReflectApply,
ReflectHas, ReflectHas,
ReflectOwnKeys, ReflectOwnKeys,
RegExpPrototypeExec, RegExpPrototypeExec,
SafeArrayIterator,
SafeMap, SafeMap,
SafeSet, SafeSet,
SafeWeakSet, SafeWeakSet,
SetPrototypeGetSize,
String, String,
StringPrototypeIndexOf, StringPrototypeIndexOf,
StringPrototypeSlice, StringPrototypeSlice,
StringPrototypeSplit, StringPrototypeSplit,
SymbolIterator, SymbolIterator,
TypedArrayPrototypeGetLength,
Uint8Array,
} = primordials; } = primordials;
const { const {
@@ -65,6 +74,8 @@ const AssertionError = require('internal/assert/assertion_error');
const { inspect } = require('internal/util/inspect'); const { inspect } = require('internal/util/inspect');
const { Buffer } = require('buffer'); const { Buffer } = require('buffer');
const { const {
isArrayBuffer,
isDataView,
isKeyObject, isKeyObject,
isPromise, isPromise,
isRegExp, isRegExp,
@@ -73,6 +84,8 @@ const {
isDate, isDate,
isWeakSet, isWeakSet,
isWeakMap, isWeakMap,
isSharedArrayBuffer,
isAnyArrayBuffer,
} = require('internal/util/types'); } = require('internal/util/types');
const { isError, deprecate, emitExperimentalWarning } = require('internal/util'); const { isError, deprecate, emitExperimentalWarning } = require('internal/util');
const { innerOk } = require('internal/assert/utils'); const { innerOk } = require('internal/assert/utils');
@@ -369,9 +382,161 @@ function isSpecial(obj) {
} }
const typesToCallDeepStrictEqualWith = [ const typesToCallDeepStrictEqualWith = [
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer, isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer, isSharedArrayBuffer,
]; ];
function compareMaps(actual, expected, comparedObjects) {
if (MapPrototypeGetSize(actual) !== MapPrototypeGetSize(expected)) {
return false;
}
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
comparedObjects ??= new SafeWeakSet();
for (const { 0: key, 1: val } of safeIterator) {
if (!MapPrototypeHas(expected, key)) {
return false;
}
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {
return false;
}
}
return true;
}
function partiallyCompareArrayBuffersOrViews(actual, expected) {
let actualView, expectedView, expectedViewLength;
if (!ArrayBufferIsView(actual)) {
let actualViewLength;
if (isArrayBuffer(actual) && isArrayBuffer(expected)) {
actualViewLength = ArrayBufferPrototypeGetByteLength(actual);
expectedViewLength = ArrayBufferPrototypeGetByteLength(expected);
} else if (isSharedArrayBuffer(actual) && isSharedArrayBuffer(expected)) {
actualViewLength = actual.byteLength;
expectedViewLength = expected.byteLength;
} else {
// Cannot compare ArrayBuffers with SharedArrayBuffers
return false;
}
if (expectedViewLength > actualViewLength) {
return false;
}
actualView = new Uint8Array(actual);
expectedView = new Uint8Array(expected);
} else if (isDataView(actual)) {
if (!isDataView(expected)) {
return false;
}
const actualByteLength = DataViewPrototypeGetByteLength(actual);
expectedViewLength = DataViewPrototypeGetByteLength(expected);
if (expectedViewLength > actualByteLength) {
return false;
}
actualView = new Uint8Array(
DataViewPrototypeGetBuffer(actual),
DataViewPrototypeGetByteOffset(actual),
actualByteLength,
);
expectedView = new Uint8Array(
DataViewPrototypeGetBuffer(expected),
DataViewPrototypeGetByteOffset(expected),
expectedViewLength,
);
} else {
if (ObjectPrototypeToString(actual) !== ObjectPrototypeToString(expected)) {
return false;
}
actualView = actual;
expectedView = expected;
expectedViewLength = TypedArrayPrototypeGetLength(expected);
if (expectedViewLength > TypedArrayPrototypeGetLength(actual)) {
return false;
}
}
for (let i = 0; i < expectedViewLength; i++) {
if (actualView[i] !== expectedView[i]) {
return false;
}
}
return true;
}
function partiallyCompareSets(actual, expected, comparedObjects) {
if (SetPrototypeGetSize(expected) > SetPrototypeGetSize(actual)) {
return false; // `expected` can't be a subset if it has more elements
}
if (isDeepEqual === undefined) lazyLoadComparison();
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
const usedIndices = new SafeSet();
expectedIteration: for (const expectedItem of expectedIterator) {
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
usedIndices.add(actualIdx);
continue expectedIteration;
}
}
return false;
}
return true;
}
function partiallyCompareArrays(actual, expected, comparedObjects) {
if (expected.length > actual.length) {
return false;
}
if (isDeepEqual === undefined) lazyLoadComparison();
// Create a map to count occurrences of each element in the expected array
const expectedCounts = new SafeMap();
for (const expectedItem of expected) {
let found = false;
for (const { 0: key, 1: count } of expectedCounts) {
if (isDeepStrictEqual(key, expectedItem)) {
expectedCounts.set(key, count + 1);
found = true;
break;
}
}
if (!found) {
expectedCounts.set(expectedItem, 1);
}
}
const safeActual = new SafeArrayIterator(actual);
// Create a map to count occurrences of relevant elements in the actual array
for (const actualItem of safeActual) {
for (const { 0: key, 1: count } of expectedCounts) {
if (isDeepStrictEqual(key, actualItem)) {
if (count === 1) {
expectedCounts.delete(key);
} else {
expectedCounts.set(key, count - 1);
}
break;
}
}
}
const { size } = expectedCounts;
expectedCounts.clear();
return size === 0;
}
/** /**
* Compares two objects or values recursively to check if they are equal. * Compares two objects or values recursively to check if they are equal.
* @param {any} actual - The actual value to compare. * @param {any} actual - The actual value to compare.
@@ -388,22 +553,16 @@ function compareBranch(
) { ) {
// Check for Map object equality // Check for Map object equality
if (isMap(actual) && isMap(expected)) { if (isMap(actual) && isMap(expected)) {
if (actual.size !== expected.size) { return compareMaps(actual, expected, comparedObjects);
return false; }
}
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);
comparedObjects ??= new SafeWeakSet(); if (
ArrayBufferIsView(actual) ||
for (const { 0: key, 1: val } of safeIterator) { isAnyArrayBuffer(actual) ||
if (!MapPrototypeHas(expected, key)) { ArrayBufferIsView(expected) ||
return false; isAnyArrayBuffer(expected)
} ) {
if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) { return partiallyCompareArrayBuffersOrViews(actual, expected);
return false;
}
}
return true;
} }
for (const type of typesToCallDeepStrictEqualWith) { for (const type of typesToCallDeepStrictEqualWith) {
@@ -415,68 +574,12 @@ function compareBranch(
// Check for Set object equality // Check for Set object equality
if (isSet(actual) && isSet(expected)) { if (isSet(actual) && isSet(expected)) {
if (expected.size > actual.size) { return partiallyCompareSets(actual, expected, comparedObjects);
return false; // `expected` can't be a subset if it has more elements
}
if (isDeepEqual === undefined) lazyLoadComparison();
const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
const usedIndices = new SafeSet();
expectedIteration: for (const expectedItem of expectedIterator) {
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
usedIndices.add(actualIdx);
continue expectedIteration;
}
}
return false;
}
return true;
} }
// Check if expected array is a subset of actual array // Check if expected array is a subset of actual array
if (ArrayIsArray(actual) && ArrayIsArray(expected)) { if (ArrayIsArray(actual) && ArrayIsArray(expected)) {
if (expected.length > actual.length) { return partiallyCompareArrays(actual, expected, comparedObjects);
return false;
}
if (isDeepEqual === undefined) lazyLoadComparison();
// Create a map to count occurrences of each element in the expected array
const expectedCounts = new SafeMap();
for (const expectedItem of expected) {
let found = false;
for (const { 0: key, 1: count } of expectedCounts) {
if (isDeepStrictEqual(key, expectedItem)) {
MapPrototypeSet(expectedCounts, key, count + 1);
found = true;
break;
}
}
if (!found) {
MapPrototypeSet(expectedCounts, expectedItem, 1);
}
}
// Create a map to count occurrences of relevant elements in the actual array
for (const actualItem of actual) {
for (const { 0: key, 1: count } of expectedCounts) {
if (isDeepStrictEqual(key, actualItem)) {
if (count === 1) {
MapPrototypeDelete(expectedCounts, key);
} else {
MapPrototypeSet(expectedCounts, key, count - 1);
}
break;
}
}
}
return !expectedCounts.size;
} }
// Comparison done when at least one of the values is not an object // Comparison done when at least one of the values is not an object

View File

@@ -39,10 +39,15 @@ describe('Object Comparison Tests', () => {
describe('throws an error', () => { describe('throws an error', () => {
const tests = [ const tests = [
{ {
description: 'throws when only one argument is provided', description: 'throws when only actual is provided',
actual: { a: 1 }, actual: { a: 1 },
expected: undefined, expected: undefined,
}, },
{
description: 'throws when only expected is provided',
actual: undefined,
expected: { a: 1 },
},
{ {
description: 'throws when expected has more properties than actual', description: 'throws when expected has more properties than actual',
actual: [1, 'two'], actual: [1, 'two'],
@@ -207,6 +212,74 @@ describe('Object Comparison Tests', () => {
actual: [1, 2, 3], actual: [1, 2, 3],
expected: ['2'], expected: ['2'],
}, },
{
description: 'throws when comparing an ArrayBuffer with a Uint8Array',
actual: new ArrayBuffer(3),
expected: new Uint8Array(3),
},
{
description: 'throws when comparing a ArrayBuffer with a SharedArrayBuffer',
actual: new ArrayBuffer(3),
expected: new SharedArrayBuffer(3),
},
{
description: 'throws when comparing a SharedArrayBuffer with an ArrayBuffer',
actual: new SharedArrayBuffer(3),
expected: new ArrayBuffer(3),
},
{
description: 'throws when comparing an Int16Array with a Uint16Array',
actual: new Int16Array(3),
expected: new Uint16Array(3),
},
{
description: 'throws when comparing two dataviews with different buffers',
actual: { dataView: new DataView(new ArrayBuffer(3)) },
expected: { dataView: new DataView(new ArrayBuffer(4)) },
},
{
description: 'throws because expected Uint8Array(SharedArrayBuffer) is not a subset of actual',
actual: { typedArray: new Uint8Array(new SharedArrayBuffer(3)) },
expected: { typedArray: new Uint8Array(new SharedArrayBuffer(5)) },
},
{
description: 'throws because expected SharedArrayBuffer is not a subset of actual',
actual: { typedArray: new SharedArrayBuffer(3) },
expected: { typedArray: new SharedArrayBuffer(5) },
},
{
description: 'throws when comparing a DataView with a TypedArray',
actual: { dataView: new DataView(new ArrayBuffer(3)) },
expected: { dataView: new Uint8Array(3) },
},
{
description: 'throws when comparing a TypedArray with a DataView',
actual: { dataView: new Uint8Array(3) },
expected: { dataView: new DataView(new ArrayBuffer(3)) },
},
{
description: 'throws when comparing SharedArrayBuffers when expected has different elements actual',
actual: (() => {
const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
const sharedArray = new Int32Array(sharedBuffer);
sharedArray[0] = 1;
sharedArray[1] = 2;
sharedArray[2] = 3;
return sharedBuffer;
})(),
expected: (() => {
const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
const sharedArray = new Int32Array(sharedBuffer);
sharedArray[0] = 1;
sharedArray[1] = 2;
sharedArray[2] = 6;
return sharedBuffer;
})(),
},
]; ];
if (common.hasCrypto) { if (common.hasCrypto) {
@@ -343,10 +416,89 @@ describe('Object Comparison Tests', () => {
expected: { error: new Error('Test error') }, expected: { error: new Error('Test error') },
}, },
{ {
description: 'compares two objects with TypedArray instances with the same content', description: 'compares two Uint8Array objects',
actual: { typedArray: new Uint8Array([1, 2, 3]) }, actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) },
expected: { typedArray: new Uint8Array([1, 2, 3]) }, expected: { typedArray: new Uint8Array([1, 2, 3]) },
}, },
{
description: 'compares two Int16Array objects',
actual: { typedArray: new Int16Array([1, 2, 3, 4, 5]) },
expected: { typedArray: new Int16Array([1, 2, 3]) },
},
{
description: 'compares two DataView objects with the same buffer and different views',
actual: { dataView: new DataView(new ArrayBuffer(8), 0, 4) },
expected: { dataView: new DataView(new ArrayBuffer(8), 4, 4) },
},
{
description: 'compares two DataView objects with different buffers',
actual: { dataView: new DataView(new ArrayBuffer(8)) },
expected: { dataView: new DataView(new ArrayBuffer(8)) },
},
{
description: 'compares two DataView objects with the same buffer and same views',
actual: { dataView: new DataView(new ArrayBuffer(8), 0, 8) },
expected: { dataView: new DataView(new ArrayBuffer(8), 0, 8) },
},
{
description: 'compares two SharedArrayBuffers with the same length',
actual: new SharedArrayBuffer(3),
expected: new SharedArrayBuffer(3),
},
{
description: 'compares two Uint8Array objects from SharedArrayBuffer',
actual: { typedArray: new Uint8Array(new SharedArrayBuffer(5)) },
expected: { typedArray: new Uint8Array(new SharedArrayBuffer(3)) },
},
{
description: 'compares two Int16Array objects from SharedArrayBuffer',
actual: { typedArray: new Int16Array(new SharedArrayBuffer(10)) },
expected: { typedArray: new Int16Array(new SharedArrayBuffer(6)) },
},
{
description: 'compares two DataView objects with the same SharedArrayBuffer and different views',
actual: { dataView: new DataView(new SharedArrayBuffer(8), 0, 4) },
expected: { dataView: new DataView(new SharedArrayBuffer(8), 4, 4) },
},
{
description: 'compares two DataView objects with different SharedArrayBuffers',
actual: { dataView: new DataView(new SharedArrayBuffer(8)) },
expected: { dataView: new DataView(new SharedArrayBuffer(8)) },
},
{
description: 'compares two DataView objects with the same SharedArrayBuffer and same views',
actual: { dataView: new DataView(new SharedArrayBuffer(8), 0, 8) },
expected: { dataView: new DataView(new SharedArrayBuffer(8), 0, 8) },
},
{
description: 'compares two SharedArrayBuffers',
actual: { typedArray: new SharedArrayBuffer(5) },
expected: { typedArray: new SharedArrayBuffer(3) },
},
{
description: 'compares two SharedArrayBuffers with data inside',
actual: (() => {
const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
const sharedArray = new Int32Array(sharedBuffer);
sharedArray[0] = 1;
sharedArray[1] = 2;
sharedArray[2] = 3;
sharedArray[3] = 4;
return sharedBuffer;
})(),
expected: (() => {
const sharedBuffer = new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT);
const sharedArray = new Int32Array(sharedBuffer);
sharedArray[0] = 1;
sharedArray[1] = 2;
sharedArray[2] = 3;
return sharedBuffer;
})(),
},
{ {
description: 'compares two Map objects with identical entries', description: 'compares two Map objects with identical entries',
actual: new Map([ actual: new Map([
@@ -358,6 +510,19 @@ describe('Object Comparison Tests', () => {
['key2', 'value2'], ['key2', 'value2'],
]), ]),
}, },
{
description: 'compares two Map where one is a subset of the other',
actual: new Map([
['key1', { nested: { property: true } }],
['key2', new Set([1, 2, 3])],
['key3', new Uint8Array([1, 2, 3])],
]),
expected: new Map([
['key1', { nested: { property: true } }],
['key2', new Set([1, 2, 3])],
['key3', new Uint8Array([1, 2, 3])],
])
},
{ {
describe: 'compares two array of objects', describe: 'compares two array of objects',
actual: [{ a: 5 }], actual: [{ a: 5 }],

View File

@@ -86,6 +86,8 @@ suite('notEqualArrayPairs', () => {
new Uint8Array(new ArrayBuffer(3)).fill(1).buffer, new Uint8Array(new ArrayBuffer(3)).fill(1).buffer,
new Uint8Array(new SharedArrayBuffer(3)).fill(2).buffer, new Uint8Array(new SharedArrayBuffer(3)).fill(2).buffer,
], ],
[new ArrayBuffer(3), new SharedArrayBuffer(3)],
[new SharedArrayBuffer(2), new ArrayBuffer(2)],
]; ];
for (const arrayPair of notEqualArrayPairs) { for (const arrayPair of notEqualArrayPairs) {
@@ -99,6 +101,10 @@ suite('notEqualArrayPairs', () => {
makeBlock(assert.deepStrictEqual, arrayPair[0], arrayPair[1]), makeBlock(assert.deepStrictEqual, arrayPair[0], arrayPair[1]),
assert.AssertionError assert.AssertionError
); );
assert.throws(
makeBlock(assert.partialDeepStrictEqual, arrayPair[0], arrayPair[1]),
assert.AssertionError
);
}); });
} }
}); });