.env.example and siblings are config/template files with no executable code. The coverage:diff script now matches them via the same dotfile pattern used for .gitignore and .npmrc. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
6.9 KiB
JavaScript
201 lines
6.9 KiB
JavaScript
import { test, describe } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { parseLcov, parseGitDiff, computeDiffCoverage } from "./diff.mjs";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const FIXTURES = path.join(__dirname, "__fixtures__");
|
|
|
|
const lcovText = fs.readFileSync(path.join(FIXTURES, "sample.lcov"), "utf8");
|
|
const diffText = fs.readFileSync(
|
|
path.join(FIXTURES, "sample-diff.patch"),
|
|
"utf8",
|
|
);
|
|
|
|
describe("parseLcov", () => {
|
|
test("groups DA records by SF file", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
assert.equal(lcov.size, 3);
|
|
assert.ok(
|
|
lcov.has(
|
|
"/repo/packages/auth/src/application/use-cases/sign-in.use-case.ts",
|
|
),
|
|
);
|
|
assert.ok(lcov.has("/repo/packages/auth/src/entities/models/user.ts"));
|
|
});
|
|
|
|
test("preserves per-line execution counts", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
const lines = lcov.get(
|
|
"/repo/packages/auth/src/application/use-cases/sign-in.use-case.ts",
|
|
);
|
|
assert.equal(lines.get(1), 5);
|
|
assert.equal(lines.get(5), 0);
|
|
assert.equal(lines.get(8), 3);
|
|
});
|
|
|
|
test("ignores non-DA records (LF, LH, BRDA)", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
const lines = lcov.get("/repo/packages/auth/src/entities/models/user.ts");
|
|
assert.equal(lines.size, 3); // Only DA records, not LF/LH counters
|
|
});
|
|
});
|
|
|
|
describe("parseGitDiff", () => {
|
|
test("extracts new + modified line numbers per file from the new version", () => {
|
|
const diff = parseGitDiff(diffText);
|
|
const signIn = diff.get(
|
|
"packages/auth/src/application/use-cases/sign-in.use-case.ts",
|
|
);
|
|
// Hunks: +2,2 (lines 2,3) and +5,2 (lines 5,6)
|
|
assert.deepEqual(
|
|
[...signIn].sort((a, b) => a - b),
|
|
[2, 3, 5, 6],
|
|
);
|
|
});
|
|
|
|
test("strips the b/ prefix from new-version paths", () => {
|
|
const diff = parseGitDiff(diffText);
|
|
assert.ok(diff.has("packages/auth/src/entities/models/user.ts"));
|
|
assert.ok(!diff.has("b/packages/auth/src/entities/models/user.ts"));
|
|
});
|
|
|
|
test("handles single-line hunks (no comma in +N,M)", () => {
|
|
const diff = parseGitDiff(diffText);
|
|
const blog = diff.get(
|
|
"packages/blog/src/application/use-cases/get-article.use-case.ts",
|
|
);
|
|
// @@ -11,0 +12 @@ -> single line at 12
|
|
assert.deepEqual([...blog], [12]);
|
|
});
|
|
|
|
test("handles new files (entire file is in the diff)", () => {
|
|
const diff = parseGitDiff(diffText);
|
|
const upload = diff.get(
|
|
"packages/media/src/application/use-cases/upload.use-case.ts",
|
|
);
|
|
// @@ -0,0 +1,5 @@ -> lines 1-5
|
|
assert.deepEqual(
|
|
[...upload].sort((a, b) => a - b),
|
|
[1, 2, 3, 4, 5],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("computeDiffCoverage", () => {
|
|
test("passes when changed executable lines are all covered", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
const diff = new Map([
|
|
["/repo/packages/auth/src/entities/models/user.ts", new Set([1, 2, 3])],
|
|
]);
|
|
const result = computeDiffCoverage(diff, lcov);
|
|
assert.equal(result.status, "pass");
|
|
assert.equal(result.uncovered.length, 0);
|
|
});
|
|
|
|
test("fails when a changed line is in lcov with count 0", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
const diff = new Map([
|
|
[
|
|
"/repo/packages/auth/src/application/use-cases/sign-in.use-case.ts",
|
|
new Set([5, 6]),
|
|
],
|
|
]);
|
|
const result = computeDiffCoverage(diff, lcov);
|
|
assert.equal(result.status, "fail");
|
|
assert.equal(result.uncovered.length, 2);
|
|
assert.deepEqual(result.uncovered.map((u) => u.line).sort(), [5, 6]);
|
|
assert.ok(result.uncovered.every((u) => u.kind === "uncovered"));
|
|
});
|
|
|
|
test("ignores lines without DA records (non-executable)", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
// Line 4 has no DA record in the fixture
|
|
const diff = new Map([
|
|
[
|
|
"/repo/packages/auth/src/application/use-cases/sign-in.use-case.ts",
|
|
new Set([4]),
|
|
],
|
|
]);
|
|
const result = computeDiffCoverage(diff, lcov);
|
|
assert.equal(result.status, "pass");
|
|
});
|
|
|
|
test("flags files with no coverage data (new untested file)", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
const diff = new Map([
|
|
[
|
|
"packages/media/src/application/use-cases/upload.use-case.ts",
|
|
new Set([1, 2, 3, 4, 5]),
|
|
],
|
|
]);
|
|
const result = computeDiffCoverage(diff, lcov);
|
|
assert.equal(result.status, "fail");
|
|
assert.equal(result.uncovered.length, 1);
|
|
assert.equal(result.uncovered[0].kind, "no-coverage-data");
|
|
});
|
|
|
|
test("skips allowed extensions (.md, .json, .test.ts, configs)", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
const diff = new Map([
|
|
["CLAUDE.md", new Set([1, 2])],
|
|
["package.json", new Set([1])],
|
|
["packages/auth/vitest.config.ts", new Set([1])],
|
|
[
|
|
"packages/auth/src/application/use-cases/sign-in.use-case.test.ts",
|
|
new Set([11]),
|
|
],
|
|
]);
|
|
const result = computeDiffCoverage(diff, lcov);
|
|
assert.equal(result.status, "pass");
|
|
assert.equal(result.summary.filesGated, 0);
|
|
assert.equal(result.summary.filesChanged, 4);
|
|
});
|
|
|
|
test("skips .env template files (.env, .env.example, .env.local)", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
const diff = new Map([
|
|
[".env.example", new Set([1, 2, 3])],
|
|
[".env.local", new Set([1])],
|
|
[".env", new Set([1])],
|
|
]);
|
|
const result = computeDiffCoverage(diff, lcov);
|
|
assert.equal(result.status, "pass");
|
|
assert.equal(result.summary.filesGated, 0);
|
|
assert.equal(result.summary.filesChanged, 3);
|
|
});
|
|
|
|
test("end-to-end fixture: mixed pass/fail/skip/no-data", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
const diff = parseGitDiff(diffText);
|
|
const result = computeDiffCoverage(diff, lcov, { repoRoot: "/repo" });
|
|
|
|
assert.equal(result.status, "fail");
|
|
// CLAUDE.md, sign-in.use-case.test.ts -> skipped (allowlist)
|
|
// sign-in.use-case.ts lines 2,3,5,6 -> 5,6 are uncovered, 2,3 don't have DA records (not in lcov for those lines) so they don't count
|
|
// user.ts line 2 -> covered (count: 1)
|
|
// get-article.use-case.ts line 12 -> uncovered (count: 0)
|
|
// upload.use-case.ts -> no coverage data
|
|
// The expected uncovered set: sign-in lines 5,6 + get-article line 12 + upload (no-data)
|
|
const uncoveredKinds = result.uncovered.map((u) => u.kind);
|
|
assert.ok(uncoveredKinds.includes("no-coverage-data"));
|
|
assert.ok(uncoveredKinds.includes("uncovered"));
|
|
});
|
|
|
|
test("resolves repo-relative diff paths against lcov absolute paths", () => {
|
|
const lcov = parseLcov(lcovText);
|
|
const diff = new Map([
|
|
// Diff uses repo-relative; lcov has absolute. Suffix match should
|
|
// bridge them.
|
|
[
|
|
"packages/auth/src/application/use-cases/sign-in.use-case.ts",
|
|
new Set([8]),
|
|
],
|
|
]);
|
|
const result = computeDiffCoverage(diff, lcov, { repoRoot: "/repo" });
|
|
assert.equal(result.status, "pass");
|
|
});
|
|
});
|