Repository: nodejs/node
Pull request: assert: partialDeepStrictEqual now handles comparisons of ArrayBuffers, SharedArrayBuffers and Int16Arrays

Fixes: https://github.com/nodejs/node/issues/56097

Added handling for ArrayBuffers, SharedArrayBuffers and Int16Arrays , which were not properly considered in the new method `assert.partialDeepStrictEqual` .  
This PR should move the feature one step closer to a stable version

Changed files:
filename: lib/assert.js
patch: @@ -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

filename: test/parallel/test-assert-objects.js
patch: @@ -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 }],

filename: test/parallel/test-assert-typedarray-deepequal.js
patch: @@ -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
+      );
     });
   }
 });