Add --pass-with-no-tests flag to test runner (#23424)

## Summary

This PR adds support for the `--pass-with-no-tests` CLI flag to the test
runner, addressing issue #20814.

With the latest v1.2.8 release, the test runner now fails when no tests
match a filter. While this is useful for agentic coding workflows, there
are legitimate cases where the previous behavior is preferred, such as
in monorepos where a standard test file pattern is used as a filter but
not all packages contain tests.

This flag makes the test runner behave like Jest and Vitest, exiting
with code 0 when no tests are found.

## Changes

- Added `--pass-with-no-tests` flag to CLI arguments in
`src/cli/Arguments.zig`
- Added `pass_with_no_tests` field to `TestOptions` struct in
`src/cli.zig`
- Updated test runner logic in `src/cli/test_command.zig` to respect the
flag
- Added comprehensive tests in
`test/cli/test/pass-with-no-tests.test.ts`

## Test Plan

All new tests pass:
-  `--pass-with-no-tests` exits with 0 when no test files found
-  `--pass-with-no-tests` exits with 0 when filters match no tests
-  Without flag, still exits with 1 when no tests found (preserves
existing behavior)
-  `--pass-with-no-tests` still fails when actual tests fail

Closes #20814

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: pfg <pfg@pfg.pw>
This commit is contained in:
robobun
2025-10-15 17:38:02 -07:00
committed by GitHub
parent fadce1001d
commit 642d04b9f2
4 changed files with 104 additions and 1 deletions

View File

@@ -342,6 +342,7 @@ pub const Command = struct {
repeat_count: u32 = 0,
run_todo: bool = false,
only: bool = false,
pass_with_no_tests: bool = false,
concurrent: bool = false,
randomize: bool = false,
seed: ?u32 = null,

View File

@@ -197,6 +197,7 @@ pub const test_only_params = [_]ParamType{
clap.parseParam("--rerun-each <NUMBER> Re-run each test file <NUMBER> times, helps catch certain bugs") catch unreachable,
clap.parseParam("--todo Include tests that are marked with \"test.todo()\"") catch unreachable,
clap.parseParam("--only Run only tests that are marked with \"test.only()\" or \"describe.only()\"") catch unreachable,
clap.parseParam("--pass-with-no-tests Exit with code 0 when no tests are found") catch unreachable,
clap.parseParam("--concurrent Treat all tests as `test.concurrent()` tests") catch unreachable,
clap.parseParam("--randomize Run tests in random order") catch unreachable,
clap.parseParam("--seed <INT> Set the random seed for test randomization") catch unreachable,
@@ -509,6 +510,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
ctx.test_options.update_snapshots = args.flag("--update-snapshots");
ctx.test_options.run_todo = args.flag("--todo");
ctx.test_options.only = args.flag("--only");
ctx.test_options.pass_with_no_tests = args.flag("--pass-with-no-tests");
ctx.test_options.concurrent = args.flag("--concurrent");
ctx.test_options.randomize = args.flag("--randomize");

View File

@@ -1766,7 +1766,8 @@ pub const TestCommand = struct {
}
const summary = reporter.summary();
if (failed_to_find_any_tests or summary.didLabelFilterOutAllTests() or summary.fail > 0 or (coverage_options.enabled and coverage_options.fractions.failing and coverage_options.fail_on_low_coverage) or !write_snapshots_success) {
const should_fail_on_no_tests = !ctx.test_options.pass_with_no_tests and (failed_to_find_any_tests or summary.didLabelFilterOutAllTests());
if (should_fail_on_no_tests or summary.fail > 0 or (coverage_options.enabled and coverage_options.fractions.failing and coverage_options.fail_on_low_coverage) or !write_snapshots_success) {
vm.exit_handler.exit_code = 1;
} else if (reporter.jest.unhandled_errors_between_tests > 0) {
vm.exit_handler.exit_code = 1;

View File

@@ -0,0 +1,99 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("--pass-with-no-tests exits with 0 when no test files found", async () => {
using dir = tempDir("pass-with-no-tests", {
"not-a-test.ts": `console.log("hello");`,
});
const { exited, stderr } = Bun.spawn({
cmd: [bunExe(), "test", "--pass-with-no-tests"],
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: bunEnv,
});
const [err, exitCode] = await Promise.all([stderr.text(), exited]);
expect(exitCode).toBe(0);
expect(err).toContain("No tests found!");
});
test("--pass-with-no-tests exits with 0 when filters match no tests", async () => {
using dir = tempDir("pass-with-no-tests-filter", {
"some.test.ts": `import { test } from "bun:test"; test("example", () => {});`,
});
const { exited, stderr } = Bun.spawn({
cmd: [bunExe(), "test", "--pass-with-no-tests", "-t", "nonexistent"],
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: bunEnv,
});
const [err, exitCode] = await Promise.all([stderr.text(), exited]);
expect(exitCode).toBe(0);
});
test("without --pass-with-no-tests, exits with 1 when no test files found", async () => {
using dir = tempDir("fail-with-no-tests", {
"not-a-test.ts": `console.log("hello");`,
});
const { exited, stderr } = Bun.spawn({
cmd: [bunExe(), "test"],
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: bunEnv,
});
const [err, exitCode] = await Promise.all([stderr.text(), exited]);
expect(exitCode).toBe(1);
expect(err).toContain("No tests found!");
});
test("without --pass-with-no-tests, exits with 1 when filters match no tests", async () => {
using dir = tempDir("fail-with-no-tests-filter", {
"some.test.ts": `import { test } from "bun:test"; test("example", () => {});`,
});
const { exited } = Bun.spawn({
cmd: [bunExe(), "test", "-t", "nonexistent"],
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: bunEnv,
});
const exitCode = await exited;
expect(exitCode).toBe(1);
});
test("--pass-with-no-tests still fails when tests fail", async () => {
using dir = tempDir("pass-with-no-tests-but-fail", {
"test.test.ts": `import { test, expect } from "bun:test"; test("failing", () => { expect(1).toBe(2); });`,
});
const { exited } = Bun.spawn({
cmd: [bunExe(), "test", "--pass-with-no-tests"],
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: bunEnv,
});
const exitCode = await exited;
expect(exitCode).toBe(1);
});