Skip to content

Commit 7125939

Browse files
authored
[vitest-pool-workers] Add regression test for Istanbul coverage across multiple test files (#13081)
1 parent 1faff35 commit 7125939

File tree

2 files changed

+123
-1
lines changed

2 files changed

+123
-1
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
import dedent from "ts-dedent";
4+
import { test } from "./helpers";
5+
6+
// Regression test for https://github.com/cloudflare/workers-sdk/issues/5825
7+
// Istanbul coverage was reporting 0% for source files exercised by test files
8+
// that ran after the first one. The root cause was that in vitest v1, module
9+
// re-evaluation after `resetModules()` replaced Istanbul's coverage counter
10+
// objects, losing data from earlier test files. This was fixed by the vitest v4
11+
// module runner architecture which correctly preserves counter objects across
12+
// module re-evaluations via hash-based reuse in istanbul-lib-instrument.
13+
test(
14+
"istanbul coverage reports correctly across multiple test files (#5825)",
15+
{ timeout: 60_000 },
16+
async ({ expect, seed, vitestRun, tmpPath }) => {
17+
await seed({
18+
"wrangler.jsonc": JSON.stringify({
19+
name: "coverage-test",
20+
main: "src/index.ts",
21+
}),
22+
"vitest.config.mts": dedent /* javascript */ `
23+
import { cloudflareTest } from "@cloudflare/vitest-pool-workers"
24+
import { BaseSequencer } from "vitest/node";
25+
26+
class DeterministicSequencer extends BaseSequencer {
27+
sort(files) {
28+
return [...files].sort((a, b) => a.moduleId.localeCompare(b.moduleId));
29+
}
30+
}
31+
32+
export default {
33+
plugins: [
34+
cloudflareTest({
35+
miniflare: {
36+
compatibilityDate: "2025-12-02",
37+
compatibilityFlags: ["nodejs_compat"],
38+
},
39+
wrangler: {
40+
configPath: "./wrangler.jsonc",
41+
},
42+
})
43+
],
44+
test: {
45+
sequence: { sequencer: DeterministicSequencer },
46+
testTimeout: 90_000,
47+
coverage: {
48+
provider: "istanbul",
49+
reporter: ["json-summary"],
50+
include: ["src/**"],
51+
},
52+
},
53+
};
54+
`,
55+
// Worker with two routes dispatching to separate source files
56+
"src/index.ts": dedent /* javascript */ `
57+
import { greetA } from "./a";
58+
import { greetB } from "./b";
59+
60+
export default {
61+
async fetch(request, env, ctx) {
62+
if (request.url.endsWith("/a")) {
63+
return new Response(greetA(request));
64+
}
65+
return new Response(greetB(request));
66+
},
67+
} satisfies ExportedHandler;
68+
`,
69+
"src/a.ts": dedent /* javascript */ `
70+
export function greetA(request: Request): string {
71+
return "A: " + request.url;
72+
}
73+
`,
74+
"src/b.ts": dedent /* javascript */ `
75+
export function greetB(request: Request): string {
76+
return "B: " + request.url;
77+
}
78+
`,
79+
// Two test files exercising different routes — a.test.ts runs first
80+
"a.test.ts": dedent /* javascript */ `
81+
import { SELF } from "cloudflare:test";
82+
import { it, expect } from "vitest";
83+
84+
it("routes to a", async () => {
85+
const response = await SELF.fetch("http://example.com/a");
86+
expect(await response.text()).toBe("A: http://example.com/a");
87+
});
88+
`,
89+
"b.test.ts": dedent /* javascript */ `
90+
import { SELF } from "cloudflare:test";
91+
import { it, expect } from "vitest";
92+
93+
it("routes to b", async () => {
94+
const response = await SELF.fetch("http://example.com/b");
95+
expect(await response.text()).toBe("B: http://example.com/b");
96+
});
97+
`,
98+
});
99+
const result = await vitestRun({ flags: ["--coverage"] });
100+
expect(await result.exitCode).toBe(0);
101+
102+
// Read the JSON coverage summary to verify actual coverage values
103+
const summaryPath = path.join(tmpPath, "coverage", "coverage-summary.json");
104+
const summaryJson = JSON.parse(await fs.readFile(summaryPath, "utf8"));
105+
106+
// Find coverage for a.ts and b.ts (keys are absolute paths)
107+
const entries = Object.entries(summaryJson) as [
108+
string,
109+
{ functions: { pct: number } },
110+
][];
111+
const aCoverage = entries.find(([k]) => k.endsWith("/src/a.ts"))?.[1];
112+
const bCoverage = entries.find(([k]) => k.endsWith("/src/b.ts"))?.[1];
113+
114+
// The bug: b.ts showed 0% coverage when both files ran together,
115+
// because its counters were lost during module re-evaluation.
116+
// Both files should now report non-zero function coverage.
117+
expect(aCoverage?.functions.pct).toBeGreaterThan(0);
118+
expect(bCoverage?.functions.pct).toBeGreaterThan(0);
119+
}
120+
);

packages/vitest-pool-workers/test/global-setup.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,16 @@ async function createTestProject() {
4242
await fs.mkdtemp(path.join(os.tmpdir(), "vitest-pool-workers temp-"))
4343
);
4444
const packageJsonPath = path.join(projectPath, "package.json");
45+
const vitestPeerDep = await getVitestPeerDep();
4546
const packageJson = {
4647
name: "vitest-pool-workers-e2e-tests",
4748
private: true,
4849
type: "module",
4950
devDependencies: {
5051
// Ensure we use the local version of vitest-pool-workers
5152
"@cloudflare/vitest-pool-workers": version,
52-
vitest: await getVitestPeerDep(),
53+
"@vitest/coverage-istanbul": vitestPeerDep,
54+
vitest: vitestPeerDep,
5355
},
5456
};
5557
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson));

0 commit comments

Comments
 (0)