Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,18 +425,26 @@ that omits them passes where CI fails; `tests/e2e-coverage-gate.test.mjs` holds
floors under them. Invalid maps, stale manifests, and empty aggregate
attribution fail visibly and leave their raw artifacts for review.

Read the percentages against the Node major CI pins. These are _region_ counts,
and V8 splits the same expression into a different number of ranges on a
different runtime, so the denominator moves even though the code, the suite and
the fixture builds do not. The same commit measures 97.29% source regions on the
major the job pins and 97.73% on a newer runtime, and per file the gap is wider
still: `DirectoryFreshness.js` is 11/15 (73.33%) on the pinned major (node
22.23.3) and 9/10 (90.00%) on v26.10.0. A local render on a newer runtime
therefore reports headroom the gate does not have, and ratcheting a floor to
match it turns the required job red. `tests/e2e-coverage-gate.test.mjs` records
the major its floors were measured on and fails if the job moves off it. The
counts also move between patch releases of that major, so a small same-major
difference is runtime drift, not a code change.
Compare percentages from equivalent build, browser and reporting toolchains,
including the Node major CI pins. These are _region_ counts: the denominator can
move between toolchains even when the code and suite do not. The same commit
measures 97.29% source regions on the major the job pins and 97.73% on a newer
runtime, and per file the gap is wider still: `DirectoryFreshness.js` is 11/15
(73.33%) on the pinned major (node 22.23.3) and 9/10 (90.00%) on v26.10.0. A
local render on a newer runtime therefore reports headroom the gate does not
have, and ratcheting a floor to match it turns the required job red.
`tests/e2e-coverage-gate.test.mjs` records the major its floors were measured on
and fails if the job moves off it. The counts also move between patch releases
of that major, so a small same-major difference is not by itself evidence of a
code change.

Reports distinguish `buildRuntimes`, `captureRuntimes` and `reporterRuntime`.
Coverage builds record their Node/V8 identity; captures record Node/V8,
Playwright and the browser's product/V8 versions. Chromium V8 produces the raw
browser ranges, not the reporting process's V8. Sealing preserves producer
identities for later re-renders, while `reporterRuntime` identifies the current
renderer. Legacy artifacts explicitly report unknown producer metadata rather
than attributing it to the renderer.

Some component branches render only for data shapes the checked-in `data/*.json`
never take — an archived End User Group, a person with neither role nor company,
Expand Down
27 changes: 27 additions & 0 deletions adr/0007-scope-end-to-end-coverage-to-user-reachable-states.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,33 @@ restrictions on test-tree guards. It does not authorize data-shape browser
fixture builds, change coverage floors, or replace surviving-mutant evidence for
assertion gaps within mutation-covered production logic.

## Clarification: measurement attribution and provenance (2026-10-21)

The calibration in [#1752](https://github.com/cncf/endusers/issues/1752) and
provenance repair in [#1748](https://github.com/cncf/endusers/pull/1748)
motivate these evidence requirements.

A mutation result of `NoCoverage` means the configured run did not attribute
execution to that mutant; it does not by itself prove an assertion gap.
Calibrate the runner and any subprocess boundary, including active-mutant
selection and child-to-parent coverage reporting, before treating those results
as missing assertions or using them to justify mutation-scope or threshold
changes. Assertion gaps within mutation-covered production logic still require
the surviving-mutant evidence in decision 3.

Browser-coverage artifacts preserve producer identities separately from the
current reporter runtime: build-process Node/V8 identity, and capture-process
Node/V8, Playwright and browser product/V8 identities. Record producer metadata
when building or capturing and preserve it when sealing the run. Later renders,
including failure reports, retain those identities and identify the current
reporter separately. Missing legacy producer metadata remains explicitly
unknown, not inferred from the renderer. The reporter's runtime alone is not
evidence of which browser produced the raw ranges or why region counts differ.

This clarification selects no new mutation-runner architecture, expands no
mutation scope, changes no thresholds, and does not relax the existing
browser-fixture or test-tree restrictions.

## Consequences

- Hive `quality` findings that do not meet decision 2 are closed as not planned
Expand Down
8 changes: 8 additions & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ const IMAGE_HOST_SOURCES = [
function endusersE2ESourceMaps() {
return {
name: 'endusers-e2e-source-maps',
async postBuild({ outDir }) {
const { writeFile } = require('node:fs/promises');
const { join } = require('node:path');
await writeFile(
join(outDir, 'e2e-coverage-runtime.json'),
JSON.stringify({ node: process.version, v8: process.versions.v8 }),
);
},
configureWebpack(config, isServer) {
if (isServer) return {};
return { devtool: 'source-map' };
Expand Down
51 changes: 41 additions & 10 deletions tests/e2e-coverage-fixture.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,21 @@ function info(testId = 'fixture-test') {
};
}

function fakePage({ result = [], startError, stopError } = {}) {
function fakePage({ result = [], startError, stopError, versionError } = {}) {
let started = false;
return {
context: () => ({
async newCDPSession() {
return {
async send(method) {
assert.equal(method, 'Browser.getVersion');
if (versionError) throw versionError;
return { product: 'Chrome/123.0', jsVersion: '12.3' };
},
async detach() {},
};
},
}),
coverage: {
async startJSCoverage() {
if (startError) throw startError;
Expand Down Expand Up @@ -111,6 +123,13 @@ test('CoverageManager writes a raw artifact with source identity metadata', asyn
const payload = JSON.parse(await readFile(join(runDir, artifact), 'utf8'));
assert.equal(payload.result[0].sourceLength, 3);
assert.match(payload.result[0].sourceSha256, /^[a-f0-9]{64}$/);
assert.equal(payload.runtime.browser, 'Chrome/123.0');
assert.equal(payload.runtime.browserV8, '12.3');
assert.equal(payload.runtime.node, process.version);
assert.equal(
payload.runtime.playwright,
require('@playwright/test/package.json').version,
);
} finally {
await rm(runDir, { recursive: true, force: true });
}
Expand Down Expand Up @@ -184,15 +203,7 @@ test('CoverageManager writes honest error artifacts for start and stop failures'
try {
await initCoverageRun(runDir, 'errors-1');
const manager = new CoverageManager(info(), runDir, 'errors-1');
await assert.rejects(() =>
manager.startPage({
coverage: {
async startJSCoverage() {
throw {};
},
},
}),
);
await assert.rejects(() => manager.startPage(fakePage({ startError: {} })));
await assert.rejects(
() => manager.startPage({ coverage: {} }),
(error) => {
Expand All @@ -215,6 +226,26 @@ test('CoverageManager writes honest error artifacts for start and stop failures'
}
});

test('CoverageManager records browser-version failures as capture errors', async (t) => {
const runDir = await mkdtemp(join(tmpdir(), 'endusers-fixture-version-'));
t.after(() => rm(runDir, { recursive: true, force: true }));
await initCoverageRun(runDir, 'version-error');
const manager = new CoverageManager(info(), runDir, 'version-error');
await assert.rejects(
() =>
manager.startPage(
fakePage({ versionError: new Error('version failed') }),
),
/version failed/,
);
const artifact = (await readdir(runDir)).find((name) =>
name.endsWith('-error.json'),
);
const payload = JSON.parse(await readFile(join(runDir, artifact), 'utf8'));
assert.equal(payload.runtime, null);
assert.equal(payload.error.message, 'version failed');
});

test('CoverageManager aggregates manual-context cleanup errors', async () => {
const runDir = await mkdtemp(
join(tmpdir(), 'endusers-fixture-context-errors-'),
Expand Down
38 changes: 30 additions & 8 deletions tests/e2e-coverage-report.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,13 @@ async function writePageCoverage(
pageIndex,
ranges,
url = 'http://localhost:3000/assets/js/app.js',
runtime,
) {
await writeCoverageArtifact(fixture.runDir, `worker-0-page-${pageIndex}`, {
schemaVersion: 1,
kind: 'endusers.playwright.v8-coverage',
runId: 'run-1',
runtime,
result: [
{
url,
Expand Down Expand Up @@ -204,6 +206,13 @@ test('main surfaces recorded capture-error artifacts instead of treating them as

test('collectE2ECoverage maps V8 ranges through an external source map to src lines', async () => {
const fixture = await fixtureRun();
const runtime = {
node: 'v20',
v8: '11',
playwright: '1.64',
browser: 'Chrome/123',
browserV8: '12.3',
};
try {
const source = 'const hit = 1;\nconst miss = 2;\n';
const generated = source;
Expand All @@ -230,21 +239,34 @@ test('collectE2ECoverage maps V8 ranges through an external source map to src li
]),
}),
);
await writePageCoverage(fixture, 0, [
{
startOffset: generated.indexOf('const miss'),
endOffset: scriptText.length,
count: 0,
},
{ startOffset: 0, endOffset: scriptText.length, count: 1 },
]);
await writePageCoverage(
fixture,
0,
[
{
startOffset: generated.indexOf('const miss'),
endOffset: scriptText.length,
count: 0,
},
{ startOffset: 0, endOffset: scriptText.length, count: 1 },
],
undefined,
runtime,
);
await sealCoverageRun(fixture.runDir, 'passed');

const report = await collectE2ECoverage(fixture.runDir, {
root: fixture.root,
buildDir: fixture.buildDir,
});
assert.equal(report.status, 'ok');
assert.deepEqual(report.captureRuntimes, [runtime]);
assert.equal(report.reporterRuntime.node, process.version);
const rerendered = await collectE2ECoverage(fixture.runDir, {
root: fixture.root,
buildDir: fixture.buildDir,
});
assert.deepEqual(rerendered.captureRuntimes, [runtime]);
assert.deepEqual(report.sources, [
{
file: 'src/components/Example/index.js',
Expand Down
Loading
Loading