From cfbdff7b45d8a7572891f857af79ae98a7d3e01c Mon Sep 17 00:00:00 2001 From: Giovanni Bucci Date: Sun, 8 Dec 2024 23:41:26 +0100 Subject: [PATCH] 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 --- lib/assert.js | 255 ++++++++++++------ test/parallel/test-assert-objects.js | 171 +++++++++++- .../test-assert-typedarray-deepequal.js | 6 + 3 files changed, 353 insertions(+), 79 deletions(-) diff --git a/lib/assert.js b/lib/assert.js index 3e212a1c3aebbe..ff1f1204f43057 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -21,35 +21,44 @@ 'use strict'; const { + ArrayBufferIsView, + ArrayBufferPrototypeGetByteLength, ArrayFrom, ArrayIsArray, ArrayPrototypeIndexOf, ArrayPrototypeJoin, ArrayPrototypePush, ArrayPrototypeSlice, + DataViewPrototypeGetBuffer, + DataViewPrototypeGetByteLength, + DataViewPrototypeGetByteOffset, Error, FunctionPrototypeCall, - MapPrototypeDelete, MapPrototypeGet, + MapPrototypeGetSize, MapPrototypeHas, - MapPrototypeSet, NumberIsNaN, ObjectAssign, ObjectIs, ObjectKeys, ObjectPrototypeIsPrototypeOf, + ObjectPrototypeToString, ReflectApply, ReflectHas, ReflectOwnKeys, RegExpPrototypeExec, + SafeArrayIterator, SafeMap, SafeSet, SafeWeakSet, + SetPrototypeGetSize, String, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeSplit, SymbolIterator, + TypedArrayPrototypeGetLength, + Uint8Array, } = primordials; const { @@ -65,6 +74,8 @@ const AssertionError = require('internal/assert/assertion_error'); const { inspect } = require('internal/util/inspect'); const { Buffer } = require('buffer'); const { + isArrayBuffer, + isDataView, isKeyObject, isPromise, isRegExp, @@ -73,6 +84,8 @@ const { isDate, isWeakSet, isWeakMap, + isSharedArrayBuffer, + isAnyArrayBuffer, } = require('internal/util/types'); const { isError, deprecate, emitExperimentalWarning } = require('internal/util'); const { innerOk } = require('internal/assert/utils'); @@ -369,9 +382,161 @@ function isSpecial(obj) { } 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. * @param {any} actual - The actual value to compare. @@ -388,22 +553,16 @@ 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) || + isAnyArrayBuffer(actual) || + ArrayBufferIsView(expected) || + isAnyArrayBuffer(expected) + ) { + return partiallyCompareArrayBuffersOrViews(actual, expected); } for (const type of typesToCallDeepStrictEqualWith) { @@ -415,68 +574,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 partiallyCompareSets(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 partiallyCompareArrays(actual, expected, comparedObjects); } // Comparison done when at least one of the values is not an object diff --git a/test/parallel/test-assert-objects.js b/test/parallel/test-assert-objects.js index d1c8bb854babb0..75d06927917911 100644 --- a/test/parallel/test-assert-objects.js +++ b/test/parallel/test-assert-objects.js @@ -39,10 +39,15 @@ describe('Object Comparison Tests', () => { describe('throws an error', () => { const tests = [ { - description: 'throws when only one argument is provided', + description: 'throws when only actual is provided', actual: { a: 1 }, expected: undefined, }, + { + description: 'throws when only expected is provided', + actual: undefined, + expected: { a: 1 }, + }, { description: 'throws when expected has more properties than actual', actual: [1, 'two'], @@ -207,6 +212,74 @@ describe('Object Comparison Tests', () => { actual: [1, 2, 3], 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) { @@ -343,10 +416,89 @@ 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 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', actual: new Map([ @@ -358,6 +510,19 @@ describe('Object Comparison Tests', () => { ['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', actual: [{ a: 5 }], diff --git a/test/parallel/test-assert-typedarray-deepequal.js b/test/parallel/test-assert-typedarray-deepequal.js index 1c1c4c030a267e..7fb18c1886ba91 100644 --- a/test/parallel/test-assert-typedarray-deepequal.js +++ b/test/parallel/test-assert-typedarray-deepequal.js @@ -86,6 +86,8 @@ suite('notEqualArrayPairs', () => { new Uint8Array(new ArrayBuffer(3)).fill(1).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) { @@ -99,6 +101,10 @@ suite('notEqualArrayPairs', () => { makeBlock(assert.deepStrictEqual, arrayPair[0], arrayPair[1]), assert.AssertionError ); + assert.throws( + makeBlock(assert.partialDeepStrictEqual, arrayPair[0], arrayPair[1]), + assert.AssertionError + ); }); } });