Skip to content
Open
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
120 changes: 91 additions & 29 deletions src/js/node/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
isWeakSet,
isWeakMap,
isAnyArrayBuffer,
isSharedArrayBuffer,
} = require("node:util/types");
const { innerOk } = require("internal/assert/utils");
const { validateFunction } = require("internal/validators");
Expand All @@ -46,6 +47,7 @@
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;
Expand All @@ -54,6 +56,11 @@
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");

Expand Down Expand Up @@ -376,7 +383,7 @@
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;
Expand Down Expand Up @@ -405,15 +412,28 @@
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));
}

Check warning on line 436 in src/js/node/assert.ts

View check run for this annotation

Claude / Claude Code Review

ArrayBufferView/ArrayBuffer branches skip Node's non-index own-property check

nit: The new ArrayBufferView / ArrayBuffer branches `return isPartialUint8Array(...)` directly, so non-index own properties on `expected` are never checked — Node's `kPartial` path falls through to `keyCheck` after the byte-subsequence check, so `assert.partialDeepStrictEqual(new Uint8Array([1,2,3]), Object.assign(new Uint8Array([1,3]), {foo:1}))` throws in Node but silently passes here. Not blocking (extra own props on views are rare and #33068 is the full port), but it's the false-positive dir
Comment thread
robobun marked this conversation as resolved.

for (const type of typesToCallDeepStrictEqualWith) {
if (type(actual) || type(expected)) {
Expand All @@ -421,27 +441,13 @@
}
}

// 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,
Expand Down Expand Up @@ -493,21 +499,77 @@
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);
Expand Down
85 changes: 85 additions & 0 deletions test/js/node/assert/assert-partial-deep-strict-equal.test.ts
Original file line number Diff line number Diff line change
@@ -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<object>();
a.add({ s: a });
const b = new Set<object>();
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);
});
});
Loading