|
| 1 | +/** |
| 2 | + * Utility script to verify that the coverage badge is up to date |
| 3 | + * This compares the coverage percentage in the badge SVG with the actual coverage from Jest |
| 4 | + */ |
| 5 | + |
| 6 | +import { readFileSync, existsSync } from 'fs' |
| 7 | +import { dirname, resolve } from 'path' |
| 8 | +import { fileURLToPath } from 'url' |
| 9 | + |
| 10 | +/** |
| 11 | + * Extracts the coverage percentage from the coverage-summary.json file |
| 12 | + * @returns The statement coverage percentage as a number |
| 13 | + * @throws Error if coverage file cannot be found or parsed |
| 14 | + */ |
| 15 | +export function getCoverageFromSummary(): number { |
| 16 | + const scriptDir = dirname(fileURLToPath(import.meta.url)) |
| 17 | + const coveragePath = resolve(scriptDir, '../coverage/coverage-summary.json') |
| 18 | + |
| 19 | + if (!existsSync(coveragePath)) { |
| 20 | + throw new Error( |
| 21 | + 'Coverage summary not found. Run "npm run test" to generate coverage data.' |
| 22 | + ) |
| 23 | + } |
| 24 | + |
| 25 | + const coverageData = JSON.parse(readFileSync(coveragePath, 'utf-8')) |
| 26 | + |
| 27 | + if (!coverageData.total || !coverageData.total.statements) { |
| 28 | + throw new Error('Invalid coverage summary format') |
| 29 | + } |
| 30 | + |
| 31 | + return coverageData.total.statements.pct |
| 32 | +} |
| 33 | + |
| 34 | +/** |
| 35 | + * Extracts the coverage percentage from the coverage badge SVG file |
| 36 | + * @returns The coverage percentage as a number |
| 37 | + * @throws Error if badge file cannot be found or parsed |
| 38 | + */ |
| 39 | +export function getCoverageFromBadge(): number { |
| 40 | + const scriptDir = dirname(fileURLToPath(import.meta.url)) |
| 41 | + const badgePath = resolve(scriptDir, '../badges/coverage.svg') |
| 42 | + |
| 43 | + if (!existsSync(badgePath)) { |
| 44 | + throw new Error( |
| 45 | + 'Coverage badge not found. Run "npm run coverage" to generate the badge.' |
| 46 | + ) |
| 47 | + } |
| 48 | + |
| 49 | + const badgeContent = readFileSync(badgePath, 'utf-8') |
| 50 | + |
| 51 | + // Extract the coverage percentage from the badge SVG |
| 52 | + // The format is: <text ...>XX.XX%</text> |
| 53 | + const match = badgeContent.match(/(\d+\.?\d*)%<\/text>/) |
| 54 | + |
| 55 | + if (!match) { |
| 56 | + throw new Error('Could not extract coverage percentage from badge') |
| 57 | + } |
| 58 | + |
| 59 | + return parseFloat(match[1]) |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Validates that the coverage badge matches the actual coverage |
| 64 | + * @throws Error if badge is out of date |
| 65 | + */ |
| 66 | +export function validateCoverageBadge(): void { |
| 67 | + const actualCoverage = getCoverageFromSummary() |
| 68 | + const badgeCoverage = getCoverageFromBadge() |
| 69 | + |
| 70 | + console.log(`Actual coverage: ${actualCoverage}%`) |
| 71 | + console.log(`Badge coverage: ${badgeCoverage}%`) |
| 72 | + |
| 73 | + if (actualCoverage !== badgeCoverage) { |
| 74 | + throw new Error( |
| 75 | + `Coverage badge is out of date!\n` + |
| 76 | + ` Actual coverage: ${actualCoverage}%\n` + |
| 77 | + ` Badge coverage: ${badgeCoverage}%\n` + |
| 78 | + ` Run "npm run coverage" to update the badge.` |
| 79 | + ) |
| 80 | + } |
| 81 | + |
| 82 | + console.log('✓ Coverage badge is up to date') |
| 83 | +} |
| 84 | + |
| 85 | +// If run directly as a script, validate the badge |
| 86 | +const isMainModule = |
| 87 | + fileURLToPath(import.meta.url) === process.argv[1] || |
| 88 | + fileURLToPath(import.meta.url) === resolve(process.argv[1]) |
| 89 | + |
| 90 | +if (isMainModule) { |
| 91 | + try { |
| 92 | + validateCoverageBadge() |
| 93 | + } catch (error) { |
| 94 | + console.error(error instanceof Error ? error.message : error) |
| 95 | + process.exit(1) |
| 96 | + } |
| 97 | +} |
0 commit comments