Skip to content
Merged
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
58 changes: 33 additions & 25 deletions src/runtime/test_runner/pretty_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,10 @@ impl<'a> Formatter<'a> {
if self.failed {
return Ok(());
}
if !bun_core::StackCheck::init().is_safe_to_recurse() {
self.failed = true;
return Err(self.global_this.throw_stack_overflow());
}
// reshaped for borrowck — `WrappedWriter` borrows both writer_
// and &mut self.estimated_line_length; we use a local wrapper and sync
// `failed` at scope exit. estimated_line_length is unused by WrappedWriter
Expand Down Expand Up @@ -2613,12 +2617,25 @@ impl bun_jsc::ConsoleFormatter for Formatter<'_> {
value: JSValue,
cell: JSType,
) -> JsResult<()> {
let mut sink = bun_io::FmtAdapter::new(writer);
let global = self.global_this;
self.format::<_, ENABLE_ANSI_COLORS>(
TagResult { tag: tag.into(), cell },
&mut sink,
value,
global,
)
}
}

impl From<bun_jsc::FormatTag> for Tag {
fn from(tag: bun_jsc::FormatTag) -> Tag {
use bun_jsc::FormatTag as Ft;
// Map the wider `console_object::Tag` onto this file's `Tag`. Only the
// variants the `write_format` hooks actually emit are reachable
// (Boolean / Double / Object / Private / String); the rest collapse
// onto `Object` so any future caller still renders something useful.
let local = match tag {
match tag {
Ft::StringPossiblyFormatted => Tag::StringPossiblyFormatted,
Ft::String => Tag::String,
Ft::Undefined => Tag::Undefined,
Expand Down Expand Up @@ -2652,15 +2669,7 @@ impl bun_jsc::ConsoleFormatter for Formatter<'_> {
| Ft::CustomGetterSetter
| Ft::Proxy
| Ft::RevokedProxy => Tag::Object,
};
let mut sink = bun_io::FmtAdapter::new(writer);
let global = self.global_this;
self.format::<_, ENABLE_ANSI_COLORS>(
TagResult { tag: local, cell },
&mut sink,
value,
global,
)
}
}
}

Expand All @@ -2676,10 +2685,10 @@ pub(crate) trait AsymmetricMatcherFormatter {
fn amf_quote_strings(&mut self) -> &mut bool;
/// `printAs(tag, …)` routed through the formatter's own runtime
/// dispatcher. Only `Object` / `String` / `Array` are reached.
fn amf_print_as<const C: bool>(
fn amf_print_as<W: bun_io::Write, const C: bool>(
&mut self,
tag: bun_jsc::FormatTag,
w: &mut dyn bun_io::Write,
w: &mut W,
v: JSValue,
cell: JSType,
) -> JsResult<()>;
Expand All @@ -2692,18 +2701,17 @@ impl AsymmetricMatcherFormatter for Formatter<'_> {
fn amf_global_this(&self) -> &JSGlobalObject { self.global_this }
#[inline]
fn amf_quote_strings(&mut self) -> &mut bool { &mut self.quote_strings }
fn amf_print_as<const C: bool>(
fn amf_print_as<W: bun_io::Write, const C: bool>(
&mut self,
tag: bun_jsc::FormatTag,
w: &mut dyn bun_io::Write,
w: &mut W,
v: JSValue,
cell: JSType,
) -> JsResult<()> {
// Reuse the `ConsoleFormatter` bridge above (FormatTag → local `Tag`
// mapping + `format` dispatch). `AsFmt` adapts `dyn bun_io::Write` →
// `core::fmt::Write` for the trait method's signature.
let mut bridge = AsFmt::new(w);
<Self as bun_jsc::ConsoleFormatter>::print_as::<_, C>(self, tag, &mut bridge, v, cell)
// The writer itself: an adapter around it for each matcher inside a matcher makes every
// write as deep as they are nested, where nothing checks the stack.
let global = self.global_this;
self.format::<W, C>(TagResult { tag: tag.into(), cell }, w, v, global)
}
}

Expand All @@ -2714,10 +2722,10 @@ impl AsymmetricMatcherFormatter for bun_jsc::console_object::Formatter<'_> {
fn amf_global_this(&self) -> &JSGlobalObject { self.global_this }
#[inline]
fn amf_quote_strings(&mut self) -> &mut bool { &mut self.quote_strings }
fn amf_print_as<const C: bool>(
fn amf_print_as<W: bun_io::Write, const C: bool>(
&mut self,
tag: bun_jsc::FormatTag,
w: &mut dyn bun_io::Write,
w: &mut W,
v: JSValue,
cell: JSType,
) -> JsResult<()> {
Expand Down Expand Up @@ -2849,7 +2857,7 @@ impl JestPrettyFormat {
this.amf_add_for_new_line(b"ObjectContaining ".len());
writer.write_all(b"ObjectContaining ");
}
this.amf_print_as::<ENABLE_ANSI_COLORS>(
this.amf_print_as::<W, ENABLE_ANSI_COLORS>(
bun_jsc::FormatTag::Object, &mut *writer.ctx, object_value, JSType::Object,
)?;
} else if let Some(matcher) = value.as_class_ref::<expect::ExpectStringContaining>() {
Expand All @@ -2868,7 +2876,7 @@ impl JestPrettyFormat {
this.amf_add_for_new_line(b"StringContaining ".len());
writer.write_all(b"StringContaining ");
}
this.amf_print_as::<ENABLE_ANSI_COLORS>(
this.amf_print_as::<W, ENABLE_ANSI_COLORS>(
bun_jsc::FormatTag::String, &mut *writer.ctx, substring_value, JSType::String,
)?;
} else if let Some(matcher) = value.as_class_ref::<expect::ExpectStringMatching>() {
Expand All @@ -2891,7 +2899,7 @@ impl JestPrettyFormat {
if test_value.is_reg_exp() {
*this.amf_quote_strings() = false;
}
this.amf_print_as::<ENABLE_ANSI_COLORS>(
this.amf_print_as::<W, ENABLE_ANSI_COLORS>(
bun_jsc::FormatTag::String, &mut *writer.ctx, test_value, JSType::String,
)?;
*this.amf_quote_strings() = original_quote_strings;
Expand Down Expand Up @@ -2923,7 +2931,7 @@ impl JestPrettyFormat {
this.amf_add_for_new_line(matcher_name.length() + 1);
writer.print(format_args!("{}", matcher_name));
writer.write_all(b" ");
this.amf_print_as::<ENABLE_ANSI_COLORS>(
this.amf_print_as::<W, ENABLE_ANSI_COLORS>(
bun_jsc::FormatTag::Array, &mut *writer.ctx, args_value, JSType::Array,
)?;
}
Expand Down
28 changes: 28 additions & 0 deletions test/cli/test/bun-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,34 @@ describe("bun test", () => {
expect(stderr).toContain(path);
});

test.each([
["arrays and objects", "i % 2 ? [value] : { a: value }"],
["instances of a class", "new (class A { a = value; })()"],
["JSX children", `{ $$typeof: Symbol.for("react.element"), type: "div", props: { children: value } }`],
["JSX props", `{ $$typeof: Symbol.for("react.element"), type: "div", props: { a: value } }`],
["the data of events", `new MessageEvent("message", { data: value })`],
["asymmetric matchers", "expect.objectContaining({ a: value })"],
])("%s nested too deeply to print are a RangeError", (_, wrap) => {
const stderr = runTest({
input: `
import { test, expect } from "bun:test";
function thrownFor(depth) {
let value = 1;
for (let i = 0; i < depth; i++) value = ${wrap};
try {
expect(value).toMatchInlineSnapshot('"x"');
} catch (error) {
return error.constructor.name;
}
}
test("deep", () => {
expect([thrownFor(10), thrownFor(100_000)]).toEqual(["Error", "RangeError"]);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
`,
expectExitCode: 0,
});
expect(stderr).toContain("1 pass");
});
describe("when filters are provided", () => {
let dir: string;
beforeAll(() => {
Expand Down
Loading