mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
assert: add partialDeepStrictEqual
Fixes: https://github.com/nodejs/node/issues/50399 Co-Authored-By: Cristian Barlutiu <cristian.barlutiu@gmail.com> PR-URL: https://github.com/nodejs/node/pull/54630 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Jithil P Ponnan <jithil@outlook.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
This commit is contained in:
parent
a2a0c22fbf
commit
d0d52092cf
4 changed files with 803 additions and 2 deletions
212
lib/assert.js
212
lib/assert.js
|
@ -21,22 +21,35 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayFrom,
|
||||
ArrayIsArray,
|
||||
ArrayPrototypeIndexOf,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeSlice,
|
||||
Error,
|
||||
FunctionPrototypeCall,
|
||||
MapPrototypeDelete,
|
||||
MapPrototypeGet,
|
||||
MapPrototypeHas,
|
||||
MapPrototypeSet,
|
||||
NumberIsNaN,
|
||||
ObjectAssign,
|
||||
ObjectIs,
|
||||
ObjectKeys,
|
||||
ObjectPrototypeIsPrototypeOf,
|
||||
ReflectApply,
|
||||
ReflectHas,
|
||||
ReflectOwnKeys,
|
||||
RegExpPrototypeExec,
|
||||
SafeMap,
|
||||
SafeSet,
|
||||
SafeWeakSet,
|
||||
String,
|
||||
StringPrototypeIndexOf,
|
||||
StringPrototypeSlice,
|
||||
StringPrototypeSplit,
|
||||
SymbolIterator,
|
||||
} = primordials;
|
||||
|
||||
const {
|
||||
|
@ -50,8 +63,18 @@ const {
|
|||
} = require('internal/errors');
|
||||
const AssertionError = require('internal/assert/assertion_error');
|
||||
const { inspect } = require('internal/util/inspect');
|
||||
const { isPromise, isRegExp } = require('internal/util/types');
|
||||
const { isError, deprecate } = require('internal/util');
|
||||
const { Buffer } = require('buffer');
|
||||
const {
|
||||
isKeyObject,
|
||||
isPromise,
|
||||
isRegExp,
|
||||
isMap,
|
||||
isSet,
|
||||
isDate,
|
||||
isWeakSet,
|
||||
isWeakMap,
|
||||
} = require('internal/util/types');
|
||||
const { isError, deprecate, emitExperimentalWarning } = require('internal/util');
|
||||
const { innerOk } = require('internal/assert/utils');
|
||||
|
||||
const CallTracker = require('internal/assert/calltracker');
|
||||
|
@ -341,6 +364,191 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
|
|||
}
|
||||
};
|
||||
|
||||
function isSpecial(obj) {
|
||||
return obj == null || typeof obj !== 'object' || isError(obj) || isRegExp(obj) || isDate(obj);
|
||||
}
|
||||
|
||||
const typesToCallDeepStrictEqualWith = [
|
||||
isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer,
|
||||
];
|
||||
|
||||
/**
|
||||
* Compares two objects or values recursively to check if they are equal.
|
||||
* @param {any} actual - The actual value to compare.
|
||||
* @param {any} expected - The expected value to compare.
|
||||
* @param {Set} [comparedObjects=new Set()] - Set to track compared objects for handling circular references.
|
||||
* @returns {boolean} - Returns `true` if the actual value matches the expected value, otherwise `false`.
|
||||
* @example
|
||||
* compareBranch({a: 1, b: 2, c: 3}, {a: 1, b: 2}); // true
|
||||
*/
|
||||
function compareBranch(
|
||||
actual,
|
||||
expected,
|
||||
comparedObjects,
|
||||
) {
|
||||
// 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();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
for (const type of typesToCallDeepStrictEqualWith) {
|
||||
if (type(actual) || type(expected)) {
|
||||
if (isDeepStrictEqual === undefined) lazyLoadComparison();
|
||||
return isDeepStrictEqual(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Set object equality
|
||||
// TODO(aduh95): switch to `SetPrototypeIsSubsetOf` when it's available
|
||||
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(actual);
|
||||
const expectedArray = ArrayFrom(expected);
|
||||
const usedIndices = new SafeSet();
|
||||
|
||||
for (let expectedIdx = 0; expectedIdx < expectedArray.length; expectedIdx++) {
|
||||
const expectedItem = expectedArray[expectedIdx];
|
||||
let found = false;
|
||||
|
||||
for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {
|
||||
if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {
|
||||
usedIndices.add(actualIdx);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Comparison done when at least one of the values is not an object
|
||||
if (isSpecial(actual) || isSpecial(expected)) {
|
||||
if (isDeepEqual === undefined) {
|
||||
lazyLoadComparison();
|
||||
}
|
||||
return isDeepStrictEqual(actual, expected);
|
||||
}
|
||||
|
||||
// Use Reflect.ownKeys() instead of Object.keys() to include symbol properties
|
||||
const keysExpected = ReflectOwnKeys(expected);
|
||||
|
||||
comparedObjects ??= new SafeWeakSet();
|
||||
|
||||
// Handle circular references
|
||||
if (comparedObjects.has(actual)) {
|
||||
return true;
|
||||
}
|
||||
comparedObjects.add(actual);
|
||||
|
||||
// Check if all expected keys and values match
|
||||
for (let i = 0; i < keysExpected.length; i++) {
|
||||
const key = keysExpected[i];
|
||||
assert(
|
||||
ReflectHas(actual, key),
|
||||
new AssertionError({ message: `Expected key ${String(key)} not found in actual object` }),
|
||||
);
|
||||
if (!compareBranch(actual[key], expected[key], comparedObjects)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The strict equivalence assertion test between two objects
|
||||
* @param {any} actual
|
||||
* @param {any} expected
|
||||
* @param {string | Error} [message]
|
||||
* @returns {void}
|
||||
*/
|
||||
assert.partialDeepStrictEqual = function partialDeepStrictEqual(
|
||||
actual,
|
||||
expected,
|
||||
message,
|
||||
) {
|
||||
emitExperimentalWarning('assert.partialDeepStrictEqual');
|
||||
if (arguments.length < 2) {
|
||||
throw new ERR_MISSING_ARGS('actual', 'expected');
|
||||
}
|
||||
|
||||
if (!compareBranch(actual, expected)) {
|
||||
innerFail({
|
||||
actual,
|
||||
expected,
|
||||
message,
|
||||
operator: 'partialDeepStrictEqual',
|
||||
stackStartFn: partialDeepStrictEqual,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
class Comparison {
|
||||
constructor(obj, keys, actual) {
|
||||
for (const key of keys) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue