mirror of
https://github.com/cfal/shoes.git
synced 2026-11-06 16:14:11 +00:00
Add advisory selector/reload performance reports, optional Linux unit/process coverage with changed-line summaries, and bounded rule-parser property tests to make regressions and verification gaps easier to spot. Reports reject invalid measurements and account for timing noise without introducing hard performance gates; existing tests and production behavior remain unchanged.
111 lines
3.9 KiB
JavaScript
111 lines
3.9 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
export function changedLines(diff) {
|
|
const files = new Map();
|
|
let file;
|
|
let inHunk = false;
|
|
for (const line of diff.split('\n')) {
|
|
if (line.startsWith('diff --git ')) {
|
|
file = undefined;
|
|
inHunk = false;
|
|
}
|
|
if (line.startsWith('+++ ') && !inHunk) {
|
|
if (line === '+++ /dev/null') {
|
|
file = null;
|
|
continue;
|
|
}
|
|
if (!line.startsWith('+++ b/')) throw Error('unsupported quoted diff path');
|
|
file = line.slice(6);
|
|
}
|
|
const match = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
if (!match) continue;
|
|
inHunk = true;
|
|
if (file === undefined) throw Error('hunk without file');
|
|
const start = Number(match[1]);
|
|
const count = Number(match[2] ?? 1);
|
|
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(count) || count > 1000000) {
|
|
throw Error('invalid hunk');
|
|
}
|
|
if (file === null) {
|
|
if (count !== 0) throw Error('deleted file has added lines');
|
|
continue;
|
|
}
|
|
if (!files.has(file)) files.set(file, new Set());
|
|
const changed = files.get(file);
|
|
for (let number = start; number < start + count; number++) changed.add(number);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export function coverageLines(lcov, root = process.cwd()) {
|
|
const files = new Map();
|
|
let lines;
|
|
for (const line of lcov.split('\n')) {
|
|
if (line.startsWith('SF:')) {
|
|
if (lines) throw Error('unterminated LCOV record');
|
|
const file = path.relative(root, path.resolve(root, line.slice(3)));
|
|
if (!files.has(file)) files.set(file, new Map());
|
|
lines = files.get(file);
|
|
} else if (line.startsWith('DA:')) {
|
|
const match = /^DA:([1-9]\d*),(\d+)(?:,[^,]+)?$/.exec(line);
|
|
if (!lines || !match) throw Error('invalid LCOV line count');
|
|
const number = Number(match[1]);
|
|
const count = Number(match[2]);
|
|
if (!Number.isSafeInteger(number) || !Number.isSafeInteger(count)) {
|
|
throw Error('invalid LCOV line count');
|
|
}
|
|
const previous = lines.get(number) || 0;
|
|
lines.set(number, Math.max(previous, count));
|
|
} else if (line === 'end_of_record') {
|
|
if (!lines) throw Error('LCOV terminator without file');
|
|
lines = undefined;
|
|
}
|
|
}
|
|
if (lines || ![...files.values()].some(counts => counts.size > 0)) {
|
|
throw Error('empty or incomplete coverage');
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export function changedCoverage(diff, lcov, root) {
|
|
const coverage = coverageLines(lcov, root);
|
|
const files = [...changedLines(diff)].map(([file, changed]) => {
|
|
const counts = coverage.get(file);
|
|
const covered = [];
|
|
const uncovered = [];
|
|
const notInReport = [];
|
|
for (const number of changed) {
|
|
if (!counts?.has(number)) notInReport.push(number);
|
|
else if (counts.get(number) > 0) covered.push(number);
|
|
else uncovered.push(number);
|
|
}
|
|
return {
|
|
file,
|
|
changed_lines: changed.size,
|
|
instrumented: Boolean(counts),
|
|
covered,
|
|
uncovered,
|
|
not_in_report: notInReport,
|
|
};
|
|
});
|
|
const covered = files.reduce((total, file) => total + file.covered.length, 0);
|
|
const executable = covered + files.reduce((total, file) => total + file.uncovered.length, 0);
|
|
return {
|
|
scope: 'Linux unit/process only; no peers, privileged, FFI or device coverage',
|
|
changed_executable_lines: executable,
|
|
covered,
|
|
percent: executable ? 100 * covered / executable : null,
|
|
note: 'not_in_report means non-executable OR uninstrumented, never implicitly covered. Untracked files are not in git diff.',
|
|
files,
|
|
};
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
const [diff, lcov] = process.argv.slice(2);
|
|
if (!lcov) throw Error('usage: node scripts/coverage-lines.mjs CHANGES.diff COVERAGE.lcov');
|
|
const report = changedCoverage(fs.readFileSync(diff, 'utf8'), fs.readFileSync(lcov, 'utf8'));
|
|
console.log(JSON.stringify(report, null, 2));
|
|
}
|