Skip to content
Open
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
951 changes: 674 additions & 277 deletions src/jsc/ConsoleObject.rs

Large diffs are not rendered by default.

22 changes: 9 additions & 13 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2403,6 +2403,11 @@ impl VirtualMachine {

pub fn global_exit(&mut self) -> ! {
debug_assert!(self.is_shutting_down());
// SAFETY: the box is live until the process ends, and no reference to
// it is live here.
if let Some(console) = unsafe { self.console.as_mut() } {
console.abandon_messages();
}
// FIXME: we should be doing this, but we're not, but unfortunately
// doing it causes like 50+ tests to break
// self.event_loop().tick();
Expand Down Expand Up @@ -3301,20 +3306,11 @@ impl VirtualMachine {
MAIN_THREAD_VM.store(vm, core::sync::atomic::Ordering::Release);
}

// ConsoleObject is self-referential (buffers + adapters) — allocate
// stable storage and init in place.
// `console.init(Output.rawErrorWriter(), Output.rawWriter())` must
// happen BEFORE the pointer is stored/passed; the previous port left
// it as raw `MaybeUninit` (UB on first C++ read).
let mut console_box: Box<core::mem::MaybeUninit<crate::console_object::ConsoleObject>> =
Box::new(core::mem::MaybeUninit::uninit());
crate::console_object::ConsoleObject::init_in_place(
&mut console_box,
bun_core::Output::raw_error_writer(),
bun_core::Output::raw_writer(),
);
let console =
bun_core::heap::into_raw(console_box).cast::<crate::console_object::ConsoleObject>();
bun_core::heap::into_raw(Box::new(crate::console_object::ConsoleObject::new(
bun_core::Output::raw_error_writer(),
bun_core::Output::raw_writer(),
)));

let context_id = opts
.context_id
Expand Down
1 change: 0 additions & 1 deletion src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1558,7 +1558,6 @@ fn on_unhandled_rejection(
jsc::console_object::FormatOptions {
enable_colors: false,
add_newline: false,
flush: false,
max_depth: 32,
..Default::default()
},
Expand Down
3 changes: 0 additions & 3 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,6 @@ fn print_table(
let mut format_options = ConsoleObject::FormatOptions {
enable_colors: false,
add_newline: false,
flush: false,
max_depth: 5,
quote_strings: true,
ordered_properties: false,
Expand Down Expand Up @@ -441,7 +440,6 @@ pub(crate) fn inspect<'a>(
let mut format_options = ConsoleObject::FormatOptions {
enable_colors: false,
add_newline: false,
flush: false,
max_depth: 8,
quote_strings: true,
ordered_properties: false,
Expand Down Expand Up @@ -489,7 +487,6 @@ pub(crate) fn bun_inspect_singleline(global_this: &JSGlobalObject, value: JSValu
ConsoleObject::FormatOptions {
enable_colors: false,
add_newline: false,
flush: false,
max_depth: u16::MAX,
quote_strings: true,
ordered_properties: false,
Expand Down
3 changes: 0 additions & 3 deletions src/runtime/cli/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2046,7 +2046,6 @@ impl<'a> Repl<'a> {
jsc::ConsoleObject::FormatOptions {
enable_colors: false,
add_newline: false,
flush: false,
quote_strings: true,
ordered_properties: false,
max_depth: 4,
Expand Down Expand Up @@ -2262,7 +2261,6 @@ impl<'a> Repl<'a> {
jsc::ConsoleObject::FormatOptions {
enable_colors,
add_newline: true,
flush: false,
quote_strings: true,
ordered_properties: false,
max_depth: 4,
Expand Down Expand Up @@ -2296,7 +2294,6 @@ impl<'a> Repl<'a> {
jsc::ConsoleObject::FormatOptions {
enable_colors: self.use_colors,
add_newline: true,
flush: false,
quote_strings: true,
ordered_properties: false,
max_depth: 4,
Expand Down
10 changes: 10 additions & 0 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1947,6 +1947,16 @@ unsafe fn retroactively_report_discovered_tests(
/// `Jest.runner.?.bun_test_root.onBeforePrint()` — flush the test reporter's
/// line state before user `console.log` output interleaves with it.
fn console_on_before_print() {
if crate::test_runner::jest::Jest::runner_ptr().is_some() {
console_on_before_print_in_test();
}
}

/// Two threads can print to different streams at once, so their calls need this lock. The runner's own code takes none.
#[inline(never)]
fn console_on_before_print_in_test() {
static LOCK: bun_threading::Mutex = bun_threading::Mutex::new();
let _lock = LOCK.lock_guard();
if let Some(runner) = crate::test_runner::jest::Jest::runner() {
runner.bun_test_root.on_before_print();
}
Expand Down
209 changes: 209 additions & 0 deletions test/js/bun/test/dots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,212 @@ test("a console group with no label does not break the line of dots", async () =
exitCode: 0,
});
});

test("a console call whose message is not printed does not break the line of dots", async () => {
using dir = tempDir("dots-no-message", {
"no-message.test.ts": `
import { expect, test } from "bun:test";
test("before", () => {});
test("the message throws", () => {
const value = {
[Symbol.for("nodejs.util.inspect.custom")]() {
throw new Error("no message");
},
};
expect(() => console.log("not printed", value)).toThrow("no message");
expect(() => console.error("not printed", value)).toThrow("no message");
});
test("after", () => {});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--dots", "./no-message.test.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({
stdout: normalizeBunSnapshot(stdout, String(dir)),
stderr: normalizeBunSnapshot(stderr, String(dir)),
exitCode,
}).toEqual({
stdout: "bun test <version> (<revision>)",
stderr: "...\n\n3 pass\n0 fail\n2 expect() calls\nRan 3 tests across 1 file.",
exitCode: 0,
});
});

// The console call of a Worker tells the reporter that output follows, on its own thread. The two console
// calls of a pair are made at the same moment: the line of dots ends once.
test("console calls of two threads end the line of dots once", async () => {
const pairs = 50;
using dir = tempDir("dots-two-threads", {
"two-threads.test.ts": `
import { afterAll, beforeAll, test } from "bun:test";
// [0]: the pair. [1]: the Worker is ready for that pair. [2]: print now. [3]: the Worker printed.
const shared = new Int32Array(new SharedArrayBuffer(16));
let worker: Worker;
beforeAll(async () => {
worker = new Worker(new URL("./worker.ts", import.meta.url).href);
const ready = new Promise(resolve => (worker.onmessage = resolve));
worker.postMessage(shared.buffer);
await ready;
});
afterAll(() => worker.terminate());
for (let pair = 1; pair <= ${pairs}; pair++) {
test("quiet " + pair, () => {});
test("both threads print " + pair, () => {
Atomics.store(shared, 0, pair);
Atomics.notify(shared, 0);
while (Atomics.load(shared, 1) !== pair) Atomics.wait(shared, 1, pair - 1);
Atomics.store(shared, 2, pair);
console.log("main");
while (Atomics.load(shared, 3) !== pair) Atomics.wait(shared, 3, pair - 1);
});
}
`,
"worker.ts": `
self.onmessage = ({ data }) => {
const shared = new Int32Array(data);
postMessage("ready");
for (let pair = 1; ; pair++) {
Atomics.wait(shared, 0, pair - 1);
Atomics.store(shared, 1, pair);
Atomics.notify(shared, 1);
// No wait here, so that this thread prints when the main thread does.
while (Atomics.load(shared, 2) !== pair);
console.error("worker");
Atomics.store(shared, 3, pair);
Atomics.notify(shared, 3);
}
};
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--dots", "./two-threads.test.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({
stdout: normalizeBunSnapshot(stdout, String(dir)),
stderr: normalizeBunSnapshot(stderr, String(dir)),
exitCode,
}).toEqual({
stdout: "bun test <version> (<revision>)\n" + "main\n".repeat(pairs).trimEnd(),
stderr:
".\n\ntwo-threads.test.ts:\n" +
"worker\n..\n".repeat(pairs - 1) +
`worker\n.\n\n${2 * pairs} pass\n0 fail\nRan ${2 * pairs} tests across 1 file.`,
exitCode: 0,
});
});

// A message of the Worker over 64 KiB is on stderr in part, and its hook waits. The main thread then prints to
// stderr too. The line feed that ends the line of dots is written after the message of the Worker, as the output
// of the main thread is. In a "nested" round the hook of the Worker prints, and the line feed is before "inner".
test("a console call does not end the line of dots inside a message of another thread on the same stream", async () => {
// The state of the message of the main thread when its first byte is written.
const kinds = ["stage", "direct", "heap", "streaming", "nested"];
const rounds = 2 * kinds.length;
using dir = tempDir("dots-inside-message", {
"inside-message.test.ts": `
import { afterAll, beforeAll, test } from "bun:test";
const kinds = ${JSON.stringify(kinds)};
const text = (length: number) => ({
[Symbol.for("nodejs.util.inspect.custom")]: () => Buffer.alloc(length, "m").toString(),
});
const print = {
stage: () => console.error("main"),
direct: () => console.error("main", Buffer.alloc(5000, "m").toString()),
heap: () => console.error("main", text(5000)),
streaming: () => console.error("main", text(70_000)),
nested: () => {},
};
// [0]: the round. [1]: the message of that round is on the stream in part.
// [2]: the Worker can complete the message. [3]: the message is complete.
const shared = new Int32Array(new SharedArrayBuffer(16));
let worker: Worker;
beforeAll(async () => {
worker = new Worker(new URL("./worker.ts", import.meta.url).href);
const ready = new Promise(resolve => (worker.onmessage = resolve));
worker.postMessage(shared.buffer);
await ready;
});
afterAll(() => worker.terminate());
for (let round = 1; round <= ${rounds}; round++) {
// The dot of this test is printed while the Worker is inside its message.
test("the Worker starts a message " + round, () => {
Atomics.store(shared, 0, round);
Atomics.notify(shared, 0);
while (Atomics.load(shared, 1) !== round) Atomics.wait(shared, 1, round - 1);
});
test("the main thread prints " + round, () => {
Atomics.store(shared, 2, round);
Atomics.notify(shared, 2);
print[kinds[(round - 1) % kinds.length]]();
while (Atomics.load(shared, 3) !== round) Atomics.wait(shared, 3, round - 1);
});
}
`,
"worker.ts": `
const kinds = ${JSON.stringify(kinds)};
self.onmessage = ({ data }) => {
const shared = new Int32Array(data);
postMessage("ready");
const start = "START" + Buffer.alloc(70_000, "a").toString();
const end = Buffer.alloc(300_000, "b").toString() + "END";
for (let round = 1; ; round++) {
Atomics.wait(shared, 0, round - 1);
const hook = {
[Symbol.for("nodejs.util.inspect.custom")]() {
Atomics.store(shared, 1, round);
Atomics.notify(shared, 1);
Atomics.wait(shared, 2, round - 1);
if (kinds[(round - 1) % kinds.length] === "nested") console.error("inner");
return "hook";
},
};
console.error(start, hook, end);
Atomics.store(shared, 3, round);
Atomics.notify(shared, 3);
}
};
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--dots", "./inside-message.test.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

// The dot of the first test of a round is in the message of the Worker, after the bytes that were written.
const short = (line: string) =>
line
.replace(/^STARTa+\.a*/, "START<a>.")
.replace(/a{100,}/g, "<a>")
.replace(/b{100,}/g, "<b>")
.replace(/m{100,}/g, "<m>");
const lines = ["", "inside-message.test.ts:"];
for (let round = 0; round < rounds; round++) {
const kind = kinds[round % kinds.length];
if (kind === "nested") lines.push("START<a>. ", "inner", "hook <b>END", ".");
else lines.push("START<a>. hook <b>END", "", kind === "stage" ? "main" : "main <m>", ".");
}
expect({
stdout: normalizeBunSnapshot(stdout, String(dir)),
lines: stderr.split("\n").slice(0, lines.length).map(short),
exitCode,
}).toEqual({
stdout: "bun test <version> (<revision>)",
lines,
exitCode: 0,
});
});
Loading
Loading