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
232 changes: 199 additions & 33 deletions tests/e2e-coverage-report.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -403,56 +403,69 @@ test('a zero-count region spanning only covered lines is not counted against the
// for (#1094, #1097). Enclosure is therefore not evidence of drift.
//
// The two artifacts below put one zero region, `1:6:1:12`, against three
// covered regions that each enclose it in a different way: `1:0:3:12` from the
// *other* artifact (whole-span containment across artifacts), `1:0:1:12` (the
// same line), and `1:6:3:12` (the same start column, differing end -- the
// shape #1066 proposed keying away). It survives all three.
// covered regions that each enclose it in a different way: `1:0:3:12` from a
// *different script* compiled from the same file (whole-span containment
// across chunks -- RadarReports' dead arms are enclosed exactly this way by
// the chunks that never record their zero), `1:0:1:12` (the same line), and
// `1:6:3:12` (the same start column, differing end -- the shape #1066
// proposed keying away). It survives all three. The enclosing script must be
// a different chunk here: an artifact of the *same* script that covers the
// arm's lines while recording no zero over them is V8's way of saying the arm
// executed, which is the contradiction fold's evidence, pinned separately
// below.
test('a single-line zero-count region survives a covered region that encloses it', async () => {
const fixture = await fixtureRun();
try {
const lines = ['const a = 1;', 'const b = 2;', 'const c = 3;'];
const source = `${lines.join('\n')}\n`;
const scriptText = `${source}\n//# sourceMappingURL=app.js.map\n`;
const original = join(fixture.root, 'src/components/Example/index.js');
await mkdir(join(fixture.root, 'src/components/Example'), {
recursive: true,
});
await writeFile(original, source);
await writeFile(join(fixture.buildDir, 'assets/js/app.js'), scriptText);
await writeFile(
join(fixture.buildDir, 'assets/js/app.js.map'),
JSON.stringify({
version: 3,
file: 'app.js',
sources: ['webpack://endusers/./src/components/Example/index.js'],
sourcesContent: [source],
names: [],
// Line 1 carries a second mapping at column 6 so a block opening and
// closing inside it resolves to columns of its own instead of
// snapping back to the start of the line. Without that the zero
// region would be a zero-width span and prove nothing about columns.
mappings: [
`${[vlq(0), vlq(0), vlq(0), vlq(0)].join('')},${[
vlq(6),
vlq(0),
vlq(0),
vlq(6),
].join('')}`,
`${[vlq(0), vlq(0), vlq(1), vlq(-6)].join('')}`,
`${[vlq(0), vlq(0), vlq(1), vlq(0)].join('')}`,
].join(';'),
}),
);
// Two chunks carry the same file, as the real bundle splits do. Line 1
// carries a second mapping at column 6 so a block opening and closing
// inside it resolves to columns of its own instead of snapping back to
// the start of the line. Without that the zero region would be a
// zero-width span and prove nothing about columns.
const mappings = [
`${[vlq(0), vlq(0), vlq(0), vlq(0)].join('')},${[
vlq(6),
vlq(0),
vlq(0),
vlq(6),
].join('')}`,
`${[vlq(0), vlq(0), vlq(1), vlq(-6)].join('')}`,
`${[vlq(0), vlq(0), vlq(1), vlq(0)].join('')}`,
].join(';');
for (const name of ['app.js', 'app2.js']) {
await writeFile(
join(fixture.buildDir, `assets/js/${name}`),
`${source}\n//# sourceMappingURL=${name}.map\n`,
);
await writeFile(
join(fixture.buildDir, `assets/js/${name}.map`),
JSON.stringify({
version: 3,
file: name,
sources: ['webpack://endusers/./src/components/Example/index.js'],
sourcesContent: [source],
names: [],
mappings,
}),
);
}
const scriptText = `${source}\n//# sourceMappingURL=app.js.map\n`;

// One page runs the whole script and nothing else, contributing a covered
// region that spans every line of the file.
// One page runs the whole of the *other* chunk and nothing else,
// contributing a covered region that spans every line of the file.
await writeCoverageArtifact(fixture.runDir, 'worker-0-page-0', {
schemaVersion: 1,
kind: 'endusers.playwright.v8-coverage',
runId: 'run-1',
result: [
{
url: 'http://localhost:3000/assets/js/app.js',
url: 'http://localhost:3000/assets/js/app2.js',
scriptId: '1',
functions: [
{
Expand Down Expand Up @@ -632,6 +645,159 @@ test('a zero-count region crossed by a covered one is treated as source-map drif
}
});

// Drift needs the twin *recorded*; one shape leaves none (#1202). V8 emits a
// deviation range only where a count differs from the enclosing one, so an
// artifact that executes a branch arm at the same count as the code around it
// carries no range over the arm at all -- `useFocusTrap`'s `previousFocus`
// arm runs exactly like that on the trigger-unmounted page, while every
// truthy-path artifact of the same chunk records `51:25-51:41` at zero. The
// zero then survives the union with no covered region sharing or crossing its
// key. The silence is still proof, but only within one script: one script has
// one source map, so had the arm been skipped in the silent artifact too, the
// identical zero mapping would have reappeared. These three tests pin the
// fold and both edges of its evidence requirement.
//
// Shared fixture shape: one source file carried by one chunk, line 1 mapped
// at columns 0, 3 and 6 so distinct spans inside it resolve to distinct
// original columns. Artifacts differ only in their V8 ranges.
async function contradictionFixture(buildArtifacts) {
const fixture = await fixtureRun();
const lines = ['const a = 1;', 'const b = 2;', 'const c = 3;'];
const source = `${lines.join('\n')}\n`;
const scriptText = `${source}\n//# sourceMappingURL=app.js.map\n`;
await mkdir(join(fixture.root, 'src/components/Example'), {
recursive: true,
});
await writeFile(
join(fixture.root, 'src/components/Example/index.js'),
source,
);
await writeFile(join(fixture.buildDir, 'assets/js/app.js'), scriptText);
await writeFile(
join(fixture.buildDir, 'assets/js/app.js.map'),
JSON.stringify({
version: 3,
file: 'app.js',
sources: ['webpack://endusers/./src/components/Example/index.js'],
sourcesContent: [source],
names: [],
mappings: [
[
[vlq(0), vlq(0), vlq(0), vlq(0)].join(''),
[vlq(3), vlq(0), vlq(0), vlq(3)].join(''),
[vlq(3), vlq(0), vlq(0), vlq(3)].join(''),
].join(','),
[vlq(0), vlq(0), vlq(1), vlq(-6)].join(''),
[vlq(0), vlq(0), vlq(1), vlq(0)].join(''),
].join(';'),
}),
);
const artifacts = buildArtifacts(scriptText.length);
for (const [index, { url, ranges }] of artifacts.entries()) {
await writeCoverageArtifact(fixture.runDir, `worker-0-page-${index}`, {
schemaVersion: 1,
kind: 'endusers.playwright.v8-coverage',
runId: 'run-1',
result: [
{
url: url ?? 'http://localhost:3000/assets/js/app.js',
scriptId: String(index + 1),
functions: [{ functionName: '', isBlockCoverage: true, ranges }],
},
],
});
}
await sealCoverageRun(fixture.runDir, 'passed');
return fixture;
}

test('a zero-count region contradicted by a silent artifact of its own script is folded', async () => {
// Page 0 skips the arm at columns 6-10 and records the zero; page 1 runs
// the whole script uniformly, so V8 emits no range over the arm at all.
// Same script, lines covered, no zero anywhere near it: the arm executed.
const fixture = await contradictionFixture((length) => [
{
ranges: [
{ startOffset: 0, endOffset: length, count: 1 },
{ startOffset: 6, endOffset: 10, count: 0 },
],
},
{ ranges: [{ startOffset: 0, endOffset: length, count: 1 }] },
]);
try {
const report = await collectE2ECoverage(fixture.runDir, {
root: fixture.root,
buildDir: fixture.buildDir,
});
assert.equal(report.status, 'ok');
const [entry] = report.sources;
assert.equal(entry.file, 'src/components/Example/index.js');
assert.deepEqual(entry.uncoveredRegions, []);
assert.equal(entry.regions, entry.coveredRegions);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});

test('a zero-count region recorded by every artifact of its script survives', async () => {
// Both pages skip the arm -- the genuinely dead shape (RadarReports'
// empty-corpus arms). No artifact of the chunk is silent about it, so
// nothing contradicts the zero.
const skipArm = (length) => [
{ startOffset: 0, endOffset: length, count: 1 },
{ startOffset: 6, endOffset: 10, count: 0 },
];
const fixture = await contradictionFixture((length) => [
{ ranges: skipArm(length) },
{ ranges: skipArm(length) },
]);
try {
const report = await collectE2ECoverage(fixture.runDir, {
root: fixture.root,
buildDir: fixture.buildDir,
});
assert.equal(report.status, 'ok');
const [entry] = report.sources;
assert.deepEqual(entry.uncoveredRegions, [1]);
assert.equal(entry.regions - entry.coveredRegions, 1);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});

test('a witness with any zero touching the region lines does not contradict it', async () => {
// Page 1 covers the arm's line but skips the *neighbouring* arm at columns
// 3-6 on the same line. A zero that near the region means the witness was
// on a path where flattening could have merged or shifted spans, so it is
// disqualified -- both zeros survive, neither contradicts the other.
const fixture = await contradictionFixture((length) => [
{
ranges: [
{ startOffset: 0, endOffset: length, count: 1 },
{ startOffset: 6, endOffset: 10, count: 0 },
],
},
{
ranges: [
{ startOffset: 0, endOffset: length, count: 1 },
{ startOffset: 3, endOffset: 6, count: 0 },
],
},
]);
try {
const report = await collectE2ECoverage(fixture.runDir, {
root: fixture.root,
buildDir: fixture.buildDir,
});
assert.equal(report.status, 'ok');
const [entry] = report.sources;
assert.deepEqual(entry.uncoveredRegions, [1]);
assert.equal(entry.regions - entry.coveredRegions, 2);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});

// `npm run build:e2e:coverage` compiles the site twice: once from the real
// data, and once with tests/e2e/fixtures/data-variants/** layered on, into
// build/e2e-coverage-variant under its own base URL. That is what makes a
Expand Down
83 changes: 82 additions & 1 deletion tests/tools/e2e-coverage-report.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,71 @@ function isDriftedRegion(region, regions) {
return false;
}

// The crossing fold above needs the drifted twin to be *recorded*. One shape
// leaves no twin at all (#1202): when an artifact executes a span at the same
// count as its enclosing code, V8 emits no deviation range over it, so the
// artifact carries no region -- covered or zero -- for the span.
// `useFocusTrap`'s cleanup runs once with the trigger unmounted and the
// `previousFocus` arm executes uniformly with the arrow around it; that
// artifact records lines 49-51 covered and no range in the cleanup at all,
// while every truthy-path artifact of the same chunk records `51:25-51:41`
// at zero. The zero survives the union because no covered region ever shares
// (or crosses) its key.
//
// Absence only proves execution inside one script: V8 emits a zero range
// wherever text inside executed code did not run, and one script has one
// source map, so the same unexecuted text always lands on the same original
// span. If some artifact of a script records a zero region and another
// artifact of the *same script* covers the region's lines while recording no
// zero on any of them, the second artifact executed that text -- had it been
// skipped there too, the identical zero mapping would reappear. Across
// scripts the inference fails: a different bundle maps the same skipped arm
// to a different original span (#1066's drift), so a chunk that never
// records the zero at these coordinates says nothing about them. The witness
// is therefore required to come from a script that produced the zero itself.
// The exclusion test is by line rather than exact span, the conservative
// direction: a zero whose flattened range grew past the arm (adjacent
// unexecuted text merges into one range) still disqualifies the witness.
function zeroTouchesLines(zero, region) {
return zero.line <= region.endLine && zero.endLine >= region.line;
}

function regionKey(region) {
return [region.line, region.column, region.endLine, region.endColumn].join(
':',
);
}

function isContradictedRegion(region, witnesses) {
if (region.count > 0) return false;
const key = regionKey(region);
const scripts = new Set();
for (const witness of witnesses) {
if (witness.zeroKeys.has(key)) scripts.add(witness.script);
}
if (scripts.size === 0) return false;
for (const witness of witnesses) {
if (!scripts.has(witness.script)) continue;
let sawExecutableLine = false;
let linesCovered = true;
for (let line = region.line; line <= region.endLine; line += 1) {
const count = witness.lines.get(line);
// Blank lines and comments carry no statement; they neither confirm
// nor contradict, exactly as in isPhantomRegion.
if (count === undefined) continue;
sawExecutableLine = true;
if (count <= 0) {
linesCovered = false;
break;
}
}
if (!sawExecutableLine || !linesCovered) continue;
if (witness.zeros.some((zero) => zeroTouchesLines(zero, region))) continue;
return true;
}
return false;
}

// A region is one branch location from the istanbul object convertScript
// already builds -- the arm of a ternary, a short-circuit operand, a default
// parameter -- data the line map cannot see because several of them share a
Expand Down Expand Up @@ -600,6 +665,7 @@ export async function collectE2ECoverage(
const target = sources.get(relativePath) ?? {
lines: new Map(),
regions: new Map(),
witnesses: [],
};
for (const [line, count] of lineCoverage) {
target.lines.set(line, Math.max(target.lines.get(line) ?? 0, count));
Expand All @@ -613,6 +679,20 @@ export async function collectE2ECoverage(
target.regions.set(key, region);
}
}
// Each conversion is one artifact's view of one script: a complete
// partition of that text into executed and zero spans. Kept whole,
// with the script's identity, so isContradictedRegion can ask whether
// an artifact of the same script executed a region's lines without
// recording any zero on them.
const zeros = [...regionCoverage.values()].filter(
(region) => region.count <= 0,
);
target.witnesses.push({
script: scriptCoverage.url,
lines: lineCoverage,
zeros,
zeroKeys: new Set(zeros.map(regionKey)),
});
sources.set(relativePath, target);
unmapped.delete(relativePath);
}
Expand All @@ -633,7 +713,8 @@ export async function collectE2ECoverage(
const regionValues = allRegions.filter(
(region) =>
!isPhantomRegion(region, coverage.lines) &&
!isDriftedRegion(region, allRegions),
!isDriftedRegion(region, allRegions) &&
!isContradictedRegion(region, coverage.witnesses),
);
const regions = regionValues.length;
const coveredRegions = regionValues.filter(
Expand Down
Loading