coverage-check 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -17
- package/dist/src/cli.mjs +3 -0
- package/dist/src/commands/check-args.d.mts +6 -0
- package/dist/src/commands/check-args.mjs +12 -0
- package/dist/src/commands/check-output.d.mts +5 -0
- package/dist/src/commands/check-output.mjs +16 -0
- package/dist/src/commands/check.mjs +7 -4
- package/dist/src/commands/merge-args.d.mts +7 -0
- package/dist/src/commands/merge-args.mjs +38 -0
- package/dist/src/commands/merge.d.mts +9 -0
- package/dist/src/commands/merge.mjs +41 -0
- package/dist/src/coverage-check.d.mts +5 -0
- package/dist/src/coverage-check.mjs +3 -0
- package/dist/src/coverage-drop.d.mts +1 -1
- package/dist/src/coverage-drop.mjs +2 -2
- package/dist/src/diff-parser.mjs +3 -0
- package/dist/src/lcov-records.d.mts +29 -0
- package/dist/src/lcov-records.mjs +155 -0
- package/dist/src/lcov-to-istanbul.mjs +33 -9
- package/dist/src/report.d.mts +1 -1
- package/dist/src/report.mjs +3 -3
- package/dist/src/rules.d.mts +6 -1
- package/dist/src/rules.mjs +7 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -120,6 +120,60 @@ Two common sources of confusion:
|
|
|
120
120
|
The annotation affects only the stdout failure output. The GitHub PR sticky comment and Actions step
|
|
121
121
|
summary are unchanged.
|
|
122
122
|
|
|
123
|
+
### Merging LCOV files
|
|
124
|
+
|
|
125
|
+
Use the `merge` subcommand to fold multiple `lcov.info` files into a single output that preserves function and branch records (`FN`, `FNDA`, `BRDA`) as well as summary counters (`LF/LH/FNF/FNH/BRF/BRH`):
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
coverage-check merge \
|
|
129
|
+
--artifacts ./coverage-artifacts \
|
|
130
|
+
--output ./coverage-merged/lcov.info
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Hit counts are **summed** across inputs (consistent with the package's internal multi-suite merge). Parent directories of `--output` are created automatically.
|
|
134
|
+
|
|
135
|
+
### Advisory (non-blocking) mode
|
|
136
|
+
|
|
137
|
+
Pass `--advisory` to exit `0` even when coverage falls short. The check still runs in full — JSON is written, the PR comment is posted, and uncovered lines are printed — but the process never exits `1`. Useful for pre-push hooks where you want feedback without blocking the push:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
coverage-check check \
|
|
141
|
+
--rules .coverage-rules.yml \
|
|
142
|
+
--artifacts ./coverage-artifacts \
|
|
143
|
+
--base origin/main \
|
|
144
|
+
--head HEAD \
|
|
145
|
+
--advisory
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### PR-scoped regression gate
|
|
149
|
+
|
|
150
|
+
By default, `no_coverage_drop` applies to every rule area regardless of what changed. Pass `--drop-only-changed-areas` to restrict the drop gate to rule areas that contain at least one changed file in the PR diff. Areas with no changed files are reported as `skipped` — they pass non-blockingly:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
coverage-check check \
|
|
154
|
+
--rules .coverage-rules.yml \
|
|
155
|
+
--artifacts ./coverage-artifacts \
|
|
156
|
+
--base origin/main \
|
|
157
|
+
--head HEAD \
|
|
158
|
+
--drop-only-changed-areas
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This avoids false positives when a CI run only exercises a subset of areas.
|
|
162
|
+
|
|
163
|
+
### Required artifacts
|
|
164
|
+
|
|
165
|
+
Pass `--require-artifact <relpath>` (repeatable) to fail early — exit `2` with a `::error::` annotation — if an expected `lcov.info` is absent from `--artifacts`. This distinguishes a missing coverage upload (CI job didn't run) from genuine uncovered lines:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
coverage-check check \
|
|
169
|
+
--rules .coverage-rules.yml \
|
|
170
|
+
--artifacts ./coverage-artifacts \
|
|
171
|
+
--require-artifact coverage-backend/lcov.info \
|
|
172
|
+
--require-artifact coverage-frontend/lcov.info
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`--require-artifact` is also accepted by `coverage-check merge`.
|
|
176
|
+
|
|
123
177
|
## Rules file
|
|
124
178
|
|
|
125
179
|
```yaml
|
|
@@ -166,22 +220,36 @@ First-match-wins means that if you have a more specific rule before a broader on
|
|
|
166
220
|
|
|
167
221
|
### `coverage-check check`
|
|
168
222
|
|
|
169
|
-
| Flag
|
|
170
|
-
|
|
|
171
|
-
| `--rules`
|
|
172
|
-
| `--artifacts`
|
|
173
|
-
| `--base`
|
|
174
|
-
| `--head`
|
|
175
|
-
| `--store-fs`
|
|
176
|
-
| `--store`
|
|
177
|
-
| `--store-s3`
|
|
178
|
-
| `--branch`
|
|
179
|
-
| `--suite`
|
|
180
|
-
| `--strip-prefix`
|
|
181
|
-
| `--pr`
|
|
182
|
-
| `--repo`
|
|
183
|
-
| `--json`
|
|
184
|
-
| `--annotate-source`
|
|
223
|
+
| Flag | Default | Description |
|
|
224
|
+
| --------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
225
|
+
| `--rules` | `.coverage-rules.yml` | Path to YAML rules file |
|
|
226
|
+
| `--artifacts` | `./coverage-artifacts` | Directory to scan for `lcov.info` files |
|
|
227
|
+
| `--base` | `origin/main` | Base git ref for `git diff` |
|
|
228
|
+
| `--head` | `HEAD` | Head git ref for `git diff` |
|
|
229
|
+
| `--store-fs` | — | Path to a filesystem suite store directory |
|
|
230
|
+
| `--store` | — | Alias for `--store-fs` |
|
|
231
|
+
| `--store-s3` | — | S3 suite store spec: `<bucket>[/<prefix>]` |
|
|
232
|
+
| `--branch` | `"main"` | Branch pointer to follow when reading from the store |
|
|
233
|
+
| `--suite` | — | Name of the current suite (no `/` or `\\`); fresh artifacts override this suite in the store |
|
|
234
|
+
| `--strip-prefix` | — | Extra path prefix to strip from LCOV `SF:` lines (repeatable) |
|
|
235
|
+
| `--pr` | — | Pull request number for sticky comment |
|
|
236
|
+
| `--repo` | `$GITHUB_REPOSITORY` | `owner/repo` for sticky comment |
|
|
237
|
+
| `--json` | — | Write JSON result to this path |
|
|
238
|
+
| `--annotate-source` | — | Print the trimmed source text of each uncovered line in stdout (does not alter PR comment or step summary) |
|
|
239
|
+
| `--advisory` | — | Exit `0` even on shortfall; still prints, writes JSON, and posts PR comment |
|
|
240
|
+
| `--drop-only-changed-areas` | — | Restrict `no_coverage_drop` to rule areas that have ≥1 changed file; others are reported as skipped |
|
|
241
|
+
| `--require-artifact` | — | Fail (exit `2`) if this relative path is absent under `--artifacts` (repeatable) |
|
|
242
|
+
|
|
243
|
+
### `coverage-check merge`
|
|
244
|
+
|
|
245
|
+
| Flag | Default | Description |
|
|
246
|
+
| -------------------- | ---------------------- | ------------------------------------------------------------- |
|
|
247
|
+
| `--artifacts` | `./coverage-artifacts` | Directory to scan for `lcov.info` files |
|
|
248
|
+
| `--output` | required | Path to write the merged `lcov.info` |
|
|
249
|
+
| `--strip-prefix` | — | Extra path prefix to strip from LCOV `SF:` lines (repeatable) |
|
|
250
|
+
| `--require-artifact` | — | Fail (exit `2`) if this relative path is absent (repeatable) |
|
|
251
|
+
|
|
252
|
+
Hit counts are summed across all inputs. Function (`FN`/`FNDA`) and branch (`BRDA`) records are preserved; summary counters (`LF/LH/FNF/FNH/BRF/BRH`) are recomputed from the merged data.
|
|
185
253
|
|
|
186
254
|
### `coverage-check store-put`
|
|
187
255
|
|
|
@@ -203,7 +271,17 @@ When `--sha` and `--branch` are both provided, `store-put` writes a SHA-addresse
|
|
|
203
271
|
## Programmatic API
|
|
204
272
|
|
|
205
273
|
```ts
|
|
206
|
-
import {
|
|
274
|
+
import {
|
|
275
|
+
runCheck,
|
|
276
|
+
runMerge,
|
|
277
|
+
runStorePut,
|
|
278
|
+
collapseRanges,
|
|
279
|
+
parseLcovFull,
|
|
280
|
+
mergeLcovFull,
|
|
281
|
+
toLcovFull,
|
|
282
|
+
FileSystemSuiteStore,
|
|
283
|
+
S3SuiteStore,
|
|
284
|
+
} from "coverage-check";
|
|
207
285
|
|
|
208
286
|
// FileSystem store
|
|
209
287
|
const fsStore = new FileSystemSuiteStore("/path/to/store");
|
|
@@ -223,6 +301,16 @@ await runCheck({
|
|
|
223
301
|
store: s3Store,
|
|
224
302
|
suite: "backend",
|
|
225
303
|
branch: "main",
|
|
304
|
+
advisory: false,
|
|
305
|
+
dropOnlyChangedAreas: false,
|
|
306
|
+
requireArtifacts: [],
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await runMerge({
|
|
310
|
+
artifacts: "./coverage-artifacts",
|
|
311
|
+
output: "./coverage-merged/lcov.info",
|
|
312
|
+
stripPrefixes: [],
|
|
313
|
+
requireArtifacts: [],
|
|
226
314
|
});
|
|
227
315
|
|
|
228
316
|
await runStorePut({
|
|
@@ -233,6 +321,15 @@ await runStorePut({
|
|
|
233
321
|
sha: "abc123",
|
|
234
322
|
branch: "main",
|
|
235
323
|
});
|
|
324
|
+
|
|
325
|
+
// Collapse a sorted line list into a compact range string
|
|
326
|
+
collapseRanges([1, 2, 3, 7, 8]); // → "L1-3, L7-8"
|
|
327
|
+
collapseRanges([1, 2, 3, 7, 8], ""); // → "1-3, 7-8"
|
|
328
|
+
|
|
329
|
+
// Full-fidelity LCOV (functions + branches + lines)
|
|
330
|
+
const full = parseLcovFull(lcovText, ["/home/runner/work/repo/repo/"]);
|
|
331
|
+
const merged = mergeLcovFull([full1, full2]); // hits are summed
|
|
332
|
+
const output = toLcovFull(merged); // includes FN/FNDA/BRDA and LF/LH/FNF/FNH/BRF/BRH
|
|
236
333
|
```
|
|
237
334
|
|
|
238
335
|
You can also implement your own `SuiteStore`:
|
package/dist/src/cli.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { main as checkMain } from "./commands/check.mjs";
|
|
2
2
|
import { main as storePutMain } from "./commands/store-put.mjs";
|
|
3
3
|
import { main as htmlMain } from "./commands/html.mjs";
|
|
4
|
+
import { main as mergeMain } from "./commands/merge.mjs";
|
|
4
5
|
import { main as summaryMain } from "./commands/summary.mjs";
|
|
5
6
|
const stderr = (msg) => process.stderr.write(`${msg}\n`);
|
|
6
7
|
export async function main(argv) {
|
|
@@ -11,6 +12,8 @@ export async function main(argv) {
|
|
|
11
12
|
return checkMain(argv.slice(1));
|
|
12
13
|
if (sub === "store-put")
|
|
13
14
|
return storePutMain(argv.slice(1));
|
|
15
|
+
if (sub === "merge")
|
|
16
|
+
return mergeMain(argv.slice(1));
|
|
14
17
|
if (sub === "html")
|
|
15
18
|
return htmlMain(argv.slice(1));
|
|
16
19
|
if (sub === "summary")
|
|
@@ -18,5 +18,11 @@ export type CheckArgs = {
|
|
|
18
18
|
summaryFile?: string | null;
|
|
19
19
|
/** Annotate each uncovered line with its trimmed source text in stdout. Default: false. */
|
|
20
20
|
annotateSource?: boolean;
|
|
21
|
+
/** Exit 0 even on coverage shortfall — compute and print, but never block. Default: false. */
|
|
22
|
+
advisory?: boolean;
|
|
23
|
+
/** Apply no_coverage_drop only to rules whose area has changed files in the diff. Default: false. */
|
|
24
|
+
dropOnlyChangedAreas?: boolean;
|
|
25
|
+
/** Relative paths under --artifacts that must exist before the check runs (repeatable). */
|
|
26
|
+
requireArtifacts?: string[];
|
|
21
27
|
};
|
|
22
28
|
export declare function parseCheckArgs(argv: string[]): CheckArgs;
|
|
@@ -17,6 +17,9 @@ export function parseCheckArgs(argv) {
|
|
|
17
17
|
branch: "main",
|
|
18
18
|
summaryFile: process.env["GITHUB_STEP_SUMMARY"] ?? null,
|
|
19
19
|
annotateSource: false,
|
|
20
|
+
advisory: false,
|
|
21
|
+
dropOnlyChangedAreas: false,
|
|
22
|
+
requireArtifacts: [],
|
|
20
23
|
};
|
|
21
24
|
for (let i = 0; i < argv.length; i++) {
|
|
22
25
|
const flag = argv[i];
|
|
@@ -80,6 +83,15 @@ export function parseCheckArgs(argv) {
|
|
|
80
83
|
case "--annotate-source":
|
|
81
84
|
args.annotateSource = true;
|
|
82
85
|
break;
|
|
86
|
+
case "--advisory":
|
|
87
|
+
args.advisory = true;
|
|
88
|
+
break;
|
|
89
|
+
case "--drop-only-changed-areas":
|
|
90
|
+
args.dropOnlyChangedAreas = true;
|
|
91
|
+
break;
|
|
92
|
+
case "--require-artifact":
|
|
93
|
+
args.requireArtifacts.push(val());
|
|
94
|
+
break;
|
|
83
95
|
default:
|
|
84
96
|
throw new Error(`unknown flag: ${flag}`);
|
|
85
97
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { DropResult, DiffLines, LcovData } from "../types.mts";
|
|
2
|
+
/**
|
|
3
|
+
* Verifies that each required artifact path exists under artifactsDir.
|
|
4
|
+
* Prints `::error::` to stderr for each missing file and returns false if any are absent.
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkRequiredArtifacts(artifactsDir: string, requireArtifacts: string[]): boolean;
|
|
2
7
|
export declare function warnNonContributing(parsedSources: {
|
|
3
8
|
name: string;
|
|
4
9
|
lcov: LcovData;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
const stdout = (msg) => process.stdout.write(`${msg}\n`);
|
|
2
4
|
const stderr = (msg) => process.stderr.write(`${msg}\n`);
|
|
3
5
|
function fmtPct(n) {
|
|
@@ -19,6 +21,20 @@ function lcovContributesToDiff(sourceLcov, diff) {
|
|
|
19
21
|
}
|
|
20
22
|
return false;
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Verifies that each required artifact path exists under artifactsDir.
|
|
26
|
+
* Prints `::error::` to stderr for each missing file and returns false if any are absent.
|
|
27
|
+
*/
|
|
28
|
+
export function checkRequiredArtifacts(artifactsDir, requireArtifacts) {
|
|
29
|
+
let ok = true;
|
|
30
|
+
for (const rel of requireArtifacts) {
|
|
31
|
+
if (!existsSync(join(artifactsDir, rel))) {
|
|
32
|
+
stderr(`::error:: missing expected coverage artifact: ${rel}`);
|
|
33
|
+
ok = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return ok;
|
|
37
|
+
}
|
|
22
38
|
export function warnNonContributing(parsedSources, diff) {
|
|
23
39
|
if (diff.size === 0)
|
|
24
40
|
return;
|
|
@@ -3,7 +3,7 @@ import { parseLcov } from "../lcov-parser.mjs";
|
|
|
3
3
|
import { mergeLcov } from "../lcov-merge.mjs";
|
|
4
4
|
import { getChangedLines } from "../diff-parser.mjs";
|
|
5
5
|
import { getChangedLineContent } from "../diff-parser-content.mjs";
|
|
6
|
-
import { loadRules } from "../rules.mjs";
|
|
6
|
+
import { loadRules, buildChangedRules } from "../rules.mjs";
|
|
7
7
|
import { computePatchCoverage } from "../patch-coverage.mjs";
|
|
8
8
|
import { computeCoverageDrop } from "../coverage-drop.mjs";
|
|
9
9
|
import { collapseRanges, renderFailureComment } from "../report.mjs";
|
|
@@ -11,7 +11,7 @@ import { upsertComment } from "../github-comment.mjs";
|
|
|
11
11
|
import { collectLcovFiles, buildStripPrefixes } from "../load-artifacts.mjs";
|
|
12
12
|
import { writeSummary } from "../step-summary.mjs";
|
|
13
13
|
import { parseCheckArgs } from "./check-args.mjs";
|
|
14
|
-
import { warnNonContributing, printDropOutput } from "./check-output.mjs";
|
|
14
|
+
import { warnNonContributing, printDropOutput, checkRequiredArtifacts } from "./check-output.mjs";
|
|
15
15
|
const stdout = (msg) => process.stdout.write(`${msg}\n`);
|
|
16
16
|
const stderr = (msg) => process.stderr.write(`${msg}\n`);
|
|
17
17
|
export async function main(argv) {
|
|
@@ -34,6 +34,8 @@ export async function runCheck(args) {
|
|
|
34
34
|
stderr(`coverage-check: failed to load rules: ${err}`);
|
|
35
35
|
return 2;
|
|
36
36
|
}
|
|
37
|
+
if (!checkRequiredArtifacts(args.artifacts, args.requireArtifacts ?? []))
|
|
38
|
+
return 2;
|
|
37
39
|
const branch = args.branch ?? "main";
|
|
38
40
|
const stripPrefixes = buildStripPrefixes(args.stripPrefixes);
|
|
39
41
|
const reports = [];
|
|
@@ -106,7 +108,8 @@ export async function runCheck(args) {
|
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
const { buckets, informational } = computePatchCoverage(diff, lcov, rules);
|
|
109
|
-
const
|
|
111
|
+
const changedRules = (args.dropOnlyChangedAreas ?? false) ? buildChangedRules(diff, rules) : undefined;
|
|
112
|
+
const drops = computeCoverageDrop(lcov, baseline, rules, changedRules);
|
|
110
113
|
const passed = buckets.every((b) => b.passed) && drops.every((d) => d.passed || d.skipped);
|
|
111
114
|
const result = { buckets, drops, informational, passed };
|
|
112
115
|
if (args.json) {
|
|
@@ -166,5 +169,5 @@ export async function runCheck(args) {
|
|
|
166
169
|
stderr(`coverage-check: failed to post PR comment: ${err}`);
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
|
-
return passed ? 0 : 1;
|
|
172
|
+
return (args.advisory ?? false) || passed ? 0 : 1;
|
|
170
173
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function parseMergeArgs(argv) {
|
|
2
|
+
const args = {
|
|
3
|
+
artifacts: "./coverage-artifacts",
|
|
4
|
+
output: "",
|
|
5
|
+
stripPrefixes: [],
|
|
6
|
+
requireArtifacts: [],
|
|
7
|
+
};
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const flag = argv[i];
|
|
10
|
+
const next = argv[i + 1];
|
|
11
|
+
const val = () => {
|
|
12
|
+
if (next === undefined || next.startsWith("--")) {
|
|
13
|
+
throw new Error(`${flag} requires a value`);
|
|
14
|
+
}
|
|
15
|
+
i++;
|
|
16
|
+
return next;
|
|
17
|
+
};
|
|
18
|
+
switch (flag) {
|
|
19
|
+
case "--artifacts":
|
|
20
|
+
args.artifacts = val();
|
|
21
|
+
break;
|
|
22
|
+
case "--output":
|
|
23
|
+
args.output = val();
|
|
24
|
+
break;
|
|
25
|
+
case "--strip-prefix":
|
|
26
|
+
args.stripPrefixes.push(val());
|
|
27
|
+
break;
|
|
28
|
+
case "--require-artifact":
|
|
29
|
+
args.requireArtifacts.push(val());
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`unknown flag: ${flag}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!args.output)
|
|
36
|
+
throw new Error("--output is required");
|
|
37
|
+
return args;
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MergeArgs } from "./merge-args.mts";
|
|
2
|
+
export type { MergeArgs } from "./merge-args.mts";
|
|
3
|
+
export declare function main(argv: string[]): Promise<number>;
|
|
4
|
+
/**
|
|
5
|
+
* Merges all lcov.info files found under args.artifacts into a single full-fidelity LCOV file.
|
|
6
|
+
* Preserves FN/FNDA/BRDA records and recomputes summary counters (FNF/FNH/BRF/BRH/LF/LH).
|
|
7
|
+
* Hit counts are summed across reports (lines, function hits, branch hits).
|
|
8
|
+
*/
|
|
9
|
+
export declare function runMerge(args: MergeArgs): number;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { collectLcovFiles, buildStripPrefixes } from "../load-artifacts.mjs";
|
|
4
|
+
import { parseLcovFull, mergeLcovFull, toLcovFull } from "../lcov-records.mjs";
|
|
5
|
+
import { checkRequiredArtifacts } from "./check-output.mjs";
|
|
6
|
+
import { parseMergeArgs } from "./merge-args.mjs";
|
|
7
|
+
const stdout = (msg) => process.stdout.write(`${msg}\n`);
|
|
8
|
+
const stderr = (msg) => process.stderr.write(`${msg}\n`);
|
|
9
|
+
export async function main(argv) {
|
|
10
|
+
let args;
|
|
11
|
+
try {
|
|
12
|
+
args = parseMergeArgs(argv);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
stderr(`coverage-check merge: ${String(err)}`);
|
|
16
|
+
return 2;
|
|
17
|
+
}
|
|
18
|
+
return runMerge(args);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Merges all lcov.info files found under args.artifacts into a single full-fidelity LCOV file.
|
|
22
|
+
* Preserves FN/FNDA/BRDA records and recomputes summary counters (FNF/FNH/BRF/BRH/LF/LH).
|
|
23
|
+
* Hit counts are summed across reports (lines, function hits, branch hits).
|
|
24
|
+
*/
|
|
25
|
+
export function runMerge(args) {
|
|
26
|
+
if (!checkRequiredArtifacts(args.artifacts, args.requireArtifacts))
|
|
27
|
+
return 2;
|
|
28
|
+
const lcovFiles = collectLcovFiles(args.artifacts);
|
|
29
|
+
if (lcovFiles.length === 0) {
|
|
30
|
+
stderr(`coverage-check merge: no lcov.info files found under ${args.artifacts}`);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const stripPrefixes = buildStripPrefixes(args.stripPrefixes);
|
|
34
|
+
const reports = lcovFiles.map((f) => parseLcovFull(readFileSync(f, "utf8"), stripPrefixes));
|
|
35
|
+
const merged = mergeLcovFull(reports);
|
|
36
|
+
const lcovText = toLcovFull(merged);
|
|
37
|
+
mkdirSync(dirname(args.output), { recursive: true });
|
|
38
|
+
writeFileSync(args.output, lcovText, "utf8");
|
|
39
|
+
stdout(`coverage-check merge: merged ${lcovFiles.length} file(s) → ${args.output}`);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
export { runCheck } from "./commands/check.mts";
|
|
2
2
|
export { runStorePut } from "./commands/store-put.mts";
|
|
3
|
+
export { runMerge } from "./commands/merge.mts";
|
|
3
4
|
export { FileSystemSuiteStore } from "./suite-store.mts";
|
|
4
5
|
export { S3SuiteStore } from "./s3-suite-store.mts";
|
|
5
6
|
export { parseDiffWithContent, getChangedLineContent } from "./diff-parser-content.mts";
|
|
7
|
+
export { collapseRanges } from "./report.mts";
|
|
8
|
+
export { parseLcovFull, mergeLcovFull, toLcovFull } from "./lcov-records.mts";
|
|
6
9
|
export type { CheckArgs } from "./commands/check.mts";
|
|
7
10
|
export type { StorePutArgs } from "./commands/store-put.mts";
|
|
11
|
+
export type { MergeArgs } from "./commands/merge.mts";
|
|
8
12
|
export type { SuiteStore, SuiteMeta } from "./suite-store.mts";
|
|
9
13
|
export type { S3SuiteStoreOptions } from "./s3-suite-store.mts";
|
|
10
14
|
export { computeCoverageDrop } from "./coverage-drop.mts";
|
|
15
|
+
export type { FullLcovData, FullFileCoverage } from "./lcov-records.mts";
|
|
11
16
|
export type { CoverageCheckResult, BucketResult, DropResult, FileCoverageResult, LcovData, DiffLines, DiffLineContent, CoverageRule, } from "./types.mts";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { runCheck } from "./commands/check.mjs";
|
|
2
2
|
export { runStorePut } from "./commands/store-put.mjs";
|
|
3
|
+
export { runMerge } from "./commands/merge.mjs";
|
|
3
4
|
export { FileSystemSuiteStore } from "./suite-store.mjs";
|
|
4
5
|
export { S3SuiteStore } from "./s3-suite-store.mjs";
|
|
5
6
|
export { parseDiffWithContent, getChangedLineContent } from "./diff-parser-content.mjs";
|
|
7
|
+
export { collapseRanges } from "./report.mjs";
|
|
8
|
+
export { parseLcovFull, mergeLcovFull, toLcovFull } from "./lcov-records.mjs";
|
|
6
9
|
export { computeCoverageDrop } from "./coverage-drop.mjs";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CoverageRule, DropResult, LcovData } from "./types.mts";
|
|
2
|
-
export declare function computeCoverageDrop(current: LcovData, baseline: LcovData | null, rules: CoverageRule[]): DropResult[];
|
|
2
|
+
export declare function computeCoverageDrop(current: LcovData, baseline: LcovData | null, rules: CoverageRule[], changedRules?: Set<CoverageRule>): DropResult[];
|
|
@@ -13,11 +13,11 @@ function fileTotals(lcov, targetRule, allRules) {
|
|
|
13
13
|
}
|
|
14
14
|
return { hit, total };
|
|
15
15
|
}
|
|
16
|
-
export function computeCoverageDrop(current, baseline, rules) {
|
|
16
|
+
export function computeCoverageDrop(current, baseline, rules, changedRules) {
|
|
17
17
|
const dropRules = rules.filter((r) => r.no_coverage_drop);
|
|
18
18
|
return dropRules.map((rule) => {
|
|
19
19
|
const maxDrop = rule.max_coverage_drop ?? 0;
|
|
20
|
-
if (baseline === null) {
|
|
20
|
+
if (baseline === null || (changedRules !== undefined && !changedRules.has(rule))) {
|
|
21
21
|
return {
|
|
22
22
|
rule: rule.paths,
|
|
23
23
|
currentPct: null,
|
package/dist/src/diff-parser.mjs
CHANGED
|
@@ -114,6 +114,9 @@ export function parseDiff(text) {
|
|
|
114
114
|
}
|
|
115
115
|
/** Runs git merge-base + git diff and returns the raw diff text. Internal shared helper. */
|
|
116
116
|
export async function runGitDiff(baseRef, headRef) {
|
|
117
|
+
if (baseRef.startsWith("-") || headRef.startsWith("-")) {
|
|
118
|
+
throw new Error("Git reference cannot start with a hyphen (prevents argument injection)");
|
|
119
|
+
}
|
|
117
120
|
const { spawn } = await import("node:child_process");
|
|
118
121
|
const spawnProcess = (cmd, args) => new Promise((resolve, reject) => {
|
|
119
122
|
const chunks = [];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-fidelity LCOV data for one source file.
|
|
3
|
+
* Preserves function, branch, and line coverage records.
|
|
4
|
+
*/
|
|
5
|
+
export type FullFileCoverage = {
|
|
6
|
+
/** FN: definitions keyed by function name. */
|
|
7
|
+
functions: Map<string, {
|
|
8
|
+
line: number;
|
|
9
|
+
name: string;
|
|
10
|
+
}>;
|
|
11
|
+
/** FNDA: hit counts keyed by function name. */
|
|
12
|
+
functionHits: Map<string, number>;
|
|
13
|
+
/** BRDA: hit counts keyed by "lineNo,blockNo,branchNo". */
|
|
14
|
+
branches: Map<string, number>;
|
|
15
|
+
/** DA: hit counts keyed by line number. */
|
|
16
|
+
lines: Map<number, number>;
|
|
17
|
+
};
|
|
18
|
+
/** Map from normalized file path → full LCOV coverage data. */
|
|
19
|
+
export type FullLcovData = Map<string, FullFileCoverage>;
|
|
20
|
+
/**
|
|
21
|
+
* Parses full-fidelity LCOV, preserving FN/FNDA/BRDA/DA records.
|
|
22
|
+
* Hits from repeated records for the same line/function/branch are summed.
|
|
23
|
+
* Summary records (TN/LF/LH/FNF/FNH/BRF/BRH) are skipped and recomputed by toLcovFull.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseLcovFull(text: string, stripPrefixes?: string[]): FullLcovData;
|
|
26
|
+
/** Merges multiple FullLcovData maps by summing all hit counts per file/line/function/branch. */
|
|
27
|
+
export declare function mergeLcovFull(reports: FullLcovData[]): FullLcovData;
|
|
28
|
+
/** Serializes FullLcovData to LCOV text with recomputed FNF/FNH/BRF/BRH/LF/LH summaries. */
|
|
29
|
+
export declare function toLcovFull(lcov: FullLcovData): string;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
function normalizeSfPath(rawPath, stripPrefixes) {
|
|
2
|
+
let path = rawPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
3
|
+
for (const prefix of stripPrefixes) {
|
|
4
|
+
if (path.startsWith(prefix))
|
|
5
|
+
return path.slice(prefix.length);
|
|
6
|
+
}
|
|
7
|
+
if (path.startsWith("/") || /^[A-Z]:\//i.test(path)) {
|
|
8
|
+
const match = path.match(/^.*?\/_?work\/([^/]+)\/\1\//);
|
|
9
|
+
if (match)
|
|
10
|
+
return path.slice(match[0].length);
|
|
11
|
+
}
|
|
12
|
+
return path;
|
|
13
|
+
}
|
|
14
|
+
function getOrCreate(data, file) {
|
|
15
|
+
let cov = data.get(file);
|
|
16
|
+
if (cov === undefined) {
|
|
17
|
+
cov = { branches: new Map(), functionHits: new Map(), functions: new Map(), lines: new Map() };
|
|
18
|
+
data.set(file, cov);
|
|
19
|
+
}
|
|
20
|
+
return cov;
|
|
21
|
+
}
|
|
22
|
+
function applyRecord(line, cov) {
|
|
23
|
+
if (line.startsWith("FN:")) {
|
|
24
|
+
const comma = line.indexOf(",", 3);
|
|
25
|
+
if (comma === -1)
|
|
26
|
+
return;
|
|
27
|
+
const lineNum = parseInt(line.slice(3, comma), 10);
|
|
28
|
+
const name = line.slice(comma + 1);
|
|
29
|
+
if (Number.isFinite(lineNum) && name)
|
|
30
|
+
cov.functions.set(name, { line: lineNum, name });
|
|
31
|
+
}
|
|
32
|
+
else if (line.startsWith("FNDA:")) {
|
|
33
|
+
const comma = line.indexOf(",", 5);
|
|
34
|
+
if (comma === -1)
|
|
35
|
+
return;
|
|
36
|
+
const hits = parseInt(line.slice(5, comma), 10);
|
|
37
|
+
const name = line.slice(comma + 1);
|
|
38
|
+
if (Number.isFinite(hits) && name)
|
|
39
|
+
cov.functionHits.set(name, (cov.functionHits.get(name) ?? 0) + hits);
|
|
40
|
+
}
|
|
41
|
+
else if (line.startsWith("BRDA:")) {
|
|
42
|
+
const parts = line.slice(5).split(",");
|
|
43
|
+
if (parts.length !== 4)
|
|
44
|
+
return;
|
|
45
|
+
const key = `${parts[0]},${parts[1]},${parts[2]}`;
|
|
46
|
+
const raw = parts[3];
|
|
47
|
+
const hits = raw === "-" ? 0 : parseInt(raw, 10);
|
|
48
|
+
if (Number.isFinite(hits))
|
|
49
|
+
cov.branches.set(key, (cov.branches.get(key) ?? 0) + hits);
|
|
50
|
+
}
|
|
51
|
+
else if (line.startsWith("DA:")) {
|
|
52
|
+
const comma = line.indexOf(",", 3);
|
|
53
|
+
if (comma === -1)
|
|
54
|
+
return;
|
|
55
|
+
const lineNum = parseInt(line.slice(3, comma), 10);
|
|
56
|
+
const hits = parseInt(line.slice(comma + 1), 10);
|
|
57
|
+
if (Number.isFinite(lineNum) && Number.isFinite(hits))
|
|
58
|
+
cov.lines.set(lineNum, (cov.lines.get(lineNum) ?? 0) + hits);
|
|
59
|
+
}
|
|
60
|
+
// TN:, LF:, LH:, FNF:, FNH:, BRF:, BRH: are summary lines — recomputed by toLcovFull
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parses full-fidelity LCOV, preserving FN/FNDA/BRDA/DA records.
|
|
64
|
+
* Hits from repeated records for the same line/function/branch are summed.
|
|
65
|
+
* Summary records (TN/LF/LH/FNF/FNH/BRF/BRH) are skipped and recomputed by toLcovFull.
|
|
66
|
+
*/
|
|
67
|
+
export function parseLcovFull(text, stripPrefixes = []) {
|
|
68
|
+
const result = new Map();
|
|
69
|
+
let current = null;
|
|
70
|
+
let start = 0;
|
|
71
|
+
while (start < text.length) {
|
|
72
|
+
let end = text.indexOf("\n", start);
|
|
73
|
+
if (end === -1)
|
|
74
|
+
end = text.length;
|
|
75
|
+
let lineEnd = end;
|
|
76
|
+
while (lineEnd > start) {
|
|
77
|
+
const c = text.charCodeAt(lineEnd - 1);
|
|
78
|
+
if (c === 32 || c === 9 || c === 13) {
|
|
79
|
+
lineEnd--;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (text.startsWith("SF:", start)) {
|
|
85
|
+
current = getOrCreate(result, normalizeSfPath(text.slice(start + 3, lineEnd), stripPrefixes));
|
|
86
|
+
}
|
|
87
|
+
else if (lineEnd - start === 13 && text.startsWith("end_of_record", start)) {
|
|
88
|
+
current = null;
|
|
89
|
+
}
|
|
90
|
+
else if (current !== null) {
|
|
91
|
+
applyRecord(text.slice(start, lineEnd), current);
|
|
92
|
+
}
|
|
93
|
+
start = end + 1;
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/** Merges multiple FullLcovData maps by summing all hit counts per file/line/function/branch. */
|
|
98
|
+
export function mergeLcovFull(reports) {
|
|
99
|
+
const merged = new Map();
|
|
100
|
+
for (const report of reports) {
|
|
101
|
+
for (const [file, cov] of report) {
|
|
102
|
+
const target = merged.get(file);
|
|
103
|
+
if (target === undefined) {
|
|
104
|
+
merged.set(file, {
|
|
105
|
+
functions: new Map(cov.functions),
|
|
106
|
+
functionHits: new Map(cov.functionHits),
|
|
107
|
+
branches: new Map(cov.branches),
|
|
108
|
+
lines: new Map(cov.lines),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
for (const [name, def] of cov.functions) {
|
|
113
|
+
if (!target.functions.has(name))
|
|
114
|
+
target.functions.set(name, def);
|
|
115
|
+
}
|
|
116
|
+
for (const [name, hits] of cov.functionHits)
|
|
117
|
+
target.functionHits.set(name, (target.functionHits.get(name) ?? 0) + hits);
|
|
118
|
+
for (const [key, hits] of cov.branches)
|
|
119
|
+
target.branches.set(key, (target.branches.get(key) ?? 0) + hits);
|
|
120
|
+
for (const [lineNum, hits] of cov.lines)
|
|
121
|
+
target.lines.set(lineNum, (target.lines.get(lineNum) ?? 0) + hits);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return merged;
|
|
126
|
+
}
|
|
127
|
+
/** Serializes FullLcovData to LCOV text with recomputed FNF/FNH/BRF/BRH/LF/LH summaries. */
|
|
128
|
+
export function toLcovFull(lcov) {
|
|
129
|
+
if (lcov.size === 0)
|
|
130
|
+
return "";
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const [file, cov] of [...lcov.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
133
|
+
out.push("TN:", `SF:${file}`);
|
|
134
|
+
const fns = [...cov.functions.values()].sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
|
|
135
|
+
for (const fn of fns)
|
|
136
|
+
out.push(`FN:${fn.line},${fn.name}`);
|
|
137
|
+
const fnHits = [...cov.functionHits.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
138
|
+
for (const [name, hits] of fnHits)
|
|
139
|
+
out.push(`FNDA:${hits},${name}`);
|
|
140
|
+
out.push(`FNF:${cov.functions.size}`);
|
|
141
|
+
out.push(`FNH:${[...cov.functionHits.values()].filter((h) => h > 0).length}`);
|
|
142
|
+
const brs = [...cov.branches.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
143
|
+
for (const [key, hits] of brs)
|
|
144
|
+
out.push(`BRDA:${key},${hits}`);
|
|
145
|
+
out.push(`BRF:${cov.branches.size}`);
|
|
146
|
+
out.push(`BRH:${[...cov.branches.values()].filter((h) => h > 0).length}`);
|
|
147
|
+
const lines = [...cov.lines.entries()].sort(([a], [b]) => a - b);
|
|
148
|
+
for (const [lineNum, hits] of lines)
|
|
149
|
+
out.push(`DA:${lineNum},${hits}`);
|
|
150
|
+
out.push(`LF:${cov.lines.size}`);
|
|
151
|
+
out.push(`LH:${[...cov.lines.values()].filter((h) => h > 0).length}`);
|
|
152
|
+
out.push("end_of_record");
|
|
153
|
+
}
|
|
154
|
+
return `${out.join("\n")}\n`;
|
|
155
|
+
}
|
|
@@ -64,9 +64,16 @@ export function lcovBufferToIstanbul(lcov, stripPrefixes) {
|
|
|
64
64
|
continue;
|
|
65
65
|
const fileCov = coverage[filePath]; // filePath was validated against coverage when SF: was processed
|
|
66
66
|
if (line.startsWith("DA:")) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
67
|
+
// Optimization: Instead of using `split(",")`, we use `indexOf` and `slice`
|
|
68
|
+
// to avoid allocating intermediate arrays, reducing garbage collection pauses.
|
|
69
|
+
const rest = line.slice(3);
|
|
70
|
+
const commaIdx = rest.indexOf(",");
|
|
71
|
+
if (commaIdx === -1)
|
|
72
|
+
continue;
|
|
73
|
+
const lineNoStr = rest.slice(0, commaIdx);
|
|
74
|
+
const hitsStr = rest.slice(commaIdx + 1);
|
|
75
|
+
const l = Number.parseInt(lineNoStr, 10);
|
|
76
|
+
const h = Number.parseInt(hitsStr, 10);
|
|
70
77
|
if (!Number.isInteger(l) || !Number.isInteger(h))
|
|
71
78
|
continue;
|
|
72
79
|
const key = String(l);
|
|
@@ -112,11 +119,25 @@ export function lcovBufferToIstanbul(lcov, stripPrefixes) {
|
|
|
112
119
|
}
|
|
113
120
|
}
|
|
114
121
|
else if (line.startsWith("BRDA:")) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
// Optimization: Instead of using `split(",")`, we use `indexOf` and `slice`
|
|
123
|
+
// to avoid allocating intermediate arrays, reducing garbage collection pauses.
|
|
124
|
+
const rest = line.slice(5);
|
|
125
|
+
let comma1 = rest.indexOf(",");
|
|
126
|
+
if (comma1 === -1)
|
|
127
|
+
continue;
|
|
128
|
+
let comma2 = rest.indexOf(",", comma1 + 1);
|
|
129
|
+
if (comma2 === -1)
|
|
130
|
+
continue;
|
|
131
|
+
let comma3 = rest.indexOf(",", comma2 + 1);
|
|
132
|
+
if (comma3 === -1)
|
|
133
|
+
continue;
|
|
134
|
+
const lineNoStr = rest.slice(0, comma1);
|
|
135
|
+
const blockId = rest.slice(comma1 + 1, comma2);
|
|
136
|
+
const branchId = rest.slice(comma2 + 1, comma3);
|
|
137
|
+
const comma4 = rest.indexOf(",", comma3 + 1);
|
|
138
|
+
const takenStr = comma4 === -1 ? rest.slice(comma3 + 1) : rest.slice(comma3 + 1, comma4);
|
|
139
|
+
const lineNo = Number.parseInt(lineNoStr, 10);
|
|
140
|
+
const taken = takenStr === "-" ? 0 : Number.parseInt(takenStr, 10);
|
|
120
141
|
if (!Number.isInteger(lineNo) || !blockId || !branchId || !Number.isInteger(taken))
|
|
121
142
|
continue;
|
|
122
143
|
const blockKey = `${lineNo}-${blockId}`;
|
|
@@ -140,7 +161,10 @@ export function lcovBufferToIstanbul(lcov, stripPrefixes) {
|
|
|
140
161
|
for (const [fp, blocks] of fileBranches) {
|
|
141
162
|
const fileCov = coverage[fp]; // fp was validated against coverage when added to fileBranches
|
|
142
163
|
for (const [blockKey, branches] of blocks) {
|
|
143
|
-
|
|
164
|
+
// blockKey is always "N-blockId". Optimization: avoid split("-")
|
|
165
|
+
const dashIdx = blockKey.indexOf("-");
|
|
166
|
+
const lineNoStr = blockKey.slice(0, dashIdx);
|
|
167
|
+
const lineNo = Number.parseInt(lineNoStr, 10);
|
|
144
168
|
const branchLoc = loc(lineNo);
|
|
145
169
|
const sorted = [...branches.entries()].sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }));
|
|
146
170
|
fileCov.branchMap[blockKey] = {
|
package/dist/src/report.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { CoverageCheckResult } from "./types.mts";
|
|
2
2
|
export declare const COMMENT_MARKER = "<!-- coverage-check -->";
|
|
3
|
-
export declare function collapseRanges(lines: number[]): string;
|
|
3
|
+
export declare function collapseRanges(lines: number[], prefix?: string): string;
|
|
4
4
|
export declare function renderFailureComment(result: CoverageCheckResult, runUrl: string, now?: string): string;
|
package/dist/src/report.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const COMMENT_MARKER = "<!-- coverage-check -->";
|
|
2
|
-
export function collapseRanges(lines) {
|
|
2
|
+
export function collapseRanges(lines, prefix = "L") {
|
|
3
3
|
if (lines.length === 0)
|
|
4
4
|
return "";
|
|
5
5
|
const sorted = [...lines].sort((a, b) => a - b);
|
|
@@ -12,12 +12,12 @@ export function collapseRanges(lines) {
|
|
|
12
12
|
end = n;
|
|
13
13
|
}
|
|
14
14
|
else {
|
|
15
|
-
ranges.push(start === end ?
|
|
15
|
+
ranges.push(start === end ? `${prefix}${start}` : `${prefix}${start}-${end}`);
|
|
16
16
|
start = n;
|
|
17
17
|
end = n;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
ranges.push(start === end ?
|
|
20
|
+
ranges.push(start === end ? `${prefix}${start}` : `${prefix}${start}-${end}`);
|
|
21
21
|
return ranges.join(", ");
|
|
22
22
|
}
|
|
23
23
|
function pct(bucket) {
|
package/dist/src/rules.d.mts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type { CoverageRule } from "./types.mts";
|
|
1
|
+
import type { CoverageRule, DiffLines } from "./types.mts";
|
|
2
2
|
export declare function loadRules(rulesPath: string): CoverageRule[];
|
|
3
3
|
/** Returns the first matching rule for a repo-root-relative file path, or null. */
|
|
4
4
|
export declare function matchRule(file: string, rules: CoverageRule[]): CoverageRule | null;
|
|
5
|
+
/**
|
|
6
|
+
* Returns the set of rules that have at least one changed file in the diff.
|
|
7
|
+
* Used by `--drop-only-changed-areas` to scope the coverage-drop gate.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildChangedRules(diff: DiffLines, rules: CoverageRule[]): Set<CoverageRule>;
|
package/dist/src/rules.mjs
CHANGED
|
@@ -44,3 +44,10 @@ export function matchRule(file, rules) {
|
|
|
44
44
|
}
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns the set of rules that have at least one changed file in the diff.
|
|
49
|
+
* Used by `--drop-only-changed-areas` to scope the coverage-drop gate.
|
|
50
|
+
*/
|
|
51
|
+
export function buildChangedRules(diff, rules) {
|
|
52
|
+
return new Set([...diff.keys()].map((f) => matchRule(f, rules)).filter((r) => r !== null));
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coverage-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Patch-coverage gate: checks that newly added lines meet per-path coverage thresholds. Supports per-suite LCOV accumulation for conditional CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan Ong",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@types/node": "^25.8.0",
|
|
41
41
|
"@vitest/coverage-v8": "^4.1.6",
|
|
42
42
|
"husky": "^9.1.7",
|
|
43
|
-
"oxfmt": "^0.
|
|
43
|
+
"oxfmt": "^0.52.0",
|
|
44
44
|
"oxlint": "^1.65.0",
|
|
45
45
|
"typescript": "^6.0.3",
|
|
46
46
|
"vitest": "^4.1.6"
|