Files
2026-07-13 13:39:12 +08:00

111 lines
3.5 KiB
JavaScript

import { existsSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
function getArg(name, fallbackValue = "") {
const index = process.argv.indexOf(name);
if (index === -1 || index === process.argv.length - 1) {
return fallbackValue;
}
return process.argv[index + 1];
}
function formatPercent(value) {
return `${Number(value ?? 0).toFixed(2)}%`;
}
function parseThreshold(name, fallbackValue) {
const rawValue = getArg(name, fallbackValue);
const value = Number(rawValue);
if (!Number.isFinite(value)) {
console.error(`Invalid coverage threshold for ${name}: ${rawValue}`);
process.exit(1);
}
return value;
}
const inputPath = getArg("--input", "coverage/coverage-summary.json");
const outputPath = getArg("--output", "");
const hasGlobalThreshold = process.argv.includes("--threshold");
const defaultThreshold = parseThreshold("--threshold", "75");
if (!existsSync(inputPath)) {
console.error(`Coverage summary file not found: ${inputPath}`);
process.exit(1);
}
const summary = JSON.parse(readFileSync(inputPath, "utf8"));
const cwd = process.cwd();
const metrics = [
["lines", "Lines"],
["statements", "Statements"],
["functions", "Functions"],
["branches", "Branches"],
];
const thresholds = Object.fromEntries(
metrics.map(([metric]) => {
const fallback = metric === "branches" && !hasGlobalThreshold ? "70" : String(defaultThreshold);
return [metric, parseThreshold(`--${metric}`, fallback)];
})
);
const total = summary.total ?? {};
const gatePassed = metrics.every(([metric]) => (total[metric]?.pct ?? 0) >= thresholds[metric]);
const files = Object.entries(summary)
.filter(([name]) => name !== "total" && /\.(?:[cm]?[jt]sx?)$/.test(name))
.map(([name, stats]) => {
const relativeName = path.relative(cwd, name);
const totalLines = stats.lines?.total ?? 0;
const coveredLines = stats.lines?.covered ?? 0;
return {
name: relativeName,
lines: stats.lines?.pct ?? 0,
branches: stats.branches?.pct ?? 0,
functions: stats.functions?.pct ?? 0,
missingLines: Math.max(totalLines - coveredLines, 0),
};
})
.sort((left, right) => {
if (left.lines !== right.lines) return left.lines - right.lines;
if (left.branches !== right.branches) return left.branches - right.branches;
return right.missingLines - left.missingLines;
})
.slice(0, 15);
const report = [
"# Coverage Report",
"",
`Gate: ${gatePassed ? "PASS" : "FAIL"} at configured metric minimums.`,
"",
"## Totals",
"",
"| Metric | Covered | Total | Percent | Threshold | Status |",
"| --- | ---: | ---: | ---: | ---: | --- |",
...metrics.map(([metric, label]) => {
const covered = total[metric]?.covered ?? 0;
const totalCount = total[metric]?.total ?? 0;
const pct = total[metric]?.pct ?? 0;
const threshold = thresholds[metric];
const status = pct >= threshold ? "PASS" : "FAIL";
return `| ${label} | ${covered} | ${totalCount} | ${formatPercent(pct)} | ${threshold}% | ${status} |`;
}),
"",
"## Lowest Coverage Files",
"",
"| File | Lines | Branches | Functions | Missing Lines |",
"| --- | ---: | ---: | ---: | ---: |",
...files.map(
(entry) =>
`| \`${entry.name}\` | ${formatPercent(entry.lines)} | ${formatPercent(entry.branches)} | ${formatPercent(entry.functions)} | ${entry.missingLines} |`
),
];
const reportContent = `${report.join("\n")}\n`;
if (outputPath) {
writeFileSync(outputPath, reportContent);
} else {
process.stdout.write(reportContent);
}