// Converts coverage/lcov.info (Bun) to HTML and Cobertura. Run: bun scripts/coverage-reports.mjs // shortcut: line coverage only, Bun's lcov has no usable branch/function data; revisit if Bun adds it. // Totals here sum lines (8351/8578); Bun's text table averages per-file percentages, so it reads lower. import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import libCoverage from 'istanbul-lib-coverage'; import libReport from 'istanbul-lib-report'; import reports from 'istanbul-reports'; const map = libCoverage.createCoverageMap({}); const loc = (line) => ({ start: { line, column: 0 }, end: { line, column: 1 } }); for (const record of readFileSync('coverage/lcov.info', 'utf8').split('end_of_record')) { const file = /^SF:(.+)$/m.exec(record)?.[1]; if (!file) continue; const statementMap = {}; const s = {}; for (const [, line, hits] of record.matchAll(/^DA:(\d+),(\d+)/gm)) { statementMap[line] = loc(+line); s[line] = +hits; } map.addFileCoverage({ path: resolve(file), statementMap, s, fnMap: {}, f: {}, branchMap: {}, b: {}, }); } const context = libReport.createContext({ dir: 'coverage', coverageMap: map }); for (const name of ['html', 'cobertura']) reports.create(name).execute(context); // Istanbul reports 0 branches/functions as 100%; relabel them instead of claiming coverage. const rewrite = (file, edit) => writeFileSync(file, edit(readFileSync(file, 'utf8'))); for (const page of readdirSync('coverage', { recursive: true }).filter((f) => f.endsWith('.html'))) rewrite(join('coverage', page), (html) => html .replace( /100% <\/span>(\s*(?:Branches|Functions)<\/span>\s*)0\/0n/a $1not collected<', ) .replace( /100%<\/td>(\s*)0\/0<\/td>/g, 'n/a$1not collected', ), ); rewrite('coverage/cobertura-coverage.xml', (xml) => xml .replaceAll('branch-rate="1"', 'branch-rate="0"') .replace( '\n