diff --git a/src/js/node/assert.ts b/src/js/node/assert.ts index ed8ac7bcf9ad..e9d41263b54e 100644 --- a/src/js/node/assert.ts +++ b/src/js/node/assert.ts @@ -32,6 +32,7 @@ const { isWeakSet, isWeakMap, isAnyArrayBuffer, + isSharedArrayBuffer, } = require("node:util/types"); const { innerOk } = require("internal/assert/utils"); const { validateFunction } = require("internal/validators"); @@ -46,6 +47,7 @@ const NumberIsNaN = Number.isNaN; const ObjectAssign = Object.assign; const ObjectIs = Object.is; const ObjectKeys = Object.keys; +const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; const ObjectPrototypeIsPrototypeOf = Object.prototype.isPrototypeOf; const ReflectHas = Reflect.has; const ReflectOwnKeys = Reflect.ownKeys; @@ -54,6 +56,11 @@ const StringPrototypeIndexOf = String.prototype.indexOf; const StringPrototypeSlice = String.prototype.slice; const StringPrototypeSplit = String.prototype.split; const SymbolIterator = Symbol.iterator; +const TypedArrayPrototypeGetSymbolToStringTag = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(Uint8Array.prototype), + Symbol.toStringTag, +)!.get!; +const Uint8ArrayFromBuffer = (view: ArrayBufferView) => new Uint8Array(view.buffer, view.byteOffset, view.byteLength); type nodeAssert = typeof import("node:assert"); @@ -376,7 +383,7 @@ function isSpecial(obj) { return obj == null || typeof obj !== "object" || Error.isError(obj) || isRegExp(obj) || isDate(obj); } -const typesToCallDeepStrictEqualWith = [isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer]; +const typesToCallDeepStrictEqualWith = [isKeyObject, isWeakSet, isWeakMap]; const SafeSetPrototypeIterator = SafeSet.prototype[SymbolIterator]; const SafeMapPrototypeIterator = SafeMap.prototype[SymbolIterator]; const SafeMapPrototypeHas = SafeMap.prototype.has; @@ -405,14 +412,27 @@ function compareBranch(actual, expected, comparedObjects?) { return withCycleGuard(actual, expected, comparedObjects, compareBranchMap); } - // Check for ArrayBuffer object equality - if ( - ArrayBufferIsView(actual) || - isAnyArrayBuffer(actual) || - ArrayBufferIsView(expected) || - isAnyArrayBuffer(expected) - ) { - return Bun.deepEquals(actual, expected, true); + // ArrayBufferView / ArrayBuffer: Node matches the expected bytes as an in-order + // subsequence of the actual bytes (isPartialArrayBufferView / isPartialUint8Array). + if (ArrayBufferIsView(actual) || ArrayBufferIsView(expected)) { + if ( + !ArrayBufferIsView(actual) || + !ArrayBufferIsView(expected) || + TypedArrayPrototypeGetSymbolToStringTag.$call(actual) !== TypedArrayPrototypeGetSymbolToStringTag.$call(expected) + ) { + return false; + } + return isPartialUint8Array(Uint8ArrayFromBuffer(actual), Uint8ArrayFromBuffer(expected)); + } + if (isAnyArrayBuffer(actual) || isAnyArrayBuffer(expected)) { + if ( + !isAnyArrayBuffer(actual) || + !isAnyArrayBuffer(expected) || + isSharedArrayBuffer(actual) !== isSharedArrayBuffer(expected) + ) { + return false; + } + return isPartialUint8Array(new Uint8Array(actual), new Uint8Array(expected)); } for (const type of typesToCallDeepStrictEqualWith) { @@ -421,27 +441,13 @@ function compareBranch(actual, expected, comparedObjects?) { } } - // Check for Set object equality + // Set: every expected member must partially match a distinct actual member. if (isSet(actual) && isSet(expected)) { if (expected.size > actual.size) { return false; // `expected` can't be a subset if it has more elements } - const actualArray = ArrayFrom(SafeSetPrototypeIterator.$call(actual)); - const expectedIterator = SafeSetPrototypeIterator.$call(expected); - const usedIndices = new SafeSet(); - - expectedIteration: for (const expectedItem of expectedIterator) { - for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) { - if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) { - usedIndices.add(actualIdx); - continue expectedIteration; - } - } - return false; - } - - return true; + return withCycleGuard(actual, expected, comparedObjects, compareBranchSet); } // The expected array must match a subsequence of the actual array, in order, @@ -493,21 +499,77 @@ function compareBranchMap(actual, expected, comparedObjects) { return true; } +function isPartialUint8Array(a, b) { + const lenA = a.byteLength; + const lenB = b.byteLength; + if (lenA < lenB) return false; + let offsetA = 0; + for (let offsetB = 0; offsetB < lenB; offsetB++) { + while (a[offsetA] !== b[offsetB]) { + offsetA++; + if (offsetA > lenA - lenB + offsetB) return false; + } + offsetA++; + } + return true; +} + +function compareBranchSet(actual, expected, comparedObjects) { + const actualArray = ArrayFrom(SafeSetPrototypeIterator.$call(actual)); + const expectedIterator = SafeSetPrototypeIterator.$call(expected); + const usedIndices = new SafeSet(); + + expectedIteration: for (const expectedItem of expectedIterator) { + for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) { + if (!usedIndices.has(actualIdx) && compareBranch(actualArray[actualIdx], expectedItem, comparedObjects)) { + usedIndices.add(actualIdx); + continue expectedIteration; + } + } + return false; + } + return true; +} + function compareBranchArray(actual, expected, comparedObjects) { let actualPos = 0; for (let i = 0; i < expected.length; i++) { - const lastCandidate = actual.length - expected.length + i; - while (actualPos <= lastCandidate && !compareBranch(actual[actualPos], expected[i], comparedObjects)) { + let isSparse = expected[i] === undefined && !ObjectPrototypeHasOwnProperty.$call(expected, i); + if (isSparse) { + return compareBranchSparseArray(actual, expected, comparedObjects, actualPos, i); + } + while ( + !(isSparse = actual[actualPos] === undefined && !ObjectPrototypeHasOwnProperty.$call(actual, actualPos)) && + !compareBranch(actual[actualPos], expected[i], comparedObjects) + ) { actualPos++; + if (actualPos > actual.length - expected.length + i) return false; } - if (actualPos > lastCandidate) { - return false; + if (isSparse) { + return compareBranchSparseArray(actual, expected, comparedObjects, actualPos, i); } actualPos++; } return true; } +function compareBranchSparseArray(actual, expected, comparedObjects, startA, startB) { + let aPos = startA; + const keysA = ObjectKeys(actual); + const keysB = ObjectKeys(expected); + const lenB = keysB.length - startB; + if (keysA.length - startA < lenB) return false; + for (let i = 0; i < lenB; i++) { + const keyB = keysB[startB + i]; + while (!compareBranch(actual[keysA[aPos]], expected[keyB], comparedObjects)) { + aPos++; + if (aPos > keysA.length - lenB + i) return false; + } + aPos++; + } + return true; +} + function compareBranchObject(actual, expected, comparedObjects) { // Use Reflect.ownKeys() instead of Object.keys() to include symbol properties const keysExpected = ReflectOwnKeys(expected); diff --git a/test/js/node/assert/assert-partial-deep-strict-equal.test.ts b/test/js/node/assert/assert-partial-deep-strict-equal.test.ts new file mode 100644 index 000000000000..bbf9d8006fee --- /dev/null +++ b/test/js/node/assert/assert-partial-deep-strict-equal.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, test } from "bun:test"; +import assert from "node:assert"; + +describe("assert.partialDeepStrictEqual", () => { + test("TypedArrays, Buffers and DataViews match the expected bytes as an in-order subsequence", () => { + assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 3])); + assert.partialDeepStrictEqual(new Uint8Array([1, 2, 1, 3]), new Uint8Array([1, 1, 3])); + assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Uint8Array([])); + assert.partialDeepStrictEqual(new Uint16Array([1, 2, 3]), new Uint16Array([1, 3])); + assert.partialDeepStrictEqual(new Float32Array([1.5, 2.5, 3.5]), new Float32Array([1.5, 3.5])); + assert.partialDeepStrictEqual(new Float64Array([1, NaN, 3]), new Float64Array([NaN])); + assert.partialDeepStrictEqual(new BigInt64Array([1n, 2n, 3n]), new BigInt64Array([1n, 3n])); + assert.partialDeepStrictEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 2])); + assert.partialDeepStrictEqual(Buffer.from([1, 2, 3]), new Uint8Array([1, 3])); + assert.partialDeepStrictEqual( + new DataView(new Uint8Array([1, 2, 3]).buffer), + new DataView(new Uint8Array([1, 3]).buffer), + ); + + expect(() => assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Uint8Array([3, 1]))).toThrow( + assert.AssertionError, + ); + expect(() => assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3, 4]))).toThrow( + assert.AssertionError, + ); + expect(() => assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Int8Array([1, 3]))).toThrow( + assert.AssertionError, + ); + expect(() => + assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new DataView(new Uint8Array([1, 3]).buffer)), + ).toThrow(assert.AssertionError); + }); + + test("ArrayBuffers match the expected bytes as an in-order subsequence", () => { + assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]).buffer, new Uint8Array([1, 3]).buffer); + assert.partialDeepStrictEqual(new SharedArrayBuffer(3), new SharedArrayBuffer(2)); + + expect(() => assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]).buffer, new Uint8Array([1, 3]))).toThrow( + assert.AssertionError, + ); + expect(() => assert.partialDeepStrictEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 3]).buffer)).toThrow( + assert.AssertionError, + ); + expect(() => assert.partialDeepStrictEqual(new ArrayBuffer(3), new SharedArrayBuffer(2))).toThrow( + assert.AssertionError, + ); + }); + + test("Set members are compared with the partial algorithm", () => { + assert.partialDeepStrictEqual(new Set([{ a: 1, b: 2 }]), new Set([{ a: 1 }])); + assert.partialDeepStrictEqual(new Set([{ a: 1, b: 2 }, { c: 3 }]), new Set([{ a: 1 }])); + assert.partialDeepStrictEqual(new Set([[1, 2, 3]]), new Set([[1, 3]])); + assert.partialDeepStrictEqual(new Set([{ a: 1 }, { a: 1, b: 2 }]), new Set([{ a: 1 }, { a: 1 }])); + assert.partialDeepStrictEqual(new Set([{ a: 1, nested: { x: 1, y: 2 } }]), new Set([{ nested: { x: 1 } }])); + + expect(() => assert.partialDeepStrictEqual(new Set([{ a: 1 }]), new Set([{ a: 1, b: 2 }]))).toThrow( + assert.AssertionError, + ); + expect(() => assert.partialDeepStrictEqual(new Set([{ a: 1 }]), new Set([{ a: 1 }, { b: 1 }]))).toThrow( + assert.AssertionError, + ); + + // Circular Set structures still terminate. + const a = new Set(); + a.add({ s: a }); + const b = new Set(); + b.add({ s: b }); + assert.partialDeepStrictEqual(a, b); + }); + + test("holes in the expected array are skipped", () => { + assert.partialDeepStrictEqual([1, 2, 3], [, 2]); + assert.partialDeepStrictEqual([1, 2, 3], [1, , 3]); + assert.partialDeepStrictEqual([1, 2, 3], [, , ,]); + assert.partialDeepStrictEqual([1, undefined, 3], [, undefined]); + assert.partialDeepStrictEqual({ x: [5, 6, 7] }, { x: [, 6] }); + assert.partialDeepStrictEqual([, 2, 3], [2]); + + // Holes do not relax the length gate. + expect(() => assert.partialDeepStrictEqual([1, 2, 3], [, , , ,])).toThrow(assert.AssertionError); + expect(() => assert.partialDeepStrictEqual([1, 2], [, , ,])).toThrow(assert.AssertionError); + // An explicit undefined in expected does not match a hole in actual. + expect(() => assert.partialDeepStrictEqual([, 2, 3], [undefined])).toThrow(assert.AssertionError); + }); +});