supercov 0.0.5 → 0.0.6
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 +28 -5
- package/dist/agentJson.d.ts +44 -0
- package/dist/agentJson.d.ts.map +1 -0
- package/dist/agentJson.js +84 -0
- package/dist/agentJson.js.map +1 -0
- package/dist/cli.js +22 -17
- package/dist/cli.js.map +1 -1
- package/dist/instrumenter.d.ts.map +1 -1
- package/dist/instrumenter.js +20 -2
- package/dist/instrumenter.js.map +1 -1
- package/dist/nodeTest.d.ts.map +1 -1
- package/dist/nodeTest.js +3 -1
- package/dist/nodeTest.js.map +1 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +11 -14
- package/dist/project.js.map +1 -1
- package/dist/query.d.ts +36 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +252 -101
- package/dist/query.js.map +1 -1
- package/dist/queryCache.d.ts +15 -0
- package/dist/queryCache.d.ts.map +1 -0
- package/dist/queryCache.js +95 -0
- package/dist/queryCache.js.map +1 -0
- package/dist/runtime.d.ts +7 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +91 -10
- package/dist/runtime.js.map +1 -1
- package/dist/vitest.js +5 -1
- package/dist/vitest.js.map +1 -1
- package/package.json +3 -3
package/dist/query.js
CHANGED
|
@@ -3,11 +3,15 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { coverageSummaryForTests, isIndependencePair } from "./analyze.js";
|
|
5
5
|
import { EVIDENCE_ARCHIVE_SCHEMA_VERSION } from "./evidenceArchive.js";
|
|
6
|
-
import {
|
|
6
|
+
import { analyzeCoverageArchiveCached, readCoverageQueryIndex, } from "./queryCache.js";
|
|
7
7
|
import { compareRunIntegrity, createRunIntegrity } from "./integrity.js";
|
|
8
8
|
import { discoverCoverageProject } from "./project.js";
|
|
9
|
-
|
|
9
|
+
import { agentPagination, agentSuccessJson, SupercovError, } from "./agentJson.js";
|
|
10
|
+
function parseOptions(command, args) {
|
|
10
11
|
const options = {
|
|
12
|
+
command: command === "runs" || command === "diff" || command === "help"
|
|
13
|
+
? command
|
|
14
|
+
: `coverage.${command}`,
|
|
11
15
|
limit: 20,
|
|
12
16
|
offset: 0,
|
|
13
17
|
json: false,
|
|
@@ -20,36 +24,58 @@ function parseOptions(args) {
|
|
|
20
24
|
const value = args[index];
|
|
21
25
|
if (value === "--json")
|
|
22
26
|
options.json = true;
|
|
23
|
-
else if (value === "--run")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
else if (value === "--run") {
|
|
28
|
+
const run = args[++index];
|
|
29
|
+
if (!run)
|
|
30
|
+
throw new SupercovError("INVALID_ARGUMENT", "--run requires a run ID");
|
|
31
|
+
options.run = run;
|
|
32
|
+
}
|
|
33
|
+
else if (value === "--kind") {
|
|
34
|
+
const kind = args[++index]?.toLowerCase();
|
|
35
|
+
if (!kind)
|
|
36
|
+
throw new SupercovError("INVALID_ARGUMENT", "--kind requires a test kind");
|
|
37
|
+
options.kind = kind;
|
|
38
|
+
}
|
|
39
|
+
else if (value === "--runner") {
|
|
40
|
+
const runner = args[++index]?.toLowerCase();
|
|
41
|
+
if (!runner)
|
|
42
|
+
throw new SupercovError("INVALID_ARGUMENT", "--runner requires a runner name");
|
|
43
|
+
options.runner = runner;
|
|
44
|
+
}
|
|
29
45
|
else if (value === "--target") {
|
|
30
46
|
const target = Number(args[++index]);
|
|
31
47
|
if (!Number.isFinite(target) || target < 0 || target > 100)
|
|
32
|
-
throw new
|
|
48
|
+
throw new SupercovError("INVALID_ARGUMENT", "--target must be between 0 and 100");
|
|
33
49
|
options.target = target;
|
|
34
50
|
}
|
|
35
51
|
else if (value === "--metric") {
|
|
36
52
|
const metric = args[++index]?.toLowerCase();
|
|
37
53
|
if (!metric || !["all", "lines", "statements", "functions", "branches", "mcdc"].includes(metric))
|
|
38
|
-
throw new
|
|
54
|
+
throw new SupercovError("INVALID_ARGUMENT", "--metric must be all, lines, statements, functions, branches, or mcdc");
|
|
39
55
|
options.metric = metric;
|
|
40
56
|
}
|
|
41
57
|
else if (value === "--filter") {
|
|
42
58
|
const filter = args[++index]?.toLowerCase();
|
|
43
59
|
if (filter !== "all" && filter !== "passed" && filter !== "failed")
|
|
44
|
-
throw new
|
|
60
|
+
throw new SupercovError("INVALID_ARGUMENT", "--filter must be all, passed, or failed");
|
|
45
61
|
options.filter = filter;
|
|
46
62
|
}
|
|
47
|
-
else if (value === "--limit")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
else if (value === "--limit") {
|
|
64
|
+
const limit = Number(args[++index]);
|
|
65
|
+
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
66
|
+
throw new SupercovError("INVALID_ARGUMENT", "--limit must be a positive integer");
|
|
67
|
+
options.limit = limit;
|
|
68
|
+
}
|
|
69
|
+
else if (value === "--offset") {
|
|
70
|
+
const offset = Number(args[++index]);
|
|
71
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
72
|
+
throw new SupercovError("INVALID_ARGUMENT", "--offset must be a non-negative integer");
|
|
73
|
+
options.offset = offset;
|
|
74
|
+
}
|
|
51
75
|
else if (value.startsWith("--"))
|
|
52
|
-
throw new
|
|
76
|
+
throw new SupercovError("INVALID_ARGUMENT", `Unknown option: ${value}`, {
|
|
77
|
+
details: { option: value },
|
|
78
|
+
});
|
|
53
79
|
else
|
|
54
80
|
options.positional.push(value);
|
|
55
81
|
}
|
|
@@ -60,7 +86,7 @@ function filteredCoverage(report, options) {
|
|
|
60
86
|
return report;
|
|
61
87
|
const filtered = report.filters?.[options.filter];
|
|
62
88
|
if (!filtered) {
|
|
63
|
-
throw new
|
|
89
|
+
throw new SupercovError("FILTER_UNAVAILABLE", "This run does not contain outcome-filtered coverage. Create a new coverage run.");
|
|
64
90
|
}
|
|
65
91
|
return filtered;
|
|
66
92
|
}
|
|
@@ -94,24 +120,32 @@ function discoverRuns(root) {
|
|
|
94
120
|
}
|
|
95
121
|
return [...runs.values()].sort((left, right) => right.id.localeCompare(left.id));
|
|
96
122
|
}
|
|
97
|
-
function
|
|
98
|
-
return
|
|
123
|
+
function storedRunAnalysisOptions(run) {
|
|
124
|
+
return {
|
|
99
125
|
runId: run.id,
|
|
100
126
|
testExitCode: run.metadata?.testExitCode,
|
|
101
127
|
integrity: run.metadata?.integrity,
|
|
102
128
|
generatedAt: run.metadata?.startedAt,
|
|
103
|
-
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function analyzeStoredRun(run) {
|
|
132
|
+
return analyzeCoverageArchiveCached(run.evidencePath, storedRunAnalysisOptions(run));
|
|
133
|
+
}
|
|
134
|
+
function readStoredRunIndex(run) {
|
|
135
|
+
return readCoverageQueryIndex(run.evidencePath, storedRunAnalysisOptions(run));
|
|
104
136
|
}
|
|
105
|
-
function selectRun(root, selector, currentIntegrity) {
|
|
137
|
+
function selectRun(root, selector, currentIntegrity, quiet = false) {
|
|
106
138
|
const runs = discoverRuns(root);
|
|
107
139
|
if (runs.length === 0)
|
|
108
|
-
throw new
|
|
140
|
+
throw new SupercovError("NO_RUNS", "No local coverage runs. Run supercov first.");
|
|
109
141
|
const selected = !selector || selector === "latest"
|
|
110
142
|
? runs[0]
|
|
111
143
|
: (runs.find((run) => run.id === selector) ??
|
|
112
144
|
runs.find((run) => run.id.startsWith(selector)));
|
|
113
145
|
if (!selected)
|
|
114
|
-
throw new
|
|
146
|
+
throw new SupercovError("RUN_NOT_FOUND", `Coverage run not found: ${selector}`, {
|
|
147
|
+
details: { selector },
|
|
148
|
+
});
|
|
115
149
|
const report = analyzeStoredRun(selected);
|
|
116
150
|
if (currentIntegrity) {
|
|
117
151
|
const comparison = compareRunIntegrity(selected.metadata?.integrity ?? report.integrity, currentIntegrity);
|
|
@@ -120,7 +154,7 @@ function selectRun(root, selector, currentIntegrity) {
|
|
|
120
154
|
stale: comparison.stale,
|
|
121
155
|
staleReasons: comparison.reasons,
|
|
122
156
|
};
|
|
123
|
-
if (comparison.stale) {
|
|
157
|
+
if (comparison.stale && !quiet) {
|
|
124
158
|
console.error(`[supercov] stale run ${selected.id}: ${comparison.reasons.join(", ")}`);
|
|
125
159
|
}
|
|
126
160
|
}
|
|
@@ -137,8 +171,11 @@ function currentProjectIntegrity(root) {
|
|
|
137
171
|
function page(values, options) {
|
|
138
172
|
return values.slice(options.offset, options.offset + options.limit);
|
|
139
173
|
}
|
|
140
|
-
function output(value, options, text) {
|
|
141
|
-
|
|
174
|
+
function output(value, options, text, pagination) {
|
|
175
|
+
process.stdout.write(options.json ? agentSuccessJson(options.command, value, pagination) : `${text}\n`);
|
|
176
|
+
}
|
|
177
|
+
function queryPagination(total, returned, options) {
|
|
178
|
+
return agentPagination(options.offset, options.limit, returned, total);
|
|
142
179
|
}
|
|
143
180
|
function pct(value) {
|
|
144
181
|
return `${value.toFixed(2)}%`;
|
|
@@ -238,7 +275,7 @@ export function minimumTestSet(report, target = 100, metric = "all") {
|
|
|
238
275
|
const unattributed = report.tests.filter((test) => test.role === "background" &&
|
|
239
276
|
(test.hits.length > 0 || test.decisions.some((decision) => decision.vectors.length > 0)));
|
|
240
277
|
if (unattributed.length > 0) {
|
|
241
|
-
throw new
|
|
278
|
+
throw new SupercovError("UNATTRIBUTED_EVIDENCE", "Cannot minimize exactly: this coverage view contains background/unattributed evidence. Use a runner with exact test attribution or select a fully attributed coverage view.");
|
|
242
279
|
}
|
|
243
280
|
const candidateTests = report.tests
|
|
244
281
|
.filter((test) => test.role === "test")
|
|
@@ -265,7 +302,7 @@ export function minimumTestSet(report, target = 100, metric = "all") {
|
|
|
265
302
|
const percentage = (selectedMetric, summary) => selectedMetric === "mcdc" ? summary.conditionCoveragePct : summary[selectedMetric].percentage;
|
|
266
303
|
const impossible = metrics.find((selectedMetric) => percentage(selectedMetric, fullSummary) + 1e-9 < target);
|
|
267
304
|
if (impossible)
|
|
268
|
-
throw new
|
|
305
|
+
throw new SupercovError("TARGET_UNREACHABLE", `The full selected test view reaches only ${percentage(impossible, fullSummary).toFixed(2)}% ${impossible}; target ${target}% is impossible`, { details: { metric: impossible, target, reachable: percentage(impossible, fullSummary) } });
|
|
269
306
|
let exploredStates = 0;
|
|
270
307
|
const seen = new Set();
|
|
271
308
|
const search = (selected, skipped, skippedByMetric) => {
|
|
@@ -351,7 +388,9 @@ function selectedTestIds(report, options) {
|
|
|
351
388
|
]
|
|
352
389
|
.filter(Boolean)
|
|
353
390
|
.join(", ");
|
|
354
|
-
throw new
|
|
391
|
+
throw new SupercovError("TEST_FILTER_EMPTY", `No tests match ${filter}`, {
|
|
392
|
+
details: { kind: options.kind, runner: options.runner },
|
|
393
|
+
});
|
|
355
394
|
}
|
|
356
395
|
return new Set(selected.map((test) => test.id));
|
|
357
396
|
}
|
|
@@ -423,6 +462,13 @@ function filterLabel(options) {
|
|
|
423
462
|
.filter(Boolean)
|
|
424
463
|
.join(", ");
|
|
425
464
|
}
|
|
465
|
+
function queryFilters(options) {
|
|
466
|
+
return {
|
|
467
|
+
outcome: options.filter,
|
|
468
|
+
kind: options.kind ?? null,
|
|
469
|
+
runner: options.runner ?? null,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
426
472
|
function attribution(report, selected) {
|
|
427
473
|
const phases = selected
|
|
428
474
|
? report.phases.filter((phase) => selected.has(phase.test))
|
|
@@ -434,7 +480,7 @@ function attribution(report, selected) {
|
|
|
434
480
|
serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
|
|
435
481
|
};
|
|
436
482
|
}
|
|
437
|
-
function fileGaps(report, selected) {
|
|
483
|
+
export function fileGaps(report, selected) {
|
|
438
484
|
const files = new Map();
|
|
439
485
|
const get = (file) => {
|
|
440
486
|
const existing = files.get(file);
|
|
@@ -447,6 +493,8 @@ function fileGaps(report, selected) {
|
|
|
447
493
|
uncoveredFunctions: 0,
|
|
448
494
|
missingBranches: 0,
|
|
449
495
|
missingMcdcConditions: 0,
|
|
496
|
+
measurementLimitations: 0,
|
|
497
|
+
limitationKinds: [],
|
|
450
498
|
coveredByOtherTests: {
|
|
451
499
|
lines: 0,
|
|
452
500
|
statements: 0,
|
|
@@ -508,37 +556,73 @@ function fileGaps(report, selected) {
|
|
|
508
556
|
}
|
|
509
557
|
}
|
|
510
558
|
}
|
|
559
|
+
for (const limitation of report.limitations ?? []) {
|
|
560
|
+
const gap = get(limitation.file);
|
|
561
|
+
gap.measurementLimitations += 1;
|
|
562
|
+
if (!gap.limitationKinds.includes(limitation.kind))
|
|
563
|
+
gap.limitationKinds.push(limitation.kind);
|
|
564
|
+
}
|
|
511
565
|
for (const gap of files.values()) {
|
|
566
|
+
gap.limitationKinds.sort();
|
|
512
567
|
gap.score =
|
|
513
568
|
gap.uncoveredLines +
|
|
514
569
|
gap.uncoveredFunctions * 2 +
|
|
515
570
|
gap.missingBranches * 2 +
|
|
516
|
-
gap.missingMcdcConditions * 3
|
|
571
|
+
gap.missingMcdcConditions * 3 +
|
|
572
|
+
gap.measurementLimitations * 3;
|
|
517
573
|
}
|
|
518
574
|
return [...files.values()].sort((left, right) => right.score - left.score || left.file.localeCompare(right.file));
|
|
519
575
|
}
|
|
520
576
|
function findFile(report, selector) {
|
|
521
|
-
const files = [
|
|
577
|
+
const files = [
|
|
578
|
+
...new Set([
|
|
579
|
+
...report.lines.map((line) => line.file),
|
|
580
|
+
...(report.limitations ?? []).map((limitation) => limitation.file),
|
|
581
|
+
]),
|
|
582
|
+
];
|
|
522
583
|
if (files.includes(selector))
|
|
523
584
|
return selector;
|
|
524
585
|
const matches = files.filter((file) => file.includes(selector));
|
|
525
586
|
if (matches.length === 1)
|
|
526
587
|
return matches[0];
|
|
527
588
|
if (matches.length === 0)
|
|
528
|
-
throw new
|
|
529
|
-
|
|
589
|
+
throw new SupercovError("SOURCE_NOT_FOUND", `Source file not found: ${selector}`, {
|
|
590
|
+
details: { selector },
|
|
591
|
+
});
|
|
592
|
+
throw new SupercovError("AMBIGUOUS_SELECTOR", `Ambiguous file selector: ${matches.join(", ")}`, {
|
|
593
|
+
details: { selector, matches },
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
export function coverageMeasurement(report) {
|
|
597
|
+
const limitations = report.limitations ?? [];
|
|
598
|
+
const byKind = {
|
|
599
|
+
"dynamic-code": 0,
|
|
600
|
+
"semantic-safety": 0,
|
|
601
|
+
"source-scope": 0,
|
|
602
|
+
};
|
|
603
|
+
for (const limitation of limitations)
|
|
604
|
+
byKind[limitation.kind] += 1;
|
|
605
|
+
return {
|
|
606
|
+
complete: limitations.length === 0,
|
|
607
|
+
limitations: limitations.length,
|
|
608
|
+
// Every current limitation removes source from the measured denominator.
|
|
609
|
+
blocking: limitations.length,
|
|
610
|
+
files: new Set(limitations.map((limitation) => limitation.file)).size,
|
|
611
|
+
byKind,
|
|
612
|
+
};
|
|
530
613
|
}
|
|
531
614
|
function locationSelector(selector) {
|
|
532
615
|
const match = /^(.*):(\d+)(?::\d+)?$/.exec(selector);
|
|
533
616
|
if (!match)
|
|
534
|
-
throw new
|
|
617
|
+
throw new SupercovError("INVALID_ARGUMENT", "Expected <source-file>:<line>", {
|
|
618
|
+
details: { selector },
|
|
619
|
+
});
|
|
535
620
|
return { file: match[1], line: Number(match[2]) };
|
|
536
621
|
}
|
|
537
622
|
function vectorText(values, outcome) {
|
|
538
623
|
return `${values.map((value) => (value === null ? "-" : value ? "T" : "F")).join("")} -> ${outcome ? "T" : "F"}`;
|
|
539
624
|
}
|
|
540
|
-
|
|
541
|
-
console.log(`Agent-oriented local coverage queries:
|
|
625
|
+
const helpText = `Agent-oriented local coverage queries:
|
|
542
626
|
supercov runs [--limit N] [--json]
|
|
543
627
|
supercov runs <run-id> coverage [--filter all|passed|failed] [--kind e2e] [--runner playwright] [--json]
|
|
544
628
|
supercov runs <run-id> coverage kinds [--json]
|
|
@@ -559,7 +643,30 @@ function help() {
|
|
|
559
643
|
Use "latest" as <run-id> to query the newest local run.
|
|
560
644
|
|
|
561
645
|
Create a run with:
|
|
562
|
-
supercov -- <test command
|
|
646
|
+
supercov -- <test command>`;
|
|
647
|
+
function help(options) {
|
|
648
|
+
if (options.json) {
|
|
649
|
+
return output({
|
|
650
|
+
usage: "supercov -- <test command>",
|
|
651
|
+
runSelector: "Use latest as <run-id> to query the newest local run.",
|
|
652
|
+
queryCommands: [
|
|
653
|
+
"runs",
|
|
654
|
+
"runs <run-id> coverage",
|
|
655
|
+
"runs <run-id> coverage kinds",
|
|
656
|
+
"runs <run-id> coverage runners",
|
|
657
|
+
"runs <run-id> coverage scope",
|
|
658
|
+
"runs <run-id> coverage files",
|
|
659
|
+
"runs <run-id> coverage gaps",
|
|
660
|
+
"runs <run-id> coverage file <source-file>",
|
|
661
|
+
"runs <run-id> coverage decision <id|source-file:line>",
|
|
662
|
+
"runs <run-id> coverage covers <source-file:line>",
|
|
663
|
+
"runs <run-id> coverage test <id|name-fragment>",
|
|
664
|
+
"runs <run-id> coverage minimize",
|
|
665
|
+
"diff <older-run> <newer-run>",
|
|
666
|
+
],
|
|
667
|
+
}, options, helpText);
|
|
668
|
+
}
|
|
669
|
+
console.log(helpText);
|
|
563
670
|
}
|
|
564
671
|
/** Resolve the instance-first coverage resource syntax. */
|
|
565
672
|
export function resolveCoverageQueryInvocation(command, args) {
|
|
@@ -587,7 +694,7 @@ export function resolveCoverageQueryInvocation(command, args) {
|
|
|
587
694
|
"minimize",
|
|
588
695
|
]);
|
|
589
696
|
if (!coverageCommands.has(child)) {
|
|
590
|
-
throw new
|
|
697
|
+
throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${child}. Try supercov help.`, { details: { command: child } });
|
|
591
698
|
}
|
|
592
699
|
return {
|
|
593
700
|
command: child,
|
|
@@ -597,20 +704,22 @@ export function resolveCoverageQueryInvocation(command, args) {
|
|
|
597
704
|
export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
598
705
|
const resolved = resolveCoverageQueryInvocation(command, args);
|
|
599
706
|
command = resolved.command;
|
|
600
|
-
const options = parseOptions(resolved.args);
|
|
707
|
+
const options = parseOptions(command, resolved.args);
|
|
601
708
|
if (command === "help")
|
|
602
|
-
return help();
|
|
709
|
+
return help(options);
|
|
603
710
|
const currentIntegrity = currentProjectIntegrity(root);
|
|
604
711
|
if (command === "runs") {
|
|
605
712
|
const availableRuns = discoverRuns(root);
|
|
606
713
|
const runs = page(availableRuns, options).map((run) => {
|
|
607
|
-
const
|
|
714
|
+
const cached = readStoredRunIndex(run);
|
|
715
|
+
const report = cached ? filteredCoverage(cached, options) : undefined;
|
|
608
716
|
return {
|
|
609
717
|
id: run.id,
|
|
610
|
-
generatedAt: report.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
718
|
+
generatedAt: report?.generatedAt ?? run.metadata?.startedAt,
|
|
719
|
+
coverageIndexed: Boolean(report),
|
|
720
|
+
lines: report?.summary.lines.percentage ?? null,
|
|
721
|
+
branches: report?.summary.branches.percentage ?? null,
|
|
722
|
+
mcdc: report?.summary.conditionCoveragePct ?? null,
|
|
614
723
|
command: run.metadata?.command,
|
|
615
724
|
durationMs: run.metadata?.durationMs,
|
|
616
725
|
timings: run.metadata?.timings,
|
|
@@ -624,18 +733,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
624
733
|
});
|
|
625
734
|
const runsBase = `npx supercov runs${options.filter !== "all" ? ` --filter ${options.filter}` : ""}`;
|
|
626
735
|
const runsNext = nextPageCommand(runsBase, availableRuns.length, runs.length, options);
|
|
627
|
-
return output(runs, options, runs
|
|
628
|
-
.map((run) => `${run.id} lines ${pct(run.lines
|
|
736
|
+
return output({ filters: queryFilters(options), runs }, options, runs
|
|
737
|
+
.map((run) => `${run.id} ${run.coverageIndexed ? `lines ${pct(run.lines)} branches ${pct(run.branches)} MC/DC ${pct(run.mcdc)}` : "coverage not indexed"}${run.stale ? ` STALE (${run.reasons.join(", ")})` : ""}`)
|
|
629
738
|
.join("\n") +
|
|
630
739
|
`\n${pageLabel(availableRuns.length, runs.length, options)}` +
|
|
631
|
-
(runsNext ? `\nnext page: ${runsNext}` : ""));
|
|
740
|
+
(runsNext ? `\nnext page: ${runsNext}` : ""), queryPagination(availableRuns.length, runs.length, options));
|
|
632
741
|
}
|
|
633
742
|
if (command === "diff") {
|
|
634
743
|
const [olderSelector, newerSelector] = options.positional;
|
|
635
744
|
if (!olderSelector || !newerSelector)
|
|
636
|
-
throw new
|
|
637
|
-
const olderSelected = selectRun(root, olderSelector, currentIntegrity);
|
|
638
|
-
const newerSelected = selectRun(root, newerSelector, currentIntegrity);
|
|
745
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov diff <older-run> <newer-run>");
|
|
746
|
+
const olderSelected = selectRun(root, olderSelector, currentIntegrity, options.json);
|
|
747
|
+
const newerSelected = selectRun(root, newerSelector, currentIntegrity, options.json);
|
|
639
748
|
const older = {
|
|
640
749
|
...olderSelected,
|
|
641
750
|
report: filteredCoverage(olderSelected.report, options),
|
|
@@ -690,6 +799,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
690
799
|
.map(([, label]) => label)
|
|
691
800
|
.sort();
|
|
692
801
|
const result = {
|
|
802
|
+
filters: queryFilters(options),
|
|
693
803
|
older: older.run.id,
|
|
694
804
|
newer: newer.run.id,
|
|
695
805
|
delta: {
|
|
@@ -728,9 +838,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
728
838
|
.filter(Boolean)
|
|
729
839
|
.join(" ");
|
|
730
840
|
const diffNext = nextPageCommand(diffBase, diffTotal, diffReturned, options);
|
|
731
|
-
return output(result, options, `${older.run.id} -> ${newer.run.id}\nlines ${result.delta.lines >= 0 ? "+" : ""}${result.delta.lines}pp, branches ${result.delta.branches >= 0 ? "+" : ""}${result.delta.branches}pp, MC/DC ${result.delta.mcdc >= 0 ? "+" : ""}${result.delta.mcdc}pp\ngained: ${gainedLines.length} lines, ${gainedBranches.length} branches, ${gainedMcdc.length} MC/DC conditions\nlost: ${lostLines.length} lines, ${lostBranches.length} branches, ${lostMcdc.length} MC/DC conditions\n${result.gained.lines.map((line) => `+ line ${line}`).join("\n")}${result.gained.branches.length ? `\n${result.gained.branches.map((item) => `+ branch ${item}`).join("\n")}` : ""}${result.gained.mcdc.length ? `\n${result.gained.mcdc.map((item) => `+ MC/DC ${item}`).join("\n")}` : ""}\n${pageLabel(diffTotal, diffReturned, options)} per category${diffNext ? `\nnext page: ${diffNext}` : ""}
|
|
841
|
+
return output(result, options, `${older.run.id} -> ${newer.run.id}\nlines ${result.delta.lines >= 0 ? "+" : ""}${result.delta.lines}pp, branches ${result.delta.branches >= 0 ? "+" : ""}${result.delta.branches}pp, MC/DC ${result.delta.mcdc >= 0 ? "+" : ""}${result.delta.mcdc}pp\ngained: ${gainedLines.length} lines, ${gainedBranches.length} branches, ${gainedMcdc.length} MC/DC conditions\nlost: ${lostLines.length} lines, ${lostBranches.length} branches, ${lostMcdc.length} MC/DC conditions\n${result.gained.lines.map((line) => `+ line ${line}`).join("\n")}${result.gained.branches.length ? `\n${result.gained.branches.map((item) => `+ branch ${item}`).join("\n")}` : ""}${result.gained.mcdc.length ? `\n${result.gained.mcdc.map((item) => `+ MC/DC ${item}`).join("\n")}` : ""}\n${pageLabel(diffTotal, diffReturned, options)} per category${diffNext ? `\nnext page: ${diffNext}` : ""}`, queryPagination(diffTotal, diffReturned, options));
|
|
732
842
|
}
|
|
733
|
-
const selectedRun = selectRun(root, options.run, currentIntegrity);
|
|
843
|
+
const selectedRun = selectRun(root, options.run, currentIntegrity, options.json);
|
|
734
844
|
const run = selectedRun.run;
|
|
735
845
|
const report = filteredCoverage(selectedRun.report, options);
|
|
736
846
|
const selectedTestSet = selectedTestIds(report, options);
|
|
@@ -739,6 +849,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
739
849
|
? coverageSummaryForTests(report, selectedTestSet)
|
|
740
850
|
: report.summary;
|
|
741
851
|
const gaps = fileGaps(report, selectedTestSet).filter((gap) => gap.score > 0);
|
|
852
|
+
const measurement = coverageMeasurement(report);
|
|
742
853
|
const selectedTests = report.tests.filter((test) => !selectedTestSet || selectedTestSet.has(test.id));
|
|
743
854
|
const testCount = selectedTests.filter((test) => (test.role ?? "test") === "test").length;
|
|
744
855
|
const setupCount = selectedTests.filter((test) => test.role === "setup").length;
|
|
@@ -748,18 +859,19 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
748
859
|
]));
|
|
749
860
|
const result = {
|
|
750
861
|
run: run.id,
|
|
751
|
-
|
|
752
|
-
...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
|
|
862
|
+
filters: queryFilters(options),
|
|
753
863
|
generatedAt: report.generatedAt,
|
|
754
864
|
valid: run.metadata?.testExitCode === 0,
|
|
755
865
|
stale: report.integrity?.stale ?? false,
|
|
756
866
|
staleReasons: report.integrity?.staleReasons ?? [],
|
|
757
|
-
structurallyComplete: summary.coverageComplete,
|
|
867
|
+
structurallyComplete: summary.coverageComplete && measurement.complete,
|
|
758
868
|
complete: options.filter === "passed" &&
|
|
759
869
|
run.metadata?.testExitCode === 0 &&
|
|
760
870
|
!report.integrity?.stale &&
|
|
761
|
-
summary.coverageComplete
|
|
871
|
+
summary.coverageComplete &&
|
|
872
|
+
measurement.complete,
|
|
762
873
|
coverage: summary,
|
|
874
|
+
measurement,
|
|
763
875
|
coverageByKind: report.coverageByKind,
|
|
764
876
|
coverageByRunner: report.coverageByRunner,
|
|
765
877
|
attribution: attribution(report, selectedTestSet),
|
|
@@ -776,6 +888,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
776
888
|
}
|
|
777
889
|
: {}),
|
|
778
890
|
filesWithGaps: gaps.length,
|
|
891
|
+
filesWithCoverageGaps: gaps.filter((gap) => gap.uncoveredLines > 0 ||
|
|
892
|
+
gap.uncoveredStatements > 0 ||
|
|
893
|
+
gap.uncoveredFunctions > 0 ||
|
|
894
|
+
gap.missingBranches > 0 ||
|
|
895
|
+
gap.missingMcdcConditions > 0).length,
|
|
896
|
+
filesWithMeasurementLimitations: measurement.files,
|
|
779
897
|
tests: testCount,
|
|
780
898
|
setups: setupCount,
|
|
781
899
|
testOutcomes,
|
|
@@ -789,7 +907,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
789
907
|
}
|
|
790
908
|
: undefined,
|
|
791
909
|
};
|
|
792
|
-
return output(result, options, `run ${run.id}${filterLabel(options) ? ` (${filterLabel(options)})` : ""}${run.metadata?.testExitCode !== 0 ? ` [INVALID: test exit ${run.metadata?.testExitCode ?? "unknown"}]` : ""}${report.integrity?.stale ? ` [STALE: ${(report.integrity.staleReasons ?? []).join(", ")}]` : ""}\nlines ${pct(summary.lines.percentage)} (${summary.lines.covered}/${summary.lines.total})\nbranches ${pct(summary.branches.percentage)} (${summary.branches.covered}/${summary.branches.total})\nMC/DC ${pct(summary.conditionCoveragePct)} (${summary.coveredConditions}/${summary.conditions})${!selectedTestSet ? `\nconfidence: ${report.lines.filter((line) => line.confidence?.level === "asserted").length} asserted lines, ${report.lines.filter((line) => line.confidence?.level === "action").length} action-linked, ${report.lines.filter((line) => line.confidence?.level === "executed").length} execution-only; ${report.decisions.reduce((total, decision) => total + decision.conditions.filter((condition) => condition.assertionCovered).length, 0)} assertion-linked MC/DC conditions` : ""}\n${testCount} test(s)${setupCount ? ` + ${setupCount} setup scope(s)` : ""}; outcomes ${Object.entries(testOutcomes).filter(([, count]) => count > 0).map(([outcome, count]) => `${outcome}=${count}`).join(", ") || "none"}; ${gaps.length} file(s) have
|
|
910
|
+
return output(result, options, `run ${run.id}${filterLabel(options) ? ` (${filterLabel(options)})` : ""}${run.metadata?.testExitCode !== 0 ? ` [INVALID: test exit ${run.metadata?.testExitCode ?? "unknown"}]` : ""}${report.integrity?.stale ? ` [STALE: ${(report.integrity.staleReasons ?? []).join(", ")}]` : ""}\nlines ${pct(summary.lines.percentage)} (${summary.lines.covered}/${summary.lines.total})\nbranches ${pct(summary.branches.percentage)} (${summary.branches.covered}/${summary.branches.total})\nMC/DC ${pct(summary.conditionCoveragePct)} (${summary.coveredConditions}/${summary.conditions})\nmeasurement: ${measurement.complete ? "complete" : `incomplete — ${measurement.blocking} blocking limitation(s) in ${measurement.files} file(s)`}${!selectedTestSet ? `\nconfidence: ${report.lines.filter((line) => line.confidence?.level === "asserted").length} asserted lines, ${report.lines.filter((line) => line.confidence?.level === "action").length} action-linked, ${report.lines.filter((line) => line.confidence?.level === "executed").length} execution-only; ${report.decisions.reduce((total, decision) => total + decision.conditions.filter((condition) => condition.assertionCovered).length, 0)} assertion-linked MC/DC conditions` : ""}\n${testCount} test(s)${setupCount ? ` + ${setupCount} setup scope(s)` : ""}; outcomes ${Object.entries(testOutcomes).filter(([, count]) => count > 0).map(([outcome, count]) => `${outcome}=${count}`).join(", ") || "none"}; ${gaps.length} file(s) have unresolved coverage or measurement gaps`);
|
|
793
911
|
}
|
|
794
912
|
if (command === "minimize") {
|
|
795
913
|
const solverReport = selectedTestSet
|
|
@@ -814,17 +932,30 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
814
932
|
const next = nextPageCommand(base, selectedDetails.length, selectedPage.length, options);
|
|
815
933
|
return output({
|
|
816
934
|
run: run.id,
|
|
935
|
+
filters: queryFilters(options),
|
|
817
936
|
...minimized,
|
|
818
937
|
selectedCount: selectedDetails.length,
|
|
819
938
|
totalCandidateTests: solverReport.tests.filter((test) => test.role === "test").length,
|
|
820
|
-
offset: options.offset,
|
|
821
939
|
tests: selectedPage,
|
|
822
|
-
}, options, `exact minimum ${selectedDetails.length}/${solverReport.tests.filter((test) => test.role === "test").length} test(s) for ${options.target}% ${options.metric === "all" ? "coverage across all measured metrics" : options.metric}; explored ${minimized.exploredStates} state(s)\nlines ${pct(minimized.summary.lines.percentage)}, statements ${pct(minimized.summary.statements.percentage)}, functions ${pct(minimized.summary.functions.percentage)}, branches ${pct(minimized.summary.branches.percentage)}, MC/DC ${pct(minimized.summary.conditionCoveragePct)}\n${selectedPage.map((test) => `${test.id} ${test.kind}/${test.runner} ${test.file ?? "unknown"} ${test.name}`).join("\n")}\n${pageLabel(selectedDetails.length, selectedPage.length, options)}${next ? `\nnext page: ${next}` : ""}
|
|
940
|
+
}, options, `exact minimum ${selectedDetails.length}/${solverReport.tests.filter((test) => test.role === "test").length} test(s) for ${options.target}% ${options.metric === "all" ? "coverage across all measured metrics" : options.metric}; explored ${minimized.exploredStates} state(s)\nlines ${pct(minimized.summary.lines.percentage)}, statements ${pct(minimized.summary.statements.percentage)}, functions ${pct(minimized.summary.functions.percentage)}, branches ${pct(minimized.summary.branches.percentage)}, MC/DC ${pct(minimized.summary.conditionCoveragePct)}\n${selectedPage.map((test) => `${test.id} ${test.kind}/${test.runner} ${test.file ?? "unknown"} ${test.name}`).join("\n")}\n${pageLabel(selectedDetails.length, selectedPage.length, options)}${next ? `\nnext page: ${next}` : ""}`, queryPagination(selectedDetails.length, selectedPage.length, options));
|
|
823
941
|
}
|
|
824
942
|
if (command === "scope") {
|
|
825
943
|
if (!report.scope)
|
|
826
|
-
throw new
|
|
827
|
-
const
|
|
944
|
+
throw new SupercovError("SCOPE_UNAVAILABLE", "This run does not contain a source-scope inventory.");
|
|
945
|
+
const limitationsByFile = new Map();
|
|
946
|
+
for (const limitation of report.limitations ?? []) {
|
|
947
|
+
const existing = limitationsByFile.get(limitation.file) ?? [];
|
|
948
|
+
existing.push(limitation);
|
|
949
|
+
limitationsByFile.set(limitation.file, existing);
|
|
950
|
+
}
|
|
951
|
+
const ordered = report.scope.entries.map((entry) => {
|
|
952
|
+
const limitations = limitationsByFile.get(entry.file) ?? [];
|
|
953
|
+
return {
|
|
954
|
+
...entry,
|
|
955
|
+
measurementLimitations: limitations.length,
|
|
956
|
+
limitationKinds: [...new Set(limitations.map((item) => item.kind))].sort(),
|
|
957
|
+
};
|
|
958
|
+
}).sort((left, right) => {
|
|
828
959
|
const rank = { ambiguous: 0, included: 1, excluded: 2 };
|
|
829
960
|
return rank[left.status] - rank[right.status] || left.file.localeCompare(right.file);
|
|
830
961
|
});
|
|
@@ -838,13 +969,13 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
838
969
|
};
|
|
839
970
|
return output({
|
|
840
971
|
run: run.id,
|
|
972
|
+
filters: queryFilters(options),
|
|
841
973
|
mode: report.scope.mode,
|
|
842
974
|
roots: report.scope.roots,
|
|
843
975
|
counts,
|
|
844
|
-
|
|
845
|
-
offset: options.offset,
|
|
976
|
+
measurement: coverageMeasurement(report),
|
|
846
977
|
entries: selectedEntries,
|
|
847
|
-
}, options, `mode ${report.scope.mode}; roots ${report.scope.roots.join(", ") || "none"}; included ${counts.included}, excluded ${counts.excluded}, ambiguous ${counts.ambiguous}\n${selectedEntries.map((entry) => `${entry.status.toUpperCase()} ${entry.file} ${entry.reason}${entry.packageRoot ? ` [package ${entry.packageRoot}]` : ""}`).join("\n")}\n${pageLabel(ordered.length, selectedEntries.length, options)}${next ? `\nnext page: ${next}` : ""}
|
|
978
|
+
}, options, `mode ${report.scope.mode}; roots ${report.scope.roots.join(", ") || "none"}; included ${counts.included}, excluded ${counts.excluded}, ambiguous ${counts.ambiguous}; measurement ${coverageMeasurement(report).complete ? "complete" : `${coverageMeasurement(report).blocking} blocking limitation(s)`}\n${selectedEntries.map((entry) => `${entry.status.toUpperCase()} ${entry.file} ${entry.reason}${entry.measurementLimitations ? ` [measurement limitations: ${entry.measurementLimitations} ${entry.limitationKinds.join(", ")}]` : ""}${entry.packageRoot ? ` [package ${entry.packageRoot}]` : ""}`).join("\n")}\n${pageLabel(ordered.length, selectedEntries.length, options)}${next ? `\nnext page: ${next}` : ""}`, queryPagination(ordered.length, selectedEntries.length, options));
|
|
848
979
|
}
|
|
849
980
|
if (command === "kinds" || command === "runners") {
|
|
850
981
|
const dimension = command === "kinds" ? report.coverageByKind : report.coverageByRunner;
|
|
@@ -852,8 +983,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
852
983
|
const dimensionNext = nextPageCommand(coverageCommand(run.id, options, command), dimension.length, selectedDimension.length, options);
|
|
853
984
|
return output({
|
|
854
985
|
run: run.id,
|
|
855
|
-
|
|
856
|
-
offset: options.offset,
|
|
986
|
+
filters: queryFilters(options),
|
|
857
987
|
[command]: selectedDimension,
|
|
858
988
|
}, options, selectedDimension
|
|
859
989
|
.map((entry) => {
|
|
@@ -862,7 +992,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
862
992
|
})
|
|
863
993
|
.join("\n") +
|
|
864
994
|
`\n${pageLabel(dimension.length, selectedDimension.length, options)}` +
|
|
865
|
-
(dimensionNext ? `\nnext page: ${dimensionNext}` : ""));
|
|
995
|
+
(dimensionNext ? `\nnext page: ${dimensionNext}` : ""), queryPagination(dimension.length, selectedDimension.length, options));
|
|
866
996
|
}
|
|
867
997
|
if (command === "files" || command === "gaps") {
|
|
868
998
|
const files = fileGaps(report, selectedTestSet);
|
|
@@ -876,25 +1006,31 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
876
1006
|
: undefined;
|
|
877
1007
|
return output({
|
|
878
1008
|
run: run.id,
|
|
879
|
-
|
|
880
|
-
total: all.length,
|
|
881
|
-
offset: options.offset,
|
|
1009
|
+
filters: queryFilters(options),
|
|
882
1010
|
[command]: selectedFiles,
|
|
883
1011
|
}, options, selectedFiles
|
|
884
1012
|
.map((gap) => {
|
|
885
|
-
const
|
|
886
|
-
|
|
1013
|
+
const missing = gap.uncoveredLines +
|
|
1014
|
+
gap.uncoveredStatements +
|
|
1015
|
+
gap.uncoveredFunctions +
|
|
1016
|
+
gap.missingBranches +
|
|
1017
|
+
gap.missingMcdcConditions;
|
|
1018
|
+
const status = missing === 0
|
|
1019
|
+
? "coverage complete"
|
|
887
1020
|
: `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}`;
|
|
888
|
-
|
|
1021
|
+
const limitations = gap.measurementLimitations
|
|
1022
|
+
? ` measurement limitations ${gap.measurementLimitations} (${gap.limitationKinds.join(", ")})`
|
|
1023
|
+
: "";
|
|
1024
|
+
return `${gap.file} ${status}${limitations}${selectedTestSet ? ` [covered elsewhere: ${Object.values(gap.coveredByOtherTests).reduce((sum, value) => sum + value, 0)}; nowhere: ${Object.values(gap.uncoveredEverywhere).reduce((sum, value) => sum + value, 0)}]` : ""}`;
|
|
889
1025
|
})
|
|
890
1026
|
.join("\n") +
|
|
891
1027
|
`\nshowing ${pageStart}-${pageEnd} of ${all.length}` +
|
|
892
|
-
(nextCommand ? `\nnext page: ${nextCommand}` : ""));
|
|
1028
|
+
(nextCommand ? `\nnext page: ${nextCommand}` : ""), queryPagination(all.length, selectedFiles.length, options));
|
|
893
1029
|
}
|
|
894
1030
|
if (command === "file") {
|
|
895
1031
|
const selector = options.positional.join(" ");
|
|
896
1032
|
if (!selector)
|
|
897
|
-
throw new
|
|
1033
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage file <source-file>");
|
|
898
1034
|
const file = findFile(report, selector);
|
|
899
1035
|
const uncoveredLines = report.lines
|
|
900
1036
|
.filter((line) => line.file === file &&
|
|
@@ -959,6 +1095,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
959
1095
|
...branches,
|
|
960
1096
|
...mcdc,
|
|
961
1097
|
].sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
|
|
1098
|
+
const allFileLimitations = (report.limitations ?? [])
|
|
1099
|
+
.filter((limitation) => limitation.file === file)
|
|
1100
|
+
.map((limitation) => ({
|
|
1101
|
+
...limitation,
|
|
1102
|
+
blocking: true,
|
|
1103
|
+
effect: "outside-measured-denominator",
|
|
1104
|
+
}))
|
|
1105
|
+
.sort((left, right) => left.line - right.line ||
|
|
1106
|
+
left.column - right.column ||
|
|
1107
|
+
left.id.localeCompare(right.id));
|
|
962
1108
|
const allFileTests = report.tests
|
|
963
1109
|
.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
|
|
964
1110
|
test.lines.some((line) => line.file === file))
|
|
@@ -969,15 +1115,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
969
1115
|
}));
|
|
970
1116
|
const tests = page(allFileTests, options);
|
|
971
1117
|
const selected = page(obligations, options);
|
|
972
|
-
const
|
|
973
|
-
const
|
|
1118
|
+
const limitations = page(allFileLimitations, options);
|
|
1119
|
+
const filePageTotal = Math.max(obligations.length, allFileTests.length, allFileLimitations.length);
|
|
1120
|
+
const filePageReturned = Math.max(selected.length, tests.length, limitations.length);
|
|
974
1121
|
const nextFileOffset = options.offset + filePageReturned;
|
|
975
1122
|
const nextFileCommand = filePageReturned > 0 && nextFileOffset < filePageTotal
|
|
976
1123
|
? `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --offset ${nextFileOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
|
|
977
1124
|
: undefined;
|
|
978
1125
|
const result = {
|
|
979
1126
|
run: run.id,
|
|
980
|
-
|
|
1127
|
+
filters: queryFilters(options),
|
|
981
1128
|
file,
|
|
982
1129
|
counts: {
|
|
983
1130
|
uncoveredLines: uncoveredLines.length,
|
|
@@ -985,14 +1132,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
985
1132
|
uncoveredFunctions: functions.length,
|
|
986
1133
|
missingBranches: branches.length,
|
|
987
1134
|
missingMcdcConditions: mcdc.length,
|
|
1135
|
+
measurementLimitations: allFileLimitations.length,
|
|
988
1136
|
},
|
|
989
1137
|
tests,
|
|
990
1138
|
totalTests: allFileTests.length,
|
|
991
1139
|
totalObligations: obligations.length,
|
|
992
|
-
offset: options.offset,
|
|
993
1140
|
obligations: selected,
|
|
1141
|
+
totalLimitations: allFileLimitations.length,
|
|
1142
|
+
limitations,
|
|
994
1143
|
};
|
|
995
|
-
return output(result, options, `${file}\nlines ${uncoveredLines.length}, statements ${statements.length}, functions ${functions.length}, branches ${branches.length}, MC/DC ${mcdc.length}\ncovered by ${allFileTests.length} test(s)\n${selected
|
|
1144
|
+
return output(result, options, `${file}\nlines ${uncoveredLines.length}, statements ${statements.length}, functions ${functions.length}, branches ${branches.length}, MC/DC ${mcdc.length}, measurement limitations ${allFileLimitations.length}\ncovered by ${allFileTests.length} test(s)\n${selected
|
|
996
1145
|
.map((item) => item.kind === "line"
|
|
997
1146
|
? `line ${item.line}: ${item.otherCoverage.coveredElsewhere ? `covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}` : "uncovered everywhere"}`
|
|
998
1147
|
: item.kind === "statement"
|
|
@@ -1002,12 +1151,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1002
1151
|
: item.kind === "branch"
|
|
1003
1152
|
? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
|
|
1004
1153
|
: `MC/DC ${item.line}:${item.column} [${item.id}]: ${item.missingCondition}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`)
|
|
1005
|
-
.join("\n")}\n${pageLabel(filePageTotal, filePageReturned, options)} obligations/tests${nextFileCommand ? `\nnext page: ${nextFileCommand}` : ""}
|
|
1154
|
+
.join("\n")}${selected.length && limitations.length ? "\n" : ""}${limitations.map((limitation) => `LIMITATION ${limitation.kind} ${limitation.line}:${limitation.column} [${limitation.id}]\n ${limitation.reason}\n source: ${limitation.source}\n effect: outside measured denominator`).join("\n")}\n${pageLabel(filePageTotal, filePageReturned, options)} obligations/tests/limitations per category${nextFileCommand ? `\nnext page: ${nextFileCommand}` : ""}`, queryPagination(filePageTotal, filePageReturned, options));
|
|
1006
1155
|
}
|
|
1007
1156
|
if (command === "decision") {
|
|
1008
1157
|
const selector = options.positional[0];
|
|
1009
1158
|
if (!selector)
|
|
1010
|
-
throw new
|
|
1159
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
|
|
1011
1160
|
let matches = report.decisions.filter((decision) => decision.meta.id === selector);
|
|
1012
1161
|
if (matches.length === 0 && /:\d+(?::\d+)?$/.test(selector)) {
|
|
1013
1162
|
const location = locationSelector(selector);
|
|
@@ -1015,7 +1164,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1015
1164
|
decision.meta.line === location.line);
|
|
1016
1165
|
}
|
|
1017
1166
|
if (matches.length === 0)
|
|
1018
|
-
throw new
|
|
1167
|
+
throw new SupercovError("DECISION_NOT_FOUND", `Decision not found: ${selector}`, {
|
|
1168
|
+
details: { selector },
|
|
1169
|
+
});
|
|
1019
1170
|
if (matches.length > 1) {
|
|
1020
1171
|
const matchingDecisions = page(matches, options).map((decision) => ({
|
|
1021
1172
|
id: decision.meta.id,
|
|
@@ -1027,10 +1178,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1027
1178
|
const matchesNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, matches.length, matchingDecisions.length, options);
|
|
1028
1179
|
return output({
|
|
1029
1180
|
run: run.id,
|
|
1030
|
-
|
|
1031
|
-
offset: options.offset,
|
|
1181
|
+
filters: queryFilters(options),
|
|
1032
1182
|
decisions: matchingDecisions,
|
|
1033
|
-
}, options, `${matchingDecisions.map((decision) => `${decision.id} ${decision.file}:${decision.line}:${decision.column} ${decision.source}`).join("\n")}\n${pageLabel(matches.length, matchingDecisions.length, options)} matching decisions${matchesNext ? `\nnext page: ${matchesNext}` : ""}
|
|
1183
|
+
}, options, `${matchingDecisions.map((decision) => `${decision.id} ${decision.file}:${decision.line}:${decision.column} ${decision.source}`).join("\n")}\n${pageLabel(matches.length, matchingDecisions.length, options)} matching decisions${matchesNext ? `\nnext page: ${matchesNext}` : ""}`, queryPagination(matches.length, matchingDecisions.length, options));
|
|
1034
1184
|
}
|
|
1035
1185
|
matches = matches.map((decision) => filterDecision(decision, selectedTestSet));
|
|
1036
1186
|
const totalDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
|
|
@@ -1048,7 +1198,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1048
1198
|
const decisionNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, totalDecisionEvidence, returnedDecisionEvidence, options);
|
|
1049
1199
|
const result = {
|
|
1050
1200
|
run: run.id,
|
|
1051
|
-
|
|
1201
|
+
filters: queryFilters(options),
|
|
1052
1202
|
decisions: matches,
|
|
1053
1203
|
};
|
|
1054
1204
|
return output(result, options, matches
|
|
@@ -1057,12 +1207,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1057
1207
|
.join("\n")}\nconfidence ${decision.confidence?.level ?? "unknown"}; asserted MC/DC ${decision.conditions.filter((condition) => condition.assertionCovered).length}/${decision.conditions.length}\nvectors:\n${decision.vectorObservations.map((observation) => ` ${vectorText(observation.vector.values, observation.vector.outcome)} tests=${observation.tests.length} confidence=${observation.confidence?.level ?? "unknown"}`).join("\n") || " none"}`)
|
|
1058
1208
|
.join("\n\n") +
|
|
1059
1209
|
`\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
|
|
1060
|
-
(decisionNext ? `\nnext page: ${decisionNext}` : ""));
|
|
1210
|
+
(decisionNext ? `\nnext page: ${decisionNext}` : ""), queryPagination(totalDecisionEvidence, returnedDecisionEvidence, options));
|
|
1061
1211
|
}
|
|
1062
1212
|
if (command === "covers") {
|
|
1063
1213
|
const selector = options.positional[0];
|
|
1064
1214
|
if (!selector)
|
|
1065
|
-
throw new
|
|
1215
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage covers <source-file:line>");
|
|
1066
1216
|
const location = locationSelector(selector);
|
|
1067
1217
|
const line = report.lines.find((candidate) => candidate.file === location.file && candidate.line === location.line);
|
|
1068
1218
|
const allTests = (line?.tests ?? [])
|
|
@@ -1095,7 +1245,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1095
1245
|
const coversNext = nextPageCommand(`${coverageCommand(run.id, options, "covers")} ${shellQuote(selector)}`, coversTotal, coversReturned, options);
|
|
1096
1246
|
const result = {
|
|
1097
1247
|
run: run.id,
|
|
1098
|
-
|
|
1248
|
+
filters: queryFilters(options),
|
|
1099
1249
|
location,
|
|
1100
1250
|
covered: includesSelectedTest(line?.tests ?? [], selectedTestSet),
|
|
1101
1251
|
confidence: line?.confidence,
|
|
@@ -1104,16 +1254,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1104
1254
|
tests,
|
|
1105
1255
|
phases,
|
|
1106
1256
|
};
|
|
1107
|
-
return output(result, options, `${location.file}:${location.line} ${result.covered ? "covered" : "uncovered"}; confidence ${result.confidence?.level ?? "unknown"}${result.confidence?.e2e ? "; E2E-covered" : ""}\n${tests.map((test) => `test: ${test.name} [${test.id}] (${test.provenance?.kind ?? "unknown"}/${test.provenance?.runner ?? "unknown"})`).join("\n") || "no covering tests"}\n${phases.map((phase) => `phase: ${phase.operation}${phase.status ? ` (${phase.status})` : ""}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(coversTotal, coversReturned, options)} tests/phases${coversNext ? `\nnext page: ${coversNext}` : ""}
|
|
1257
|
+
return output(result, options, `${location.file}:${location.line} ${result.covered ? "covered" : "uncovered"}; confidence ${result.confidence?.level ?? "unknown"}${result.confidence?.e2e ? "; E2E-covered" : ""}\n${tests.map((test) => `test: ${test.name} [${test.id}] (${test.provenance?.kind ?? "unknown"}/${test.provenance?.runner ?? "unknown"})`).join("\n") || "no covering tests"}\n${phases.map((phase) => `phase: ${phase.operation}${phase.status ? ` (${phase.status})` : ""}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(coversTotal, coversReturned, options)} tests/phases${coversNext ? `\nnext page: ${coversNext}` : ""}`, queryPagination(coversTotal, coversReturned, options));
|
|
1108
1258
|
}
|
|
1109
1259
|
if (command === "test") {
|
|
1110
1260
|
const selector = options.positional.join(" ").toLowerCase();
|
|
1111
1261
|
if (!selector)
|
|
1112
|
-
throw new
|
|
1262
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage test <id|name-fragment>");
|
|
1113
1263
|
const matches = report.tests.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
|
|
1114
1264
|
(test.id === selector || test.name.toLowerCase().includes(selector)));
|
|
1115
1265
|
if (matches.length === 0)
|
|
1116
|
-
throw new
|
|
1266
|
+
throw new SupercovError("TEST_NOT_FOUND", `Test not found: ${selector}`, {
|
|
1267
|
+
details: { selector },
|
|
1268
|
+
});
|
|
1117
1269
|
const testBase = `${coverageCommand(run.id, options, "test")} ${shellQuote(options.positional.join(" "))}`;
|
|
1118
1270
|
if (matches.length > 1) {
|
|
1119
1271
|
const matchingTests = page(matches, options).map((test) => ({
|
|
@@ -1125,10 +1277,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1125
1277
|
const matchesNext = nextPageCommand(testBase, matches.length, matchingTests.length, options);
|
|
1126
1278
|
return output({
|
|
1127
1279
|
run: run.id,
|
|
1128
|
-
|
|
1129
|
-
offset: options.offset,
|
|
1280
|
+
filters: queryFilters(options),
|
|
1130
1281
|
tests: matchingTests,
|
|
1131
|
-
}, options, `${matchingTests.map((test) => `${test.name} [${test.id}] — ${test.outcome}`).join("\n")}\n${pageLabel(matches.length, matchingTests.length, options)} matching tests${matchesNext ? `\nnext page: ${matchesNext}` : ""}
|
|
1282
|
+
}, options, `${matchingTests.map((test) => `${test.name} [${test.id}] — ${test.outcome}`).join("\n")}\n${pageLabel(matches.length, matchingTests.length, options)} matching tests${matchesNext ? `\nnext page: ${matchesNext}` : ""}`, queryPagination(matches.length, matchingTests.length, options));
|
|
1132
1283
|
}
|
|
1133
1284
|
const test = matches[0];
|
|
1134
1285
|
const allPhases = report.phases
|
|
@@ -1161,11 +1312,11 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1161
1312
|
const testNext = nextPageCommand(testBase, testTotal, testReturned, options);
|
|
1162
1313
|
return output({
|
|
1163
1314
|
run: run.id,
|
|
1164
|
-
|
|
1315
|
+
filters: queryFilters(options),
|
|
1165
1316
|
tests: [selected],
|
|
1166
|
-
}, options, `${selected.name}\noutcome ${selected.outcome}${selected.attempts.length ? `; ${selected.attempts.map((attempt) => `retry ${attempt.retry}=${attempt.status}`).join(", ")}` : ""}\n${selected.totals.lines} lines, ${selected.totals.hits} hits, ${selected.totals.decisions} decisions, ${selected.totals.phases} phases\n${selected.lines.map((line) => `line: ${line.file}:${line.line}`).join("\n")}${selected.lines.length && selected.phases.length ? "\n" : ""}${selected.phases.map((phase) => `${phase.kind}: ${phase.operation}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(testTotal, testReturned, options)} per evidence category${testNext ? `\nnext page: ${testNext}` : ""}
|
|
1317
|
+
}, options, `${selected.name}\noutcome ${selected.outcome}${selected.attempts.length ? `; ${selected.attempts.map((attempt) => `retry ${attempt.retry}=${attempt.status}`).join(", ")}` : ""}\n${selected.totals.lines} lines, ${selected.totals.hits} hits, ${selected.totals.decisions} decisions, ${selected.totals.phases} phases\n${selected.lines.map((line) => `line: ${line.file}:${line.line}`).join("\n")}${selected.lines.length && selected.phases.length ? "\n" : ""}${selected.phases.map((phase) => `${phase.kind}: ${phase.operation}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(testTotal, testReturned, options)} per evidence category${testNext ? `\nnext page: ${testNext}` : ""}`, queryPagination(testTotal, testReturned, options));
|
|
1167
1318
|
}
|
|
1168
|
-
throw new
|
|
1319
|
+
throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${command}. Try supercov help.`, { details: { command } });
|
|
1169
1320
|
}
|
|
1170
1321
|
export const coverageQueryCommands = new Set(["help", "runs", "diff"]);
|
|
1171
1322
|
//# sourceMappingURL=query.js.map
|