Files
tech-log-frontend/tests/unit/v8-coverage-counter-semantics.test.ts

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([]);
});
});