From ea681fa9ec27e420430388d85ec5e2841b68fb40 Mon Sep 17 00:00:00 2001 From: pfg Date: Fri, 25 Apr 2025 23:36:07 -0700 Subject: [PATCH] test-assert-typedarray-deepequal (#19285) --- src/bun.js/ConsoleObject.zig | 2 + src/bun.js/bindings/bindings.cpp | 43 +++++++ src/js/node/assert.ts | 25 +++- test/js/bun/test/expect.test.js | 6 + .../assert-typedarray-deepequal.test.ts | 107 +++++++++++++++++ .../test-assert-typedarray-deepequal.js | 110 ++++++++++++++++++ test/js/web/console/console-log.test.ts | 26 +++++ test/js/web/fetch/body.test.ts | 5 +- 8 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 test/js/node/assert/assert-typedarray-deepequal.test.ts create mode 100644 test/js/node/test/parallel/test-assert-typedarray-deepequal.js diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 57e9b78400..fb52650076 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -3361,6 +3361,8 @@ pub const Formatter = struct { if (arrayBuffer.typed_array_type == .Uint8Array and arrayBuffer.value.isBuffer(this.globalThis)) "Buffer" + else if (arrayBuffer.typed_array_type == .ArrayBuffer and arrayBuffer.shared) + "SharedArrayBuffer" else bun.asByteSlice(@tagName(arrayBuffer.typed_array_type)), ); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 4ab3ed9cea..c0c1e4055d 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1085,6 +1085,10 @@ std::optional specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark return false; } + if (UNLIKELY(left->isShared() != right->isShared())) { + return false; + } + if (byteLength == 0) return true; @@ -1292,6 +1296,45 @@ std::optional specialObjectsDequal(JSC__JSGlobalObject* globalObject, Mark if (UNLIKELY(vector == rightVector)) return true; + // For Float32Array and Float64Array, when not in strict mode, we need to + // handle +0 and -0 as equal, and NaN as not equal to itself. + if (!isStrict && (c1Type == Float16ArrayType || c1Type == Float32ArrayType || c1Type == Float64ArrayType)) { + if (c1Type == Float16ArrayType) { + auto* leftFloat = static_cast(vector); + auto* rightFloat = static_cast(rightVector); + size_t numElements = byteLength / sizeof(WTF::Float16); + + for (size_t i = 0; i < numElements; i++) { + if (leftFloat[i] != rightFloat[i]) { + return false; + } + } + return true; + } else if (c1Type == Float32ArrayType) { + auto* leftFloat = static_cast(vector); + auto* rightFloat = static_cast(rightVector); + size_t numElements = byteLength / sizeof(float); + + for (size_t i = 0; i < numElements; i++) { + if (leftFloat[i] != rightFloat[i]) { + return false; + } + } + return true; + } else { // Float64Array + auto* leftDouble = static_cast(vector); + auto* rightDouble = static_cast(rightVector); + size_t numElements = byteLength / sizeof(double); + + for (size_t i = 0; i < numElements; i++) { + if (leftDouble[i] != rightDouble[i]) { + return false; + } + } + return true; + } + } + return (memcmp(vector, rightVector, byteLength) == 0); } case StringObjectType: { diff --git a/src/js/node/assert.ts b/src/js/node/assert.ts index 89d2452c18..3d35366fa4 100644 --- a/src/js/node/assert.ts +++ b/src/js/node/assert.ts @@ -23,7 +23,17 @@ const { SafeMap, SafeSet, SafeWeakSet } = require("internal/primordials"); const { Buffer } = require("node:buffer"); -const { isKeyObject, isPromise, isRegExp, isMap, isSet, isDate, isWeakSet, isWeakMap } = require("node:util/types"); +const { + isKeyObject, + isPromise, + isRegExp, + isMap, + isSet, + isDate, + isWeakSet, + isWeakMap, + isAnyArrayBuffer, +} = require("node:util/types"); const { innerOk } = require("internal/assert/utils"); const { validateFunction } = require("internal/validators"); @@ -32,6 +42,7 @@ const ArrayPrototypeIndexOf = Array.prototype.indexOf; const ArrayPrototypeJoin = Array.prototype.join; const ArrayPrototypePush = Array.prototype.push; const ArrayPrototypeSlice = Array.prototype.slice; +const ArrayBufferIsView = ArrayBuffer.isView; const NumberIsNaN = Number.isNaN; const ObjectAssign = Object.assign; const ObjectIs = Object.is; @@ -383,6 +394,16 @@ function compareBranch(actual, expected, comparedObjects?) { return Bun.deepEquals(actual, expected, true); } + // Check for ArrayBuffer object equality + if ( + ArrayBufferIsView(actual) || + isAnyArrayBuffer(actual) || + ArrayBufferIsView(expected) || + isAnyArrayBuffer(expected) + ) { + return Bun.deepEquals(actual, expected, true); + } + for (const type of typesToCallDeepStrictEqualWith) { if (type(actual) || type(expected)) { return isDeepStrictEqual(actual, expected); @@ -693,7 +714,7 @@ async function waitForActual(promiseFn) { } else if (checkIsPromise(promiseFn)) { resultPromise = promiseFn; } else { - throw $ERR_INVALID_ARG_TYPE("promiseFn", ["Function", "Promise"], promiseFn); + throw $ERR_INVALID_ARG_TYPE("promiseFn", ["function", "an instance of Promise"], promiseFn); } try { diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index d9e272937c..148285d263 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -1706,6 +1706,12 @@ describe("expect()", () => { expect(array2).not.toEqual(expect.arrayContaining([{ a: 2, b: 3 }])); }); + test("toEqual ArrayBuffer with SharedArrayBuffer", () => { + const ab1 = new SharedArrayBuffer(1); + expect(ab1).toEqual(new SharedArrayBuffer(1)); + expect(ab1).not.toEqual(new ArrayBuffer(1)); + }); + test("symbol based keys in arrays are processed correctly", () => { const mySymbol = Symbol("test"); diff --git a/test/js/node/assert/assert-typedarray-deepequal.test.ts b/test/js/node/assert/assert-typedarray-deepequal.test.ts new file mode 100644 index 0000000000..ef8bacac75 --- /dev/null +++ b/test/js/node/assert/assert-typedarray-deepequal.test.ts @@ -0,0 +1,107 @@ +import assert from "assert"; +import { test, describe, expect } from "bun:test"; + +function makeBlock(f: Function, ...args: any[]) { + return function () { + return f.apply(this, args); + }; +} + +describe("TypedArray deepEqual", () => { + describe("equalArrayPairs", () => { + const equalArrayPairs = [ + [new Uint8Array(1e5), new Uint8Array(1e5)], + [new Uint16Array(1e5), new Uint16Array(1e5)], + [new Uint32Array(1e5), new Uint32Array(1e5)], + [new Uint8ClampedArray(1e5), new Uint8ClampedArray(1e5)], + [new Int8Array(1e5), new Int8Array(1e5)], + [new Int16Array(1e5), new Int16Array(1e5)], + [new Int32Array(1e5), new Int32Array(1e5)], + [new Float32Array(1e5), new Float32Array(1e5)], + [new Float64Array(1e5), new Float64Array(1e5)], + [new Float32Array([+0.0]), new Float32Array([+0.0])], + [new Uint8Array([1, 2, 3, 4]).subarray(1), new Uint8Array([2, 3, 4])], + [new Uint16Array([1, 2, 3, 4]).subarray(1), new Uint16Array([2, 3, 4])], + [new Uint32Array([1, 2, 3, 4]).subarray(1, 3), new Uint32Array([2, 3])], + [new ArrayBuffer(3), new ArrayBuffer(3)], + [new SharedArrayBuffer(3), new SharedArrayBuffer(3)], + ]; + + for (const arrayPair of equalArrayPairs) { + test(`${arrayPair[0].constructor.name} should equal`, () => { + assert.deepEqual(arrayPair[0], arrayPair[1]); + assert.deepStrictEqual(arrayPair[0], arrayPair[1]); + }); + } + }); + + describe("looseEqualArrayPairs", () => { + const looseEqualArrayPairs = [ + // @ts-ignore + [new Float16Array([+0.0]), new Float16Array([-0.0])], + [new Float32Array([+0.0]), new Float32Array([-0.0])], + [new Float64Array([+0.0]), new Float64Array([-0.0])], + ]; + + for (const arrayPair of looseEqualArrayPairs) { + test(`${arrayPair[0].constructor.name} should be loosely equal but not strictly equal`, () => { + assert.deepEqual(arrayPair[0], arrayPair[1]); + expect(() => assert.deepStrictEqual(arrayPair[0], arrayPair[1])).toThrow(assert.AssertionError); + }); + } + }); + + describe("looseNotEqualArrayPairs", () => { + const looseNotEqualArrayPairs = [ + // @ts-ignore + [new Float16Array([NaN]), new Float16Array([NaN])], + [new Float32Array([NaN]), new Float32Array([NaN])], + [new Float64Array([NaN]), new Float64Array([NaN])], + ]; + + for (const arrayPair of looseNotEqualArrayPairs) { + test(`${arrayPair[0].constructor.name} should be strictly equal but not loosely equal`, () => { + expect(() => assert.deepEqual(arrayPair[0], arrayPair[1])).toThrow(assert.AssertionError); + assert.deepStrictEqual(arrayPair[0], arrayPair[1]); + }); + } + }); + + describe("notEqualArrayPairs", () => { + const notEqualArrayPairs = [ + [new ArrayBuffer(3), new SharedArrayBuffer(3)], + [new Int16Array(256), new Uint16Array(256)], + [new Int16Array([256]), new Uint16Array([256])], + [new Float64Array([+0.0]), new Float32Array([-0.0])], + [new Uint8Array(2), new Uint8Array(3)], + [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])], + [new Uint8ClampedArray([300, 2, 3]), new Uint8Array([300, 2, 3])], + [new Uint16Array([2]), new Uint16Array([3])], + [new Uint16Array([0]), new Uint16Array([256])], + [new Int16Array([0]), new Uint16Array([256])], + [new Int16Array([-256]), new Uint16Array([0xff00])], // same bits + [new Int32Array([-256]), new Uint32Array([0xffffff00])], // ditto + [new Float32Array([0.1]), new Float32Array([0.0])], + [new Float32Array([0.1]), new Float32Array([0.1, 0.2])], + [new Float64Array([0.1]), new Float64Array([0.0])], + [new Uint8Array([1, 2, 3]).buffer, new Uint8Array([4, 5, 6]).buffer], + [ + new Uint8Array(new SharedArrayBuffer(3)).fill(1).buffer, + new Uint8Array(new SharedArrayBuffer(3)).fill(2).buffer, + ], + [new ArrayBuffer(2), new ArrayBuffer(3)], + [new SharedArrayBuffer(2), new SharedArrayBuffer(3)], + [new ArrayBuffer(2), new SharedArrayBuffer(3)], + [new Uint8Array(new ArrayBuffer(3)).fill(1).buffer, new Uint8Array(new SharedArrayBuffer(3)).fill(2).buffer], + [new ArrayBuffer(3), new SharedArrayBuffer(3)], + [new SharedArrayBuffer(2), new ArrayBuffer(2)], + ]; + + for (const arrayPair of notEqualArrayPairs) { + test(`${arrayPair[0].constructor.name} should not equal ${arrayPair[1].constructor.name}`, () => { + expect(() => assert.deepEqual(arrayPair[0], arrayPair[1])).toThrow(assert.AssertionError); + expect(() => assert.deepStrictEqual(arrayPair[0], arrayPair[1])).toThrow(assert.AssertionError); + }); + } + }); +}); diff --git a/test/js/node/test/parallel/test-assert-typedarray-deepequal.js b/test/js/node/test/parallel/test-assert-typedarray-deepequal.js new file mode 100644 index 0000000000..7fb18c1886 --- /dev/null +++ b/test/js/node/test/parallel/test-assert-typedarray-deepequal.js @@ -0,0 +1,110 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const { test, suite } = require('node:test'); + +function makeBlock(f) { + const args = Array.prototype.slice.call(arguments, 1); + return function() { + return f.apply(this, args); + }; +} + +suite('equalArrayPairs', () => { + const equalArrayPairs = [ + [new Uint8Array(1e5), new Uint8Array(1e5)], + [new Uint16Array(1e5), new Uint16Array(1e5)], + [new Uint32Array(1e5), new Uint32Array(1e5)], + [new Uint8ClampedArray(1e5), new Uint8ClampedArray(1e5)], + [new Int8Array(1e5), new Int8Array(1e5)], + [new Int16Array(1e5), new Int16Array(1e5)], + [new Int32Array(1e5), new Int32Array(1e5)], + [new Float32Array(1e5), new Float32Array(1e5)], + [new Float64Array(1e5), new Float64Array(1e5)], + [new Float32Array([+0.0]), new Float32Array([+0.0])], + [new Uint8Array([1, 2, 3, 4]).subarray(1), new Uint8Array([2, 3, 4])], + [new Uint16Array([1, 2, 3, 4]).subarray(1), new Uint16Array([2, 3, 4])], + [new Uint32Array([1, 2, 3, 4]).subarray(1, 3), new Uint32Array([2, 3])], + [new ArrayBuffer(3), new ArrayBuffer(3)], + [new SharedArrayBuffer(3), new SharedArrayBuffer(3)], + ]; + + for (const arrayPair of equalArrayPairs) { + test('', () => { + // eslint-disable-next-line no-restricted-properties + assert.deepEqual(arrayPair[0], arrayPair[1]); + assert.deepStrictEqual(arrayPair[0], arrayPair[1]); + }); + } +}); + +suite('looseEqualArrayPairs', () => { + const looseEqualArrayPairs = [ + [new Float32Array([+0.0]), new Float32Array([-0.0])], + [new Float64Array([+0.0]), new Float64Array([-0.0])], + ]; + + for (const arrayPair of looseEqualArrayPairs) { + test('', () => { + // eslint-disable-next-line no-restricted-properties + assert.deepEqual(arrayPair[0], arrayPair[1]); + assert.throws( + makeBlock(assert.deepStrictEqual, arrayPair[0], arrayPair[1]), + assert.AssertionError + ); + }); + } +}); + +suite('notEqualArrayPairs', () => { + const notEqualArrayPairs = [ + [new ArrayBuffer(3), new SharedArrayBuffer(3)], + [new Int16Array(256), new Uint16Array(256)], + [new Int16Array([256]), new Uint16Array([256])], + [new Float64Array([+0.0]), new Float32Array([-0.0])], + [new Uint8Array(2), new Uint8Array(3)], + [new Uint8Array([1, 2, 3]), new Uint8Array([4, 5, 6])], + [new Uint8ClampedArray([300, 2, 3]), new Uint8Array([300, 2, 3])], + [new Uint16Array([2]), new Uint16Array([3])], + [new Uint16Array([0]), new Uint16Array([256])], + [new Int16Array([0]), new Uint16Array([256])], + [new Int16Array([-256]), new Uint16Array([0xff00])], // same bits + [new Int32Array([-256]), new Uint32Array([0xffffff00])], // ditto + [new Float32Array([0.1]), new Float32Array([0.0])], + [new Float32Array([0.1]), new Float32Array([0.1, 0.2])], + [new Float64Array([0.1]), new Float64Array([0.0])], + [new Uint8Array([1, 2, 3]).buffer, new Uint8Array([4, 5, 6]).buffer], + [ + new Uint8Array(new SharedArrayBuffer(3)).fill(1).buffer, + new Uint8Array(new SharedArrayBuffer(3)).fill(2).buffer, + ], + [new ArrayBuffer(2), new ArrayBuffer(3)], + [new SharedArrayBuffer(2), new SharedArrayBuffer(3)], + [new ArrayBuffer(2), new SharedArrayBuffer(3)], + [ + new Uint8Array(new ArrayBuffer(3)).fill(1).buffer, + new Uint8Array(new SharedArrayBuffer(3)).fill(2).buffer, + ], + [new ArrayBuffer(3), new SharedArrayBuffer(3)], + [new SharedArrayBuffer(2), new ArrayBuffer(2)], + ]; + + for (const arrayPair of notEqualArrayPairs) { + test('', () => { + assert.throws( + // eslint-disable-next-line no-restricted-properties + makeBlock(assert.deepEqual, arrayPair[0], arrayPair[1]), + assert.AssertionError + ); + assert.throws( + makeBlock(assert.deepStrictEqual, arrayPair[0], arrayPair[1]), + assert.AssertionError + ); + assert.throws( + makeBlock(assert.partialDeepStrictEqual, arrayPair[0], arrayPair[1]), + assert.AssertionError + ); + }); + } +}); diff --git a/test/js/web/console/console-log.test.ts b/test/js/web/console/console-log.test.ts index 66fbc24373..e1a2b83bec 100644 --- a/test/js/web/console/console-log.test.ts +++ b/test/js/web/console/console-log.test.ts @@ -152,3 +152,29 @@ NamedError: console.error a named error Error log" `); }); + +it("console.log with SharedArrayBuffer", () => { + const proc = Bun.spawnSync({ + cmd: [ + bunExe(), + "-e", + ` + console.log(new ArrayBuffer(0)); + console.log(new SharedArrayBuffer(0)); + console.log(new ArrayBuffer(3)); + console.log(new SharedArrayBuffer(3)); + `, + ], + env: bunEnv, + stdio: ["inherit", "pipe", "pipe"], + }); + expect(proc.stderr.toString("utf8")).toBeEmpty(); + expect(proc.exitCode).toBe(0); + expect(proc.stdout.toString("utf8")).toMatchInlineSnapshot(` + "ArrayBuffer(0) [] + SharedArrayBuffer(0) [] + ArrayBuffer(3) [ 0, 0, 0 ] + SharedArrayBuffer(3) [ 0, 0, 0 ] + " + `); +}); diff --git a/test/js/web/fetch/body.test.ts b/test/js/web/fetch/body.test.ts index 74d4e04576..c5def76373 100644 --- a/test/js/web/fetch/body.test.ts +++ b/test/js/web/fetch/body.test.ts @@ -660,8 +660,11 @@ for (const { body, fn } of bodyTypes) { } function arrayBuffer(buffer: BufferSource) { - if (buffer instanceof ArrayBuffer || buffer instanceof SharedArrayBuffer) { + if (buffer instanceof ArrayBuffer) { return buffer; } + if (buffer instanceof SharedArrayBuffer) { + return new Uint8Array(new Uint8Array(buffer)).buffer; + } return buffer.buffer; }