Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
11 changes: 10 additions & 1 deletion src/runtime/test_runner/pretty_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::test_runner::expect::JSValueTestExt;
use core::ffi::c_void;

use bun_collections::HashMap;
use bun_core::{fmt as bun_fmt, Output};
use bun_core::{fmt as bun_fmt, Output, StackCheck};
use bun_jsc::{
self as jsc, ComptimeStringMapExt as _, JSGlobalObject, JSObject,
JSPropertyIterator, JSType, JSValue, JsError, JsResult, VM,
Expand Down Expand Up @@ -342,6 +342,7 @@ pub struct Formatter<'a> {
pub failed: bool,
pub estimated_line_length: usize,
pub always_newline_scope: bool,
pub stack_check: StackCheck,
}

impl<'a> Formatter<'a> {
Expand All @@ -357,6 +358,7 @@ impl<'a> Formatter<'a> {
failed: false,
estimated_line_length: 0,
always_newline_scope: false,
stack_check: StackCheck::init(),
}
}

Expand Down Expand Up @@ -1163,6 +1165,13 @@ impl<'a> Formatter<'a> {
let mut writer = WrappedWriter::new(writer_);

if FORMAT.can_have_circular_references() {
if !self.stack_check.is_safe_to_recurse() {
// Deeply nested (non-cyclic) values can exhaust the native stack.
// Stop recursion; the matcher still reports a normal failure.
self.failed = true;
return Ok(());
}
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
Outdated

if self.map_node.is_none() {
// `visited::Pool::get()` returns an RAII `PoolGuard` that
// would release on scope exit; instead the raw node is stashed on
Expand Down
81 changes: 80 additions & 1 deletion test/js/bun/test/pretty-format-overflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Platform: Windows x86_64_baseline, Bun v1.3.0

import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
import { bunEnv, bunExe, tempDir, tempDirWithFiles } from "harness";

describe("pretty_format should handle deeply nested objects without crashing", () => {
test("deeply nested object with many properties", async () => {
Expand Down Expand Up @@ -51,3 +51,82 @@ test("deep nesting", () => {
expect(stderr).toContain("expect(received).toEqual(expected)");
}, 30000);
});

// A failing toEqual / toMatchSnapshot on a deeply nested (non-circular) value used to exhaust
// the native stack in pretty_format's Formatter::print_as and SIGSEGV the runner mid-run. Run
// in a subprocess so a regression fails these tests instead of segfaulting the outer runner.
describe.concurrent("pretty_format stops recursion before native stack overflow", () => {
const depth = 20000;

test("failing toEqual on a deeply nested array", async () => {
using dir = tempDir("pretty-format-deep-array", {
"deep.test.ts": `
import { test, expect } from "bun:test";
test("deep array", () => {
let a: any = [];
for (let i = 0; i < ${depth}; i++) a = [a];
expect(a).toEqual([1]);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "deep.test.ts"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("expect(received).toEqual(expected)");
expect(stderr).toContain("1 fail");
expect(exitCode).toBe(1);
});

test("failing toEqual on a deeply nested object", async () => {
using dir = tempDir("pretty-format-deep-object", {
"deep.test.ts": `
import { test, expect } from "bun:test";
test("deep object", () => {
let a: any = {};
for (let i = 0; i < ${depth}; i++) a = { k: a };
expect(a).toEqual({ k: 1 });
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "deep.test.ts"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("expect(received).toEqual(expected)");
expect(stderr).toContain("1 fail");
expect(exitCode).toBe(1);
});

test("toMatchSnapshot on a deeply nested array", async () => {
using dir = tempDir("pretty-format-deep-snapshot", {
"deep.test.ts": `
import { test, expect } from "bun:test";
test("deep snapshot", () => {
let a: any = [];
for (let i = 0; i < ${depth}; i++) a = [a];
expect(a).toMatchSnapshot();
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "deep.test.ts"],
env: { ...bunEnv, CI: "false" },
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("1 pass");
expect(stderr).toContain("Ran 1 test");
expect(exitCode).toBe(0);
});
});
Loading