supercov 0.0.5 → 0.0.7
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 +37 -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/integrity.d.ts.map +1 -1
- package/dist/integrity.js +14 -0
- package/dist/integrity.js.map +1 -1
- package/dist/launchSupervisor.d.ts +8 -0
- package/dist/launchSupervisor.d.ts.map +1 -1
- package/dist/launchSupervisor.js +53 -2
- package/dist/launchSupervisor.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 +316 -107
- 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/runAnalysis.d.ts.map +1 -1
- package/dist/runAnalysis.js +21 -1
- package/dist/runAnalysis.js.map +1 -1
- 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/sourceDiscovery.js +1 -1
- package/dist/sourceDiscovery.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.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
|
+
};
|
|
104
130
|
}
|
|
105
|
-
function
|
|
131
|
+
function analyzeStoredRun(run) {
|
|
132
|
+
return analyzeCoverageArchiveCached(run.evidencePath, storedRunAnalysisOptions(run));
|
|
133
|
+
}
|
|
134
|
+
function readStoredRunIndex(run) {
|
|
135
|
+
return readCoverageQueryIndex(run.evidencePath, storedRunAnalysisOptions(run));
|
|
136
|
+
}
|
|
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) => {
|
|
@@ -324,6 +361,7 @@ function coverageCommand(runId, options, child) {
|
|
|
324
361
|
options.filter !== "all" ? `--filter ${options.filter}` : undefined,
|
|
325
362
|
options.kind ? `--kind ${shellQuote(options.kind)}` : undefined,
|
|
326
363
|
options.runner ? `--runner ${shellQuote(options.runner)}` : undefined,
|
|
364
|
+
options.metric !== "all" ? `--metric ${options.metric}` : undefined,
|
|
327
365
|
]
|
|
328
366
|
.filter(Boolean)
|
|
329
367
|
.join(" ");
|
|
@@ -351,7 +389,9 @@ function selectedTestIds(report, options) {
|
|
|
351
389
|
]
|
|
352
390
|
.filter(Boolean)
|
|
353
391
|
.join(", ");
|
|
354
|
-
throw new
|
|
392
|
+
throw new SupercovError("TEST_FILTER_EMPTY", `No tests match ${filter}`, {
|
|
393
|
+
details: { kind: options.kind, runner: options.runner },
|
|
394
|
+
});
|
|
355
395
|
}
|
|
356
396
|
return new Set(selected.map((test) => test.id));
|
|
357
397
|
}
|
|
@@ -423,6 +463,13 @@ function filterLabel(options) {
|
|
|
423
463
|
.filter(Boolean)
|
|
424
464
|
.join(", ");
|
|
425
465
|
}
|
|
466
|
+
function queryFilters(options) {
|
|
467
|
+
return {
|
|
468
|
+
outcome: options.filter,
|
|
469
|
+
kind: options.kind ?? null,
|
|
470
|
+
runner: options.runner ?? null,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
426
473
|
function attribution(report, selected) {
|
|
427
474
|
const phases = selected
|
|
428
475
|
? report.phases.filter((phase) => selected.has(phase.test))
|
|
@@ -434,7 +481,46 @@ function attribution(report, selected) {
|
|
|
434
481
|
serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
|
|
435
482
|
};
|
|
436
483
|
}
|
|
437
|
-
function
|
|
484
|
+
function coverageDiagnostics(report, selected) {
|
|
485
|
+
const observed = attribution(report, selected);
|
|
486
|
+
if ((report.transport?.remoteLaunches ?? 0) > 0 &&
|
|
487
|
+
(report.transport?.scopedServerRecords ?? 0) === 0 &&
|
|
488
|
+
observed.serverExplicit === 0 &&
|
|
489
|
+
observed.serverFallback === 0) {
|
|
490
|
+
return [{
|
|
491
|
+
code: "REMOTE_SERVER_EVIDENCE_MISSING",
|
|
492
|
+
severity: "warning",
|
|
493
|
+
message: "Remote launches were supervised, but no server evidence returned. Coverage may describe only browser/test processes; inspect how the application server is launched.",
|
|
494
|
+
}];
|
|
495
|
+
}
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
function gapMetricValue(gap, metric) {
|
|
499
|
+
if (metric === "all")
|
|
500
|
+
return gap.score;
|
|
501
|
+
if (metric === "lines")
|
|
502
|
+
return gap.uncoveredLines;
|
|
503
|
+
if (metric === "statements")
|
|
504
|
+
return gap.uncoveredStatements;
|
|
505
|
+
if (metric === "functions")
|
|
506
|
+
return gap.uncoveredFunctions;
|
|
507
|
+
if (metric === "branches")
|
|
508
|
+
return gap.missingBranches;
|
|
509
|
+
return gap.missingMcdcConditions;
|
|
510
|
+
}
|
|
511
|
+
function obligationMatchesMetric(obligation, metric) {
|
|
512
|
+
if (metric === "all")
|
|
513
|
+
return true;
|
|
514
|
+
const kindByMetric = {
|
|
515
|
+
lines: "line",
|
|
516
|
+
statements: "statement",
|
|
517
|
+
functions: "function",
|
|
518
|
+
branches: "branch",
|
|
519
|
+
mcdc: "mcdc",
|
|
520
|
+
};
|
|
521
|
+
return obligation.kind === kindByMetric[metric];
|
|
522
|
+
}
|
|
523
|
+
export function fileGaps(report, selected) {
|
|
438
524
|
const files = new Map();
|
|
439
525
|
const get = (file) => {
|
|
440
526
|
const existing = files.get(file);
|
|
@@ -447,6 +533,8 @@ function fileGaps(report, selected) {
|
|
|
447
533
|
uncoveredFunctions: 0,
|
|
448
534
|
missingBranches: 0,
|
|
449
535
|
missingMcdcConditions: 0,
|
|
536
|
+
measurementLimitations: 0,
|
|
537
|
+
limitationKinds: [],
|
|
450
538
|
coveredByOtherTests: {
|
|
451
539
|
lines: 0,
|
|
452
540
|
statements: 0,
|
|
@@ -508,45 +596,81 @@ function fileGaps(report, selected) {
|
|
|
508
596
|
}
|
|
509
597
|
}
|
|
510
598
|
}
|
|
599
|
+
for (const limitation of report.limitations ?? []) {
|
|
600
|
+
const gap = get(limitation.file);
|
|
601
|
+
gap.measurementLimitations += 1;
|
|
602
|
+
if (!gap.limitationKinds.includes(limitation.kind))
|
|
603
|
+
gap.limitationKinds.push(limitation.kind);
|
|
604
|
+
}
|
|
511
605
|
for (const gap of files.values()) {
|
|
606
|
+
gap.limitationKinds.sort();
|
|
512
607
|
gap.score =
|
|
513
608
|
gap.uncoveredLines +
|
|
514
609
|
gap.uncoveredFunctions * 2 +
|
|
515
610
|
gap.missingBranches * 2 +
|
|
516
|
-
gap.missingMcdcConditions * 3
|
|
611
|
+
gap.missingMcdcConditions * 3 +
|
|
612
|
+
gap.measurementLimitations * 3;
|
|
517
613
|
}
|
|
518
614
|
return [...files.values()].sort((left, right) => right.score - left.score || left.file.localeCompare(right.file));
|
|
519
615
|
}
|
|
520
616
|
function findFile(report, selector) {
|
|
521
|
-
const files = [
|
|
617
|
+
const files = [
|
|
618
|
+
...new Set([
|
|
619
|
+
...report.lines.map((line) => line.file),
|
|
620
|
+
...(report.limitations ?? []).map((limitation) => limitation.file),
|
|
621
|
+
]),
|
|
622
|
+
];
|
|
522
623
|
if (files.includes(selector))
|
|
523
624
|
return selector;
|
|
524
625
|
const matches = files.filter((file) => file.includes(selector));
|
|
525
626
|
if (matches.length === 1)
|
|
526
627
|
return matches[0];
|
|
527
628
|
if (matches.length === 0)
|
|
528
|
-
throw new
|
|
529
|
-
|
|
629
|
+
throw new SupercovError("SOURCE_NOT_FOUND", `Source file not found: ${selector}`, {
|
|
630
|
+
details: { selector },
|
|
631
|
+
});
|
|
632
|
+
throw new SupercovError("AMBIGUOUS_SELECTOR", `Ambiguous file selector: ${matches.join(", ")}`, {
|
|
633
|
+
details: { selector, matches },
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
export function coverageMeasurement(report) {
|
|
637
|
+
const limitations = report.limitations ?? [];
|
|
638
|
+
const byKind = {
|
|
639
|
+
"dynamic-code": 0,
|
|
640
|
+
"semantic-safety": 0,
|
|
641
|
+
"source-scope": 0,
|
|
642
|
+
};
|
|
643
|
+
for (const limitation of limitations)
|
|
644
|
+
byKind[limitation.kind] += 1;
|
|
645
|
+
return {
|
|
646
|
+
complete: limitations.length === 0,
|
|
647
|
+
limitations: limitations.length,
|
|
648
|
+
// Every current limitation removes source from the measured denominator.
|
|
649
|
+
blocking: limitations.length,
|
|
650
|
+
files: new Set(limitations.map((limitation) => limitation.file)).size,
|
|
651
|
+
byKind,
|
|
652
|
+
};
|
|
530
653
|
}
|
|
531
654
|
function locationSelector(selector) {
|
|
532
655
|
const match = /^(.*):(\d+)(?::\d+)?$/.exec(selector);
|
|
533
656
|
if (!match)
|
|
534
|
-
throw new
|
|
657
|
+
throw new SupercovError("INVALID_ARGUMENT", "Expected <source-file>:<line>", {
|
|
658
|
+
details: { selector },
|
|
659
|
+
});
|
|
535
660
|
return { file: match[1], line: Number(match[2]) };
|
|
536
661
|
}
|
|
537
662
|
function vectorText(values, outcome) {
|
|
538
663
|
return `${values.map((value) => (value === null ? "-" : value ? "T" : "F")).join("")} -> ${outcome ? "T" : "F"}`;
|
|
539
664
|
}
|
|
540
|
-
|
|
541
|
-
console.log(`Agent-oriented local coverage queries:
|
|
665
|
+
const helpText = `Agent-oriented local coverage queries:
|
|
542
666
|
supercov runs [--limit N] [--json]
|
|
543
667
|
supercov runs <run-id> coverage [--filter all|passed|failed] [--kind e2e] [--runner playwright] [--json]
|
|
544
668
|
supercov runs <run-id> coverage kinds [--json]
|
|
545
669
|
supercov runs <run-id> coverage runners [--json]
|
|
546
670
|
supercov runs <run-id> coverage scope [--limit N] [--offset N] [--json]
|
|
547
|
-
supercov runs <run-id> coverage files [--filter all|passed|failed] [--limit N] [--offset N] [--json]
|
|
548
|
-
supercov runs <run-id> coverage gaps [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
|
|
549
|
-
supercov runs <run-id> coverage file <source-file> [--kind e2e] [--limit N] [--offset N] [--json]
|
|
671
|
+
supercov runs <run-id> coverage files [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--limit N] [--offset N] [--json]
|
|
672
|
+
supercov runs <run-id> coverage gaps [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
|
|
673
|
+
supercov runs <run-id> coverage file <source-file> [--metric all|lines|statements|functions|branches|mcdc] [--kind e2e] [--limit N] [--offset N] [--json]
|
|
550
674
|
supercov runs <run-id> coverage decision <id|source-file:line> [--kind e2e] [--json]
|
|
551
675
|
supercov runs <run-id> coverage covers <source-file:line> [--kind e2e] [--json]
|
|
552
676
|
supercov runs <run-id> coverage test <id|name-fragment> [--kind e2e] [--limit N] [--json]
|
|
@@ -559,7 +683,30 @@ function help() {
|
|
|
559
683
|
Use "latest" as <run-id> to query the newest local run.
|
|
560
684
|
|
|
561
685
|
Create a run with:
|
|
562
|
-
supercov -- <test command
|
|
686
|
+
supercov -- <test command>`;
|
|
687
|
+
function help(options) {
|
|
688
|
+
if (options.json) {
|
|
689
|
+
return output({
|
|
690
|
+
usage: "supercov -- <test command>",
|
|
691
|
+
runSelector: "Use latest as <run-id> to query the newest local run.",
|
|
692
|
+
queryCommands: [
|
|
693
|
+
"runs",
|
|
694
|
+
"runs <run-id> coverage",
|
|
695
|
+
"runs <run-id> coverage kinds",
|
|
696
|
+
"runs <run-id> coverage runners",
|
|
697
|
+
"runs <run-id> coverage scope",
|
|
698
|
+
"runs <run-id> coverage files",
|
|
699
|
+
"runs <run-id> coverage gaps",
|
|
700
|
+
"runs <run-id> coverage file <source-file>",
|
|
701
|
+
"runs <run-id> coverage decision <id|source-file:line>",
|
|
702
|
+
"runs <run-id> coverage covers <source-file:line>",
|
|
703
|
+
"runs <run-id> coverage test <id|name-fragment>",
|
|
704
|
+
"runs <run-id> coverage minimize",
|
|
705
|
+
"diff <older-run> <newer-run>",
|
|
706
|
+
],
|
|
707
|
+
}, options, helpText);
|
|
708
|
+
}
|
|
709
|
+
console.log(helpText);
|
|
563
710
|
}
|
|
564
711
|
/** Resolve the instance-first coverage resource syntax. */
|
|
565
712
|
export function resolveCoverageQueryInvocation(command, args) {
|
|
@@ -587,7 +734,7 @@ export function resolveCoverageQueryInvocation(command, args) {
|
|
|
587
734
|
"minimize",
|
|
588
735
|
]);
|
|
589
736
|
if (!coverageCommands.has(child)) {
|
|
590
|
-
throw new
|
|
737
|
+
throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${child}. Try supercov help.`, { details: { command: child } });
|
|
591
738
|
}
|
|
592
739
|
return {
|
|
593
740
|
command: child,
|
|
@@ -597,20 +744,22 @@ export function resolveCoverageQueryInvocation(command, args) {
|
|
|
597
744
|
export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
598
745
|
const resolved = resolveCoverageQueryInvocation(command, args);
|
|
599
746
|
command = resolved.command;
|
|
600
|
-
const options = parseOptions(resolved.args);
|
|
747
|
+
const options = parseOptions(command, resolved.args);
|
|
601
748
|
if (command === "help")
|
|
602
|
-
return help();
|
|
749
|
+
return help(options);
|
|
603
750
|
const currentIntegrity = currentProjectIntegrity(root);
|
|
604
751
|
if (command === "runs") {
|
|
605
752
|
const availableRuns = discoverRuns(root);
|
|
606
753
|
const runs = page(availableRuns, options).map((run) => {
|
|
607
|
-
const
|
|
754
|
+
const cached = readStoredRunIndex(run);
|
|
755
|
+
const report = cached ? filteredCoverage(cached, options) : undefined;
|
|
608
756
|
return {
|
|
609
757
|
id: run.id,
|
|
610
|
-
generatedAt: report.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
758
|
+
generatedAt: report?.generatedAt ?? run.metadata?.startedAt,
|
|
759
|
+
coverageIndexed: Boolean(report),
|
|
760
|
+
lines: report?.summary.lines.percentage ?? null,
|
|
761
|
+
branches: report?.summary.branches.percentage ?? null,
|
|
762
|
+
mcdc: report?.summary.conditionCoveragePct ?? null,
|
|
614
763
|
command: run.metadata?.command,
|
|
615
764
|
durationMs: run.metadata?.durationMs,
|
|
616
765
|
timings: run.metadata?.timings,
|
|
@@ -624,18 +773,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
624
773
|
});
|
|
625
774
|
const runsBase = `npx supercov runs${options.filter !== "all" ? ` --filter ${options.filter}` : ""}`;
|
|
626
775
|
const runsNext = nextPageCommand(runsBase, availableRuns.length, runs.length, options);
|
|
627
|
-
return output(runs, options, runs
|
|
628
|
-
.map((run) => `${run.id} lines ${pct(run.lines
|
|
776
|
+
return output({ filters: queryFilters(options), runs }, options, runs
|
|
777
|
+
.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
778
|
.join("\n") +
|
|
630
779
|
`\n${pageLabel(availableRuns.length, runs.length, options)}` +
|
|
631
|
-
(runsNext ? `\nnext page: ${runsNext}` : ""));
|
|
780
|
+
(runsNext ? `\nnext page: ${runsNext}` : ""), queryPagination(availableRuns.length, runs.length, options));
|
|
632
781
|
}
|
|
633
782
|
if (command === "diff") {
|
|
634
783
|
const [olderSelector, newerSelector] = options.positional;
|
|
635
784
|
if (!olderSelector || !newerSelector)
|
|
636
|
-
throw new
|
|
637
|
-
const olderSelected = selectRun(root, olderSelector, currentIntegrity);
|
|
638
|
-
const newerSelected = selectRun(root, newerSelector, currentIntegrity);
|
|
785
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov diff <older-run> <newer-run>");
|
|
786
|
+
const olderSelected = selectRun(root, olderSelector, currentIntegrity, options.json);
|
|
787
|
+
const newerSelected = selectRun(root, newerSelector, currentIntegrity, options.json);
|
|
639
788
|
const older = {
|
|
640
789
|
...olderSelected,
|
|
641
790
|
report: filteredCoverage(olderSelected.report, options),
|
|
@@ -690,6 +839,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
690
839
|
.map(([, label]) => label)
|
|
691
840
|
.sort();
|
|
692
841
|
const result = {
|
|
842
|
+
filters: queryFilters(options),
|
|
693
843
|
older: older.run.id,
|
|
694
844
|
newer: newer.run.id,
|
|
695
845
|
delta: {
|
|
@@ -728,9 +878,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
728
878
|
.filter(Boolean)
|
|
729
879
|
.join(" ");
|
|
730
880
|
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}` : ""}
|
|
881
|
+
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
882
|
}
|
|
733
|
-
const selectedRun = selectRun(root, options.run, currentIntegrity);
|
|
883
|
+
const selectedRun = selectRun(root, options.run, currentIntegrity, options.json);
|
|
734
884
|
const run = selectedRun.run;
|
|
735
885
|
const report = filteredCoverage(selectedRun.report, options);
|
|
736
886
|
const selectedTestSet = selectedTestIds(report, options);
|
|
@@ -739,6 +889,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
739
889
|
? coverageSummaryForTests(report, selectedTestSet)
|
|
740
890
|
: report.summary;
|
|
741
891
|
const gaps = fileGaps(report, selectedTestSet).filter((gap) => gap.score > 0);
|
|
892
|
+
const measurement = coverageMeasurement(report);
|
|
742
893
|
const selectedTests = report.tests.filter((test) => !selectedTestSet || selectedTestSet.has(test.id));
|
|
743
894
|
const testCount = selectedTests.filter((test) => (test.role ?? "test") === "test").length;
|
|
744
895
|
const setupCount = selectedTests.filter((test) => test.role === "setup").length;
|
|
@@ -746,23 +897,27 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
746
897
|
outcome,
|
|
747
898
|
selectedTests.filter((test) => test.role === "test" && test.outcome === outcome).length,
|
|
748
899
|
]));
|
|
900
|
+
const diagnostics = coverageDiagnostics(report, selectedTestSet);
|
|
749
901
|
const result = {
|
|
750
902
|
run: run.id,
|
|
751
|
-
|
|
752
|
-
...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
|
|
903
|
+
filters: queryFilters(options),
|
|
753
904
|
generatedAt: report.generatedAt,
|
|
754
905
|
valid: run.metadata?.testExitCode === 0,
|
|
755
906
|
stale: report.integrity?.stale ?? false,
|
|
756
907
|
staleReasons: report.integrity?.staleReasons ?? [],
|
|
757
|
-
structurallyComplete: summary.coverageComplete,
|
|
908
|
+
structurallyComplete: summary.coverageComplete && measurement.complete,
|
|
758
909
|
complete: options.filter === "passed" &&
|
|
759
910
|
run.metadata?.testExitCode === 0 &&
|
|
760
911
|
!report.integrity?.stale &&
|
|
761
|
-
summary.coverageComplete
|
|
912
|
+
summary.coverageComplete &&
|
|
913
|
+
measurement.complete,
|
|
762
914
|
coverage: summary,
|
|
915
|
+
measurement,
|
|
763
916
|
coverageByKind: report.coverageByKind,
|
|
764
917
|
coverageByRunner: report.coverageByRunner,
|
|
765
918
|
attribution: attribution(report, selectedTestSet),
|
|
919
|
+
transport: report.transport,
|
|
920
|
+
diagnostics,
|
|
766
921
|
...(!selectedTestSet
|
|
767
922
|
? {
|
|
768
923
|
confidence: {
|
|
@@ -776,6 +931,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
776
931
|
}
|
|
777
932
|
: {}),
|
|
778
933
|
filesWithGaps: gaps.length,
|
|
934
|
+
filesWithCoverageGaps: gaps.filter((gap) => gap.uncoveredLines > 0 ||
|
|
935
|
+
gap.uncoveredStatements > 0 ||
|
|
936
|
+
gap.uncoveredFunctions > 0 ||
|
|
937
|
+
gap.missingBranches > 0 ||
|
|
938
|
+
gap.missingMcdcConditions > 0).length,
|
|
939
|
+
filesWithMeasurementLimitations: measurement.files,
|
|
779
940
|
tests: testCount,
|
|
780
941
|
setups: setupCount,
|
|
781
942
|
testOutcomes,
|
|
@@ -789,7 +950,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
789
950
|
}
|
|
790
951
|
: undefined,
|
|
791
952
|
};
|
|
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
|
|
953
|
+
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)`}${diagnostics.length ? `\ndiagnostic: ${diagnostics.map((item) => `${item.code}: ${item.message}`).join("; ")}` : ""}${!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
954
|
}
|
|
794
955
|
if (command === "minimize") {
|
|
795
956
|
const solverReport = selectedTestSet
|
|
@@ -810,21 +971,34 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
810
971
|
};
|
|
811
972
|
});
|
|
812
973
|
const selectedPage = page(selectedDetails, options);
|
|
813
|
-
const base = `${coverageCommand(run.id, options, "minimize")} --target ${options.target}
|
|
974
|
+
const base = `${coverageCommand(run.id, options, "minimize")} --target ${options.target}`;
|
|
814
975
|
const next = nextPageCommand(base, selectedDetails.length, selectedPage.length, options);
|
|
815
976
|
return output({
|
|
816
977
|
run: run.id,
|
|
978
|
+
filters: queryFilters(options),
|
|
817
979
|
...minimized,
|
|
818
980
|
selectedCount: selectedDetails.length,
|
|
819
981
|
totalCandidateTests: solverReport.tests.filter((test) => test.role === "test").length,
|
|
820
|
-
offset: options.offset,
|
|
821
982
|
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}` : ""}
|
|
983
|
+
}, 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
984
|
}
|
|
824
985
|
if (command === "scope") {
|
|
825
986
|
if (!report.scope)
|
|
826
|
-
throw new
|
|
827
|
-
const
|
|
987
|
+
throw new SupercovError("SCOPE_UNAVAILABLE", "This run does not contain a source-scope inventory.");
|
|
988
|
+
const limitationsByFile = new Map();
|
|
989
|
+
for (const limitation of report.limitations ?? []) {
|
|
990
|
+
const existing = limitationsByFile.get(limitation.file) ?? [];
|
|
991
|
+
existing.push(limitation);
|
|
992
|
+
limitationsByFile.set(limitation.file, existing);
|
|
993
|
+
}
|
|
994
|
+
const ordered = report.scope.entries.map((entry) => {
|
|
995
|
+
const limitations = limitationsByFile.get(entry.file) ?? [];
|
|
996
|
+
return {
|
|
997
|
+
...entry,
|
|
998
|
+
measurementLimitations: limitations.length,
|
|
999
|
+
limitationKinds: [...new Set(limitations.map((item) => item.kind))].sort(),
|
|
1000
|
+
};
|
|
1001
|
+
}).sort((left, right) => {
|
|
828
1002
|
const rank = { ambiguous: 0, included: 1, excluded: 2 };
|
|
829
1003
|
return rank[left.status] - rank[right.status] || left.file.localeCompare(right.file);
|
|
830
1004
|
});
|
|
@@ -838,13 +1012,13 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
838
1012
|
};
|
|
839
1013
|
return output({
|
|
840
1014
|
run: run.id,
|
|
1015
|
+
filters: queryFilters(options),
|
|
841
1016
|
mode: report.scope.mode,
|
|
842
1017
|
roots: report.scope.roots,
|
|
843
1018
|
counts,
|
|
844
|
-
|
|
845
|
-
offset: options.offset,
|
|
1019
|
+
measurement: coverageMeasurement(report),
|
|
846
1020
|
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}` : ""}
|
|
1021
|
+
}, 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
1022
|
}
|
|
849
1023
|
if (command === "kinds" || command === "runners") {
|
|
850
1024
|
const dimension = command === "kinds" ? report.coverageByKind : report.coverageByRunner;
|
|
@@ -852,8 +1026,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
852
1026
|
const dimensionNext = nextPageCommand(coverageCommand(run.id, options, command), dimension.length, selectedDimension.length, options);
|
|
853
1027
|
return output({
|
|
854
1028
|
run: run.id,
|
|
855
|
-
|
|
856
|
-
offset: options.offset,
|
|
1029
|
+
filters: queryFilters(options),
|
|
857
1030
|
[command]: selectedDimension,
|
|
858
1031
|
}, options, selectedDimension
|
|
859
1032
|
.map((entry) => {
|
|
@@ -862,11 +1035,17 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
862
1035
|
})
|
|
863
1036
|
.join("\n") +
|
|
864
1037
|
`\n${pageLabel(dimension.length, selectedDimension.length, options)}` +
|
|
865
|
-
(dimensionNext ? `\nnext page: ${dimensionNext}` : ""));
|
|
1038
|
+
(dimensionNext ? `\nnext page: ${dimensionNext}` : ""), queryPagination(dimension.length, selectedDimension.length, options));
|
|
866
1039
|
}
|
|
867
1040
|
if (command === "files" || command === "gaps") {
|
|
868
1041
|
const files = fileGaps(report, selectedTestSet);
|
|
869
|
-
const all =
|
|
1042
|
+
const all = files
|
|
1043
|
+
.filter((gap) => command === "files" ||
|
|
1044
|
+
gapMetricValue(gap, options.metric) > 0 ||
|
|
1045
|
+
gap.measurementLimitations > 0)
|
|
1046
|
+
.sort((left, right) => gapMetricValue(right, options.metric) - gapMetricValue(left, options.metric) ||
|
|
1047
|
+
right.measurementLimitations - left.measurementLimitations ||
|
|
1048
|
+
left.file.localeCompare(right.file));
|
|
870
1049
|
const selectedFiles = page(all, options);
|
|
871
1050
|
const pageStart = all.length === 0 ? 0 : options.offset + 1;
|
|
872
1051
|
const pageEnd = Math.min(options.offset + selectedFiles.length, all.length);
|
|
@@ -876,25 +1055,32 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
876
1055
|
: undefined;
|
|
877
1056
|
return output({
|
|
878
1057
|
run: run.id,
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
offset: options.offset,
|
|
1058
|
+
filters: queryFilters(options),
|
|
1059
|
+
metric: options.metric,
|
|
882
1060
|
[command]: selectedFiles,
|
|
883
1061
|
}, options, selectedFiles
|
|
884
1062
|
.map((gap) => {
|
|
885
|
-
const
|
|
886
|
-
|
|
1063
|
+
const missing = gap.uncoveredLines +
|
|
1064
|
+
gap.uncoveredStatements +
|
|
1065
|
+
gap.uncoveredFunctions +
|
|
1066
|
+
gap.missingBranches +
|
|
1067
|
+
gap.missingMcdcConditions;
|
|
1068
|
+
const status = missing === 0
|
|
1069
|
+
? "coverage complete"
|
|
887
1070
|
: `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}`;
|
|
888
|
-
|
|
1071
|
+
const limitations = gap.measurementLimitations
|
|
1072
|
+
? ` measurement limitations ${gap.measurementLimitations} (${gap.limitationKinds.join(", ")})`
|
|
1073
|
+
: "";
|
|
1074
|
+
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
1075
|
})
|
|
890
1076
|
.join("\n") +
|
|
891
1077
|
`\nshowing ${pageStart}-${pageEnd} of ${all.length}` +
|
|
892
|
-
(nextCommand ? `\nnext page: ${nextCommand}` : ""));
|
|
1078
|
+
(nextCommand ? `\nnext page: ${nextCommand}` : ""), queryPagination(all.length, selectedFiles.length, options));
|
|
893
1079
|
}
|
|
894
1080
|
if (command === "file") {
|
|
895
1081
|
const selector = options.positional.join(" ");
|
|
896
1082
|
if (!selector)
|
|
897
|
-
throw new
|
|
1083
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage file <source-file>");
|
|
898
1084
|
const file = findFile(report, selector);
|
|
899
1085
|
const uncoveredLines = report.lines
|
|
900
1086
|
.filter((line) => line.file === file &&
|
|
@@ -958,7 +1144,17 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
958
1144
|
...functions,
|
|
959
1145
|
...branches,
|
|
960
1146
|
...mcdc,
|
|
961
|
-
].sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
|
|
1147
|
+
].filter((obligation) => obligationMatchesMetric(obligation, options.metric)).sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
|
|
1148
|
+
const allFileLimitations = (report.limitations ?? [])
|
|
1149
|
+
.filter((limitation) => limitation.file === file)
|
|
1150
|
+
.map((limitation) => ({
|
|
1151
|
+
...limitation,
|
|
1152
|
+
blocking: true,
|
|
1153
|
+
effect: "outside-measured-denominator",
|
|
1154
|
+
}))
|
|
1155
|
+
.sort((left, right) => left.line - right.line ||
|
|
1156
|
+
left.column - right.column ||
|
|
1157
|
+
left.id.localeCompare(right.id));
|
|
962
1158
|
const allFileTests = report.tests
|
|
963
1159
|
.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
|
|
964
1160
|
test.lines.some((line) => line.file === file))
|
|
@@ -969,30 +1165,34 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
969
1165
|
}));
|
|
970
1166
|
const tests = page(allFileTests, options);
|
|
971
1167
|
const selected = page(obligations, options);
|
|
972
|
-
const
|
|
973
|
-
const
|
|
1168
|
+
const limitations = page(allFileLimitations, options);
|
|
1169
|
+
const filePageTotal = Math.max(obligations.length, allFileTests.length, allFileLimitations.length);
|
|
1170
|
+
const filePageReturned = Math.max(selected.length, tests.length, limitations.length);
|
|
974
1171
|
const nextFileOffset = options.offset + filePageReturned;
|
|
975
1172
|
const nextFileCommand = filePageReturned > 0 && nextFileOffset < filePageTotal
|
|
976
1173
|
? `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --offset ${nextFileOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
|
|
977
1174
|
: undefined;
|
|
978
1175
|
const result = {
|
|
979
1176
|
run: run.id,
|
|
980
|
-
|
|
1177
|
+
filters: queryFilters(options),
|
|
981
1178
|
file,
|
|
1179
|
+
metric: options.metric,
|
|
982
1180
|
counts: {
|
|
983
1181
|
uncoveredLines: uncoveredLines.length,
|
|
984
1182
|
uncoveredStatements: statements.length,
|
|
985
1183
|
uncoveredFunctions: functions.length,
|
|
986
1184
|
missingBranches: branches.length,
|
|
987
1185
|
missingMcdcConditions: mcdc.length,
|
|
1186
|
+
measurementLimitations: allFileLimitations.length,
|
|
988
1187
|
},
|
|
989
1188
|
tests,
|
|
990
1189
|
totalTests: allFileTests.length,
|
|
991
1190
|
totalObligations: obligations.length,
|
|
992
|
-
offset: options.offset,
|
|
993
1191
|
obligations: selected,
|
|
1192
|
+
totalLimitations: allFileLimitations.length,
|
|
1193
|
+
limitations,
|
|
994
1194
|
};
|
|
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
|
|
1195
|
+
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
1196
|
.map((item) => item.kind === "line"
|
|
997
1197
|
? `line ${item.line}: ${item.otherCoverage.coveredElsewhere ? `covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}` : "uncovered everywhere"}`
|
|
998
1198
|
: item.kind === "statement"
|
|
@@ -1002,12 +1202,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1002
1202
|
: item.kind === "branch"
|
|
1003
1203
|
? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
|
|
1004
1204
|
: `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}` : ""}
|
|
1205
|
+
.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
1206
|
}
|
|
1007
1207
|
if (command === "decision") {
|
|
1008
1208
|
const selector = options.positional[0];
|
|
1009
1209
|
if (!selector)
|
|
1010
|
-
throw new
|
|
1210
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
|
|
1011
1211
|
let matches = report.decisions.filter((decision) => decision.meta.id === selector);
|
|
1012
1212
|
if (matches.length === 0 && /:\d+(?::\d+)?$/.test(selector)) {
|
|
1013
1213
|
const location = locationSelector(selector);
|
|
@@ -1015,7 +1215,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1015
1215
|
decision.meta.line === location.line);
|
|
1016
1216
|
}
|
|
1017
1217
|
if (matches.length === 0)
|
|
1018
|
-
throw new
|
|
1218
|
+
throw new SupercovError("DECISION_NOT_FOUND", `Decision not found: ${selector}`, {
|
|
1219
|
+
details: { selector },
|
|
1220
|
+
});
|
|
1019
1221
|
if (matches.length > 1) {
|
|
1020
1222
|
const matchingDecisions = page(matches, options).map((decision) => ({
|
|
1021
1223
|
id: decision.meta.id,
|
|
@@ -1027,17 +1229,22 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1027
1229
|
const matchesNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, matches.length, matchingDecisions.length, options);
|
|
1028
1230
|
return output({
|
|
1029
1231
|
run: run.id,
|
|
1030
|
-
|
|
1031
|
-
offset: options.offset,
|
|
1232
|
+
filters: queryFilters(options),
|
|
1032
1233
|
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}` : ""}
|
|
1234
|
+
}, 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
1235
|
}
|
|
1035
1236
|
matches = matches.map((decision) => filterDecision(decision, selectedTestSet));
|
|
1036
1237
|
const totalDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
|
|
1037
1238
|
matches = matches.map((decision) => {
|
|
1239
|
+
const totals = {
|
|
1240
|
+
conditions: decision.conditions.length,
|
|
1241
|
+
vectorObservations: decision.vectorObservations.length,
|
|
1242
|
+
tests: decision.tests.length,
|
|
1243
|
+
};
|
|
1038
1244
|
const vectorObservations = page(decision.vectorObservations, options);
|
|
1039
1245
|
return {
|
|
1040
1246
|
...decision,
|
|
1247
|
+
totals,
|
|
1041
1248
|
vectors: vectorObservations.map((observation) => observation.vector),
|
|
1042
1249
|
vectorObservations,
|
|
1043
1250
|
conditions: page(decision.conditions, options),
|
|
@@ -1048,7 +1255,8 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1048
1255
|
const decisionNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, totalDecisionEvidence, returnedDecisionEvidence, options);
|
|
1049
1256
|
const result = {
|
|
1050
1257
|
run: run.id,
|
|
1051
|
-
|
|
1258
|
+
filters: queryFilters(options),
|
|
1259
|
+
paginationAppliesTo: "conditions, vectorObservations, and tests independently within each decision",
|
|
1052
1260
|
decisions: matches,
|
|
1053
1261
|
};
|
|
1054
1262
|
return output(result, options, matches
|
|
@@ -1057,12 +1265,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1057
1265
|
.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
1266
|
.join("\n\n") +
|
|
1059
1267
|
`\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
|
|
1060
|
-
(decisionNext ? `\nnext page: ${decisionNext}` : ""));
|
|
1268
|
+
(decisionNext ? `\nnext page: ${decisionNext}` : ""), queryPagination(totalDecisionEvidence, returnedDecisionEvidence, options));
|
|
1061
1269
|
}
|
|
1062
1270
|
if (command === "covers") {
|
|
1063
1271
|
const selector = options.positional[0];
|
|
1064
1272
|
if (!selector)
|
|
1065
|
-
throw new
|
|
1273
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage covers <source-file:line>");
|
|
1066
1274
|
const location = locationSelector(selector);
|
|
1067
1275
|
const line = report.lines.find((candidate) => candidate.file === location.file && candidate.line === location.line);
|
|
1068
1276
|
const allTests = (line?.tests ?? [])
|
|
@@ -1095,7 +1303,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1095
1303
|
const coversNext = nextPageCommand(`${coverageCommand(run.id, options, "covers")} ${shellQuote(selector)}`, coversTotal, coversReturned, options);
|
|
1096
1304
|
const result = {
|
|
1097
1305
|
run: run.id,
|
|
1098
|
-
|
|
1306
|
+
filters: queryFilters(options),
|
|
1099
1307
|
location,
|
|
1100
1308
|
covered: includesSelectedTest(line?.tests ?? [], selectedTestSet),
|
|
1101
1309
|
confidence: line?.confidence,
|
|
@@ -1104,16 +1312,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1104
1312
|
tests,
|
|
1105
1313
|
phases,
|
|
1106
1314
|
};
|
|
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}` : ""}
|
|
1315
|
+
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
1316
|
}
|
|
1109
1317
|
if (command === "test") {
|
|
1110
1318
|
const selector = options.positional.join(" ").toLowerCase();
|
|
1111
1319
|
if (!selector)
|
|
1112
|
-
throw new
|
|
1320
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage test <id|name-fragment>");
|
|
1113
1321
|
const matches = report.tests.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
|
|
1114
1322
|
(test.id === selector || test.name.toLowerCase().includes(selector)));
|
|
1115
1323
|
if (matches.length === 0)
|
|
1116
|
-
throw new
|
|
1324
|
+
throw new SupercovError("TEST_NOT_FOUND", `Test not found: ${selector}`, {
|
|
1325
|
+
details: { selector },
|
|
1326
|
+
});
|
|
1117
1327
|
const testBase = `${coverageCommand(run.id, options, "test")} ${shellQuote(options.positional.join(" "))}`;
|
|
1118
1328
|
if (matches.length > 1) {
|
|
1119
1329
|
const matchingTests = page(matches, options).map((test) => ({
|
|
@@ -1125,10 +1335,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1125
1335
|
const matchesNext = nextPageCommand(testBase, matches.length, matchingTests.length, options);
|
|
1126
1336
|
return output({
|
|
1127
1337
|
run: run.id,
|
|
1128
|
-
|
|
1129
|
-
offset: options.offset,
|
|
1338
|
+
filters: queryFilters(options),
|
|
1130
1339
|
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}` : ""}
|
|
1340
|
+
}, 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
1341
|
}
|
|
1133
1342
|
const test = matches[0];
|
|
1134
1343
|
const allPhases = report.phases
|
|
@@ -1161,11 +1370,11 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1161
1370
|
const testNext = nextPageCommand(testBase, testTotal, testReturned, options);
|
|
1162
1371
|
return output({
|
|
1163
1372
|
run: run.id,
|
|
1164
|
-
|
|
1373
|
+
filters: queryFilters(options),
|
|
1165
1374
|
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}` : ""}
|
|
1375
|
+
}, 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
1376
|
}
|
|
1168
|
-
throw new
|
|
1377
|
+
throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${command}. Try supercov help.`, { details: { command } });
|
|
1169
1378
|
}
|
|
1170
1379
|
export const coverageQueryCommands = new Set(["help", "runs", "diff"]);
|
|
1171
1380
|
//# sourceMappingURL=query.js.map
|