215 lines
6.6 KiB
TypeScript
215 lines
6.6 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import {
|
|
access,
|
|
mkdtemp,
|
|
readFile,
|
|
rm,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
assertV8CoverageCounterSemantics,
|
|
checkV8CoverageCounterSemantics,
|
|
} from "../../scripts/lib/v8-coverage-counter-semantics.ts";
|
|
|
|
const roots: string[] = [];
|
|
const execFileAsync = promisify(execFile);
|
|
const zeroCounter = Object.freeze({
|
|
total: 0,
|
|
covered: 0,
|
|
skipped: 0,
|
|
pct: 100,
|
|
});
|
|
const zeroMetrics = Object.freeze({
|
|
lines: zeroCounter,
|
|
statements: zeroCounter,
|
|
functions: zeroCounter,
|
|
branches: zeroCounter,
|
|
});
|
|
const counterlessPaths = Object.freeze([
|
|
"src/import-empty.ts",
|
|
"src/import-side-effect.ts",
|
|
"src/import-type-empty.ts",
|
|
"src/import-value.ts",
|
|
"src/reexport-named.ts",
|
|
"src/reexport-star.ts",
|
|
"src/type-only.ts",
|
|
]);
|
|
|
|
function fixtureSummary(root: string): Record<string, unknown> {
|
|
return Object.fromEntries([
|
|
[
|
|
"total",
|
|
{
|
|
lines: { total: 1, covered: 1, skipped: 0, pct: 100 },
|
|
statements: { total: 1, covered: 1, skipped: 0, pct: 100 },
|
|
functions: zeroCounter,
|
|
branches: zeroCounter,
|
|
},
|
|
],
|
|
[
|
|
path.join(root, "src/runtime.ts"),
|
|
{
|
|
lines: { total: 1, covered: 1, skipped: 0, pct: 100 },
|
|
statements: { total: 1, covered: 1, skipped: 0, pct: 100 },
|
|
functions: zeroCounter,
|
|
branches: zeroCounter,
|
|
},
|
|
],
|
|
...counterlessPaths.map((modulePath) => [path.join(root, modulePath), zeroMetrics]),
|
|
]);
|
|
}
|
|
|
|
async function captureFailure(operation: Promise<unknown>): Promise<Error> {
|
|
try {
|
|
await operation;
|
|
} catch (error) {
|
|
if (error instanceof Error) return error;
|
|
throw error;
|
|
}
|
|
throw new Error("expected operation to fail");
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
roots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
describe("V8 coverage counter semantics", () => {
|
|
it("accepts one counter-bearing row and seven exact counterless rows", () => {
|
|
const root = "/owned-fixture";
|
|
|
|
expect(assertV8CoverageCounterSemantics(fixtureSummary(root), root)).toEqual({
|
|
counterBearingModules: ["src/runtime.ts"],
|
|
counterlessModules: counterlessPaths,
|
|
});
|
|
});
|
|
|
|
it("rejects a missing or additional producer row", () => {
|
|
const root = "/owned-fixture";
|
|
const missing = fixtureSummary(root);
|
|
delete missing[path.join(root, "src/import-empty.ts")];
|
|
expect(() => assertV8CoverageCounterSemantics(missing, root)).toThrow(
|
|
/row set.*missing.*import-empty/u,
|
|
);
|
|
|
|
const additional = fixtureSummary(root);
|
|
additional[path.join(root, "src/unexpected.ts")] = zeroMetrics;
|
|
expect(() => assertV8CoverageCounterSemantics(additional, root)).toThrow(
|
|
/row set.*additional.*unexpected/u,
|
|
);
|
|
});
|
|
|
|
it("rejects counterless nonzero and runtime all-zero drift", () => {
|
|
const root = "/owned-fixture";
|
|
const counterlessDrift = fixtureSummary(root);
|
|
counterlessDrift[path.join(root, "src/import-value.ts")] = {
|
|
...zeroMetrics,
|
|
lines: { total: 1, covered: 1, skipped: 0, pct: 100 },
|
|
};
|
|
expect(() =>
|
|
assertV8CoverageCounterSemantics(counterlessDrift, root),
|
|
).toThrow(/counterless.*exact all-zero.*import-value/u);
|
|
|
|
const runtimeDrift = fixtureSummary(root);
|
|
runtimeDrift[path.join(root, "src/runtime.ts")] = zeroMetrics;
|
|
expect(() => assertV8CoverageCounterSemantics(runtimeDrift, root)).toThrow(
|
|
/runtime.*counter-bearing/u,
|
|
);
|
|
});
|
|
|
|
it("rejects producer percentages that do not exactly match their counts", () => {
|
|
const root = "/owned-fixture";
|
|
const totalDrift = fixtureSummary(root);
|
|
totalDrift["total"] = {
|
|
...totalDrift["total"] as Record<string, unknown>,
|
|
lines: { total: 1, covered: 1, skipped: 0, pct: -500 },
|
|
};
|
|
expect(() => assertV8CoverageCounterSemantics(totalDrift, root)).toThrow(
|
|
/total\.lines.*invalid coverage counters/u,
|
|
);
|
|
|
|
const runtimeDrift = fixtureSummary(root);
|
|
runtimeDrift[path.join(root, "src/runtime.ts")] = {
|
|
...runtimeDrift[path.join(root, "src/runtime.ts")] as Record<
|
|
string,
|
|
unknown
|
|
>,
|
|
lines: { total: 1, covered: 1, skipped: 0, pct: 99.99 },
|
|
};
|
|
expect(() => assertV8CoverageCounterSemantics(runtimeDrift, root)).toThrow(
|
|
/runtime.*lines.*invalid coverage counters/u,
|
|
);
|
|
});
|
|
|
|
it("bounds child diagnostics and cleans its owned root on child exit", async () => {
|
|
const ownedRoot = await mkdtemp(path.join(tmpdir(), "v8-counter-exit-"));
|
|
roots.push(ownedRoot);
|
|
const childFailure = Object.assign(new Error("child exit 1"), {
|
|
stdout: "o".repeat(20_000),
|
|
stderr: "e".repeat(20_000),
|
|
});
|
|
|
|
const failure = await captureFailure(
|
|
checkV8CoverageCounterSemantics({
|
|
repositoryRoot: process.cwd(),
|
|
createOwnedRoot: async () => ownedRoot,
|
|
runVitest: async () => Promise.reject(childFailure),
|
|
}),
|
|
);
|
|
|
|
expect(failure.message).toMatch(/child Vitest failed/u);
|
|
expect(failure.message).toMatch(/truncated/u);
|
|
expect(failure.message.length).toBeLessThan(5_000);
|
|
await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("fails closed on a missing summary and cleans its owned root", async () => {
|
|
const ownedRoot = await mkdtemp(path.join(tmpdir(), "v8-counter-summary-"));
|
|
roots.push(ownedRoot);
|
|
|
|
await expect(
|
|
checkV8CoverageCounterSemantics({
|
|
repositoryRoot: process.cwd(),
|
|
createOwnedRoot: async () => ownedRoot,
|
|
runVitest: async () => ({ stdout: "", stderr: "" }),
|
|
}),
|
|
).rejects.toThrow(/coverage summary is missing/u);
|
|
await expect(access(ownedRoot)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("keeps the child fixture outside main Vitest discovery", async () => {
|
|
const outputRoot = await mkdtemp(path.join(tmpdir(), "v8-counter-list-"));
|
|
roots.push(outputRoot);
|
|
const outputPath = path.join(outputRoot, "listed.json");
|
|
await execFileAsync(
|
|
process.execPath,
|
|
[
|
|
path.join(process.cwd(), "node_modules/vitest/vitest.mjs"),
|
|
"list",
|
|
"tests/fixtures/v8-coverage-counter-semantics",
|
|
"--config",
|
|
path.join(process.cwd(), "vitest.config.ts"),
|
|
"--filesOnly",
|
|
"--passWithNoTests",
|
|
"--json",
|
|
outputPath,
|
|
"--no-color",
|
|
],
|
|
{
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
timeout: 30_000,
|
|
maxBuffer: 256 * 1024,
|
|
},
|
|
);
|
|
|
|
expect(JSON.parse(await readFile(outputPath, "utf8"))).toEqual([]);
|
|
});
|
|
});
|