From 1976fb20ff1304fb63cf3ad6759bc2076aee737a Mon Sep 17 00:00:00 2001 From: quality Date: Tue, 6 Oct 2026 22:39:39 -0400 Subject: [PATCH] test: pin the e2e region fold's same-key max arm getRegionCoverage keys a region on its original coordinates and keeps the higher of two counts recorded against one key. normalizeRanges splits a line carrying an inner zero-count range into an uncovered half and a covered remainder, and both halves map back to the same original span, so that fold is what stops every measured file carrying a phantom zero region -- yet line 360 had no unit coverage. Adds a fixture whose inner zero-count range stops short of the end of its line, asserting the file reports two regions both covered rather than three with the line named, and that the same source measured by the real build and the variant build folds to the same two regions instead of four. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality --- tests/e2e-coverage-region-union.test.mjs | 179 +++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 tests/e2e-coverage-region-union.test.mjs diff --git a/tests/e2e-coverage-region-union.test.mjs b/tests/e2e-coverage-region-union.test.mjs new file mode 100644 index 00000000..2a53b3f7 --- /dev/null +++ b/tests/e2e-coverage-region-union.test.mjs @@ -0,0 +1,179 @@ +// Unit coverage for the fold that makes the e2e region count mean anything: +// `getRegionCoverage` (tests/tools/e2e-coverage-report.mjs:344-365) keys a +// region on its original `line:startColumn:endLine:endColumn` and keeps the +// higher of two counts recorded against the same key. +// +// const count = counts?.[index] ?? 0; +// const existing = regions.get(key); +// if (!existing || count > existing.count) { +// regions.set(key, { line, endLine, count }); +// } +// +// Two branch locations sharing one key is not a corner case, it is what the +// pipeline produces for every file it measures. `normalizeRanges` rewrites a +// V8 record into a flat, non-overlapping run of ranges, so an inner zero-count +// range in the middle of a line splits that line into an uncovered range and a +// covered remainder. Both halves map back through the source map to the same +// original span -- the generated columns they start at carry no mapping of +// their own, so each resolves to the mapping that precedes it -- and +// v8-to-istanbul emits one branch per half. Keyed by branch id they would be +// two regions, one of them permanently zero; keyed by coordinates they are one +// region that the covered half carries. +// +// `collectE2ECoverage unions the two builds of one source file` in +// tests/e2e-coverage-report.test.mjs pins the union across *scripts*, but +// every region in that fixture lands on a key only one build emits, so the +// `count > existing.count` arm inside `getRegionCoverage` is never taken -- +// `npm run test:unit:coverage` names line 360 among the uncovered regions of +// tests/tools/e2e-coverage-report.mjs on `main`, with the file at 92.89%. +// +// This file is separate from tests/e2e-coverage-report.test.mjs so the two +// bodies of work stay independent, the same way tests/e2e-coverage-run.test.mjs +// and tests/e2e-coverage-scripts.test.mjs are separate from it. +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { collectE2ECoverage } from './tools/e2e-coverage-report.mjs'; +import { + initCoverageRun, + sealCoverageRun, + writeCoverageArtifact, +} from './tools/e2e-coverage-run.mjs'; + +// One identity mapping per generated line, both at column 0: 'AAAA' is +// [generatedColumn 0, source 0, originalLine 0, originalColumn 0] and 'AACA' +// advances the original line by one with every other field unchanged. Spelled +// out rather than encoded by a helper so the fixture states exactly which +// coordinates the halves of line 2 collapse onto: there is no mapping at any +// column inside a line, which is why both halves resolve to the same span. +const IDENTITY_MAPPINGS = 'AAAA;AACA'; + +const SOURCE = 'const always = 1;\nconst split = 2;\n'; +const SCRIPT_TEXT = `${SOURCE}\n//# sourceMappingURL=app.js.map\n`; +const SOURCE_PATH = 'src/components/Example/index.js'; + +// An inner zero-count range that stops short of the end of line 2, so the +// remainder of that line is a separate covered range rather than running to +// the end of the script. Those two ranges are the two same-key branches. +const SPLIT_START = SOURCE.indexOf('const split'); +const SPLIT_END = SPLIT_START + 'const split'.length; + +const BUILDS = [ + { dir: 'assets/js', path: '/assets/js' }, + { + dir: 'e2e-coverage-variant/assets/js', + path: '/e2e-coverage-variant/assets/js', + }, +]; + +async function fixtureRun(builds) { + const root = await mkdtemp(join(tmpdir(), 'endusers-region-union-')); + const runDir = join(root, 'coverage'); + const buildDir = join(root, 'build'); + await mkdir(join(root, 'src/components/Example'), { recursive: true }); + await writeFile(join(root, SOURCE_PATH), SOURCE); + for (const build of builds) { + await mkdir(join(buildDir, build.dir), { recursive: true }); + await writeFile(join(buildDir, build.dir, 'app.js'), SCRIPT_TEXT); + await writeFile( + join(buildDir, build.dir, 'app.js.map'), + JSON.stringify({ + version: 3, + file: 'app.js', + sources: [`webpack://endusers/./${SOURCE_PATH}`], + sourcesContent: [SOURCE], + names: [], + mappings: IDENTITY_MAPPINGS, + }), + ); + } + await initCoverageRun(runDir, 'run-1', { project: 'chromium' }); + return { root, runDir, buildDir }; +} + +async function writeBuildCoverage(runDir, artifactName, build) { + await writeCoverageArtifact(runDir, artifactName, { + schemaVersion: 1, + kind: 'endusers.playwright.v8-coverage', + runId: 'run-1', + result: [ + { + url: `http://localhost:3000${build.path}/app.js`, + scriptId: '1', + functions: [ + { + functionName: '', + isBlockCoverage: true, + ranges: [ + { startOffset: 0, endOffset: SCRIPT_TEXT.length, count: 1 }, + { startOffset: SPLIT_START, endOffset: SPLIT_END, count: 0 }, + ], + }, + ], + }, + ], + }); +} + +// Line 1's range and line 2's two halves, with the halves folded onto one +// key. Keyed by branch id instead, line 2 would contribute two regions and +// the report would read 3 regions / 2 covered / 66.67% and name line 2. +const EXPECTED = { + file: SOURCE_PATH, + executableLines: 2, + coveredLines: 2, + linePercent: 100, + uncoveredLines: [], + regions: 2, + coveredRegions: 2, + regionPercent: 100, + uncoveredRegions: [], +}; + +async function reportFor(fixture) { + const report = await collectE2ECoverage(fixture.runDir, { + root: fixture.root, + buildDir: fixture.buildDir, + }); + assert.equal(report.status, 'ok'); + return report; +} + +test('two branch locations on one original span fold to a single covered region', async () => { + const fixture = await fixtureRun([BUILDS[0]]); + try { + await writeBuildCoverage(fixture.runDir, 'worker-0-page-0', BUILDS[0]); + await sealCoverageRun(fixture.runDir, 'passed'); + + const report = await reportFor(fixture); + assert.deepEqual(report.sources, [EXPECTED]); + // Stated against the summary as well, because the merge gate reads that + // and not the per-file rows. + assert.equal(report.summary.regions, 2); + assert.equal(report.summary.coveredRegions, 2); + } finally { + await rm(fixture.root, { recursive: true, force: true }); + } +}); + +test('the same file measured by both builds folds rather than doubling', async () => { + const fixture = await fixtureRun(BUILDS); + try { + for (const [index, build] of BUILDS.entries()) { + await writeBuildCoverage(fixture.runDir, `worker-0-page-${index}`, build); + } + await sealCoverageRun(fixture.runDir, 'passed'); + + const report = await reportFor(fixture); + // The real build and the data-variant build compile the same source, so + // the second script contributes the same two keys the first did. Counted + // per script rather than per coordinate, this would read 4 regions. + assert.deepEqual(report.sources, [EXPECTED]); + assert.equal(report.scripts.converted, 2); + } finally { + await rm(fixture.root, { recursive: true, force: true }); + } +});