diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index 9626a14b65..27152e6cdc 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -1828,6 +1828,13 @@ pub const TestCommand = struct { vm.onUnhandledRejection = jest.on_unhandled_rejection.onUnhandledRejection; while (repeat_index < repeat_count) : (repeat_index += 1) { + // Clear the module cache before re-running (except for the first run) + if (repeat_index > 0) { + try vm.clearEntryPoint(); + var entry = jsc.ZigString.init(file_path); + try vm.global.deleteModuleRegistryEntry(&entry); + } + var bun_test_root = &jest.Jest.runner.?.bun_test_root; // Determine if this file should run tests concurrently based on glob pattern const should_run_concurrent = reporter.jest.shouldFileRunConcurrently(file_id); @@ -1838,7 +1845,10 @@ pub const TestCommand = struct { bun.jsc.Jest.bun_test.debug.group.log("loadEntryPointForTestRunner(\"{}\")", .{std.zig.fmtEscapes(file_path)}); var promise = try vm.loadEntryPointForTestRunner(file_path); - reporter.summary().files += 1; + // Only count the file once, not once per repeat + if (repeat_index == 0) { + reporter.summary().files += 1; + } switch (promise.status(vm.global.vm())) { .rejected => { @@ -1905,11 +1915,6 @@ pub const TestCommand = struct { } vm.global.handleRejectedPromises(); - if (repeat_index > 0) { - try vm.clearEntryPoint(); - var entry = jsc.ZigString.init(file_path); - try vm.global.deleteModuleRegistryEntry(&entry); - } if (Output.is_github_action) { Output.prettyErrorln("\n::endgroup::\n", .{}); diff --git a/test/cli/test/rerun-each.test.ts b/test/cli/test/rerun-each.test.ts new file mode 100644 index 0000000000..a8166b3285 --- /dev/null +++ b/test/cli/test/rerun-each.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("--rerun-each should run tests exactly N times", async () => { + using dir = tempDir("test-rerun-each", { + "counter.test.ts": ` + import { test, expect } from "bun:test"; + + // Use a global counter that persists across module reloads + if (!globalThis.testRunCounter) { + globalThis.testRunCounter = 0; + } + + test("should increment counter", () => { + globalThis.testRunCounter++; + console.log(\`Run #\${globalThis.testRunCounter}\`); + expect(true).toBe(true); + }); + `, + }); + + // Test with --rerun-each=3 + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "counter.test.ts", "--rerun-each=3"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); + + // Should see "Run #1", "Run #2", "Run #3" in the output + expect(stdout).toContain("Run #1"); + expect(stdout).toContain("Run #2"); + expect(stdout).toContain("Run #3"); + + // Should NOT see "Run #4" + expect(stdout).not.toContain("Run #4"); + + // Should run exactly 3 tests - check stderr for test summary + const combined = stdout + stderr; + expect(combined).toMatch(/3 pass/); + + // Test with --rerun-each=1 (should run once) + await using proc2 = Bun.spawn({ + cmd: [bunExe(), "test", "counter.test.ts", "--rerun-each=1"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]); + + expect(exitCode2).toBe(0); + const combined2 = stdout2 + stderr2; + expect(combined2).toMatch(/1 pass/); +}); + +test("--rerun-each should report correct file count", async () => { + using dir = tempDir("test-rerun-each-file-count", { + "test1.test.ts": ` + import { test, expect } from "bun:test"; + test("test in file 1", () => { + expect(true).toBe(true); + }); + `, + }); + + // Run with --rerun-each=3 + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "test1.test.ts", "--rerun-each=3"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); + + // Should report "Ran 3 tests across 1 file" not "across 3 files" + const combined = stdout + stderr; + expect(combined).toContain("Ran 3 tests across 1 file"); + expect(combined).not.toContain("across 3 files"); +}); + +test("--rerun-each should handle test failures correctly", async () => { + using dir = tempDir("test-rerun-each-fail", { + "fail.test.ts": ` + import { test, expect } from "bun:test"; + + if (!globalThis.failCounter) { + globalThis.failCounter = 0; + } + + test("fails on second run", () => { + globalThis.failCounter++; + console.log(\`Attempt #\${globalThis.failCounter}\`); + // Fail on the second run + expect(globalThis.failCounter).not.toBe(2); + }); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "fail.test.ts", "--rerun-each=3"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should have non-zero exit code due to failure + expect(exitCode).not.toBe(0); + + // Should see all three attempts + expect(stdout).toContain("Attempt #1"); + expect(stdout).toContain("Attempt #2"); + expect(stdout).toContain("Attempt #3"); + + // Should report 2 passes and 1 failure - check both stdout and stderr + const combined = stdout + stderr; + expect(combined).toMatch(/2 pass/); + expect(combined).toMatch(/1 fail/); +});