diff --git a/benchmark/assert/assertion-error.js b/benchmark/assert/assertion-error.js index 1e70a70805dc32..51fb52ae1a4286 100644 --- a/benchmark/assert/assertion-error.js +++ b/benchmark/assert/assertion-error.js @@ -3,9 +3,8 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - n: [10, 50, 200, 500], - size: [10, 100], - datasetName: ['objects'], + n: [200], + size: [2, 75], }); const baseObject = { diff --git a/benchmark/assert/deepequal-map.js b/benchmark/assert/deepequal-map.js index fb3f7cd316028f..4f651551c58c82 100644 --- a/benchmark/assert/deepequal-map.js +++ b/benchmark/assert/deepequal-map.js @@ -5,7 +5,7 @@ const { deepEqual, deepStrictEqual, notDeepEqual, notDeepStrictEqual } = require('assert'); const bench = common.createBenchmark(main, { - n: [5e3], + n: [2e3], len: [5e2], strict: [0, 1], method: [ diff --git a/benchmark/assert/deepequal-object.js b/benchmark/assert/deepequal-object.js index c480faf10cbae8..e1d1baf838d9c6 100644 --- a/benchmark/assert/deepequal-object.js +++ b/benchmark/assert/deepequal-object.js @@ -4,12 +4,12 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - n: [25, 2e2], + n: [50, 2e2], size: [1e2, 1e4], method: ['deepEqual', 'notDeepEqual', 'deepStrictEqual', 'notDeepStrictEqual'], }, { combinationFilter: (p) => { - return p.size === 1e4 && p.n === 25 || + return p.size === 1e4 && p.n === 50 || p.size === 1e3 && p.n === 2e2 || p.size === 1e2 && p.n === 2e3 || p.size === 1; diff --git a/benchmark/assert/deepequal-set.js b/benchmark/assert/deepequal-set.js index 27ca7c92bce1b0..e771c81928a897 100644 --- a/benchmark/assert/deepequal-set.js +++ b/benchmark/assert/deepequal-set.js @@ -5,7 +5,7 @@ const { deepEqual, deepStrictEqual, notDeepEqual, notDeepStrictEqual } = require('assert'); const bench = common.createBenchmark(main, { - n: [5e2], + n: [1e3], len: [5e2], strict: [0, 1], method: [ diff --git a/benchmark/assert/deepequal-simple-array-and-set.js b/benchmark/assert/deepequal-simple-array-and-set.js index a1f6820696d7b8..08bbc87a1c5b1c 100644 --- a/benchmark/assert/deepequal-simple-array-and-set.js +++ b/benchmark/assert/deepequal-simple-array-and-set.js @@ -5,7 +5,7 @@ const { deepEqual, deepStrictEqual, notDeepEqual, notDeepStrictEqual } = require('assert'); const bench = common.createBenchmark(main, { - n: [5e2], + n: [1e3], len: [1e4], strict: [1], method: [ diff --git a/benchmark/assert/deepequal-typedarrays.js b/benchmark/assert/deepequal-typedarrays.js index 86826d6588ef86..5684cd520d258b 100644 --- a/benchmark/assert/deepequal-typedarrays.js +++ b/benchmark/assert/deepequal-typedarrays.js @@ -16,6 +16,12 @@ const bench = common.createBenchmark(main, { 'notDeepEqual', ], len: [1e2, 5e3], +}, { + combinationFilter(p) { + return p.strict === 1 || + p.type !== 'Float32Array' || + p.len === 1e2; + }, }); function main({ type, n, len, method, strict }) { diff --git a/benchmark/assert/match.js b/benchmark/assert/match.js index fab86a23944c59..5ad9292c4b012b 100644 --- a/benchmark/assert/match.js +++ b/benchmark/assert/match.js @@ -4,8 +4,15 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - n: [25, 2e7], + n: [2e7], method: ['match', 'doesNotMatch'], +}, { + combinationFilter(p) { + // These benchmarks purposefully do not run by default. They do not provide + // might insight, due to only being a small wrapper around a native regexp + // call. + return p.n === 1; + }, }); function main({ n, method }) { diff --git a/benchmark/assert/partial-deep-equal.js b/benchmark/assert/partial-deep-equal.js new file mode 100644 index 00000000000000..eee88167525cee --- /dev/null +++ b/benchmark/assert/partial-deep-equal.js @@ -0,0 +1,150 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [25], + size: [500], + extraProps: [0], + datasetName: [ + 'objects', + 'sets', + 'maps', + 'circularRefs', + 'typedArrays', + 'arrayBuffers', + 'dataViewArrayBuffers', + 'array', + ], +}); + +function createArray(length, extraProps) { + if (extraProps) { + return Array.from({ length: length * 4 }, (_, i) => i); + } + return Array.from({ length }, (_, i) => i * 4); +} + +function createObjects(length, extraProps, depth = 0) { + return Array.from({ length }, (_, i) => ({ + foo: 'yarp', + nope: { + bar: '123', + ...extraProps ? { a: [1, 2, i] } : {}, + c: {}, + b: !depth ? createObjects(2, extraProps, depth + 1) : [], + }, + })); +} + +function createSets(length, extraProps, depth = 0) { + return Array.from({ length }, (_, i) => new Set([ + 'yarp', + ...extraProps ? ['123', 1, 2] : [], + i + 3, + null, + { + simple: 'object', + number: i, + }, + ['array', 'with', 'values'], + !depth ? new Set([1, 2, { nested: i }]) : new Set(), + !depth ? createSets(2, extraProps, depth + 1) : null, + ])); +} + +function createMaps(length, extraProps, depth = 0) { + return Array.from({ length }, (_, i) => new Map([ + ...extraProps ? [['primitiveKey', 'primitiveValue']] : [], + [42, 'numberKey'], + ['objectValue', { a: 1, b: i }], + ['arrayValue', [1, 2, i]], + ['nestedMap', new Map([['a', i], ['b', { deep: true }]])], + [{ objectKey: true }, 'value from object key'], + [[1, i, 3], 'value from array key'], + [!depth ? createMaps(2, extraProps, depth + 1) : null, 'recursive value' + i], + ])); +} + +function createCircularRefs(length, extraProps) { + return Array.from({ length }, (_, i) => { + const circularSet = new Set(); + const circularMap = new Map(); + const circularObj = { name: 'circular object' }; + + circularSet.add('some value' + i); + 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, + ...extraProps ? { extra: i } : {}, + circularObj, + objA, + objB, + }; + }); +} + +function createTypedArrays(length, extraParts) { + const extra = extraParts ? [9, 8, 7] : []; + return Array.from({ length }, (_, i) => { + return { + uint8: new Uint8Array(new ArrayBuffer(32), 4, 4), + int16: new Int16Array([1, 2, ...extra, 3]), + uint32: new Uint32Array([i + 1, i + 2, ...extra, i + 3]), + float64: new Float64Array([1.1, 2.2, ...extra, i + 3.3]), + bigUint64: new BigUint64Array([1n, 2n, 3n]), + }; + }); +} + +function createArrayBuffers(length, extra) { + return Array.from({ length }, (_, n) => new ArrayBuffer(n + extra ? 1 : 0)); +} + +function createDataViewArrayBuffers(length, extra) { + return Array.from({ length }, (_, n) => new DataView(new ArrayBuffer(n + extra ? 1 : 0))); +} + +const datasetMappings = { + objects: createObjects, + sets: createSets, + maps: createMaps, + circularRefs: createCircularRefs, + typedArrays: createTypedArrays, + arrayBuffers: createArrayBuffers, + dataViewArrayBuffers: createDataViewArrayBuffers, + array: createArray, +}; + +function getDatasets(datasetName, size, extra) { + return { + actual: datasetMappings[datasetName](size, true), + expected: datasetMappings[datasetName](size, !extra), + }; +} + +function main({ size, n, datasetName, extraProps }) { + const { actual, expected } = getDatasets(datasetName, size, extraProps); + + bench.start(); + for (let i = 0; i < n; ++i) { + assert.partialDeepStrictEqual(actual, expected); + } + bench.end(n); +} diff --git a/benchmark/assert/rejects.js b/benchmark/assert/rejects.js index 43ec500177a625..d8a6d6f4bb8058 100644 --- a/benchmark/assert/rejects.js +++ b/benchmark/assert/rejects.js @@ -4,8 +4,15 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - n: [25, 2e5], + n: [2e5], method: ['rejects', 'doesNotReject'], +}, { + combinationFilter(p) { + // These benchmarks purposefully do not run by default. They do not provide + // much insight, due to only being a small wrapper around a native promise + // with a few extra checks. + return p.n === 1; + }, }); async function main({ n, method }) { diff --git a/benchmark/assert/strictequal.js b/benchmark/assert/strictequal.js index 21a77f0472c5fc..fef74ffb1ecb5b 100644 --- a/benchmark/assert/strictequal.js +++ b/benchmark/assert/strictequal.js @@ -4,9 +4,15 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - n: [25, 2e5], + n: [2e5], type: ['string', 'object', 'number'], method: ['strictEqual', 'notStrictEqual'], +}, { + combinationFilter(p) { + // These benchmarks purposefully do not run by default. They do not provide + // much insight, due to only being a small wrapper around `Object.is()`. + return p.n === 1; + }, }); function main({ type, n, method }) { diff --git a/benchmark/assert/throws.js b/benchmark/assert/throws.js index 9c070ac8281551..df2fdf2dbf0e07 100644 --- a/benchmark/assert/throws.js +++ b/benchmark/assert/throws.js @@ -4,8 +4,14 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - n: [25, 2e5], + n: [2e5], method: ['throws', 'doesNotThrow'], +}, { + combinationFilter(p) { + // These benchmarks purposefully do not run by default. They do not provide + // much insight, due to only being a small wrapper around a try / catch. + return p.n === 1; + }, }); function main({ n, method }) { diff --git a/doc/api/assert.md b/doc/api/assert.md index b8cd4f59c42717..3fb67cb8d1e54a 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -2594,89 +2594,157 @@ argument. added: - v23.4.0 - v22.13.0 +changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/57370 + description: partialDeepStrictEqual is now Stable. Previously, it had been Experimental. --> -> Stability: 1.0 - Early development +> Stability: 2 - Stable * `actual` {any} * `expected` {any} * `message` {string|Error} -[`assert.partialDeepStrictEqual()`][] Asserts the equivalence between the `actual` and `expected` parameters through a -deep comparison, ensuring that all properties in the `expected` parameter are -present in the `actual` parameter with equivalent values, not allowing type coercion. -The main difference with [`assert.deepStrictEqual()`][] is that [`assert.partialDeepStrictEqual()`][] does not require -all properties in the `actual` parameter to be present in the `expected` parameter. -This method should always pass the same test cases as [`assert.deepStrictEqual()`][], behaving as a super set of it. - -```mjs -import assert from 'node:assert'; +Tests for partial deep equality between the `actual` and `expected` parameters. +"Deep" equality means that the enumerable "own" properties of child objects +are recursively evaluated also by the following rules. "Partial" equality means +that only properties that exist on the `expected` parameter are going to be +compared. -assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); -// OK +This method always passes the same test cases as [`assert.deepStrictEqual()`][], +behaving as a super set of it. -assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); -// OK +### Comparison details -assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }); -// OK +* Primitive values are compared using [`Object.is()`][]. +* [Type tags][Object.prototype.toString()] of objects should be the same. +* [`[[Prototype]]`][prototype-spec] of objects are not compared. +* Only [enumerable "own" properties][] are considered. +* {Error} names, messages, causes, and errors are always compared, + even if these are not enumerable properties. + `errors` is also compared. +* Enumerable own {Symbol} properties are compared as well. +* [Object wrappers][] are compared both as objects and unwrapped values. +* `Object` properties are compared unordered. +* {Map} keys and {Set} items are compared unordered. +* Recursion stops when both sides differ or both sides encounter a circular + reference. +* {WeakMap} and {WeakSet} instances are **not** compared structurally. + They are only equal if they reference the same object. Any comparison between + different `WeakMap` or `WeakSet` instances will result in inequality, + even if they contain the same entries. +* {RegExp} lastIndex, flags, and source are always compared, even if these + are not enumerable properties. +* Holes in sparse arrays are ignored. -assert.partialDeepStrictEqual(new Set(['value1', 'value2']), new Set(['value1', 'value2'])); -// OK +```mjs +import assert from 'node:assert'; -assert.partialDeepStrictEqual(new Map([['key1', 'value1']]), new Map([['key1', 'value1']])); +assert.partialDeepStrictEqual( + { a: { b: { c: 1 } } }, + { a: { b: { c: 1 } } }, +); // OK -assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])); +assert.partialDeepStrictEqual( + { a: 1, b: 2, c: 3 }, + { b: 2 }, +); // OK -assert.partialDeepStrictEqual(/abc/, /abc/); +assert.partialDeepStrictEqual( + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [4, 5, 8], +); // OK -assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]); +assert.partialDeepStrictEqual( + new Set([{ a: 1 }, { b: 1 }]), + new Set([{ a: 1 }]), +); // OK -assert.partialDeepStrictEqual(new Set([{ a: 1 }, { b: 1 }]), new Set([{ a: 1 }])); +assert.partialDeepStrictEqual( + new Map([['key1', 'value1'], ['key2', 'value2']]), + new Map([['key2', 'value2']]), +); // OK -assert.partialDeepStrictEqual(new Date(0), new Date(0)); +assert.partialDeepStrictEqual(123n, 123n); // OK -assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 }); +assert.partialDeepStrictEqual( + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [5, 4, 8], +); // AssertionError -assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 }); +assert.partialDeepStrictEqual( + { a: 1 }, + { a: 1, b: 2 }, +); // AssertionError -assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } }); +assert.partialDeepStrictEqual( + { a: { b: 2 } }, + { a: { b: '2' } }, +); // AssertionError ``` ```cjs const assert = require('node:assert'); -assert.partialDeepStrictEqual({ a: 1, b: 2 }, { a: 1, b: 2 }); +assert.partialDeepStrictEqual( + { a: { b: { c: 1 } } }, + { a: { b: { c: 1 } } }, +); +// OK + +assert.partialDeepStrictEqual( + { a: 1, b: 2, c: 3 }, + { b: 2 }, +); // OK -assert.partialDeepStrictEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); +assert.partialDeepStrictEqual( + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [4, 5, 8], +); // OK -assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }); +assert.partialDeepStrictEqual( + new Set([{ a: 1 }, { b: 1 }]), + new Set([{ a: 1 }]), +); // OK -assert.partialDeepStrictEqual([{ a: 5 }, { b: 5 }], [{ a: 5 }]); +assert.partialDeepStrictEqual( + new Map([['key1', 'value1'], ['key2', 'value2']]), + new Map([['key2', 'value2']]), +); // OK -assert.partialDeepStrictEqual(new Set([{ a: 1 }, { b: 1 }]), new Set([{ a: 1 }])); +assert.partialDeepStrictEqual(123n, 123n); // OK -assert.partialDeepStrictEqual({ a: 1 }, { a: 1, b: 2 }); +assert.partialDeepStrictEqual( + [1, 2, 3, 4, 5, 6, 7, 8, 9], + [5, 4, 8], +); // AssertionError -assert.partialDeepStrictEqual({ a: 1, b: '2' }, { a: 1, b: 2 }); +assert.partialDeepStrictEqual( + { a: 1 }, + { a: 1, b: 2 }, +); // AssertionError -assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } }); +assert.partialDeepStrictEqual( + { a: { b: 2 } }, + { a: { b: '2' } }, +); // AssertionError ``` @@ -2700,7 +2768,6 @@ assert.partialDeepStrictEqual({ a: { b: 2 } }, { a: { b: '2' } }); [`assert.notEqual()`]: #assertnotequalactual-expected-message [`assert.notStrictEqual()`]: #assertnotstrictequalactual-expected-message [`assert.ok()`]: #assertokvalue-message -[`assert.partialDeepStrictEqual()`]: #assertpartialdeepstrictequalactual-expected-message [`assert.strictEqual()`]: #assertstrictequalactual-expected-message [`assert.throws()`]: #assertthrowsfn-error-message [`getColorDepth()`]: tty.md#writestreamgetcolordepthenv diff --git a/lib/assert.js b/lib/assert.js index 603f2a026313c9..657ac6225d9833 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -21,45 +21,22 @@ 'use strict'; const { - ArrayBufferIsView, - ArrayBufferPrototypeGetByteLength, - ArrayFrom, - ArrayIsArray, ArrayPrototypeIndexOf, ArrayPrototypeJoin, ArrayPrototypePush, ArrayPrototypeSlice, - DataViewPrototypeGetBuffer, - DataViewPrototypeGetByteLength, - DataViewPrototypeGetByteOffset, Error, - FunctionPrototypeCall, - MapPrototypeGet, - MapPrototypeGetSize, - MapPrototypeHas, NumberIsNaN, ObjectAssign, ObjectIs, ObjectKeys, ObjectPrototypeIsPrototypeOf, - ObjectPrototypeToString, ReflectApply, - ReflectHas, - ReflectOwnKeys, RegExpPrototypeExec, - SafeArrayIterator, - SafeMap, - SafeSet, - SafeWeakSet, - SetPrototypeGetSize, String, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeSplit, - Symbol, - SymbolIterator, - TypedArrayPrototypeGetLength, - Uint8Array, } = primordials; const { @@ -73,37 +50,27 @@ const { } = require('internal/errors'); const AssertionError = require('internal/assert/assertion_error'); const { inspect } = require('internal/util/inspect'); -const { Buffer } = require('buffer'); const { - isArrayBuffer, - isDataView, - isKeyObject, isPromise, isRegExp, - isMap, - isSet, - isDate, - isWeakSet, - isWeakMap, - isSharedArrayBuffer, - isAnyArrayBuffer, } = require('internal/util/types'); -const { isError, deprecate, emitExperimentalWarning } = require('internal/util'); +const { isError, deprecate } = require('internal/util'); const { innerOk } = require('internal/assert/utils'); const CallTracker = require('internal/assert/calltracker'); const { validateFunction, } = require('internal/validators'); -const { isURL } = require('internal/url'); let isDeepEqual; let isDeepStrictEqual; +let isPartialStrictEqual; function lazyLoadComparison() { const comparison = require('internal/util/comparisons'); isDeepEqual = comparison.isDeepEqual; isDeepStrictEqual = comparison.isDeepStrictEqual; + isPartialStrictEqual = comparison.isPartialStrictEqual; } let warned = false; @@ -379,282 +346,6 @@ 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, isSharedArrayBuffer, isURL, -]; - -function partiallyCompareMaps(actual, expected, comparedObjects) { - if (MapPrototypeGetSize(expected) > MapPrototypeGetSize(actual)) { - return false; - } - - comparedObjects ??= new SafeWeakSet(); - const expectedIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], expected); - - for (const { 0: key, 1: expectedValue } of expectedIterator) { - if (!MapPrototypeHas(actual, key)) { - return false; - } - - const actualValue = MapPrototypeGet(actual, key); - - if (!compareBranch(actualValue, expectedValue, 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 (!ObjectIs(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; -} - -const minusZeroSymbol = Symbol('-0'); -const zeroSymbol = Symbol('0'); - -// Helper function to get a unique key for 0, -0 to avoid collisions -function getZeroKey(item) { - if (item === 0) { - return ObjectIs(item, -0) ? minusZeroSymbol : zeroSymbol; - } - return item; -} - -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(); - 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 - if (expectedItem === 0) { - const zeroKey = getZeroKey(expectedItem); - expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey)?.count || 0) + 1); - } else { - 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); - - for (const actualItem of safeActual) { - // Check if the item is a zero or a -0, as these need to be handled separately - if (actualItem === 0) { - const zeroKey = getZeroKey(actualItem); - - if (expectedCounts.has(zeroKey)) { - const count = expectedCounts.get(zeroKey); - if (count === 1) { - expectedCounts.delete(zeroKey); - } else { - expectedCounts.set(zeroKey, count - 1); - } - } - } else { - for (const { 0: expectedItem, 1: count } of expectedCounts) { - if (isDeepStrictEqual(expectedItem, actualItem)) { - if (count === 1) { - expectedCounts.delete(expectedItem); - } else { - expectedCounts.set(expectedItem, count - 1); - } - break; - } - } - } - } - - return expectedCounts.size === 0; -} - -/** - * 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, -) { - // Checking for the simplest case possible. - if (actual === expected) { - return true; - } - // Check for Map object equality - if (isMap(actual) && isMap(expected)) { - return partiallyCompareMaps(actual, expected, comparedObjects); - } - - if ( - ArrayBufferIsView(actual) || - isAnyArrayBuffer(actual) || - ArrayBufferIsView(expected) || - isAnyArrayBuffer(expected) - ) { - return partiallyCompareArrayBuffersOrViews(actual, expected); - } - - for (const type of typesToCallDeepStrictEqualWith) { - if (type(actual) || type(expected)) { - if (isDeepStrictEqual === undefined) lazyLoadComparison(); - return isDeepStrictEqual(actual, expected); - } - } - - // Check for Set object equality - if (isSet(actual) && isSet(expected)) { - return partiallyCompareSets(actual, expected, comparedObjects); - } - - // Check if expected array is a subset of actual array - if (ArrayIsArray(actual) && ArrayIsArray(expected)) { - return partiallyCompareArrays(actual, expected, comparedObjects); - } - - // 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]; - if (!ReflectHas(actual, key)) { - return false; - } - if (!compareBranch(actual[key], expected[key], comparedObjects)) { - return false; - } - } - - return true; -} - /** * The strict equivalence assertion test between two objects * @param {any} actual @@ -667,12 +358,11 @@ assert.partialDeepStrictEqual = function partialDeepStrictEqual( expected, message, ) { - emitExperimentalWarning('assert.partialDeepStrictEqual'); if (arguments.length < 2) { throw new ERR_MISSING_ARGS('actual', 'expected'); } - - if (!compareBranch(actual, expected)) { + if (isDeepEqual === undefined) lazyLoadComparison(); + if (!isPartialStrictEqual(actual, expected)) { innerFail({ actual, expected, diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 554e513b96e9fe..dcb53f09d2d7cb 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -10,17 +10,17 @@ const { Error, NumberIsNaN, NumberPrototypeValueOf, - ObjectGetOwnPropertySymbols, + ObjectGetOwnPropertySymbols: getOwnSymbols, ObjectGetPrototypeOf, ObjectIs, ObjectKeys, - ObjectPrototypeHasOwnProperty, - ObjectPrototypePropertyIsEnumerable, + ObjectPrototypeHasOwnProperty: hasOwn, + ObjectPrototypePropertyIsEnumerable: hasEnumerable, ObjectPrototypeToString, SafeSet, StringPrototypeValueOf, SymbolPrototypeValueOf, - TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeGetByteLength: getByteLength, TypedArrayPrototypeGetSymbolToStringTag, Uint8Array, } = primordials; @@ -59,8 +59,9 @@ const { getOwnNonIndexProperties, } = internalBinding('util'); -const kStrict = true; -const kLoose = false; +const kStrict = 1; +const kLoose = 0; +const kPartial = 2; const kNoIterator = 0; const kIsArray = 1; @@ -76,11 +77,40 @@ function areSimilarRegExps(a, b) { a.lastIndex === b.lastIndex; } +function isPartialUint8Array(a, b) { + const lenA = getByteLength(a); + const lenB = getByteLength(b); + if (lenA < lenB) { + return false; + } + let offsetA = 0; + for (let offsetB = 0; offsetB < lenB; offsetB++) { + while (!ObjectIs(a[offsetA], b[offsetB])) { + offsetA++; + if (offsetA > lenA - lenB + offsetB) { + return false; + } + } + offsetA++; + } + return true; +} + +function isPartialArrayBufferView(a, b) { + if (a.byteLength < b.byteLength) { + return false; + } + return isPartialUint8Array( + new Uint8Array(a.buffer, a.byteOffset, a.byteLength), + new Uint8Array(b.buffer, b.byteOffset, b.byteLength), + ); +} + function areSimilarFloatArrays(a, b) { - if (a.byteLength !== b.byteLength) { + const len = getByteLength(a); + if (len !== getByteLength(b)) { return false; } - const len = TypedArrayPrototypeGetByteLength(a); for (let offset = 0; offset < len; offset++) { if (a[offset] !== b[offset]) { return false; @@ -128,6 +158,12 @@ function isEqualBoxedPrimitive(val1, val2) { assert.fail(`Unknown boxed type ${val1}`); } +function isEnumerableOrIdentical(val1, val2, prop, mode, memos, method) { + return hasEnumerable(val2, prop) || // This is handled by Object.keys() + (mode === kPartial && (val2[prop] === undefined || (prop === 'message' && val2[prop] === ''))) || + innerDeepEqual(val1[prop], val2[prop], mode, memos); +} + // Notes: Type tags are historical [[Class]] properties that can be set by // FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS // and retrieved using Object.prototype.toString.call(obj) in JS @@ -140,14 +176,14 @@ function isEqualBoxedPrimitive(val1, val2) { // a) The same built-in type tag. // b) The same prototypes. -function innerDeepEqual(val1, val2, strict, memos) { +function innerDeepEqual(val1, val2, mode, memos) { // All identical values are equivalent, as determined by ===. if (val1 === val2) { - return val1 !== 0 || ObjectIs(val1, val2) || !strict; + return val1 !== 0 || ObjectIs(val1, val2) || mode === kLoose; } // Check more closely if val1 and val2 are equal. - if (strict) { + if (mode !== kLoose) { if (typeof val1 === 'number') { return NumberIsNaN(val1) && NumberIsNaN(val2); } @@ -155,7 +191,7 @@ function innerDeepEqual(val1, val2, strict, memos) { typeof val1 !== 'object' || val1 === null || val2 === null || - ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2)) { + (mode === kStrict && ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2))) { return false; } } else { @@ -179,18 +215,20 @@ function innerDeepEqual(val1, val2, strict, memos) { if (ArrayIsArray(val1)) { // Check for sparse arrays and general fast path - if (!ArrayIsArray(val2) || val1.length !== val2.length) { + if (!ArrayIsArray(val2) || + (val1.length !== val2.length && (mode !== kPartial || val1.length < val2.length))) { return false; } - const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS; - const keys1 = getOwnNonIndexProperties(val1, filter); + + const filter = mode !== kLoose ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS; const keys2 = getOwnNonIndexProperties(val2, filter); - if (keys1.length !== keys2.length) { + if (mode !== kPartial && + keys2.length !== getOwnNonIndexProperties(val1, filter).length) { return false; } - return keyCheck(val1, val2, strict, memos, kIsArray, keys1); + return keyCheck(val1, val2, mode, memos, kIsArray, keys2); } else if (val1Tag === '[object Object]') { - return keyCheck(val1, val2, strict, memos, kNoIterator); + return keyCheck(val1, val2, mode, memos, kNoIterator); } else if (isDate(val1)) { if (!isDate(val2) || DatePrototypeGetTime(val1) !== DatePrototypeGetTime(val2)) { @@ -205,7 +243,11 @@ function innerDeepEqual(val1, val2, strict, memos) { TypedArrayPrototypeGetSymbolToStringTag(val2)) { return false; } - if (!strict && (isFloat32Array(val1) || isFloat64Array(val1))) { + if (mode === kPartial) { + if (!isPartialArrayBufferView(val1, val2)) { + return false; + } + } else if (mode === kLoose && (isFloat32Array(val1) || isFloat64Array(val1))) { if (!areSimilarFloatArrays(val1, val2)) { return false; } @@ -215,49 +257,48 @@ function innerDeepEqual(val1, val2, strict, memos) { // Buffer.compare returns true, so val1.length === val2.length. If they both // only contain numeric keys, we don't need to exam further than checking // the symbols. - const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS; - const keys1 = getOwnNonIndexProperties(val1, filter); + const filter = mode !== kLoose ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS; const keys2 = getOwnNonIndexProperties(val2, filter); - if (keys1.length !== keys2.length) { + if (mode !== kPartial && + keys2.length !== getOwnNonIndexProperties(val1, filter).length) { return false; } - return keyCheck(val1, val2, strict, memos, kNoIterator, keys1); + return keyCheck(val1, val2, mode, memos, kNoIterator, keys2); } else if (isSet(val1)) { - if (!isSet(val2) || val1.size !== val2.size) { + if (!isSet(val2) || + (val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) { return false; } - return keyCheck(val1, val2, strict, memos, kIsSet); + return keyCheck(val1, val2, mode, memos, kIsSet); } else if (isMap(val1)) { - if (!isMap(val2) || val1.size !== val2.size) { + if (!isMap(val2) || + (val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) { return false; } - return keyCheck(val1, val2, strict, memos, kIsMap); + return keyCheck(val1, val2, mode, memos, kIsMap); } else if (isAnyArrayBuffer(val1)) { - if (!isAnyArrayBuffer(val2) || !areEqualArrayBuffers(val1, val2)) { + if (!isAnyArrayBuffer(val2)) { + return false; + } + if (mode !== kPartial) { + if (!areEqualArrayBuffers(val1, val2)) { + return false; + } + } else if (!isPartialUint8Array(new Uint8Array(val1), new Uint8Array(val2))) { return false; } } else if (isError(val1)) { // Do not compare the stack as it might differ even though the error itself // is otherwise identical. - if (!isError(val2)) { + if (!isError(val2) || + !isEnumerableOrIdentical(val1, val2, 'message', mode, memos) || + !isEnumerableOrIdentical(val1, val2, 'name', mode, memos) || + !isEnumerableOrIdentical(val1, val2, 'cause', mode, memos) || + !isEnumerableOrIdentical(val1, val2, 'errors', mode, memos)) { return false; } - - const message1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'message'); - const name1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'name'); - const cause1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'cause'); - const errors1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'errors'); - - if ((message1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'message') || - (!message1Enumerable && val1.message !== val2.message)) || - (name1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'name') || - (!name1Enumerable && val1.name !== val2.name)) || - (cause1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'cause') || - (!cause1Enumerable && ( - ObjectPrototypeHasOwnProperty(val1, 'cause') !== ObjectPrototypeHasOwnProperty(val2, 'cause') || - !innerDeepEqual(val1.cause, val2.cause, strict, memos)))) || - (errors1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'errors') || - (!errors1Enumerable && !innerDeepEqual(val1.errors, val2.errors, strict, memos)))) { + const hasOwnVal2Cause = hasOwn(val2, 'cause'); + if ((hasOwnVal2Cause !== hasOwn(val1, 'cause') && (mode !== kPartial || hasOwnVal2Cause))) { return false; } } else if (isBoxedPrimitive(val1)) { @@ -283,9 +324,9 @@ function innerDeepEqual(val1, val2, strict, memos) { kKeyObject ??= require('internal/crypto/util').kKeyObject; if (!isCryptoKey(val2) || val1.extractable !== val2.extractable || - !innerDeepEqual(val1.algorithm, val2.algorithm, strict, memos) || - !innerDeepEqual(val1.usages, val2.usages, strict, memos) || - !innerDeepEqual(val1[kKeyObject], val2[kKeyObject], strict, memos) + !innerDeepEqual(val1.algorithm, val2.algorithm, mode, memos) || + !innerDeepEqual(val1.usages, val2.usages, mode, memos) || + !innerDeepEqual(val1[kKeyObject], val2[kKeyObject], mode, memos) ) { return false; } @@ -297,17 +338,14 @@ function innerDeepEqual(val1, val2, strict, memos) { } } - return keyCheck(val1, val2, strict, memos, kNoIterator); + return keyCheck(val1, val2, mode, memos, kNoIterator); } function getEnumerables(val, keys) { - return ArrayPrototypeFilter( - keys, - (k) => ObjectPrototypePropertyIsEnumerable(val, k), - ); + return ArrayPrototypeFilter(keys, (key) => hasEnumerable(val, key)); } -function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { +function keyCheck(val1, val2, mode, memos, iterationType, keys2) { // For all remaining Object pairs, including Array, objects and Maps, // equivalence is determined by having: // a) The same number of owned enumerable properties @@ -315,16 +353,16 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { // c) Equivalent values for every corresponding key/index // d) For Sets and Maps, equal contents // Note: this accounts for both named and indexed properties on Arrays. - const isArrayLikeObject = aKeys !== undefined; + const isArrayLikeObject = keys2 !== undefined; - if (aKeys === undefined) { - aKeys = ObjectKeys(val1); + if (keys2 === undefined) { + keys2 = ObjectKeys(val2); } // Cheap key test - if (aKeys.length > 0) { - for (const key of aKeys) { - if (!ObjectPrototypePropertyIsEnumerable(val2, key)) { + if (keys2.length > 0) { + for (const key of keys2) { + if (!hasEnumerable(val1, key)) { return false; } } @@ -332,32 +370,44 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { if (!isArrayLikeObject) { // The pair must have the same number of owned properties. - if (aKeys.length !== ObjectKeys(val2).length) { + if (mode === kPartial) { + const symbolKeys = getOwnSymbols(val2); + if (symbolKeys.length !== 0) { + for (const key of symbolKeys) { + if (hasEnumerable(val2, key)) { + if (!hasEnumerable(val1, key)) { + return false; + } + ArrayPrototypePush(keys2, key); + } + } + } + } else if (keys2.length !== ObjectKeys(val1).length) { return false; } - if (strict) { - const symbolKeysA = ObjectGetOwnPropertySymbols(val1); + if (mode === kStrict) { + const symbolKeysA = getOwnSymbols(val1); if (symbolKeysA.length !== 0) { let count = 0; for (const key of symbolKeysA) { - if (ObjectPrototypePropertyIsEnumerable(val1, key)) { - if (!ObjectPrototypePropertyIsEnumerable(val2, key)) { + if (hasEnumerable(val1, key)) { + if (!hasEnumerable(val2, key)) { return false; } - ArrayPrototypePush(aKeys, key); + ArrayPrototypePush(keys2, key); count++; - } else if (ObjectPrototypePropertyIsEnumerable(val2, key)) { + } else if (hasEnumerable(val2, key)) { return false; } } - const symbolKeysB = ObjectGetOwnPropertySymbols(val2); + const symbolKeysB = getOwnSymbols(val2); if (symbolKeysA.length !== symbolKeysB.length && getEnumerables(val2, symbolKeysB).length !== count) { return false; } } else { - const symbolKeysB = ObjectGetOwnPropertySymbols(val2); + const symbolKeysB = getOwnSymbols(val2); if (symbolKeysB.length !== 0 && getEnumerables(val2, symbolKeysB).length !== 0) { return false; @@ -366,10 +416,10 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { } } - if (aKeys.length === 0 && + if (keys2.length === 0 && (iterationType === kNoIterator || - (iterationType === kIsArray && val1.length === 0) || - val1.size === 0)) { + (iterationType === kIsArray && val2.length === 0) || + val2.size === 0)) { return true; } @@ -382,9 +432,8 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { c: undefined, d: undefined, deep: false, - deleteFailures: false, }; - return objEquiv(val1, val2, strict, aKeys, memos, iterationType); + return objEquiv(val1, val2, mode, keys2, memos, iterationType); } if (memos.set === undefined) { @@ -395,7 +444,7 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { memos.c = val1; memos.d = val2; memos.deep = true; - const result = objEquiv(val1, val2, strict, aKeys, memos, iterationType); + const result = objEquiv(val1, val2, mode, keys2, memos, iterationType); memos.deep = false; return result; } @@ -415,29 +464,23 @@ function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { return true; } - const areEq = objEquiv(val1, val2, strict, aKeys, memos, iterationType); + const areEq = objEquiv(val1, val2, mode, keys2, memos, iterationType); - if (areEq || memos.deleteFailures) { - set.delete(val1); - set.delete(val2); - } + set.delete(val1); + set.delete(val2); return areEq; } -function setHasEqualElement(set, val2, strict, memo) { - const { deleteFailures } = memo; - memo.deleteFailures = true; - for (const val1 of set) { - if (innerDeepEqual(val1, val2, strict, memo)) { +function setHasEqualElement(set, val1, mode, memo) { + for (const val2 of set) { + if (innerDeepEqual(val1, val2, mode, memo)) { // Remove the matching element to make sure we do not check that again. - set.delete(val1); - memo.deleteFailures = deleteFailures; + set.delete(val2); return true; } } - memo.deleteFailures = deleteFailures; return false; } @@ -471,174 +514,292 @@ function setMightHaveLoosePrim(a, b, prim) { if (altValue != null) return altValue; - return b.has(altValue) && !a.has(altValue); + return !b.has(altValue) && a.has(altValue); } -function mapMightHaveLoosePrim(a, b, prim, item, memo) { +function mapMightHaveLoosePrim(a, b, prim, item2, memo) { const altValue = findLooseMatchingPrimitives(prim); if (altValue != null) { return altValue; } - const curB = b.get(altValue); - if ((curB === undefined && !b.has(altValue)) || - !innerDeepEqual(item, curB, false, memo)) { + const item1 = a.get(altValue); + if ((item1 === undefined && !a.has(altValue)) || + !innerDeepEqual(item1, item2, kLoose, memo)) { return false; } - return !a.has(altValue) && innerDeepEqual(item, curB, false, memo); + return !b.has(altValue) && innerDeepEqual(item1, item2, kLoose, memo); +} + +function partialObjectSetEquiv(a, b, mode, set, memo) { + let aPos = 0; + for (const val of a) { + aPos++; + if (!b.has(val) && setHasEqualElement(set, val, mode, memo) && set.size === 0) { + return true; + } + if (a.size - aPos < set.size) { + return false; + } + } + /* c8 ignore next */ + assert.fail('Unreachable code'); } -function setEquiv(a, b, strict, 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) { + for (const val of a) { + if (!setHasEqualElement(set, val, mode, memo)) { + return false; + } + } + return true; + } + + for (const val of a) { + // Primitive values have already been handled above. + if (typeof val === 'object') { + if (!b.has(val) && !setHasEqualElement(set, val, mode, memo)) { + return false; + } + } else if (mode === kLoose && + !b.has(val) && + !setHasEqualElement(set, val, mode, memo)) { + return false; + } + } + return set.size === 0; +} + +function setEquiv(a, b, mode, memo) { // This is a lazily initiated Set of entries which have to be compared // pairwise. let set = null; - for (const val of a) { - if (!b.has(val)) { + for (const val of b) { + if (!a.has(val)) { if ((typeof val !== 'object' || val === null) && - (strict || !setMightHaveLoosePrim(a, b, val))) { + (mode !== kLoose || !setMightHaveLoosePrim(a, b, val))) { return false; } if (set === null) { - if (b.size === 1) { - return innerDeepEqual(val, b.values().next().value, strict, memo); + if (a.size === 1) { + return innerDeepEqual(a.values().next().value, val, mode, memo); } set = new SafeSet(); } // If the specified value doesn't exist in the second set it's a object // (or in loose mode: a non-matching primitive). Find the - // deep-(strict-)equal element in a set copy to reduce duplicate checks. + // deep-(mode-)equal element in a set copy to reduce duplicate checks. set.add(val); } } if (set !== null) { - for (const val of b) { - // Primitive values have already been handled above. - if (typeof val === 'object' && val !== null) { - if (!a.has(val) && !setHasEqualElement(set, val, strict, memo)) { - return false; - } - } else if (!strict && - !a.has(val) && - !setHasEqualElement(set, val, strict, memo)) { - return false; - } - } - return set.size === 0; + return setObjectEquiv(a, b, mode, set, memo); } return true; } -function mapHasEqualEntry(set, map, key2, item2, strict, memo) { +function mapHasEqualEntry(set, map, key1, item1, mode, memo) { // To be able to handle cases like: // Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']]) // ... we need to consider *all* matching keys, not just the first we find. - const { deleteFailures } = memo; - memo.deleteFailures = true; - for (const key1 of set) { - if (innerDeepEqual(key1, key2, strict, memo) && - innerDeepEqual(map.get(key1), item2, strict, memo)) { - set.delete(key1); - memo.deleteFailures = deleteFailures; + for (const key2 of set) { + if (innerDeepEqual(key1, key2, mode, memo) && + innerDeepEqual(item1, map.get(key2), mode, memo)) { + set.delete(key2); return true; } } - memo.deleteFailures = deleteFailures; return false; } -function mapEquiv(a, b, strict, memo) { +function partialObjectMapEquiv(a, b, mode, set, memo) { + let aPos = 0; + for (const { 0: key1, 1: item1 } of a) { + aPos++; + if (typeof key1 === 'object' && + key1 !== null && + mapHasEqualEntry(set, b, key1, item1, mode, memo) && + set.size === 0) { + return true; + } + if (a.size - aPos < set.size) { + return false; + } + } + /* c8 ignore next */ + assert.fail('Unreachable code'); +} + +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) { + for (const { 0: key1, 1: item1 } of a) { + if (!mapHasEqualEntry(set, b, key1, item1, mode, memo)) { + return false; + } + } + return true; + } + for (const { 0: key1, 1: item1 } of a) { + if (typeof key1 === 'object' && key1 !== null) { + if (!mapHasEqualEntry(set, b, key1, item1, mode, memo)) + return false; + } else if (set.size === 0) { + return true; + } else if (mode === kLoose && + (!b.has(key1) || + !innerDeepEqual(item1, b.get(key1), mode, memo)) && + !mapHasEqualEntry(set, b, key1, item1, mode, memo)) { + return false; + } + } + return set.size === 0; +} + +function mapEquiv(a, b, mode, memo) { let set = null; - for (const { 0: key, 1: item1 } of a) { - if (typeof key === 'object' && key !== null) { + for (const { 0: key2, 1: item2 } of b) { + if (typeof key2 === 'object' && key2 !== null) { if (set === null) { - if (b.size === 1) { - const { 0: key2, 1: item2 } = b.entries().next().value; - return innerDeepEqual(key, key2, strict, memo) && - innerDeepEqual(item1, item2, strict, memo); + if (a.size === 1) { + const { 0: key1, 1: item1 } = a.entries().next().value; + return innerDeepEqual(key1, key2, mode, memo) && + innerDeepEqual(item1, item2, mode, memo); } set = new SafeSet(); } - set.add(key); + set.add(key2); } else { - // By directly retrieving the value we prevent another b.has(key) check in + // By directly retrieving the value we prevent another b.has(key2) check in // almost all possible cases. - const item2 = b.get(key); - if (((item2 === undefined && !b.has(key)) || - !innerDeepEqual(item1, item2, strict, memo))) { - if (strict) + const item1 = a.get(key2); + if (((item1 === undefined && !a.has(key2)) || + !innerDeepEqual(item1, item2, mode, memo))) { + if (mode !== kLoose) return false; // Fast path to detect missing string, symbol, undefined and null // keys. - if (!mapMightHaveLoosePrim(a, b, key, item1, memo)) + if (!mapMightHaveLoosePrim(a, b, key2, item2, memo)) return false; if (set === null) { set = new SafeSet(); } - set.add(key); + set.add(key2); } } } if (set !== null) { - for (const { 0: key, 1: item } of b) { - if (typeof key === 'object' && key !== null) { - if (!mapHasEqualEntry(set, a, key, item, strict, memo)) - return false; - } else if (!strict && - (!a.has(key) || - !innerDeepEqual(a.get(key), item, strict, memo)) && - !mapHasEqualEntry(set, a, key, item, strict, memo)) { + return mapObjectEquivalence(a, b, mode, set, memo); + } + + return true; +} + +function partialSparseArrayEquiv(a, b, mode, memos, startA, startB) { + let aPos = 0; + const keysA = ObjectKeys(a).slice(startA); + const keysB = ObjectKeys(b).slice(startB); + if (keysA.length < keysB.length) { + return false; + } + for (let i = 0; i < keysB.length; i++) { + const keyB = keysB[i]; + while (!innerDeepEqual(a[keysA[aPos]], b[keyB], mode, memos)) { + aPos++; + if (aPos > keysA.length - keysB.length + i) { return false; } } - return set.size === 0; + aPos++; } + return true; +} +function partialArrayEquiv(a, b, mode, memos) { + let aPos = 0; + for (let i = 0; i < b.length; i++) { + let isSparse = b[i] === undefined && !hasOwn(b, i); + if (isSparse) { + return partialSparseArrayEquiv(a, b, mode, memos, aPos, i); + } + while (!(isSparse = a[aPos] === undefined && !hasOwn(a, aPos)) && + !innerDeepEqual(a[aPos], b[i], mode, memos)) { + aPos++; + if (aPos > a.length - b.length + i) { + return false; + } + } + if (isSparse) { + return partialSparseArrayEquiv(a, b, mode, memos, aPos, i); + } + aPos++; + } return true; } -function objEquiv(a, b, strict, keys, memos, iterationType) { +function sparseArrayEquiv(a, b, mode, memos, i) { + // TODO(BridgeAR): Use internal method to only get index properties. The + // same applies to the partial implementation. + const keysA = ObjectKeys(a); + const keysB = ObjectKeys(b); + if (keysA.length !== keysB.length) { + return false; + } + for (; i < keysA.length; i++) { + const key = keysA[i]; + if (!hasOwn(b, key) || !innerDeepEqual(a[key], b[key], mode, memos)) { + return false; + } + } + return true; +} + +function objEquiv(a, b, mode, keys2, memos, iterationType) { // The pair must have equivalent values for every corresponding key. - if (keys.length > 0) { - for (const key of keys) { - if (!innerDeepEqual(a[key], b[key], strict, memos)) { + if (keys2.length > 0) { + for (const key of keys2) { + if (!innerDeepEqual(a[key], b[key], mode, memos)) { return false; } } } if (iterationType === kIsArray) { + if (mode === kPartial) { + return partialArrayEquiv(a, b, mode, memos); + } for (let i = 0; i < a.length; i++) { - if (ObjectPrototypeHasOwnProperty(a, i)) { - if (!ObjectPrototypeHasOwnProperty(b, i) || - !innerDeepEqual(a[i], b[i], strict, memos)) { - return false; - } - } else if (ObjectPrototypeHasOwnProperty(b, i)) { + if (!innerDeepEqual(a[i], b[i], mode, memos)) { return false; - } else { - // Array is sparse. - const keysA = ObjectKeys(a); - for (; i < keysA.length; i++) { - const key = keysA[i]; - if (!ObjectPrototypeHasOwnProperty(b, key) || - !innerDeepEqual(a[key], b[key], strict, memos)) { - return false; - } - } - return keysA.length === ObjectKeys(b).length; + } + const isSparseA = a[i] === undefined && !hasOwn(a, i); + const isSparseB = b[i] === undefined && !hasOwn(b, i); + if (isSparseA !== isSparseB) { + return false; + } + if (isSparseA) { + return sparseArrayEquiv(a, b, mode, memos, i); } } } else if (iterationType === kIsSet) { - if (!setEquiv(a, b, strict, memos)) { + if (!setEquiv(a, b, mode, memos)) { return false; } } else if (iterationType === kIsMap) { - if (!mapEquiv(a, b, strict, memos)) { + if (!mapEquiv(a, b, mode, memos)) { return false; } } @@ -646,15 +807,14 @@ function objEquiv(a, b, strict, keys, memos, iterationType) { return true; } -function isDeepEqual(val1, val2) { - return innerDeepEqual(val1, val2, kLoose); -} - -function isDeepStrictEqual(val1, val2) { - return innerDeepEqual(val1, val2, kStrict); -} - module.exports = { - isDeepEqual, - isDeepStrictEqual, + isDeepEqual(val1, val2) { + return innerDeepEqual(val1, val2, kLoose); + }, + isDeepStrictEqual(val1, val2) { + return innerDeepEqual(val1, val2, kStrict); + }, + isPartialStrictEqual(val1, val2) { + return innerDeepEqual(val1, val2, kPartial); + }, }; diff --git a/test/parallel/test-assert-deep-with-error.js b/test/parallel/test-assert-deep-with-error.js index f6bc5c6359cfe8..8cfb9ce1bf909b 100644 --- a/test/parallel/test-assert-deep-with-error.js +++ b/test/parallel/test-assert-deep-with-error.js @@ -3,6 +3,13 @@ require('../common'); const assert = require('assert'); const { test } = require('node:test'); +// Disable colored output to prevent color codes from breaking assertion +// message comparisons. This should only be an issue when process.stdout +// is a TTY. +if (process.stdout.isTTY) { + process.env.NODE_DISABLE_COLORS = '1'; +} + const defaultStartMessage = 'Expected values to be strictly deep-equal:\n' + '+ actual - expected\n' + '\n'; diff --git a/test/parallel/test-assert-deep.js b/test/parallel/test-assert-deep.js index 93cc248160e6a1..03de8fafe25676 100644 --- a/test/parallel/test-assert-deep.js +++ b/test/parallel/test-assert-deep.js @@ -198,12 +198,14 @@ test('deepEqual should pass for these weird cases', () => { function assertDeepAndStrictEqual(a, b) { assert.deepEqual(a, b); assert.deepStrictEqual(a, b); + assert.partialDeepStrictEqual(a, b); assert.deepEqual(b, a); assert.deepStrictEqual(b, a); + assert.partialDeepStrictEqual(b, a); } -function assertNotDeepOrStrict(a, b, err) { +function assertNotDeepOrStrict(a, b, err, options) { assert.throws( () => assert.deepEqual(a, b), err || re`${a}\n\nshould loosely deep-equal\n\n${b}` @@ -221,6 +223,15 @@ function assertNotDeepOrStrict(a, b, err) { () => assert.deepStrictEqual(b, a), err || { code: 'ERR_ASSERTION' } ); + const partial = () => { + assert.partialDeepStrictEqual(b, a); + assert.partialDeepStrictEqual(a, b); + }; + if (options?.partial === 'pass') { + partial(); + } else { + assert.throws(partial, err || { code: 'ERR_ASSERTION' }); + } } function assertOnlyDeepEqual(a, b, err) { @@ -598,16 +609,17 @@ test('Handle sparse arrays', () => { /* eslint-disable no-sparse-arrays */ assertDeepAndStrictEqual([1, , , 3], [1, , , 3]); assertNotDeepOrStrict([1, , , 3], [1, , , 3, , , ]); + assertNotDeepOrStrict([1, , , 3], [1, undefined, , 3]); /* eslint-enable no-sparse-arrays */ const a = new Array(3); const b = new Array(3); a[2] = true; b[1] = true; - assertNotDeepOrStrict(a, b); + assertNotDeepOrStrict(a, b, AssertionError, { partial: 'pass' }); b[2] = true; assertNotDeepOrStrict(a, b); a[0] = true; - assertNotDeepOrStrict(a, b); + assertNotDeepOrStrict(a, b, AssertionError, { partial: 'pass' }); }); test('Handle different error messages', () => { @@ -1246,6 +1258,14 @@ test('Verify object types being identical on both sides', () => { }); assertNotDeepOrStrict(a, b); + a = new ArrayBuffer(3); + b = new Uint8Array(3); + Object.setPrototypeOf(b, ArrayBuffer.prototype); + Object.defineProperty(b, Symbol.toStringTag, { + value: 'ArrayBuffer' + }); + assertNotDeepOrStrict(a, b); + a = new Date(2000); b = Object.create( Object.getPrototypeOf(a), diff --git a/test/parallel/test-assert-objects.js b/test/parallel/test-assert-partial-deep-equal.js similarity index 69% rename from test/parallel/test-assert-objects.js rename to test/parallel/test-assert-partial-deep-equal.js index 4cf8f424f91913..08031d4e0aa747 100644 --- a/test/parallel/test-assert-objects.js +++ b/test/parallel/test-assert-partial-deep-equal.js @@ -5,9 +5,12 @@ const vm = require('node:vm'); const assert = require('node:assert'); const { describe, it } = require('node:test'); +const x = ['x']; + function createCircularObject() { const obj = {}; obj.self = obj; + obj.set = new Set([x, ['y']]); return obj; } @@ -43,6 +46,11 @@ describe('Object Comparison Tests', () => { actual: { a: 1 }, expected: undefined, }, + { + description: 'throws when unequal zeros are compared', + actual: 0, + expected: -0, + }, { description: 'throws when only expected is provided', actual: undefined, @@ -168,10 +176,32 @@ describe('Object Comparison Tests', () => { }, { description: - 'throws when comparing two objects with different Error instances', + 'throws when comparing two objects with different Error message', actual: { error: new Error('Test error 1') }, expected: { error: new Error('Test error 2') }, }, + { + description: + 'throws when comparing two objects with missing cause on the actual Error', + actual: { error: new Error('Test error 1') }, + expected: { error: new Error('Test error 1', { cause: 42 }) }, + }, + { + description: + 'throws when comparing two objects with missing message on the actual Error', + actual: { error: new Error() }, + expected: { error: new Error('Test error 1') }, + }, + { + description: 'throws when comparing two Errors with missing cause on the actual Error', + actual: { error: new Error('Test error 1') }, + expected: { error: new Error('Test error 1', { cause: undefined }) }, + }, + { + description: 'throws when comparing two AggregateErrors with missing message on the actual Error', + actual: { error: new AggregateError([], 'Test error 1') }, + expected: { error: new AggregateError([new Error()], 'Test error 1') }, + }, { description: 'throws when comparing two objects with different TypedArray instances and content', @@ -225,6 +255,46 @@ describe('Object Comparison Tests', () => { ['key1', ['value3']], ]), }, + { + description: 'throws for maps with object keys and different values', + actual: new Map([ + [{ a: 1 }, 'value1'], + [{ b: 2 }, 'value2'], + [{ b: 2 }, 'value4'], + ]), + expected: new Map([ + [{ a: 1 }, 'value1'], + [{ b: 2 }, 'value3'], + ]), + }, + { + description: 'throws for maps with multiple identical object keys, just not enough', + actual: new Map([ + [{ a: 1 }, 'value1'], + [{ b: 1 }, 'value2'], + [{ a: 1 }, 'value1'], + ]), + expected: new Map([ + [{ a: 1 }, 'value1'], + [{ a: 1 }, 'value1'], + [{ a: 1 }, 'value1'], + ]), + }, + { + description: 'throws for sets with different object values', + actual: new Set([ + { a: 1 }, + { a: 2 }, + { a: 1 }, + { a: 2 }, + ]), + expected: new Set([ + { a: 1 }, + { a: 2 }, + { a: 1 }, + { a: 1 }, + ]), + }, { description: 'throws when comparing two TypedArray instances with different content', @@ -261,11 +331,124 @@ describe('Object Comparison Tests', () => { actual: [1, 2, 3], expected: ['2'], }, + { + description: 'throws when comparing an array with symbol properties not matching', + actual: (() => { + const array = [1, 2, 3]; + array[Symbol.for('test')] = 'test'; + return array; + })(), + expected: (() => { + const array = [1, 2, 3]; + array[Symbol.for('test')] = 'different'; + return array; + })(), + }, + { + description: 'throws when comparing an array with extra properties not matching', + actual: (() => { + const array = [1, 2, 3]; + array.extra = 'test'; + return array; + })(), + expected: (() => { + const array = [1, 2, 3]; + array.extra = 'different'; + return array; + })(), + }, + { + description: 'throws when comparing a non matching sparse array', + actual: (() => { + const array = new Array(1000); + array[90] = 1; + array[92] = 2; + array[95] = 1; + array[96] = 2; + array.foo = 'bar'; + array.extra = 'test'; + return array; + })(), + expected: (() => { + const array = new Array(1000); + array[90] = 1; + array[92] = 1; + array[95] = 1; + array.extra = 'test'; + array.foo = 'bar'; + return array; + })(), + }, + { + description: 'throws when comparing a same length sparse array with actual less keys', + actual: (() => { + const array = new Array(1000); + array[90] = 1; + array[92] = 1; + return array; + })(), + expected: (() => { + const array = new Array(1000); + array[90] = 1; + array[92] = 1; + array[95] = 1; + return array; + })(), + }, + { + description: 'throws when comparing an array with symbol properties matching but other enumerability', + actual: (() => { + const array = [1, 2, 3]; + array[Symbol.for('abc')] = 'test'; + Object.defineProperty(array, Symbol.for('test'), { + value: 'test', + enumerable: false, + }); + array[Symbol.for('other')] = 'test'; + return array; + })(), + expected: (() => { + const array = [1, 2, 3]; + array[Symbol.for('test')] = 'test'; + return array; + })(), + }, + { + description: 'throws comparing an array with extra properties matching but other enumerability', + actual: (() => { + const array = [1, 2, 3]; + array.alsoIgnored = [{ nested: { property: true } }]; + Object.defineProperty(array, 'extra', { + value: 'test', + enumerable: false, + }); + array.ignored = 'test'; + return array; + })(), + expected: (() => { + const array = [1, 2, 3]; + array.extra = 'test'; + return array; + })(), + }, { description: 'throws when comparing an ArrayBuffer with a Uint8Array', actual: new ArrayBuffer(3), expected: new Uint8Array(3), }, + { + description: 'throws when comparing an TypedArrays with symbol properties not matching', + actual: (() => { + const typed = new Uint8Array(3); + typed[Symbol.for('test')] = 'test'; + return typed; + })(), + expected: (() => { + const typed = new Uint8Array(3); + typed[Symbol.for('test')] = 'different'; + return typed; + })(), + }, { description: 'throws when comparing a ArrayBuffer with a SharedArrayBuffer', actual: new ArrayBuffer(3), @@ -445,6 +628,44 @@ describe('Object Comparison Tests', () => { actual: [0, -0, 0], expected: [0, 0], }, + { + description: 'comparing an array with symbol properties matching', + actual: (() => { + const array = [1, 2, 3]; + array[Symbol.for('abc')] = 'test'; + array[Symbol.for('test')] = 'test'; + Object.defineProperty(array, Symbol.for('hidden'), { + value: 'hidden', + enumerable: false, + }); + return array; + })(), + expected: (() => { + const array = [1, 2, 3]; + array[Symbol.for('test')] = 'test'; + return array; + })(), + }, + { + description: 'comparing an array with extra properties matching', + actual: (() => { + const array = [1, 2, 3]; + array.alsoIgnored = [{ nested: { property: true } }]; + array.extra = 'test'; + array.ignored = 'test'; + return array; + })(), + expected: (() => { + const array = [1, 2, 3]; + array.extra = 'test'; + Object.defineProperty(array, 'ignored', { enumerable: false }); + Object.defineProperty(array, Symbol.for('hidden'), { + value: 'hidden', + enumerable: false, + }); + return array; + })(), + }, { description: 'compares two Date objects with the same time', actual: new Date(0), @@ -597,6 +818,62 @@ describe('Object Comparison Tests', () => { ['key3', new Uint8Array([1, 2, 3])], ]) }, + { + description: 'compares maps with object keys', + actual: new Map([ + [{ a: 1 }, 'value1'], + [{ a: 2 }, 'value2'], + [{ a: 2 }, 'value3'], + [{ a: 2 }, 'value3'], + [{ a: 2 }, 'value4'], + [{ a: 1 }, 'value2'], + ]), + expected: new Map([ + [{ a: 2 }, 'value3'], + [{ a: 1 }, 'value1'], + [{ a: 2 }, 'value3'], + [{ a: 1 }, 'value2'], + ]), + }, + { + describe: 'compares two simple sparse arrays', + actual: new Array(1_000), + expected: new Array(100), + }, + { + describe: 'compares two identical sparse arrays', + actual: (() => { + const array = new Array(100); + array[1] = 2; + return array; + })(), + expected: (() => { + const array = new Array(100); + array[1] = 2; + return array; + })(), + }, + { + describe: 'compares two big sparse arrays', + actual: (() => { + const array = new Array(150_000_000); + array[0] = 1; + array[1] = 2; + array[100] = 100n; + array[200_000] = 3; + array[1_200_000] = 4; + array[120_200_000] = []; + return array; + })(), + expected: (() => { + const array = new Array(100_000_000); + array[0] = 1; + array[1] = 2; + array[200_000] = 3; + array[1_200_000] = 4; + return array; + })(), + }, { describe: 'compares two array of objects', actual: [{ a: 5 }], @@ -617,6 +894,21 @@ describe('Object Comparison Tests', () => { actual: new Set([{ a: 1 }, { b: 1 }]), expected: new Set([{ a: 1 }]), }, + { + description: 'compares two Sets with mixed entries', + actual: new Set([{ b: 1 }, [], 1, { a: 1 }, 2, []]), + expected: new Set([{ a: 1 }, 2, []]), + }, + { + description: 'compares two Sets with mixed entries different order', + actual: new Set([{ a: 1 }, 1, { b: 1 }, [], 2, { a: 1 }]), + expected: new Set([{ a: 1 }, [], 2, { a: 1 }]), + }, + { + description: 'compares two Sets with mixed entries different order 2', + actual: new Set([{ a: 1 }, { a: 1 }, 1, { b: 1 }, [], 2, { a: 1 }]), + expected: new Set([{ a: 1 }, [], 2, { a: 1 }]), + }, { description: 'compares two Set objects with identical arrays', actual: new Set(['value1', 'value2']), @@ -785,8 +1077,8 @@ describe('Object Comparison Tests', () => { { description: 'compares one subset array with another', - actual: [1, 2, 3], - expected: [2], + actual: [1, 2, 3, 4, 5, 6, 7, 8, 9], + expected: [2, 5, 6, 7, 8], }, { description: 'ensures that File extends Blob', @@ -803,6 +1095,58 @@ describe('Object Comparison Tests', () => { actual: new URL('http://foo'), expected: new URL('http://foo'), }, + { + description: 'compares a more complex object with additional parts on the actual', + actual: [{ + foo: 'yarp', + nope: { + bar: '123', + a: [ 1, 2, 0 ], + c: {}, + b: [ + { + foo: 'yarp', + nope: { bar: '123', a: [ 1, 2, 0 ], c: {}, b: [] } + }, + { + foo: 'yarp', + nope: { bar: '123', a: [ 1, 2, 1 ], c: {}, b: [] } + }, + ], + } + }], + expected: [{ + foo: 'yarp', + nope: { + bar: '123', + c: {}, + b: [ + { foo: 'yarp', nope: { bar: '123', c: {}, b: [] } }, + { foo: 'yarp', nope: { bar: '123', c: {}, b: [] } }, + ], + } + }] + }, + { + description: 'comparing two Errors with missing cause on the expected Error', + actual: { error: new Error('Test error 1', { cause: 42 }) }, + expected: { error: new Error('Test error 1') }, + }, + { + description: 'comparing two Errors with cause set to undefined on the actual Error', + actual: { error: new Error('Test error 1', { cause: undefined }) }, + expected: { error: new Error('Test error 1') }, + }, + { + description: 'comparing two Errors with missing message on the expected Error', + actual: { error: new Error('Test error 1') }, + expected: { error: new Error() }, + }, + { + description: 'comparing two AggregateErrors with no message or errors on the expected Error', + actual: { error: new AggregateError([new Error(), 123]) }, + expected: { error: new AggregateError([]) }, + }, ].forEach(({ description, actual, expected }) => { it(description, () => { assert.partialDeepStrictEqual(actual, expected); diff --git a/test/parallel/test-assert-typedarray-deepequal.js b/test/parallel/test-assert-typedarray-deepequal.js index 403cd6748d507e..7fb18c1886ba91 100644 --- a/test/parallel/test-assert-typedarray-deepequal.js +++ b/test/parallel/test-assert-typedarray-deepequal.js @@ -101,15 +101,10 @@ suite('notEqualArrayPairs', () => { makeBlock(assert.deepStrictEqual, arrayPair[0], arrayPair[1]), assert.AssertionError ); - // TODO(puskin94): remove emitWarning override once the partialDeepStrictEqual method is not experimental anymore - // Suppress warnings, necessary otherwise the tools/pseudo-tty.py runner will fail - const originalEmitWarning = process.emitWarning; - process.emitWarning = () => {}; assert.throws( makeBlock(assert.partialDeepStrictEqual, arrayPair[0], arrayPair[1]), assert.AssertionError ); - process.emitWarning = originalEmitWarning; // Restore original process.emitWarning }); } }); diff --git a/test/pseudo-tty/test-assert-colors.js b/test/pseudo-tty/test-assert-colors.js index 889e9a6e006e99..846326a9c4936a 100644 --- a/test/pseudo-tty/test-assert-colors.js +++ b/test/pseudo-tty/test-assert-colors.js @@ -1,4 +1,3 @@ -// Flags: --no-warnings 'use strict'; require('../common'); const assert = require('assert').strict; diff --git a/test/pseudo-tty/test-assert-no-color.js b/test/pseudo-tty/test-assert-no-color.js index c2e79d26701413..d6765d6f06c598 100644 --- a/test/pseudo-tty/test-assert-no-color.js +++ b/test/pseudo-tty/test-assert-no-color.js @@ -1,4 +1,3 @@ -// Flags: --no-warnings 'use strict'; require('../common'); const assert = require('assert').strict;