Repository: nodejs/node
Pull request: assert: improve partialDeepStrictEqual

This implements fast paths for typed arrays, array buffers and sets
and maps that contain only objects as keys.

Changed files:
filename: benchmark/assert/deepequal-buffer.js
patch: @@ -7,7 +7,7 @@ const bench = common.createBenchmark(main, {
   len: [1e2, 1e3],
   strict: [0, 1],
   arrayBuffer: [0, 1],
-  method: ['deepEqual', 'notDeepEqual', 'unequal_length'],
+  method: ['deepEqual', 'notDeepEqual', 'unequal_length', 'partial'],
 }, {
   combinationFilter: (p) => {
     return p.strict === 1 || p.method === 'deepEqual';
@@ -18,11 +18,16 @@ function main({ len, n, method, strict, arrayBuffer }) {
   let actual = Buffer.alloc(len);
   let expected = Buffer.alloc(len + Number(method === 'unequal_length'));
 
-
   if (method === 'unequal_length') {
     method = 'notDeepEqual';
   }
 
+  if (method === 'partial') {
+    method = 'partialDeepStrictEqual';
+  } else if (strict) {
+    method = method.replace('eep', 'eepStrict');
+  }
+
   for (let i = 0; i < len; i++) {
     actual.writeInt8(i % 128, i);
     expected.writeInt8(i % 128, i);
@@ -33,10 +38,6 @@ function main({ len, n, method, strict, arrayBuffer }) {
     expected[position] = expected[position] + 1;
   }
 
-  if (strict) {
-    method = method.replace('eep', 'eepStrict');
-  }
-
   const fn = assert[method];
 
   if (arrayBuffer) {

filename: benchmark/assert/partial-deep-equal.js
patch: @@ -4,12 +4,13 @@ const common = require('../common.js');
 const assert = require('assert');
 
 const bench = common.createBenchmark(main, {
-  n: [25],
+  n: [125],
   size: [500],
-  extraProps: [0],
+  extraProps: [0, 1],
   datasetName: [
     'objects',
     'sets',
+    'setsWithObjects',
     'maps',
     'circularRefs',
     'typedArrays',
@@ -31,17 +32,29 @@ function createObjects(length, extraProps, depth = 0) {
     foo: 'yarp',
     nope: {
       bar: '123',
-      ...extraProps ? { a: [1, 2, i] } : {},
+      ...(extraProps ? { a: [1, 2, i] } : {}),
       c: {},
       b: !depth ? createObjects(2, extraProps, depth + 1) : [],
     },
   }));
 }
 
+function createSetsWithObjects(length, extraProps, depth = 0) {
+  return Array.from({ length }, (_, i) => new Set([
+    ...(extraProps ? [{}] : []),
+    {
+      simple: 'object',
+      number: i,
+    },
+    ['array', 'with', 'values'],
+    new Set([[], {}, { nested: i }]),
+  ]));
+}
+
 function createSets(length, extraProps, depth = 0) {
   return Array.from({ length }, (_, i) => new Set([
     'yarp',
-    ...extraProps ? ['123', 1, 2] : [],
+    ...(extraProps ? ['123', 1, 2] : []),
     i + 3,
     null,
     {
@@ -56,7 +69,7 @@ function createSets(length, extraProps, depth = 0) {
 
 function createMaps(length, extraProps, depth = 0) {
   return Array.from({ length }, (_, i) => new Map([
-    ...extraProps ? [['primitiveKey', 'primitiveValue']] : [],
+    ...(extraProps ? [['primitiveKey', 'primitiveValue']] : []),
     [42, 'numberKey'],
     ['objectValue', { a: 1, b: i }],
     ['arrayValue', [1, 2, i]],
@@ -114,16 +127,23 @@ function createTypedArrays(length, extraParts) {
 }
 
 function createArrayBuffers(length, extra) {
-  return Array.from({ length }, (_, n) => new ArrayBuffer(n + extra ? 1 : 0));
+  return Array.from({ length }, (_, n) => {
+    const buffer = Buffer.alloc(n + (extra ? 1 : 0));
+    for (let i = 0; i < n; i++) {
+      buffer.writeInt8(i % 128, i);
+    }
+    return buffer.buffer;
+  });
 }
 
 function createDataViewArrayBuffers(length, extra) {
-  return Array.from({ length }, (_, n) => new DataView(new ArrayBuffer(n + extra ? 1 : 0)));
+  return createArrayBuffers(length, extra).map((buffer) => new DataView(buffer));
 }
 
 const datasetMappings = {
   objects: createObjects,
   sets: createSets,
+  setsWithObjects: createSetsWithObjects,
   maps: createMaps,
   circularRefs: createCircularRefs,
   typedArrays: createTypedArrays,

filename: lib/internal/util/comparisons.js
patch: @@ -243,7 +243,7 @@ function innerDeepEqual(val1, val2, mode, memos) {
         TypedArrayPrototypeGetSymbolToStringTag(val2)) {
       return false;
     }
-    if (mode === kPartial) {
+    if (mode === kPartial && val1.byteLength !== val2.byteLength) {
       if (!isPartialArrayBufferView(val1, val2)) {
         return false;
       }
@@ -280,7 +280,7 @@ function innerDeepEqual(val1, val2, mode, memos) {
     if (!isAnyArrayBuffer(val2)) {
       return false;
     }
-    if (mode !== kPartial) {
+    if (mode !== kPartial || val1.byteLength === val2.byteLength) {
       if (!areEqualArrayBuffers(val1, val2)) {
         return false;
       }
@@ -546,18 +546,18 @@ function partialObjectSetEquiv(a, b, mode, set, memo) {
 }
 
 function setObjectEquiv(a, b, mode, set, memo) {
-  if (mode === kPartial) {
-    return partialObjectSetEquiv(a, b, mode, set, memo);
-  }
   // Fast path for objects only
-  if (mode === kStrict && set.size === a.size) {
+  if (mode !== kLoose && set.size === a.size) {
     for (const val of a) {
       if (!setHasEqualElement(set, val, mode, memo)) {
         return false;
       }
     }
     return true;
   }
+  if (mode === kPartial) {
+    return partialObjectSetEquiv(a, b, mode, set, memo);
+  }
 
   for (const val of a) {
     // Primitive values have already been handled above.
@@ -639,18 +639,18 @@ function partialObjectMapEquiv(a, b, mode, set, memo) {
 }
 
 function mapObjectEquivalence(a, b, mode, set, memo) {
-  if (mode === kPartial) {
-    return partialObjectMapEquiv(a, b, mode, set, memo);
-  }
   // Fast path for objects only
-  if (mode === kStrict && set.size === a.size) {
+  if (mode !== kLoose && set.size === a.size) {
     for (const { 0: key1, 1: item1 } of a) {
       if (!mapHasEqualEntry(set, b, key1, item1, mode, memo)) {
         return false;
       }
     }
     return true;
   }
+  if (mode === kPartial) {
+    return partialObjectMapEquiv(a, b, mode, set, memo);
+  }
   for (const { 0: key1, 1: item1 } of a) {
     if (typeof key1 === 'object' && key1 !== null) {
       if (!mapHasEqualEntry(set, b, key1, item1, mode, memo))

filename: test/parallel/test-assert-partial-deep-equal.js
patch: @@ -280,6 +280,11 @@ describe('Object Comparison Tests', () => {
             [{ a: 1 }, 'value1'],
           ]),
         },
+        {
+          description: 'throws for Maps with mixed unequal entries',
+          actual: new Map([[{ a: 2 }, 1], [1, 1], [{ b: 1 }, 1], [[], 1], [2, 1], [{ a: 1 }, 1]]),
+          expected: new Map([[{ a: 1 }, 1], [[], 1], [2, 1], [{ a: 1 }, 1]]),
+        },
         {
           description: 'throws for sets with different object values',
           actual: new Set([
@@ -494,6 +499,11 @@ describe('Object Comparison Tests', () => {
           actual: new Float32Array([+0.0]),
           expected: new Float32Array([-0.0]),
         },
+        {
+          description: 'throws when comparing two Uint8Array objects with non-matching entries',
+          actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) },
+          expected: { typedArray: new Uint8Array([1, 333, 2, 4]) },
+        },
         {
           description: 'throws when comparing two different urls',
           actual: new URL('http://foo'),
@@ -713,7 +723,7 @@ describe('Object Comparison Tests', () => {
       {
         description: 'compares two Uint8Array objects',
         actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) },
-        expected: { typedArray: new Uint8Array([1, 2, 3]) },
+        expected: { typedArray: new Uint8Array([1, 2, 3, 5]) },
       },
       {
         description: 'compares two Int16Array objects',