Skip to content
Open
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
7 changes: 4 additions & 3 deletions docs/guides/test/coverage-threshold.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ bun test --coverage
```

```txt
test.test.ts:
✓ math > add [0.71ms]
✓ math > multiply [0.03ms]
✓ test.test.ts:
✓ math
✓ add [0.71ms]
✓ multiply [0.03ms]
✓ random [0.13ms]
-------------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
Expand Down
7 changes: 4 additions & 3 deletions docs/guides/test/coverage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ bun test --coverage

```txt

test.test.ts:
✓ math > add [0.71ms]
✓ math > multiply [0.03ms]
✓ test.test.ts:
✓ math
✓ add [0.71ms]
✓ multiply [0.03ms]
✓ random [0.13ms]
-------------|---------|---------|-------------------
File | % Funcs | % Lines | Uncovered Line #s
Expand Down
22 changes: 22 additions & 0 deletions docs/test/reporters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ test/package-json-lint.test.ts:
Ran 4 tests across 1 files. [0.66ms]
```

Tests inside a `describe` block are nested beneath their scope, which is printed
once. File and `describe` lines carry the worst status found beneath them, so a
failure is visible without reading the whole block:

```sh terminal icon="terminal"
✗ test/math.test.ts:
✗ math
✗ arithmetic
✓ add [0.12ms]
✗ subtract [0.04ms]
✓ is a module [0.03ms]
✓ top-level test [0.02ms]

3 pass
1 fail
Ran 4 tests across 1 files. [0.31ms]
```

Each file's results are buffered and printed when the file finishes, so a
`describe` block is never split apart — even with `test.concurrent` or
`--parallel`.

### Dots Reporter

The dots reporter shows `.` for passing tests and prints full error details for failures, useful for large test suites.
Expand Down
68 changes: 64 additions & 4 deletions src/runtime/cli/test/parallel/Coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use bun_sys::FdExt as _;

use super::frame::{self, Frame};
use super::worker::{PipeRole, Worker, WorkerPipe};
use crate::test_command::CommandLineReporter;
use crate::test_command::{self, CommandLineReporter, FileBlock};

// `Status` lives in `crate::api::bun::process`
// (not the lower-tier `bun_spawn` crate). Worker.exit_status is this type.
Expand Down Expand Up @@ -50,6 +50,11 @@ pub struct Coordinator<'a> {
/// from concurrent workers interleave; whenever the source file changes the
/// header is re-emitted so every line has visible context. None at start.
pub last_header_idx: Option<u32>,
/// Buffered blocks for the files currently in flight, keyed by file index.
/// Workers interleave, so a file's rows are held until its `FileDone` and
/// then rendered as one block — groups never split, and the file/describe
/// lines can carry an aggregate glyph.
pub blocks: Vec<(u32, FileBlock)>,
pub frame: Frame,
pub parallel_limit: u32,
pub scale_up_after_ms: i64,
Expand Down Expand Up @@ -267,6 +272,7 @@ impl<'a> Coordinator<'a> {
return;
}
self.bailed = true;
self.flush_all_blocks();
self.break_dots();
bun_core::pretty_error!(
"\nBailed out after {} failure{}<r>\n",
Expand Down Expand Up @@ -299,6 +305,49 @@ impl<'a> Coordinator<'a> {
)
}

/// Buffered block for `file_idx`, created on first use.
fn block_for(&mut self, file_idx: u32) -> &mut FileBlock {
if let Some(i) = self.blocks.iter().position(|(idx, _)| *idx == file_idx) {
return &mut self.blocks[i].1;
}
self.blocks.push((file_idx, FileBlock::default()));
&mut self.blocks.last_mut().unwrap().1
}

/// Render `file_idx`'s buffered block, led by its `path:` header carrying the
/// file's aggregate glyph.
fn flush_block(&mut self, file_idx: u32) {
let Some(i) = self.blocks.iter().position(|(idx, _)| *idx == file_idx) else {
return;
};
let (_, block) = self.blocks.remove(i);
if block.is_empty() {
return;
}
self.break_dots();
self.last_header_idx = Some(file_idx);
let colors = Output::enable_ansi_colors_stderr();
let glyph = test_command::fmt_file_glyph(block.file_status(), colors);
let _ = write!(
Output::error_writer(),
"\n{} {}:\n",
bstr::BStr::new(&glyph),
bstr::BStr::new(self.rel_path(file_idx))
);
block.render(Output::error_writer());
block.log_peak();
Output::flush();
}

/// Bail/crash paths exit without a `FileDone`; emit whatever each in-flight
/// file has produced so far rather than dropping it.
fn flush_all_blocks(&mut self) {
while let Some((idx, _)) = self.blocks.first() {
let idx = *idx;
self.flush_block(idx);
}
}

fn ensure_header(&mut self, file_idx: u32) {
if self.dots {
return;
Expand Down Expand Up @@ -344,6 +393,8 @@ impl<'a> Coordinator<'a> {
}
frame::Kind::TestDone => {
let idx = rd.u32_();
let scope_path = rd.str();
let status = rd.u32_() as u8;
let formatted = rd.str();
if w.inflight != Some(idx) {
return;
Expand All @@ -352,9 +403,14 @@ impl<'a> Coordinator<'a> {
if formatted.is_empty() {
return; // e.g. pass under --only-failures
}
// dots-mode failures print a full line (writeTestStatusLine);
// dots themselves are unterminated.
let is_dot = self.dots && !strings::ends_with_char(formatted, b'\n');
if !self.dots {
let status = test_command::basic_result_from_u8(status);
let line = formatted.to_vec();
self.block_for(idx).push(scope_path, status, line);
return;
}
// dots-mode failures print a full line; dots are unterminated.
let is_dot = !strings::ends_with_char(formatted, b'\n');
if !is_dot {
self.break_dots();
self.ensure_header(idx);
Expand All @@ -381,6 +437,7 @@ impl<'a> Coordinator<'a> {
] = nums;

self.flush_captured(w);
self.flush_block(idx);

// A worker can write file_done and crash before the coordinator
// reads the frame; onWorkerExit() will already have called
Expand Down Expand Up @@ -533,6 +590,9 @@ impl<'a> Coordinator<'a> {
}

fn account_crash(&mut self, file_idx: u32, status: &SpawnStatus) {
// Whatever the worker managed to report before dying still deserves to
// be shown, grouped, above the crash line.
self.flush_block(file_idx);
self.break_dots();
let mut buf = [0u8; 32];
bun_core::pretty_error!(
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/cli/test/parallel/Frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ pub enum Kind {
Ready,
/// u32 file_idx
FileStart,
/// u32 file_idx, str formatted_line (ANSI included; printed verbatim)
/// u32 file_idx, str scope_path (`\x1f`-separated describe names, outermost
/// first), u32 basic_status, str formatted_line (ANSI included; printed
/// verbatim). The coordinator buffers these into the file's block.
TestDone,
/// 9 × u32: file_idx, pass, fail, skip, todo, expectations, skipped_label, files, unhandled
FileDone,
Expand Down
11 changes: 8 additions & 3 deletions src/runtime/cli/test/parallel/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub fn run_as_coordinator(
junit_fragments: Vec::new(),
coverage_fragments: Vec::new(),
last_header_idx: None,
blocks: Vec::new(),
frame: Frame::default(),
files_done: 0,
spawned_count: 0,
Expand Down Expand Up @@ -803,9 +804,11 @@ static WORKER_CMDS: bun_core::RacyCell<Option<*mut WorkerCommands>> = bun_core::
// pointee outlives all callers (process exits before it's dropped).

/// Called from `CommandLineReporter.handleTestCompleted` in the worker with the
/// fully-formatted status line (✓/✗ + scopes + name + duration, including ANSI
/// codes). The coordinator prints these bytes verbatim so output matches serial.
pub fn worker_emit_test_done(file_idx: u32, formatted_line: &[u8]) {
/// describe-scope path, the result status, and the fully-formatted status line
/// (✓/✗ + indent + name + duration, including ANSI codes). The coordinator
/// buffers these into the file's block and renders it on `FileDone`, so output
/// matches serial.
pub fn worker_emit_test_done(file_idx: u32, scope_path: &[u8], status: u8, formatted_line: &[u8]) {
// SAFETY: single-threaded worker; WORKER_CMDS only written/read on this thread.
let Some(cmds_ptr) = (unsafe { WORKER_CMDS.read() }) else {
return;
Expand All @@ -817,6 +820,8 @@ pub fn worker_emit_test_done(file_idx: u32, formatted_line: &[u8]) {
let wf = unsafe { &mut *WORKER_FRAME.get() };
wf.begin(frame::Kind::TestDone);
wf.u32_(file_idx);
wf.str(scope_path);
wf.u32_(status as u32);
wf.str(formatted_line);
cmds.send(wf.finish());
}
Loading