Skip to content
Closed
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
10 changes: 6 additions & 4 deletions src/runtime/test_runner/diff_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,25 @@ impl<'a> fmt::Display for DiffFormatter<'a> {
flush: false,
quote_strings: true,
};
let _ = JestPrettyFormat::format(
JestPrettyFormat::format(
MessageLevel::Debug,
global_this,
core::slice::from_ref(&received),
1,
&mut received_buf,
fmt_options,
); // TODO:
)
.map_err(|_| fmt::Error)?;

let _ = JestPrettyFormat::format(
JestPrettyFormat::format(
MessageLevel::Debug,
global_this,
core::slice::from_ref(&expected),
1,
&mut expected_buf,
fmt_options,
); // TODO:
)
.map_err(|_| fmt::Error)?;
}

let mut received_slice: &[u8] = received_buf.as_slice();
Expand Down
6 changes: 5 additions & 1 deletion src/runtime/test_runner/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2928,7 +2928,11 @@ impl ExpectMatcherUtils {
} else {
bun_core::pretty_fmt!("<d>(<r><green>expected<r><d>)<r>", false)
};
let buf = format!("{head}{not}{matcher_name}{expected_hint}\n\n{diff_formatter}\n");
let mut buf = format!("{head}{not}{matcher_name}{expected_hint}\n\n");
use fmt::Write as _;
if writeln!(buf, "{diff_formatter}").is_err() {
return Err(if global_this.has_exception() { JsError::Thrown } else { JsError::OutOfMemory });
}
bun_jsc::bun_string_jsc::create_utf8_for_js(global_this, buf.as_bytes())
}
}
Expand Down
90 changes: 90 additions & 0 deletions test/js/bun/test/expect-diff-format-throw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";

// The failure message of toEqual/toStrictEqual (and matcherHint) is a diff of the pretty-printed
// values. When pretty-printing one of them throws, the exception must not be left pending while
// the other value is formatted.
//
// The pretty-printer reads `$$typeof` to detect React elements, and deepEquals ignores
// non-enumerable properties, so a non-enumerable throwing `$$typeof` getter only fires while the
// failure message is being built.
test.concurrent("matcher failure message when pretty-printing a value throws", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const { expect } = Bun.jest();
function value(a) {
const o = { a };
Object.defineProperty(o, "$$typeof", { get() { throw new Error("getter threw"); } });
return o;
}
function message(fn) {
try { fn(); } catch (e) { return e.message; }
return "did not throw";
}
let utils;
expect.extend({ captureUtils() { utils = this.utils; return { pass: true, message: () => "" }; } });
expect(0).captureUtils();
console.log(JSON.stringify([
message(() => expect(value(1)).toStrictEqual({ a: 2 })),
message(() => expect(value(1)).toEqual({ a: 2 })),
message(() => expect({ a: 1 }).toStrictEqual(value(2))),
message(() => utils.matcherHint("toFoo", value(1), { a: 2 })),
]));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect({ stdout, stderr, exitCode }).toEqual({
stdout:
JSON.stringify([
"expect(received).toStrictEqual(expected)\n\n",
"expect(received).toEqual(expected)\n\n",
"expect(received).toStrictEqual(expected)\n\n",
"getter threw",
]) + "\n",
stderr: "",
exitCode: 0,
});
});

// Same bug as found by the fuzzer: the matcher fails right where the JS stack ran out, so the
// getter the pretty-printer calls while formatting `received` throws a stack overflow, and
// formatting `expected` then ran with that exception still pending.
test.concurrent("matcher failure message when pretty-printing overflows the stack", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const { expect } = Bun.jest();
const received = { get $$typeof() { return undefined; } };
const expected = new Uint8ClampedArray();
let ran = false;
function recurse() {
try { recurse(); } catch {}
if (!ran) {
ran = true;
try { expect(received).toStrictEqual(expected); } catch {}
}
}
recurse();
console.log("OK");
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect({ stdout, stderr, exitCode }).toEqual({ stdout: "OK\n", stderr: "", exitCode: 0 });
});