import { execFile } from "node:child_process"; import { cp, mkdtemp, readFile, rm, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const coverageMetrics = [ "lines", "statements", "functions", "branches", ] as const; const runtimeModule = "src/runtime.ts"; const counterlessModules = 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", ] as const); const expectedModules = Object.freeze( [runtimeModule, ...counterlessModules].sort(), ); const childOutputLimit = 1_800; type ChildOutput = Readonly<{ stdout: string; stderr: string }>; type ChildInput = Readonly<{ repositoryRoot: string; ownedRoot: string; configPath: string; }>; type CheckerOptions = Readonly<{ repositoryRoot?: string; createOwnedRoot?: () => Promise; runVitest?: (input: ChildInput) => Promise; }>; export type V8CoverageCounterSemantics = Readonly<{ counterBearingModules: readonly string[]; counterlessModules: readonly string[]; }>; function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function hasErrorCode(error: unknown, code: string): boolean { return isRecord(error) && error.code === code; } function normalizeSummaryPath(value: string, fixtureRoot: string): string { const relative = path.isAbsolute(value) ? path.relative(fixtureRoot, value) : value; const normalized = relative.split(path.sep).join("/"); if ( normalized.length === 0 || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized) ) { throw new TypeError(`V8 coverage row is outside the owned fixture: ${value}`); } return normalized; } function coverageCounter( value: unknown, label: string, ): Readonly<{ total: number; covered: number; skipped: number; pct: number }> { if (!isRecord(value)) throw new TypeError(`${label} must be an object`); const { total, covered, skipped, pct } = value; if ( typeof total !== "number" || !Number.isSafeInteger(total) || typeof covered !== "number" || !Number.isSafeInteger(covered) || typeof skipped !== "number" || !Number.isSafeInteger(skipped) || typeof pct !== "number" || !Number.isFinite(pct) || total < 0 || covered < 0 || skipped < 0 || covered + skipped > total ) { throw new TypeError(`${label} contains invalid coverage counters`); } const expectedPercentage = total === 0 ? 100 : Math.floor((covered / total) * 10_000) / 100; if (pct !== expectedPercentage) { throw new TypeError(`${label} contains invalid coverage counters`); } return { total, covered, skipped, pct }; } function coverageRow( value: unknown, label: string, ): Readonly>> { if (!isRecord(value)) throw new TypeError(`${label} must be an object`); return Object.fromEntries( coverageMetrics.map((metric) => [ metric, coverageCounter(value[metric], `${label}.${metric}`), ]), ) as Readonly< Record<(typeof coverageMetrics)[number], ReturnType> >; } export function assertV8CoverageCounterSemantics( value: unknown, fixtureRoot: string, ): V8CoverageCounterSemantics { if (!isRecord(value)) throw new TypeError("V8 coverage summary must be an object"); coverageRow(value.total, "V8 coverage total"); const rows = new Map>(); for (const [producerPath, producerRow] of Object.entries(value)) { if (producerPath === "total") continue; const normalizedPath = normalizeSummaryPath(producerPath, fixtureRoot); if (rows.has(normalizedPath)) { throw new TypeError(`V8 coverage row is duplicated: ${normalizedPath}`); } rows.set( normalizedPath, coverageRow(producerRow, `V8 coverage row ${normalizedPath}`), ); } const actualModules = [...rows.keys()].sort(); const missing = expectedModules.filter((modulePath) => !rows.has(modulePath)); const additional = actualModules.filter( (modulePath) => !expectedModules.includes(modulePath), ); if (missing.length > 0 || additional.length > 0) { throw new Error( `V8 coverage row set mismatch; missing: ${missing.join(", ") || "none"}; additional: ${additional.join(", ") || "none"}`, ); } const runtimeRow = rows.get(runtimeModule)!; if (!coverageMetrics.some((metric) => runtimeRow[metric].total > 0)) { throw new Error(`V8 runtime module is not counter-bearing: ${runtimeModule}`); } for (const modulePath of counterlessModules) { const row = rows.get(modulePath)!; const exactAllZero = coverageMetrics.every((metric) => { const counter = row[metric]; return ( counter.total === 0 && counter.covered === 0 && counter.skipped === 0 && counter.pct === 100 ); }); if (!exactAllZero) { throw new Error( `V8 counterless module must have exact all-zero counters: ${modulePath}`, ); } } return Object.freeze({ counterBearingModules: Object.freeze([runtimeModule]), counterlessModules: Object.freeze([...counterlessModules]), }); } function boundedChildOutput(value: unknown): string { const output = typeof value === "string" ? value : value instanceof Uint8Array ? new TextDecoder().decode(value) : ""; if (output.length <= childOutputLimit) return output; return `${output.slice(0, childOutputLimit)}\n[truncated ${output.length - childOutputLimit} characters]`; } async function defaultRunVitest(input: ChildInput): Promise { const result = await execFileAsync( process.execPath, [ path.join(input.repositoryRoot, "node_modules/vitest/vitest.mjs"), "run", "--config", input.configPath, "--coverage", "--reporter=dot", "--no-color", ], { cwd: input.ownedRoot, encoding: "utf8", timeout: 30_000, maxBuffer: 256 * 1024, }, ); return { stdout: result.stdout, stderr: result.stderr }; } function assertOwnedTemporaryRoot(value: string): string { const temporaryRoot = path.resolve(tmpdir()); const ownedRoot = path.resolve(value); const relative = path.relative(temporaryRoot, ownedRoot); if ( relative.length === 0 || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) ) { throw new TypeError(`V8 coverage fixture root is not an owned temp path: ${ownedRoot}`); } return ownedRoot; } export async function checkV8CoverageCounterSemantics( options: CheckerOptions = {}, ): Promise { const repositoryRoot = path.resolve(options.repositoryRoot ?? process.cwd()); const createOwnedRoot = options.createOwnedRoot ?? (() => mkdtemp(path.join(tmpdir(), "v8-counter-semantics-"))); const runVitest = options.runVitest ?? defaultRunVitest; const ownedRoot = assertOwnedTemporaryRoot(await createOwnedRoot()); const fixtureSource = path.join( repositoryRoot, "tests/fixtures/v8-coverage-counter-semantics", ); const configPath = path.join(ownedRoot, "vitest.config.mjs"); const reportsDirectory = path.join(ownedRoot, "coverage"); try { await cp(fixtureSource, ownedRoot, { recursive: true }); await writeFile( configPath, `export default ${JSON.stringify( { root: ownedRoot, test: { globals: true, include: ["counter-semantics.fixture.ts"], setupFiles: [], coverage: { provider: "v8", reportsDirectory, reporter: ["json-summary"], include: ["src/**/*.ts"], }, }, }, null, 2, )};\n`, "utf8", ); try { await runVitest({ repositoryRoot, ownedRoot, configPath }); } catch (error) { const record = isRecord(error) ? error : {}; throw new Error( `V8 coverage counter semantics child Vitest failed\nstdout:\n${boundedChildOutput(record.stdout)}\nstderr:\n${boundedChildOutput(record.stderr)}`, { cause: error }, ); } const summaryPath = path.join(reportsDirectory, "coverage-summary.json"); let summaryText: string; try { summaryText = await readFile(summaryPath, "utf8"); } catch (error) { if (hasErrorCode(error, "ENOENT")) { throw new Error("V8 coverage counter semantics coverage summary is missing", { cause: error, }); } throw error; } let summary: unknown; try { summary = JSON.parse(summaryText) as unknown; } catch (error) { throw new TypeError("V8 coverage counter semantics summary is invalid JSON", { cause: error, }); } return assertV8CoverageCounterSemantics(summary, ownedRoot); } finally { await rm(ownedRoot, { recursive: true, force: true }); } }