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
144 changes: 144 additions & 0 deletions src/scripts/__tests__/coverage-contract.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { describe, expect, it } from "vitest"

import { mergeCoverageSources, parseCoverageSourceLines } from "../coverage-contract.mjs"

const coverage = (records) =>
records
.map(
([source, lines]) =>
`SF:${source}\n${lines.map((line) => `DA:${line},1`).join("\n")}\nLF:${lines.length}\nend_of_record`,
)
.join("\n")

const parse = (records, lane) => parseCoverageSourceLines(coverage(records), lane)

describe("coverage source equivalence", () => {
it("accepts legitimate changes to the instrumented source population", () => {
const before = [
["src/a.ts", [1]],
["src/b.ts", [1]],
]
const after = [
["src/a.ts", [1, 2]],
["src/b.ts", [1]],
]

expect(() =>
mergeCoverageSources(
["api", "core"],
[
["api", parse(after, "api")],
["core", parse([["src/a.ts", [1, 2]]], "core")],
],
),
).not.toThrow()
expect([...parse(after, "api").values()].reduce((sum, lines) => sum + lines.size, 0)).toBe(
[...parse(before, "api").values()].reduce((sum, lines) => sum + lines.size, 0) + 1,
)
})

it("rejects omitted lane coverage", () => {
expect(() => mergeCoverageSources(["api", "core"], [["api", parse([["src/a.ts", [1]]], "api")]])).toThrow(
"Coverage lane is missing: core",
)
})

it("rejects duplicated lane coverage", () => {
expect(() =>
mergeCoverageSources(
["api"],
[
["api", parse([["src/a.ts", [1]]], "api")],
["api", parse([["src/a.ts", [1]]], "api")],
],
),
).toThrow("Coverage lane is duplicated: api")
})

it("rejects duplicate source records within a lane", () => {
expect(() =>
parse(
[
["src/a.ts", [1]],
["src/a.ts", [1]],
],
"api",
),
).toThrow("api coverage contains duplicate source record: src/a.ts")
})

it("rejects unfinished source records", () => {
expect(() => parseCoverageSourceLines("SF:src/a.ts\nDA:1,1\nSF:src/b.ts\nLF:1", "api")).toThrow(
"api coverage contains an unfinished source record: src/a.ts",
)
expect(() => parseCoverageSourceLines("SF:src/a.ts\nDA:1,1\n", "api")).toThrow(
"api coverage contains an unfinished source record: src/a.ts",
)
expect(() => parseCoverageSourceLines("SF:src/a.ts\nDA:1,1\nLF:1\n", "api")).toThrow(
"api coverage contains an unfinished source record: src/a.ts",
)
})

it.each([
["empty source paths", "SF:\nLF:0\nend_of_record", "empty source path"],
["DA outside a record", "DA:1,1", "DA outside a source record"],
["LF outside a record", "LF:0", "LF outside a source record"],
["DA after LF", "SF:src/a.ts\nDA:1,1\nLF:1\nDA:2,1\nend_of_record", "DA after LF"],
["missing DA counts", "SF:src/a.ts\nDA:1\nLF:1\nend_of_record", "invalid DA"],
["nonnumeric DA counts", "SF:src/a.ts\nDA:1,nope\nLF:1\nend_of_record", "invalid DA"],
["zero DA line numbers", "SF:src/a.ts\nDA:0,1\nLF:1\nend_of_record", "invalid DA"],
[
"unsafe DA line numbers",
`SF:src/a.ts\nDA:${Number.MAX_SAFE_INTEGER + 1},0\nLF:1\nend_of_record`,
"invalid DA",
],
["unsafe DA counts", `SF:src/a.ts\nDA:1,${Number.MAX_SAFE_INTEGER + 1}\nLF:1\nend_of_record`, "invalid DA"],
["duplicate DA lines", "SF:src/a.ts\nDA:1,0\nDA:1,1\nLF:2\nend_of_record", "duplicate DA"],
["empty LF values", "SF:src/a.ts\nLF:\nend_of_record", "invalid LF"],
["nonnumeric LF values", "SF:src/a.ts\nLF:nope\nend_of_record", "invalid LF"],
["mismatched LF values", "SF:src/a.ts\nDA:1,1\nLF:2\nend_of_record", "invalid LF"],
["records without LF", "SF:src/a.ts\nend_of_record", "invalid record terminator"],
["invalid terminators", "end_of_record", "invalid record terminator"],
])("rejects %s", (_name, lcov, error) => {
expect(() => parseCoverageSourceLines(lcov, "api")).toThrow(error)
Comment thread
zoomote[bot] marked this conversation as resolved.
})

it("accepts DA and LF numeric boundaries", () => {
const sources = parseCoverageSourceLines(
`SF:src/a.ts\nDA:1,0\nDA:${Number.MAX_SAFE_INTEGER},0,checksum\nLF:2\nend_of_record`,
"api",
)

expect(sources).toEqual(new Map([["src/a.ts", new Set([1, Number.MAX_SAFE_INTEGER])]]))
})

it("rejects empty and unexpected lane coverage", () => {
expect(() => mergeCoverageSources(["api"], [["api", new Map()]])).toThrow(
"Coverage lane has no instrumented lines: api",
)
Comment thread
zoomote[bot] marked this conversation as resolved.
expect(() => mergeCoverageSources(["api"], [["api", new Map([["src/a.ts", new Set()]])]])).toThrow(
"Coverage lane has no instrumented lines: api",
)
expect(() =>
mergeCoverageSources(
["api"],
[
["api", parse([["src/a.ts", [1]]], "api")],
["core", parse([["src/a.ts", [1]]], "core")],
],
),
).toThrow("Unexpected coverage lane: core")
})

it("rejects conflicting instrumented line counts", () => {
expect(() =>
mergeCoverageSources(
["api", "core"],
[
["api", parse([["src/a.ts", [1, 3]]], "api")],
["core", parse([["src/a.ts", [1, 2]]], "core")],
],
),
).toThrow("core coverage has conflicting instrumented lines for src/a.ts")
})
})
70 changes: 70 additions & 0 deletions src/scripts/coverage-contract.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export const parseCoverageSourceLines = (lcov, lane) => {
const sources = new Map()
let source
let instrumentedLines = new Set()
let hasSummary = false

Check warning on line 5 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:5: Survived BooleanLiteral mutant (replacement: true). See the job summary for the complete list and resolution guidance.

Check warning on line 5 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:5: Survived BooleanLiteral mutant (replacement: true). See the job summary for the complete list and resolution guidance.

for (const line of lcov.split(/\r?\n/)) {
if (line.startsWith("SF:")) {
if (source) throw new Error(`${lane} coverage contains an unfinished source record: ${source}`)
source = line.slice(3)
if (!source) throw new Error(`${lane} coverage contains an empty source path`)
instrumentedLines = new Set()
hasSummary = false
} else if (line.startsWith("DA:")) {
if (!source) throw new Error(`${lane} coverage contains DA outside a source record`)
if (hasSummary) throw new Error(`${lane} coverage contains DA after LF for ${source}`)
const match = /^DA:(\d+),(\d+)(?:,[^,\r\n]+)?$/.exec(line)

Check warning on line 17 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:17: 3 mutation test gaps; example: Survived Regex mutant (replacement: /DA:(\d+),(\d+)(?:,[^,\r\n]+)?$/). See the job summary for the complete list and resolution guidance.

Check warning on line 17 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:17: 3 mutation test gaps; example: Survived Regex mutant (replacement: /DA:(\d+),(\d+)(?:,[^,\r\n]+)?$/). See the job summary for the complete list and resolution guidance.
const lineNumber = match ? Number(match[1]) : Number.NaN
const executionCount = match ? Number(match[2]) : Number.NaN
if (!Number.isSafeInteger(lineNumber) || lineNumber < 1)
throw new Error(`${lane} coverage contains invalid DA for ${source}`)
if (!Number.isSafeInteger(executionCount))
throw new Error(`${lane} coverage contains invalid DA for ${source}`)
if (instrumentedLines.has(lineNumber))
throw new Error(`${lane} coverage contains duplicate DA for ${source}:${lineNumber}`)
instrumentedLines.add(lineNumber)
} else if (line.startsWith("LF:")) {
if (!source) throw new Error(`${lane} coverage contains LF outside a source record`)
if (sources.has(source)) throw new Error(`${lane} coverage contains duplicate source record: ${source}`)

const match = /^LF:(\d+)$/.exec(line)

Check warning on line 31 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:31: 3 mutation test gaps; example: Survived Regex mutant (replacement: /LF:(\d+)$/). See the job summary for the complete list and resolution guidance.

Check warning on line 31 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:31: 3 mutation test gaps; example: Survived Regex mutant (replacement: /LF:(\d+)$/). See the job summary for the complete list and resolution guidance.
const linesFound = match ? Number(match[1]) : Number.NaN
if (!Number.isSafeInteger(linesFound) || linesFound < 0 || linesFound !== instrumentedLines.size)

Check warning on line 33 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:33: 4 mutation test gaps; example: Survived ConditionalExpression mutant (replacement: false). See the job summary for the complete list and resolution guidance.

Check warning on line 33 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:33: 4 mutation test gaps; example: Survived ConditionalExpression mutant (replacement: false). See the job summary for the complete list and resolution guidance.
throw new Error(`${lane} coverage contains invalid LF for ${source}`)
sources.set(source, instrumentedLines)
hasSummary = true
} else if (line === "end_of_record") {
if (!source || !hasSummary) throw new Error(`${lane} coverage contains an invalid record terminator`)
source = undefined
}
}
if (source) throw new Error(`${lane} coverage contains an unfinished source record: ${source}`)

return sources
}

export const mergeCoverageSources = (expectedLanes, coverageByLane) => {
const lanes = new Set()
const combinedSources = new Map()
for (const [lane, sources] of coverageByLane) {
if (lanes.has(lane)) throw new Error(`Coverage lane is duplicated: ${lane}`)
lanes.add(lane)
if (sources.size === 0 || [...sources.values()].every((lines) => lines.size === 0))

Check warning on line 53 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:53: 2 mutation test gaps; example: Survived ConditionalExpression mutant (replacement: false). See the job summary for the complete list and resolution guidance.

Check warning on line 53 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:53: 2 mutation test gaps; example: Survived ConditionalExpression mutant (replacement: false). See the job summary for the complete list and resolution guidance.
throw new Error(`Coverage lane has no instrumented lines: ${lane}`)
for (const [source, instrumentedLines] of sources) {
const existingLines = combinedSources.get(source)
if (
existingLines &&
(existingLines.size !== instrumentedLines.size ||

Check warning on line 59 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:59: Survived ConditionalExpression mutant (replacement: false). See the job summary for the complete list and resolution guidance.

Check warning on line 59 in src/scripts/coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/coverage-contract.mjs:59: Survived ConditionalExpression mutant (replacement: false). See the job summary for the complete list and resolution guidance.
[...existingLines].some((line) => !instrumentedLines.has(line)))
)
throw new Error(`${lane} coverage has conflicting instrumented lines for ${source}`)
combinedSources.set(source, instrumentedLines)
}
}

for (const lane of expectedLanes) if (!lanes.has(lane)) throw new Error(`Coverage lane is missing: ${lane}`)
for (const lane of lanes) if (!expectedLanes.includes(lane)) throw new Error(`Unexpected coverage lane: ${lane}`)
return combinedSources
}
21 changes: 8 additions & 13 deletions src/scripts/verify-coverage-contract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import process from "node:process"
import { promisify } from "node:util"

import { mergeCoverageSources, parseCoverageSourceLines } from "./coverage-contract.mjs"

const pnpm = process.platform === "win32" ? process.env.npm_execpath : "pnpm"
if (!pnpm) throw new Error("pnpm executable path is unavailable")
const command = process.platform === "win32" ? process.execPath : pnpm
Expand Down Expand Up @@ -151,16 +153,9 @@
rmSync(collectionDirectory, { recursive: true, force: true })
}

const coverageSources = new Map()
for (const lane of [...ownershipLanes, "tree-sitter"]) {
let source
for (const line of readFileSync(resolve(root, "coverage", lane, "lcov.info"), "utf8").split(/\r?\n/)) {
if (line.startsWith("SF:")) source = line.slice(3)
if (line.startsWith("LF:")) coverageSources.set(source, Number(line.slice(3)))
}
}
const instrumentedLines = [...coverageSources.values()].reduce((sum, lines) => sum + lines, 0)
if (coverageSources.size !== 469 || instrumentedLines !== 30_229)
throw new Error(
`Coverage source population changed: ${coverageSources.size} records and ${instrumentedLines} lines; verify equivalence and update the baseline deliberately`,
)
const coverageLanes = [...ownershipLanes, "tree-sitter"]

Check warning on line 156 in src/scripts/verify-coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/verify-coverage-contract.mjs:156: 2 mutation test gaps; example: NoCoverage ArrayDeclaration mutant (replacement: []). See the job summary for the complete list and resolution guidance.

Check warning on line 156 in src/scripts/verify-coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/verify-coverage-contract.mjs:156: 2 mutation test gaps; example: NoCoverage ArrayDeclaration mutant (replacement: []). See the job summary for the complete list and resolution guidance.
const coverageByLane = coverageLanes.map((lane) => [

Check warning on line 157 in src/scripts/verify-coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/verify-coverage-contract.mjs:157: 2 mutation test gaps; example: NoCoverage ArrowFunction mutant (replacement: () => undefined). See the job summary for the complete list and resolution guidance.

Check warning on line 157 in src/scripts/verify-coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/verify-coverage-contract.mjs:157: 2 mutation test gaps; example: NoCoverage ArrowFunction mutant (replacement: () => undefined). See the job summary for the complete list and resolution guidance.
lane,
parseCoverageSourceLines(readFileSync(resolve(root, "coverage", lane, "lcov.info"), "utf8"), lane),

Check warning on line 159 in src/scripts/verify-coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/verify-coverage-contract.mjs:159: 3 mutation test gaps; example: NoCoverage StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.

Check warning on line 159 in src/scripts/verify-coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/verify-coverage-contract.mjs:159: 3 mutation test gaps; example: NoCoverage StringLiteral mutant (replacement: ""). See the job summary for the complete list and resolution guidance.
])
mergeCoverageSources(coverageLanes, coverageByLane)

Check warning on line 161 in src/scripts/verify-coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/verify-coverage-contract.mjs:161: NoCoverage CallExpression mutant (replacement: ;). See the job summary for the complete list and resolution guidance.

Check warning on line 161 in src/scripts/verify-coverage-contract.mjs

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/scripts/verify-coverage-contract.mjs:161: NoCoverage CallExpression mutant (replacement: ;). See the job summary for the complete list and resolution guidance.
Loading