// The last coverage run of each suite, for Grafana (#394): lines the tests reach and lines there // are, by suite and part, as a file node-exporter's textfile collector serves to Prometheus. // // node scripts/coverage-metrics.ts (scripts/coverage.sh runs it after each report) // // Suites: `cargo` (the cargo tests, target/coverage/cargo), `browser` (the browser suite against an // instrumented ipx and ipx-directory, target/coverage/api) and `page` (web/src under the browser // suite, target/coverage/page). Parts: the crate, or `page`. Each run writes every suite that has // a report, so one suite's run leaves the others' numbers as they were. import fs from 'node:fs'; import path from 'node:path'; const COVERAGE = path.join(import.meta.dirname, '..', 'target', 'coverage'); // node-exporter reads it as /host/... (it mounts / at /host), on /mnt/fast beside its appdata. const OUT = process.env.IPX_COVERAGE_TEXTFILE ?? '/mnt/fast/appdata/node-exporter/textfile'; type Count = { covered: number, total: number }; type Suite = { parts: Map, at: number }; /// The crate a path in cargo-llvm-cov's summary belongs to: directory/src/... is ipx-directory's. const crateOf = (file: string) => file.startsWith('directory/') ? 'ipx-directory' : file.startsWith('feed/') ? 'ipx-feed' : 'ipx'; function add(parts: Map, part: string, covered: number, total: number) { const c = parts.get(part) ?? { covered: 0, total: 0 }; c.covered += covered; c.total += total; parts.set(part, c); } /// cargo-llvm-cov's summary: a line a file, Lines and Missed Lines its 8th and 9th columns. function rust(dir: string): Suite | null { const file = path.join(COVERAGE, dir, 'summary.txt'); if (!fs.existsSync(file)) return null; const parts = new Map(); for (const line of fs.readFileSync(file, 'utf8').split('\n')) { const cols = line.trim().split(/\s+/); if (cols.length < 10 || !cols[0]!.endsWith('.rs')) continue; const lines = Number(cols[7]), missed = Number(cols[8]); if (Number.isFinite(lines) && Number.isFinite(missed)) add(parts, crateOf(cols[0]!), lines - missed, lines); } return { parts, at: fs.statSync(file).mtimeMs / 1000 }; } function page(): Suite | null { const file = path.join(COVERAGE, 'page', 'summary.json'); if (!fs.existsSync(file)) return null; const { lines } = JSON.parse(fs.readFileSync(file, 'utf8')) as { lines: ({ file: string } & Count)[] }; const parts = new Map(); for (const l of lines) add(parts, 'page', l.covered, l.total); return { parts, at: fs.statSync(file).mtimeMs / 1000 }; } const suites: [string, Suite | null][] = [['cargo', rust('cargo')], ['browser', rust('api')], ['page', page()]]; const out = [ '# HELP ipx_coverage_lines Lines of iPX the tests reached and lines there are, as each suite\'s last coverage run found them (#394).', '# TYPE ipx_coverage_lines gauge', ]; for (const [suite, s] of suites) if (s) for (const [part, c] of s.parts) { out.push(`ipx_coverage_lines{suite="${suite}",part="${part}",state="covered"} ${c.covered}`); out.push(`ipx_coverage_lines{suite="${suite}",part="${part}",state="total"} ${c.total}`); } out.push('# HELP ipx_coverage_run_timestamp_seconds When each suite\'s last coverage report was made.'); out.push('# TYPE ipx_coverage_run_timestamp_seconds gauge'); for (const [suite, s] of suites) if (s) out.push(`ipx_coverage_run_timestamp_seconds{suite="${suite}"} ${Math.round(s.at)}`); fs.mkdirSync(OUT, { recursive: true }); // Written aside and renamed in, so a scrape never reads half a file. const file = path.join(OUT, 'ipx_coverage.prom'); fs.writeFileSync(file + '.tmp', out.join('\n') + '\n'); fs.renameSync(file + '.tmp', file); console.log(`coverage for Grafana: ${file}`); for (const [suite, s] of suites) if (s) for (const [part, c] of s.parts) console.log(` ${suite} ${part}: ${(100 * c.covered / c.total).toFixed(1)}% of ${c.total} lines`);