Skip to content

Commit

Permalink
assert: partialDeepStrictEqual works with ArrayBuffers
Browse files Browse the repository at this point in the history
Fixes: #56097
  • Loading branch information
puskin94 committed Dec 1, 2024
1 parent 3f9c6c0 commit 2cc5ee6
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 76 deletions.
194 changes: 120 additions & 74 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
'use strict';

const {
ArrayBuffer,
ArrayBufferIsView,
ArrayFrom,
ArrayIsArray,
ArrayPrototypeIndexOf,
Expand All @@ -38,6 +40,7 @@ const {
ObjectIs,
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
ObjectPrototypeToString,
ReflectApply,
ReflectHas,
ReflectOwnKeys,
Expand All @@ -50,6 +53,7 @@ const {
StringPrototypeSlice,
StringPrototypeSplit,
SymbolIterator,
Uint8Array,
} = primordials;

const {
Expand All @@ -73,6 +77,7 @@ const {
isDate,
isWeakSet,
isWeakMap,
isSharedArrayBuffer,
} = require('internal/util/types');
const { isError, deprecate, emitExperimentalWarning } = require('internal/util');
const { innerOk } = require('internal/assert/utils');
Expand Down Expand Up @@ -369,9 +374,114 @@ function isSpecial(obj) {
}

const typesToCallDeepStrictEqualWith = [
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer,
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer, isSharedArrayBuffer,
];

function compareMaps(actual, expected, comparedObjects) {
if (actual.size !== expected.size) {
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 compareArrayBuffers(actual, expected) {
const actualView = ArrayBufferIsView(actual) ? actual : new Uint8Array(actual);
const expectedView = ArrayBufferIsView(expected) ? expected : new Uint8Array(expected);

if (ObjectPrototypeToString(actualView) !== ObjectPrototypeToString(expectedView)) {
return false;
}

// Compare the lengths of the views (not just byte length, but actual element count)
if (expectedView.length > actualView.length) {
return false;
}

for (let i = 0; i < expectedView.length; i++) {
if (actualView[i] !== expectedView[i]) {
return false;
}
}

return true;
}

function compareSets(actual, expected, comparedObjects) {
if (expected.size > actual.size) {
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 compareArrays(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)) {
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;
}

/**
* Compares two objects or values recursively to check if they are equal.
* @param {any} actual - The actual value to compare.
Expand All @@ -388,22 +498,14 @@ function compareBranch(
) {
// Check for Map object equality
if (isMap(actual) && isMap(expected)) {
if (actual.size !== expected.size) {
return false;
}
const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);

comparedObjects ??= new SafeWeakSet();
return compareMaps(actual, expected, comparedObjects);
}

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;
if (
(ArrayBufferIsView(actual) && ArrayBufferIsView(expected)) ||
(actual instanceof ArrayBuffer && expected instanceof ArrayBuffer)
) {
return compareArrayBuffers(actual, expected);
}

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

// Check for Set object equality
if (isSet(actual) && isSet(expected)) {
if (expected.size > actual.size) {
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;
return compareSets(actual, expected, comparedObjects);
}

// Check if expected array is a subset of actual array
if (ArrayIsArray(actual) && ArrayIsArray(expected)) {
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)) {
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;
return compareArrays(actual, expected, comparedObjects);
}

// Comparison done when at least one of the values is not an object
Expand Down
19 changes: 17 additions & 2 deletions test/parallel/test-assert-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@ describe('Object Comparison Tests', () => {
actual: [1, 2, 3],
expected: ['2'],
},
{
description: 'throws when comparing a ArrayBuffer with a SharedArrayBuffer',
actual: new ArrayBuffer(3),
expected: new SharedArrayBuffer(3),
},
{
description: 'throws when comparing an Int16Array with a Uint16Array',
actual: new Int16Array(3),
expected: new Uint16Array(3),
},
];

if (common.hasCrypto) {
Expand Down Expand Up @@ -343,10 +353,15 @@ describe('Object Comparison Tests', () => {
expected: { error: new Error('Test error') },
},
{
description: 'compares two objects with TypedArray instances with the same content',
actual: { typedArray: new Uint8Array([1, 2, 3]) },
description: 'compares two Uint8Array objects',
actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) },
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 Map objects with identical entries',
actual: new Map([
Expand Down
4 changes: 4 additions & 0 deletions test/parallel/test-assert-typedarray-deepequal.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ suite('notEqualArrayPairs', () => {
makeBlock(assert.deepStrictEqual, arrayPair[0], arrayPair[1]),
assert.AssertionError
);
assert.throws(
makeBlock(assert.partialDeepStrictEqual, arrayPair[0], arrayPair[1]),
assert.AssertionError
);
});
}
});

0 comments on commit 2cc5ee6

Please sign in to comment.