From 9c076abfcc7555ff86993bc141979db738339df9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:58:01 +0000 Subject: [PATCH] bun:test: throw in toHaveLength() and toBeEmpty() for a Blob of unknown size The length probe returned f64::MAX for a Blob whose size is unknown (a file that does not exist, a directory, a pipe). toHaveLength() and toBeEmpty() took that number for a length. toHaveLength() printed it as 309 digits, toHaveLength(Number.MAX_VALUE) passed, and the negated forms passed. The probe now returns NaN for such a Blob. The two matchers throw "Received value is a Blob whose size is not known", with and without .not. JSValue::get_length does not change: NaN casts to 0, which is what it returned for such a Blob before. --- src/jsc/JSValue.rs | 2 +- src/jsc/bindings/bindings.cpp | 3 +- src/runtime/test_runner/expect.rs | 12 ++++ src/runtime/test_runner/expect/toBeEmpty.rs | 5 +- .../test_runner/expect/toHaveLength.rs | 5 +- test/js/bun/test/expect.test.js | 61 +++++++++++++++++++ 6 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 5cc7e4d81362..04d793f1a84b 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -2792,7 +2792,7 @@ impl JSValue { } // ── Length introspection. ────────────────────────── - /// `JSValue.getLengthIfPropertyExistsInternal` — returns `f64::MAX` when + /// `JSValue.getLengthIfPropertyExistsInternal` — returns `+Infinity` when /// no `length`-ish property exists. Do not call directly; prefer /// [`JSValue::get_length`]. pub fn get_length_if_property_exists_internal(self, global: &JSGlobalObject) -> JsResult { diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 5a7e0c83bb19..447f57eb9547 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -2988,8 +2988,9 @@ double JSC__JSValue__getLengthIfPropertyExistsInternal(JSC::EncodedJSValue value if (auto* blob = dynamicDowncast(cell)) { uint64_t size = Bun__Blob__getSizeForBindings(blob->wrapped()); + // The size is unknown. NaN is not a number that a caller can compare or print as a length. if (size == std::numeric_limits::max()) - return std::numeric_limits::max(); + return PNaN; return static_cast(size); } } diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index 423603d29e4f..af07eff3197e 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -1636,6 +1636,18 @@ impl Expect { Ok((this, value, not)) } + /// For `toHaveLength()` and `toBeEmpty()`: the length probe answers NaN for a Blob whose size is unknown. + pub(crate) fn throw_nan_length(global: &JSGlobalObject, value: JSValue) -> JsError { + if crate::webcore::blob::js::from_js(value).is_none() { + return global.throw(format_args!("Received value has non-number length property: NaN")); + } + let mut formatter = make_formatter(global); + global.throw(format_args!( + "Received value is a Blob whose size is not known: {}", + value.to_fmt(&mut formatter), + )) + } + pub(crate) fn unreachable(cx: &mut JsScope<'_>, message: JsLocal<'_>) -> JsResult<()> { let global_this = cx.global(); let arg = message.to_js(); diff --git a/src/runtime/test_runner/expect/toBeEmpty.rs b/src/runtime/test_runner/expect/toBeEmpty.rs index b494c70468be..bb6cff33559c 100644 --- a/src/runtime/test_runner/expect/toBeEmpty.rs +++ b/src/runtime/test_runner/expect/toBeEmpty.rs @@ -75,10 +75,7 @@ impl Expect { ); } } else if actual_length.is_nan() { - return Err(global.throw(format_args!( - "Received value has non-number length property: {}", - actual_length - ))); + return Err(Expect::throw_nan_length(global, value)); } else { pass = actual_length == 0.0; } diff --git a/src/runtime/test_runner/expect/toHaveLength.rs b/src/runtime/test_runner/expect/toHaveLength.rs index 587e054b5ba8..c814986241a8 100644 --- a/src/runtime/test_runner/expect/toHaveLength.rs +++ b/src/runtime/test_runner/expect/toHaveLength.rs @@ -58,10 +58,7 @@ impl Expect { value.to_fmt(&mut fmt), ))); } else if actual_length.is_nan() { - return Err(global.throw(format_args!( - "Received value has non-number length property: {}", - actual_length, - ))); + return Err(Expect::throw_nan_length(global, value)); } if actual_length == expected_length { diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index 1614cb8be8ab..c5dd6cd8e9d6 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -2627,6 +2627,67 @@ describe("expect()", () => { // WeakMap expect(new WeakMap([[globalThis, 1]])).toHaveLength(1); }); + + // The length of a Blob is its size. A file that does not exist, a directory and a pipe have none. + describe("toHaveLength() and toBeEmpty() on a Blob of unknown size", () => { + const { isWindows, tempDir } = require("harness"); + const { join } = require("path"); + const unknownSize = "Received value is a Blob whose size is not known: FileRef"; + + test.each( + /** @type {[string, (dir: string) => string][]} */ ([ + ["a path that does not exist", dir => join(dir, "missing")], + ["a directory", dir => dir], + ...(isWindows + ? [] + : [ + [ + "a FIFO", + dir => { + const fifo = join(dir, "fifo"); + require("mkfifo").mkfifo(fifo); + return fifo; + }, + ], + ]), + ]), + )("Bun.file() of %s", async (_kind, pathIn) => { + using dir = tempDir("expect-unknown-size", {}); + const file = Bun.file(pathIn(String(dir))); + + expect(() => expect(file).toHaveLength(0)).toThrow(unknownSize); + expect(() => expect(file).toHaveLength(100)).toThrow(unknownSize); + expect(() => expect(file).toHaveLength(Number.MAX_VALUE)).toThrow(unknownSize); + expect(() => expect(file).not.toHaveLength(0)).toThrow(unknownSize); + expect(() => expect(file).toBeEmpty()).toThrow(unknownSize); + expect(() => expect(file).not.toBeEmpty()).toThrow(unknownSize); + await expectFailure(() => expect(Promise.resolve(file)).resolves.not.toHaveLength(0)).toThrow(unknownSize); + }); + + test.each( + /** @type {[string, (dir: string) => string][]} */ ([ + ["a path that does not exist", dir => join(dir, "missing")], + ["a directory", dir => dir], + ]), + )("a slice of the Bun.file() of %s", (_kind, pathIn) => { + using dir = tempDir("expect-unknown-size", {}); + const slice = Bun.file(pathIn(String(dir))).slice(0, 10); + + expect(() => expect(slice).toHaveLength(10)).toThrow(unknownSize); + expect(() => expect(slice).not.toHaveLength(10)).toThrow(unknownSize); + expect(() => expect(slice).toBeEmpty()).toThrow(unknownSize); + expect(() => expect(slice).not.toBeEmpty()).toThrow(unknownSize); + }); + + test("a length that is a number is not taken for an unknown size", () => { + expect({ length: Number.MAX_VALUE }).toHaveLength(Number.MAX_VALUE); + expect({ length: Number.MAX_VALUE }).not.toBeEmpty(); + expect(() => expect({ length: NaN }).toHaveLength(0)).toThrow( + "Received value has non-number length property: NaN", + ); + expect(() => expect({ length: NaN }).toBeEmpty()).toThrow("Received value has non-number length property: NaN"); + }); + }); } test.each([