|
| 1 | +// turns coverage/coverage-summary.json (npm run coverage) into coverage/report.md for the CI's job summary and |
| 2 | +// comments: the totals and the files with the most untested lines, and with the summary of a base commit |
| 3 | +// (node scripts/coverage-report.js <base summary> <base checkout>) how they changed and which files changed most |
| 4 | +const fs = require('fs'); |
| 5 | +const path = require('path'); |
| 6 | + |
| 7 | +const root = path.join(__dirname, '..'); |
| 8 | +// per file (relative to its checkout) the line, branch and function coverage, and the untested lines |
| 9 | +const read = (file, checkout) => { |
| 10 | + const summary = JSON.parse(fs.readFileSync(file, 'utf8')); |
| 11 | + const files = new Map(); |
| 12 | + for (const [name, s] of Object.entries(summary)) |
| 13 | + if (name !== 'total') files.set(path.relative(checkout, name), {...s, missing: s.lines.total - s.lines.covered}); |
| 14 | + return {total: summary.total, files}; |
| 15 | +}; |
| 16 | + |
| 17 | +const head = read(path.join(root, 'coverage/coverage-summary.json'), root); |
| 18 | +const base = process.argv[2] ? read(process.argv[2], process.argv[3]) : null; |
| 19 | + |
| 20 | +const percent = ({pct}) => `${pct.toFixed(1)}%`; |
| 21 | +const change = (now, before) => { |
| 22 | + if (!before) return ''; |
| 23 | + const d = now.pct - before.pct; |
| 24 | + return Math.abs(d) < 0.05 ? '' : ` (${d > 0 ? '+' : ''}${d.toFixed(1)})`; |
| 25 | +}; |
| 26 | +const row = (name, s, b) => `| ${name} | ${percent(s.lines)}${change(s.lines, b?.lines)} | ${percent(s.branches)}${change(s.branches, b?.branches)} | ${percent(s.functions)}${change(s.functions, b?.functions)} |`; |
| 27 | +const header = ['| | Lines | Branches | Functions |', '|---|--:|--:|--:|']; |
| 28 | + |
| 29 | +const report = ['<!-- coverage -->', '### Test coverage', '']; |
| 30 | +if (base) report.push(`Compared with ${process.env.BASE_NAME || 'the base commit'}.`, ''); |
| 31 | +report.push(...header, row('**total**', head.total, base?.total)); |
| 32 | +const untested = [...head.files].sort((a, b) => b[1].missing - a[1].missing).slice(0, 10); |
| 33 | +for (const [file, s] of untested) report.push(row(`\`${file}\` (${s.missing} lines untested)`, s, base?.files.get(file))); |
| 34 | + |
| 35 | +if (base) { |
| 36 | + // the files whose line coverage changed, new and removed ones |
| 37 | + const changed = []; |
| 38 | + for (const [file, s] of head.files) { |
| 39 | + const b = base.files.get(file); |
| 40 | + if (!b) changed.push([file, Infinity, `| \`${file}\` (new) | ${percent(s.lines)} | ${percent(s.branches)} | ${percent(s.functions)} |`]); |
| 41 | + else if (Math.abs(s.lines.pct - b.lines.pct) >= 0.05) changed.push([file, Math.abs(s.lines.pct - b.lines.pct), row(`\`${file}\``, s, b)]); |
| 42 | + } |
| 43 | + for (const file of base.files.keys()) if (!head.files.has(file)) changed.push([file, Infinity, `| \`${file}\` (removed) | | | |`]); |
| 44 | + changed.sort((a, b) => b[1] - a[1]); |
| 45 | + report.push('', changed.length ? '#### Changed' : 'No file\'s line coverage changed.'); |
| 46 | + if (changed.length) report.push('', ...header, ...changed.slice(0, 20).map(c => c[2])); |
| 47 | + if (changed.length > 20) report.push('', `and ${changed.length - 20} more`); |
| 48 | +} |
| 49 | +report.push(''); |
| 50 | + |
| 51 | +fs.writeFileSync(path.join(root, 'coverage/report.md'), report.join('\n')); |
| 52 | +console.log(report.join('\n')); |
0 commit comments