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
35 changes: 35 additions & 0 deletions docs/runtime/bunfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,41 @@ This is useful for:

The `--concurrent` CLI flag will override this setting when specified.

### `test.onlyFailures`

When enabled, only failed tests are displayed in the output. This helps reduce noise in large test suites by hiding passing tests. Default `false`.

```toml
[test]
onlyFailures = true
```

This is equivalent to using the `--only-failures` flag when running `bun test`.
Comment on lines +252 to +261
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Document the interaction with dots reporter.

While this documentation is clear, consider adding a note about the behavior when test.onlyFailures is used alongside test.reporter.dots. Based on the code in test_command.zig, when both are enabled, dots are printed for passing tests and full output is shown for failures. This interaction should be documented to avoid user confusion.

Apply this diff to clarify the interaction:

 ### `test.onlyFailures`
 
 When enabled, only failed tests are displayed in the output. This helps reduce noise in large test suites by hiding passing tests. Default `false`.
 
 ```toml
 [test]
 onlyFailures = true

This is equivalent to using the --only-failures flag when running bun test.
+
+Note: When combined with test.reporter.dots, passing tests will display as dots while failures show full output.


<details>
<summary>🤖 Prompt for AI Agents</summary>

In docs/runtime/bunfig.md around lines 252 to 261, the docs for
test.onlyFailures lack a note about how it interacts with test.reporter.dots;
add a short sentence after the existing paragraph that states: "Note: When
combined with test.reporter.dots, passing tests will display as dots while
failures show full output." so users understand that dots are still printed for
passes and full failure output is shown.


</details>

<!-- This is an auto-generated comment by CodeRabbit -->


### `test.reporter`

Configure the test reporter settings.

#### `test.reporter.dots`

Enable the dots reporter, which displays a compact output showing a dot for each test. Default `false`.

```toml
[test.reporter]
dots = true
```

#### `test.reporter.junit`

Enable JUnit XML reporting and specify the output file path.

```toml
[test.reporter]
junit = "test-results.xml"
```

This generates a JUnit XML report that can be consumed by CI systems and other tools.

### `test.randomize`

Run tests in random order. Default `false`.
Expand Down
2 changes: 1 addition & 1 deletion src/bun.js/test/bun_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ pub const BunTestRoot = struct {
pub fn onBeforePrint(this: *BunTestRoot) void {
if (this.active_file.get()) |active_file| {
if (active_file.reporter) |reporter| {
if (reporter.last_printed_dot and reporter.reporters.dots) {
if (reporter.last_printed_dot and (reporter.reporters.dots or reporter.reporters.only_failures)) {
bun.Output.prettyError("<r>\n", .{});
bun.Output.flush();
reporter.last_printed_dot = false;
Expand Down
2 changes: 1 addition & 1 deletion src/bun.js/test/jest.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const CurrentFile = struct {
repeat_index: u32,
reporter: *CommandLineReporter,
) void {
if (Output.isAIAgent() or reporter.reporters.dots) {
if (reporter.reporters.dots or reporter.reporters.only_failures) {
this.freeAndClear();
this.title = bun.handleOom(bun.default_allocator.dupe(u8, title));
this.prefix = bun.handleOom(bun.default_allocator.dupe(u8, prefix));
Expand Down
5 changes: 5 additions & 0 deletions src/bunfig.zig
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ pub const Bunfig = struct {
this.ctx.test_options.coverage.enabled = expr.data.e_boolean.value;
}

if (test_.get("onlyFailures")) |expr| {
try this.expect(expr, .e_boolean);
this.ctx.test_options.reporters.only_failures = expr.data.e_boolean.value;
}

if (test_.get("reporter")) |expr| {
try this.expect(expr, .e_object);
if (expr.get("junit")) |junit_expr| {
Expand Down
1 change: 1 addition & 0 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ pub const Command = struct {

reporters: struct {
dots: bool = false,
only_failures: bool = false,
junit: bool = false,
} = .{},
reporter_outfile: ?[]const u8 = null,
Expand Down
6 changes: 6 additions & 0 deletions src/cli/Arguments.zig
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ pub const test_only_params = [_]ParamType{
clap.parseParam("--reporter <STR> Test output reporter format. Available: 'junit' (requires --reporter-outfile), 'dots'. Default: console output.") catch unreachable,
clap.parseParam("--reporter-outfile <STR> Output file path for the reporter format (required with --reporter).") catch unreachable,
clap.parseParam("--dots Enable dots reporter. Shorthand for --reporter=dots.") catch unreachable,
clap.parseParam("--only-failures Only display test failures, hiding passing tests.") catch unreachable,
clap.parseParam("--max-concurrency <NUMBER> Maximum number of concurrent tests to execute at once. Default is 20.") catch unreachable,
};
pub const test_params = test_only_params ++ runtime_params_ ++ transpiler_params_ ++ base_params_;
Expand Down Expand Up @@ -461,6 +462,11 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
ctx.test_options.reporters.dots = true;
}

// Handle --only-failures flag
if (args.flag("--only-failures")) {
ctx.test_options.reporters.only_failures = true;
}

if (args.option("--coverage-dir")) |dir| {
ctx.test_options.coverage.reports_directory = dir;
}
Expand Down
12 changes: 9 additions & 3 deletions src/cli/test_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ pub const CommandLineReporter = struct {

reporters: struct {
dots: bool = false,
only_failures: bool = false,
junit: ?*JunitReporter = null,
} = .{},

Expand Down Expand Up @@ -874,8 +875,8 @@ pub const CommandLineReporter = struct {
},
}
buntest.reporter.?.last_printed_dot = true;
} else if (Output.isAIAgent() and (comptime result.basicResult()) != .fail) {
// when using AI agents, only print failures
} else if (((comptime result.basicResult()) != .fail) and (buntest.reporter != null and buntest.reporter.?.reporters.only_failures)) {
// when using --only-failures, only print failures
} else {
buntest.bun_test_root.onBeforePrint();

Expand All @@ -900,7 +901,7 @@ pub const CommandLineReporter = struct {

var this: *CommandLineReporter = buntest.reporter orelse return; // command line reporter is missing! uh oh!

if (!this.reporters.dots) switch (sequence.result.basicResult()) {
if (!this.reporters.dots and !this.reporters.only_failures) switch (sequence.result.basicResult()) {
.skip => bun.handleOom(this.skips_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])),
.todo => bun.handleOom(this.todos_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])),
.fail => bun.handleOom(this.failures_to_repeat_buf.appendSlice(bun.default_allocator, output_buf.items[initial_length..])),
Expand Down Expand Up @@ -1362,6 +1363,11 @@ pub const TestCommand = struct {
if (ctx.test_options.reporters.dots) {
reporter.reporters.dots = true;
}
if (ctx.test_options.reporters.only_failures) {
reporter.reporters.only_failures = true;
} else if (Output.isAIAgent()) {
reporter.reporters.only_failures = true; // only-failures defaults to true for ai agents
}

js_ast.Expr.Data.Store.create();
js_ast.Stmt.Data.Store.create();
Expand Down
27 changes: 27 additions & 0 deletions test/js/bun/test/only-failures.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, test } from "bun:test";

test("passing test 1", () => {
expect(1 + 1).toBe(2);
});

test("passing test 2", () => {
expect(2 + 2).toBe(4);
});

test("failing test", () => {
expect(1 + 1).toBe(3);
});

test("passing test 3", () => {
expect(3 + 3).toBe(6);
});

test.skip("skipped test", () => {
expect(true).toBe(false);
});

test.todo("todo test");

test("another failing test", () => {
throw new Error("This test fails");
});
120 changes: 120 additions & 0 deletions test/js/bun/test/only-failures.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";

test.concurrent("only-failures flag should show only failures", async () => {
const result = await Bun.spawn({
cmd: [bunExe(), "test", import.meta.dir + "/only-failures.fixture.ts", "--only-failures"],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const exitCode = await result.exited;
const stdout = await result.stdout.text();
const stderr = await result.stderr.text();
expect({
exitCode,
stdout: normalizeBunSnapshot(stdout),
stderr: normalizeBunSnapshot(stderr),
}).toMatchInlineSnapshot(`
{
"exitCode": 1,
"stderr":
"test/js/bun/test/only-failures.fixture.ts:
7 | test("passing test 2", () => {
8 | expect(2 + 2).toBe(4);
9 | });
10 |
11 | test("failing test", () => {
12 | expect(1 + 1).toBe(3);
^
error: expect(received).toBe(expected)

Expected: 3
Received: 2
at <anonymous> (file:NN:NN)
(fail) failing test
21 | });
22 |
23 | test.todo("todo test");
24 |
25 | test("another failing test", () => {
26 | throw new Error("This test fails");
^
error: This test fails
at <anonymous> (file:NN:NN)
(fail) another failing test

3 pass
1 skip
1 todo
2 fail
4 expect() calls
Ran 7 tests across 1 file."
,
"stdout": "bun test <version> (<revision>)",
}
`);
});

test.concurrent("only-failures flag should work with multiple files", async () => {
const result = await Bun.spawn({
cmd: [
bunExe(),
"test",
import.meta.dir + "/printing/dots/dots1.fixture.ts",
import.meta.dir + "/only-failures.fixture.ts",
"--only-failures",
],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const exitCode = await result.exited;
const stdout = await result.stdout.text();
const stderr = await result.stderr.text();
expect(exitCode).toBe(1);
expect(normalizeBunSnapshot(stderr)).toContain("(fail) failing test");
expect(normalizeBunSnapshot(stderr)).toContain("(fail) another failing test");
expect(normalizeBunSnapshot(stderr)).not.toContain("(pass)");
});

test.concurrent("only-failures should work via bunfig.toml", async () => {
using dir = tempDir("bunfig-only-failures", {
"bunfig.toml": `
[test]
onlyFailures = true
`,
"my.test.ts": `
import { test, expect } from "bun:test";

test("passing test", () => {
expect(1 + 1).toBe(2);
});

test("failing test", () => {
expect(1 + 1).toBe(3);
});

test("another passing test", () => {
expect(true).toBe(true);
});
`,
});

const result = await Bun.spawn({
cmd: [bunExe(), "test"],
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
cwd: String(dir),
});

const exitCode = await result.exited;
const stderr = await result.stderr.text();

expect(exitCode).toBe(1);
// Should only show the failing test
expect(normalizeBunSnapshot(stderr, dir)).toContain("(fail) failing test");
// Should not show passing tests
expect(normalizeBunSnapshot(stderr, dir)).not.toContain("(pass)");
});