ci(coverage): fast check that every frontend file has a coverage baseline (#271)
The frontend coverage job only reports a missing baseline entry after its full browser run. coverage:baseline-check compares the files the gate measures (same discoverSources) with frontend-coverage-baseline.json in milliseconds and also flags entries for files that no longer exist. npm test runs the same check.
This commit is contained in:
@@ -109,6 +109,13 @@ the baseline updater. Raise floors with `npm run --prefix tools/vendor
|
||||
coverage:update`; it reruns the suites and cannot lower an existing floor.
|
||||
Review baseline changes like code changes.
|
||||
|
||||
`npm run --prefix tools/vendor coverage:baseline-check` runs in well under a
|
||||
second without a browser and fails, naming the files, when a measured
|
||||
`web/js` file has no baseline entry or the baseline lists a file that no longer
|
||||
exists. It uses the gate's own `discoverSources`, and `npm test` runs the same
|
||||
check, so run it before pushing a new or deleted frontend file instead of
|
||||
waiting for the `frontend coverage` job.
|
||||
|
||||
The `viewerjs` check runs on every PR, including backend-only changes. It is an
|
||||
aggregate job that passes only when the `frontend coverage (chromium and touch)`
|
||||
job and both `page workflows (firefox)` and `page workflows (webkit)` jobs
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { baselineGaps, measuredSources, readBaseline } from '../../tools/vendor/coverage-baseline-check.mjs';
|
||||
|
||||
const floor = { lines: 80, branches: 80, functions: 80, statements: 80 };
|
||||
|
||||
test('baseline gaps name measured files without a baseline entry', () => {
|
||||
const gaps = baselineGaps(['a.js', 'nested/new.mjs'], { 'a.js': floor });
|
||||
assert.deepEqual(gaps.missing, ['nested/new.mjs']);
|
||||
assert.deepEqual(gaps.stale, []);
|
||||
});
|
||||
|
||||
test('baseline gaps name baseline entries whose file no longer exists', () => {
|
||||
const gaps = baselineGaps(['a.js'], { 'a.js': floor, 'gone.js': floor });
|
||||
assert.deepEqual(gaps.missing, []);
|
||||
assert.deepEqual(gaps.stale, ['gone.js']);
|
||||
});
|
||||
|
||||
test('baseline gaps are empty when files and baseline agree', () => {
|
||||
assert.deepEqual(baselineGaps(['a.js'], { 'a.js': floor }), { missing: [], stale: [] });
|
||||
});
|
||||
|
||||
test('the committed baseline covers exactly the measured frontend sources', () => {
|
||||
const gaps = baselineGaps(measuredSources(), readBaseline());
|
||||
assert.deepEqual(gaps.missing, [], `Missing baseline (run coverage:update): ${gaps.missing.join(', ')}`);
|
||||
assert.deepEqual(gaps.stale, [], `Stale baseline entries (file removed): ${gaps.stale.join(', ')}`);
|
||||
});
|
||||
|
||||
test('the real gate and this check select files through the same function', () => {
|
||||
const gate = readFileSync(new URL('../../tools/vendor/frontend-coverage.mjs', import.meta.url), 'utf8');
|
||||
assert.match(gate, /discoverSources\(sourceRoot\)/);
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Fast, browser-free check that frontend-coverage-baseline.json lists exactly the
|
||||
// files the coverage gate measures. CI's `frontend coverage` job reports a missing
|
||||
// entry only after ~20 minutes; this reports it in milliseconds.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { repositoryRoot } from './vendor-viewerjs.mjs';
|
||||
import { discoverSources } from './frontend-coverage.mjs';
|
||||
|
||||
const baselinePath = join(dirname(fileURLToPath(import.meta.url)), 'frontend-coverage-baseline.json');
|
||||
|
||||
export function measuredSources() {
|
||||
return discoverSources(join(repositoryRoot, 'web/js'));
|
||||
}
|
||||
|
||||
export function readBaseline() {
|
||||
return JSON.parse(readFileSync(baselinePath, 'utf8'));
|
||||
}
|
||||
|
||||
export function baselineGaps(files, baseline) {
|
||||
const measured = new Set(files);
|
||||
return {
|
||||
missing: files.filter(file => !baseline[file]),
|
||||
stale: Object.keys(baseline).filter(file => !measured.has(file)).sort(),
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { missing, stale } = baselineGaps(measuredSources(), readBaseline());
|
||||
if (!missing.length && !stale.length) {
|
||||
console.log('Coverage baseline matches the measured frontend files.');
|
||||
return;
|
||||
}
|
||||
if (missing.length) console.error(`Missing baseline for:\n ${missing.join('\n ')}\nRun: npm run --prefix tools/vendor coverage:update`);
|
||||
if (stale.length) console.error(`Baseline lists files that no longer exist:\n ${stale.join('\n ')}\nRemove them from tools/vendor/frontend-coverage-baseline.json`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();
|
||||
Vendored
+1
@@ -15,6 +15,7 @@
|
||||
"test:comfyui": "playwright test --config comfyui.config.mjs",
|
||||
"test:executions": "playwright test --config playwright.execution.config.mjs",
|
||||
"test:coverage": "node frontend-coverage.mjs",
|
||||
"coverage:baseline-check": "node coverage-baseline-check.mjs",
|
||||
"coverage:update": "node frontend-coverage.mjs --update-baseline",
|
||||
"test:compat": "playwright test --config pack-compat.config.mjs"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user