Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions benchmark/assert/partial-deep-strict-equal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
'use strict';

const common = require('../common.js');
const assert = require('assert');

const bench = common.createBenchmark(main, {
n: [10, 50, 200],
size: [1e3],
datasetName: [
'objects',
'sets',
'maps',
'circularRefs',
'typedArrays',
'arrayBuffers',
'dataViewArrayBuffers',
],
});

function createObjects(length, depth = 0) {
return Array.from({ length }, () => ({
foo: 'yarp',
nope: {
bar: '123',
a: [1, 2, 3],
c: {},
b: !depth ? createObjects(2, depth + 1) : [],
},
}));
}

function createSets(length, depth = 0) {
return Array.from({ length }, () => new Set([
'yarp',
'123',
1,
2,
3,
null,
{
simple: 'object',
number: 42,
},
['array', 'with', 'values'],
!depth ? new Set([1, 2, { nested: true }]) : new Set(),
!depth ? createSets(2, depth + 1) : null,
]));
}

function createMaps(length, depth = 0) {
return Array.from({ length }, () => new Map([
['primitiveKey', 'primitiveValue'],
[42, 'numberKey'],
['objectValue', { a: 1, b: 2 }],
['arrayValue', [1, 2, 3]],
['nestedMap', new Map([['a', 1], ['b', { deep: true }]])],
[{ objectKey: true }, 'value from object key'],
[[1, 2, 3], 'value from array key'],
[!depth ? createMaps(2, depth + 1) : null, 'recursive value'],
]));
}

function createCircularRefs(length) {
return Array.from({ length }, () => {
const circularSet = new Set();
const circularMap = new Map();
const circularObj = { name: 'circular object' };

circularSet.add('some value');
circularSet.add(circularSet);

circularMap.set('self', circularMap);
circularMap.set('value', 'regular value');

circularObj.self = circularObj;

const objA = { name: 'A' };
const objB = { name: 'B' };
objA.ref = objB;
objB.ref = objA;

circularSet.add(objA);
circularMap.set('objB', objB);

return {
circularSet,
circularMap,
circularObj,
objA,
objB,
};
});
}

function createTypedArrays(length) {
return Array.from({ length }, () => {
const buffer = new ArrayBuffer(32);

return {
int8: new Int8Array(buffer, 0, 4),
uint8: new Uint8Array(buffer, 4, 4),
uint8Clamped: new Uint8ClampedArray(buffer, 8, 4),
int16: new Int16Array([1, 2, 3]),
uint16: new Uint16Array([1, 2, 3]),
int32: new Int32Array([1, 2, 3]),
uint32: new Uint32Array([1, 2, 3]),
float32: new Float32Array([1.1, 2.2, 3.3]),
float64: new Float64Array([1.1, 2.2, 3.3]),
bigInt64: new BigInt64Array([1n, 2n, 3n]),
bigUint64: new BigUint64Array([1n, 2n, 3n]),
};
});
}

function createArrayBuffers(length) {
return Array.from({ length }, (_, n) => new ArrayBuffer(n));
}

function createDataViewArrayBuffers(length) {
return Array.from({ length }, (_, n) => new DataView(new ArrayBuffer(n)));
}

const datasetMappings = {
objects: createObjects,
sets: createSets,
maps: createMaps,
circularRefs: createCircularRefs,
typedArrays: createTypedArrays,
arrayBuffers: createArrayBuffers,
dataViewArrayBuffers: createDataViewArrayBuffers,
};

function getDatasets(datasetName, size) {
return {
actual: datasetMappings[datasetName](size),
expected: datasetMappings[datasetName](size),
};
}

function main({ size, n, datasetName }) {
const { actual, expected } = getDatasets(datasetName, size);

bench.start();
for (let i = 0; i < n; ++i) {
assert.partialDeepStrictEqual(actual, expected);
}
bench.end(n);
}
94 changes: 71 additions & 23 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
const {
ArrayBufferIsView,
ArrayBufferPrototypeGetByteLength,
ArrayFrom,
ArrayIsArray,
ArrayPrototypeIndexOf,
ArrayPrototypeJoin,
Expand Down Expand Up @@ -395,12 +394,11 @@ function partiallyCompareMaps(actual, expected, comparedObjects) {
const expectedIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], expected);

for (const { 0: key, 1: expectedValue } of expectedIterator) {
if (!MapPrototypeHas(actual, key)) {
const actualValue = MapPrototypeGet(actual, key);
if (actualValue === undefined && !MapPrototypeHas(actual, key)) {
return false;
}

const actualValue = MapPrototypeGet(actual, key);

if (!compareBranch(actualValue, expectedValue, comparedObjects)) {
return false;
}
Expand Down Expand Up @@ -474,28 +472,71 @@ function partiallyCompareArrayBuffersOrViews(actual, expected) {
return true;
}

// Adapted version of the "setEquiv" function in lib/internal/util/comparisons.js
function partiallyCompareSets(actual, expected, comparedObjects) {
if (SetPrototypeGetSize(expected) > SetPrototypeGetSize(actual)) {
return false; // `expected` can't be a subset if it has more elements
return false;
}

if (isDeepEqual === undefined) lazyLoadComparison();
let set = null;

const actualArray = ArrayFrom(FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual));
const expectedIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], expected);
const usedIndices = new SafeSet();
// First, check if elements from expected exist in actual
for (const val of expected) {
// Fast path: direct inclusion check for both primitives and reference equality
if (actual.has(val)) {
continue;
}

// For primitives, if not found directly, return false immediately
if (typeof val !== 'object' || val === null) {
return false;
}

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;
if (set === null) {
// Special case to avoid set creation for single-element comparison
if (SetPrototypeGetSize(expected) === 1) {
// Try to find any deep-equal object in actual
for (const actualItem of actual) {
if (!(typeof actualItem !== 'object' || actualItem === null) && isDeepStrictEqual(actualItem, val)) {
return true;
}
}
return false;
}
set = new SafeSet();
}
return false;

// Add this object for later deep comparison
set.add(val);
}

return true;
// If all items were found directly, we're done
if (set === null) {
return true;
}

// For remaining objects that need deep comparison
for (const actualItem of actual) {
// Only consider non-primitive values for deep comparison
if (!(typeof actualItem !== 'object' || actualItem === null)) {
// Check if this actual item deep-equals any remaining expected item
for (const expectedItem of set) {
if (isDeepStrictEqual(actualItem, expectedItem)) {
// Remove the matched item so we don't match it again
set.delete(expectedItem);
// If all items are matched, we can return early
if (set.size === 0) {
return true;
}
break;
}
}
}
}

// If all objects in expected found matches, set will be empty
return set.size === 0;
}

const minusZeroSymbol = Symbol('-0');
Expand All @@ -510,21 +551,26 @@ function getZeroKey(item) {
}

function partiallyCompareArrays(actual, expected, comparedObjects) {
if (actual === expected) return true;

if (expected.length > actual.length) {
return false;
}

if (expected.length === 0) {
return true;
}

if (isDeepEqual === undefined) lazyLoadComparison();

// Create a map to count occurrences of each element in the expected array
const expectedCounts = new SafeMap();
const safeExpected = new SafeArrayIterator(expected);

for (const expectedItem of safeExpected) {
// Check if the item is a zero or a -0, as these need to be handled separately
const expectedIterator = new SafeArrayIterator(expected);
for (const expectedItem of expectedIterator) {
if (expectedItem === 0) {
const zeroKey = getZeroKey(expectedItem);
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey)?.count || 0) + 1);
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey) ?? 0) + 1);
} else {
let found = false;
for (const { 0: key, 1: count } of expectedCounts) {
Expand All @@ -540,10 +586,8 @@ function partiallyCompareArrays(actual, expected, comparedObjects) {
}
}

const safeActual = new SafeArrayIterator(actual);

for (const actualItem of safeActual) {
// Check if the item is a zero or a -0, as these need to be handled separately
const actualIterator = new SafeArrayIterator(actual);
for (const actualItem of actualIterator) {
if (actualItem === 0) {
const zeroKey = getZeroKey(actualItem);

Expand All @@ -567,6 +611,10 @@ function partiallyCompareArrays(actual, expected, comparedObjects) {
}
}
}

if (expectedCounts.size === 0) {
return true;
}
}

return expectedCounts.size === 0;
Expand Down
Loading