grafana/scripts/compare-coverage-by-codeowner.js

185 lines
5.6 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
const fs = require('fs');
const COVERAGE_MAIN_PATH = './coverage-main/coverage-summary.json';
const COVERAGE_PR_PATH = './coverage-pr/coverage-summary.json';
const COMPARISON_OUTPUT_PATH = './coverage-comparison.md';
/**
* Reads and parses a coverage summary JSON file
* @param {string} filePath - Path to coverage summary file
* @returns {Object} Parsed coverage data
*/
function readCoverageFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (err) {
console.error(`Error reading coverage file ${filePath}: ${err.message}`);
process.exit(1);
}
}
/**
* Formats a number as a percentage string
* @param {number} value - Percentage value
* @returns {string} Formatted percentage (e.g., "85.34%")
*/
function formatPercentage(value) {
return `${value.toFixed(2)}%`;
}
/**
* Returns status icon based on coverage comparison
* @param {number} mainValue - Main branch coverage percentage
* @param {number} prValue - PR branch coverage percentage
* @returns {string} Status icon and text
*/
function getStatusIcon(mainValue, prValue) {
// Round to 2 decimal places for comparison to match display precision
const prPct = Math.round(prValue * 100) / 100;
const mainPct = Math.round(mainValue * 100) / 100;
if (prPct >= mainPct) {
return '✅ Pass';
}
return '❌ Fail';
}
/**
* Determines overall pass/fail status for all coverage metrics
* @param {Object} mainSummary - Main branch coverage summary
* @param {Object} prSummary - PR branch coverage summary
* @returns {boolean} True if all metrics maintained or improved
*/
function getOverallStatus(mainSummary, prSummary) {
const metrics = ['lines', 'statements', 'functions', 'branches'];
const allPass = metrics.every((metric) => {
// Round to 2 decimal places for comparison to match display precision
const prPct = Math.round(prSummary[metric].pct * 100) / 100;
const mainPct = Math.round(mainSummary[metric].pct * 100) / 100;
return prPct >= mainPct;
});
return allPass;
}
/**
* Calculates the difference between PR and main coverage
* @param {number} prValue - PR coverage percentage
* @param {number} mainValue - Main coverage percentage
* @returns {string} Formatted delta (e.g., "+1.2%" or "-0.5%")
*/
function formatDelta(prValue, mainValue) {
const delta = prValue - mainValue;
if (delta > 0) {
return `+${delta.toFixed(2)}%`;
} else if (delta < 0) {
return `${delta.toFixed(2)}%`;
}
return '—';
}
/**
* Generates markdown report comparing main and PR coverage
* @param {Object} mainCoverage - Main branch coverage data
* @param {Object} prCoverage - PR branch coverage data
* @returns {string} Markdown formatted report
*/
function generateMarkdown(mainCoverage, prCoverage) {
const teamName = prCoverage.team;
const mainSummary = mainCoverage.summary;
const prSummary = prCoverage.summary;
const overallPass = getOverallStatus(mainSummary, prSummary);
const rows = [
{
metric: 'Lines',
main: mainSummary.lines.pct,
pr: prSummary.lines.pct,
},
{
metric: 'Statements',
main: mainSummary.statements.pct,
pr: prSummary.statements.pct,
},
{
metric: 'Functions',
main: mainSummary.functions.pct,
pr: prSummary.functions.pct,
},
{
metric: 'Branches',
main: mainSummary.branches.pct,
pr: prSummary.branches.pct,
},
];
const tableRows = rows
.map((row) => {
const status = getStatusIcon(row.main, row.pr);
const delta = formatDelta(row.pr, row.main);
return `| ${row.metric} | ${formatPercentage(row.main)} | ${formatPercentage(row.pr)} | ${delta} | ${status} |`;
})
.join('\n');
const overallStatus = overallPass ? '✅ Passed' : '❌ Failed';
return `## Test Coverage Checks ${overallStatus} for ${teamName}
| Metric | Main | PR | Change | Status |
|--------|------|----|----|--------|
${tableRows}
**Run locally:** 💻 \`yarn test:coverage:by-codeowner ${teamName}\`
**Break glass:** 🚨 In case of emergency, adding the \`no-check-frontend-test-coverage\` label to this PR will skip checks.
`;
}
/**
* Compares coverage between main and PR branches and generates a markdown report
* @param {string} mainPath - Path to main branch coverage summary JSON
* @param {string} prPath - Path to PR branch coverage summary JSON
* @param {string} outputPath - Path to write comparison markdown
* @returns {boolean} True if coverage check passed
*/
function compareCoverageByCodeowner(
mainPath = COVERAGE_MAIN_PATH,
prPath = COVERAGE_PR_PATH,
outputPath = COMPARISON_OUTPUT_PATH
) {
const mainCoverage = readCoverageFile(mainPath);
const prCoverage = readCoverageFile(prPath);
if (!mainCoverage.summary || !prCoverage.summary) {
console.error('Error: Coverage summary data is missing or invalid');
process.exit(1);
}
const markdown = generateMarkdown(mainCoverage, prCoverage);
const overallPass = getOverallStatus(mainCoverage.summary, prCoverage.summary);
try {
fs.writeFileSync(outputPath, markdown, 'utf8');
console.log(`✅ Coverage comparison written to ${outputPath}`);
} catch (err) {
console.error(`Error writing output file: ${err.message}`);
process.exit(1);
}
return overallPass;
}
if (require.main === module) {
const passed = compareCoverageByCodeowner();
if (!passed) {
console.error('❌ Coverage check failed: One or more metrics decreased');
process.exit(1);
}
console.log('✅ Coverage check passed: All metrics maintained or improved');
}
module.exports = { compareCoverageByCodeowner, generateMarkdown, getOverallStatus };