The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/scripts/compare-coverage-by-codeown...

184 lines
5.6 KiB

#!/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 };