supercov 0.0.11 → 0.0.13
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 -23
- package/bin/supercov.js +8 -19
- package/package.json +31 -43
- package/{dist → runtime/javascript}/atomic.js +0 -1
- package/{dist → runtime/javascript}/launchSupervisor.js +45 -2
- package/{dist → runtime/javascript}/nodeAssert.js +0 -1
- package/{dist → runtime/javascript}/nodeAssertAdapter.js +0 -1
- package/{dist → runtime/javascript}/nodeAssertStrict.js +0 -1
- package/{dist → runtime/javascript}/nodeTest.js +0 -1
- package/{dist → runtime/javascript}/playwright.js +0 -1
- package/{dist → runtime/javascript}/playwrightReporter.js +0 -1
- package/{dist → runtime/javascript}/provenance.js +0 -1
- package/{dist → runtime/javascript}/register.mjs +3 -2
- package/{dist → runtime/javascript}/resolve-loader.mjs +0 -42
- package/{dist → runtime/javascript}/runnerEvidence.js +0 -1
- package/{dist → runtime/javascript}/transport.js +0 -1
- package/{dist → runtime/javascript}/vitest.js +0 -1
- package/{dist → runtime/javascript}/vitestReporter.js +0 -1
- package/dist/agentJson.d.ts +0 -45
- package/dist/agentJson.d.ts.map +0 -1
- package/dist/agentJson.js +0 -102
- package/dist/agentJson.js.map +0 -1
- package/dist/analyze.d.ts +0 -12
- package/dist/analyze.d.ts.map +0 -1
- package/dist/analyze.js +0 -704
- package/dist/analyze.js.map +0 -1
- package/dist/atomic.d.ts +0 -6
- package/dist/atomic.d.ts.map +0 -1
- package/dist/atomic.js.map +0 -1
- package/dist/buildCache.d.ts +0 -14
- package/dist/buildCache.d.ts.map +0 -1
- package/dist/buildCache.js +0 -84
- package/dist/buildCache.js.map +0 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -797
- package/dist/cli.js.map +0 -1
- package/dist/directInstrumenter.d.ts +0 -8
- package/dist/directInstrumenter.d.ts.map +0 -1
- package/dist/directInstrumenter.js +0 -112
- package/dist/directInstrumenter.js.map +0 -1
- package/dist/engineEvidence.d.ts +0 -9
- package/dist/engineEvidence.d.ts.map +0 -1
- package/dist/engineEvidence.js +0 -31
- package/dist/engineEvidence.js.map +0 -1
- package/dist/engineInstrumenter.d.ts +0 -16
- package/dist/engineInstrumenter.d.ts.map +0 -1
- package/dist/engineInstrumenter.js +0 -50
- package/dist/engineInstrumenter.js.map +0 -1
- package/dist/engineProcess.d.ts +0 -11
- package/dist/engineProcess.d.ts.map +0 -1
- package/dist/engineProcess.js +0 -59
- package/dist/engineProcess.js.map +0 -1
- package/dist/esmInterceptor.d.ts +0 -6
- package/dist/esmInterceptor.d.ts.map +0 -1
- package/dist/esmInterceptor.js +0 -63
- package/dist/esmInterceptor.js.map +0 -1
- package/dist/evidenceArchive.d.ts +0 -38
- package/dist/evidenceArchive.d.ts.map +0 -1
- package/dist/evidenceArchive.js +0 -156
- package/dist/evidenceArchive.js.map +0 -1
- package/dist/instrumenter.d.ts +0 -18
- package/dist/instrumenter.d.ts.map +0 -1
- package/dist/instrumenter.js +0 -1644
- package/dist/instrumenter.js.map +0 -1
- package/dist/integrity.d.ts +0 -10
- package/dist/integrity.d.ts.map +0 -1
- package/dist/integrity.js +0 -213
- package/dist/integrity.js.map +0 -1
- package/dist/launchSupervisor.d.ts +0 -38
- package/dist/launchSupervisor.d.ts.map +0 -1
- package/dist/launchSupervisor.js.map +0 -1
- package/dist/merge.d.ts +0 -2
- package/dist/merge.d.ts.map +0 -1
- package/dist/merge.js +0 -119
- package/dist/merge.js.map +0 -1
- package/dist/nodeAssert.d.ts +0 -31
- package/dist/nodeAssert.d.ts.map +0 -1
- package/dist/nodeAssert.js.map +0 -1
- package/dist/nodeAssertAdapter.d.ts +0 -6
- package/dist/nodeAssertAdapter.d.ts.map +0 -1
- package/dist/nodeAssertAdapter.js.map +0 -1
- package/dist/nodeAssertStrict.d.ts +0 -31
- package/dist/nodeAssertStrict.d.ts.map +0 -1
- package/dist/nodeAssertStrict.js.map +0 -1
- package/dist/nodeAssertionInstrumenter.d.ts +0 -8
- package/dist/nodeAssertionInstrumenter.d.ts.map +0 -1
- package/dist/nodeAssertionInstrumenter.js +0 -271
- package/dist/nodeAssertionInstrumenter.js.map +0 -1
- package/dist/nodeTest.d.ts +0 -15
- package/dist/nodeTest.d.ts.map +0 -1
- package/dist/nodeTest.js.map +0 -1
- package/dist/playwright.d.ts +0 -9
- package/dist/playwright.d.ts.map +0 -1
- package/dist/playwright.js.map +0 -1
- package/dist/playwrightReporter.d.ts +0 -6
- package/dist/playwrightReporter.d.ts.map +0 -1
- package/dist/playwrightReporter.js.map +0 -1
- package/dist/processDiagnostics.d.ts +0 -34
- package/dist/processDiagnostics.d.ts.map +0 -1
- package/dist/processDiagnostics.js +0 -155
- package/dist/processDiagnostics.js.map +0 -1
- package/dist/project.d.ts +0 -21
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js +0 -331
- package/dist/project.js.map +0 -1
- package/dist/provenance.d.ts +0 -9
- package/dist/provenance.d.ts.map +0 -1
- package/dist/provenance.js.map +0 -1
- package/dist/query.d.ts +0 -81
- package/dist/query.d.ts.map +0 -1
- package/dist/query.js +0 -1656
- package/dist/query.js.map +0 -1
- package/dist/queryCache.d.ts +0 -15
- package/dist/queryCache.d.ts.map +0 -1
- package/dist/queryCache.js +0 -95
- package/dist/queryCache.js.map +0 -1
- package/dist/queueAdapters.d.ts +0 -15
- package/dist/queueAdapters.d.ts.map +0 -1
- package/dist/queueAdapters.js +0 -55
- package/dist/queueAdapters.js.map +0 -1
- package/dist/register.d.mts +0 -2
- package/dist/register.d.mts.map +0 -1
- package/dist/register.mjs.map +0 -1
- package/dist/reporter.d.ts +0 -13
- package/dist/reporter.d.ts.map +0 -1
- package/dist/reporter.js +0 -252
- package/dist/reporter.js.map +0 -1
- package/dist/resolve-loader.d.mts +0 -3
- package/dist/resolve-loader.d.mts.map +0 -1
- package/dist/resolve-loader.mjs.map +0 -1
- package/dist/runAnalysis.d.ts +0 -19
- package/dist/runAnalysis.d.ts.map +0 -1
- package/dist/runAnalysis.js +0 -171
- package/dist/runAnalysis.js.map +0 -1
- package/dist/runnerEvidence.d.ts +0 -19
- package/dist/runnerEvidence.d.ts.map +0 -1
- package/dist/runnerEvidence.js.map +0 -1
- package/dist/runtime.d.ts +0 -140
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js.map +0 -1
- package/dist/runtimeIsolation.d.ts +0 -3
- package/dist/runtimeIsolation.d.ts.map +0 -1
- package/dist/runtimeIsolation.js +0 -9
- package/dist/runtimeIsolation.js.map +0 -1
- package/dist/sourceDiscovery.d.ts +0 -9
- package/dist/sourceDiscovery.d.ts.map +0 -1
- package/dist/sourceDiscovery.js +0 -242
- package/dist/sourceDiscovery.js.map +0 -1
- package/dist/transport.d.ts +0 -17
- package/dist/transport.d.ts.map +0 -1
- package/dist/transport.js.map +0 -1
- package/dist/types.d.ts +0 -378
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/vitePlugin.d.ts +0 -13
- package/dist/vitePlugin.d.ts.map +0 -1
- package/dist/vitePlugin.js +0 -139
- package/dist/vitePlugin.js.map +0 -1
- package/dist/vitest.d.ts +0 -2
- package/dist/vitest.d.ts.map +0 -1
- package/dist/vitest.js.map +0 -1
- package/dist/vitestReporter.d.ts +0 -43
- package/dist/vitestReporter.d.ts.map +0 -1
- package/dist/vitestReporter.js.map +0 -1
- package/dist/waivers.d.ts +0 -52
- package/dist/waivers.d.ts.map +0 -1
- package/dist/waivers.js +0 -121
- package/dist/waivers.js.map +0 -1
- package/dist/workspace.d.ts +0 -117
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -680
- package/dist/workspace.js.map +0 -1
- /package/{dist → runtime/javascript}/runtime.js +0 -0
package/dist/query.js
DELETED
|
@@ -1,1656 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { coverageSummaryForTests, isIndependencePair } from "./analyze.js";
|
|
5
|
-
import { EVIDENCE_ARCHIVE_SCHEMA_VERSION } from "./evidenceArchive.js";
|
|
6
|
-
import { analyzeCoverageArchiveCached, readCoverageQueryIndex, } from "./queryCache.js";
|
|
7
|
-
import { compareRunIntegrity, createRunIntegrity } from "./integrity.js";
|
|
8
|
-
import { evaluateCoverageWaivers, readCoverageWaivers, WAIVERS_FILE, } from "./waivers.js";
|
|
9
|
-
import { discoverCoverageProject } from "./project.js";
|
|
10
|
-
import { agentPagination, agentSuccessJson, SupercovError, } from "./agentJson.js";
|
|
11
|
-
function parseOptions(command, args) {
|
|
12
|
-
const options = {
|
|
13
|
-
command: command === "runs" || command === "diff" || command === "help"
|
|
14
|
-
? command
|
|
15
|
-
: `coverage.${command}`,
|
|
16
|
-
limit: 20,
|
|
17
|
-
offset: 0,
|
|
18
|
-
json: false,
|
|
19
|
-
filter: "all",
|
|
20
|
-
target: 100,
|
|
21
|
-
metric: "all",
|
|
22
|
-
group: "none",
|
|
23
|
-
sort: "location",
|
|
24
|
-
positional: [],
|
|
25
|
-
};
|
|
26
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
27
|
-
const value = args[index];
|
|
28
|
-
if (value === "--json")
|
|
29
|
-
options.json = true;
|
|
30
|
-
else if (value === "--run") {
|
|
31
|
-
const run = args[++index];
|
|
32
|
-
if (!run)
|
|
33
|
-
throw new SupercovError("INVALID_ARGUMENT", "--run requires a run ID");
|
|
34
|
-
options.run = run;
|
|
35
|
-
}
|
|
36
|
-
else if (value === "--kind") {
|
|
37
|
-
const kind = args[++index]?.toLowerCase();
|
|
38
|
-
if (!kind)
|
|
39
|
-
throw new SupercovError("INVALID_ARGUMENT", "--kind requires a test kind");
|
|
40
|
-
options.kind = kind;
|
|
41
|
-
}
|
|
42
|
-
else if (value === "--runner") {
|
|
43
|
-
const runner = args[++index]?.toLowerCase();
|
|
44
|
-
if (!runner)
|
|
45
|
-
throw new SupercovError("INVALID_ARGUMENT", "--runner requires a runner name");
|
|
46
|
-
options.runner = runner;
|
|
47
|
-
}
|
|
48
|
-
else if (value === "--target") {
|
|
49
|
-
const target = Number(args[++index]);
|
|
50
|
-
if (!Number.isFinite(target) || target < 0 || target > 100)
|
|
51
|
-
throw new SupercovError("INVALID_ARGUMENT", "--target must be between 0 and 100");
|
|
52
|
-
options.target = target;
|
|
53
|
-
}
|
|
54
|
-
else if (value === "--metric") {
|
|
55
|
-
const metric = args[++index]?.toLowerCase();
|
|
56
|
-
if (!metric || !["all", "lines", "statements", "functions", "branches", "mcdc"].includes(metric))
|
|
57
|
-
throw new SupercovError("INVALID_ARGUMENT", "--metric must be all, lines, statements, functions, branches, or mcdc");
|
|
58
|
-
options.metric = metric;
|
|
59
|
-
}
|
|
60
|
-
else if (value === "--filter") {
|
|
61
|
-
const filter = args[++index]?.toLowerCase();
|
|
62
|
-
if (filter !== "all" && filter !== "passed" && filter !== "failed")
|
|
63
|
-
throw new SupercovError("INVALID_ARGUMENT", "--filter must be all, passed, or failed");
|
|
64
|
-
options.filter = filter;
|
|
65
|
-
}
|
|
66
|
-
else if (value === "--limit") {
|
|
67
|
-
const limit = Number(args[++index]);
|
|
68
|
-
if (!Number.isSafeInteger(limit) || limit < 1)
|
|
69
|
-
throw new SupercovError("INVALID_ARGUMENT", "--limit must be a positive integer");
|
|
70
|
-
options.limit = limit;
|
|
71
|
-
}
|
|
72
|
-
else if (value === "--offset") {
|
|
73
|
-
const offset = Number(args[++index]);
|
|
74
|
-
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
75
|
-
throw new SupercovError("INVALID_ARGUMENT", "--offset must be a non-negative integer");
|
|
76
|
-
options.offset = offset;
|
|
77
|
-
}
|
|
78
|
-
else if (value === "--group") {
|
|
79
|
-
const group = args[++index]?.toLowerCase();
|
|
80
|
-
if (group !== "decision")
|
|
81
|
-
throw new SupercovError("INVALID_ARGUMENT", "--group must be decision");
|
|
82
|
-
if (command !== "file")
|
|
83
|
-
throw new SupercovError("INVALID_ARGUMENT", "--group is only supported by: supercov runs <run-id> coverage file <source-file>");
|
|
84
|
-
options.group = "decision";
|
|
85
|
-
}
|
|
86
|
-
else if (value === "--sort") {
|
|
87
|
-
const sort = args[++index]?.toLowerCase();
|
|
88
|
-
if (sort !== "location" && sort !== "missing")
|
|
89
|
-
throw new SupercovError("INVALID_ARGUMENT", "--sort must be location or missing");
|
|
90
|
-
options.sort = sort;
|
|
91
|
-
}
|
|
92
|
-
else if (value.startsWith("--"))
|
|
93
|
-
throw new SupercovError("INVALID_ARGUMENT", `Unknown option: ${value}`, {
|
|
94
|
-
details: { option: value },
|
|
95
|
-
});
|
|
96
|
-
else
|
|
97
|
-
options.positional.push(value);
|
|
98
|
-
}
|
|
99
|
-
if (options.sort !== "location" && options.group === "none")
|
|
100
|
-
throw new SupercovError("INVALID_ARGUMENT", "--sort requires --group decision");
|
|
101
|
-
if (options.group === "decision" && options.metric !== "all" && options.metric !== "mcdc")
|
|
102
|
-
throw new SupercovError("INVALID_ARGUMENT", "--group decision lists MC/DC decisions; omit --metric or use --metric mcdc");
|
|
103
|
-
return options;
|
|
104
|
-
}
|
|
105
|
-
function filteredCoverage(report, options) {
|
|
106
|
-
if (options.filter === "all")
|
|
107
|
-
return report;
|
|
108
|
-
const filtered = report.filters?.[options.filter];
|
|
109
|
-
if (!filtered) {
|
|
110
|
-
throw new SupercovError("FILTER_UNAVAILABLE", "This run does not contain outcome-filtered coverage. Create a new coverage run.");
|
|
111
|
-
}
|
|
112
|
-
return filtered;
|
|
113
|
-
}
|
|
114
|
-
function readJson(path) {
|
|
115
|
-
try {
|
|
116
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
function discoverRuns(root) {
|
|
123
|
-
const runs = new Map();
|
|
124
|
-
const canonical = resolve(root, ".supercov/runs");
|
|
125
|
-
if (existsSync(canonical)) {
|
|
126
|
-
for (const entry of readdirSync(canonical, { withFileTypes: true })) {
|
|
127
|
-
if (!entry.isDirectory())
|
|
128
|
-
continue;
|
|
129
|
-
const evidencePath = resolve(canonical, entry.name, "evidence.raw.gz");
|
|
130
|
-
const metadata = readJson(resolve(canonical, entry.name, "run.json"));
|
|
131
|
-
if (!existsSync(evidencePath) ||
|
|
132
|
-
!metadata ||
|
|
133
|
-
metadata.rawEvidence?.schemaVersion !== EVIDENCE_ARCHIVE_SCHEMA_VERSION)
|
|
134
|
-
continue;
|
|
135
|
-
runs.set(entry.name, {
|
|
136
|
-
id: entry.name,
|
|
137
|
-
evidencePath,
|
|
138
|
-
metadata,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return [...runs.values()].sort((left, right) => right.id.localeCompare(left.id));
|
|
143
|
-
}
|
|
144
|
-
function storedRunAnalysisOptions(run) {
|
|
145
|
-
return {
|
|
146
|
-
runId: run.id,
|
|
147
|
-
testExitCode: run.metadata?.testExitCode,
|
|
148
|
-
integrity: run.metadata?.integrity,
|
|
149
|
-
generatedAt: run.metadata?.startedAt,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
function analyzeStoredRun(run) {
|
|
153
|
-
return analyzeCoverageArchiveCached(run.evidencePath, storedRunAnalysisOptions(run));
|
|
154
|
-
}
|
|
155
|
-
function readStoredRunIndex(run) {
|
|
156
|
-
return readCoverageQueryIndex(run.evidencePath, storedRunAnalysisOptions(run));
|
|
157
|
-
}
|
|
158
|
-
function selectRun(root, selector, currentIntegrity, quiet = false) {
|
|
159
|
-
const runs = discoverRuns(root);
|
|
160
|
-
if (runs.length === 0)
|
|
161
|
-
throw new SupercovError("NO_RUNS", "No local coverage runs. Run supercov first.");
|
|
162
|
-
const selected = !selector || selector === "latest"
|
|
163
|
-
? runs[0]
|
|
164
|
-
: (runs.find((run) => run.id === selector) ??
|
|
165
|
-
runs.find((run) => run.id.startsWith(selector)));
|
|
166
|
-
if (!selected)
|
|
167
|
-
throw new SupercovError("RUN_NOT_FOUND", `Coverage run not found: ${selector}`, {
|
|
168
|
-
details: { selector },
|
|
169
|
-
});
|
|
170
|
-
const report = analyzeStoredRun(selected);
|
|
171
|
-
if (currentIntegrity) {
|
|
172
|
-
const comparison = compareRunIntegrity(selected.metadata?.integrity ?? report.integrity, currentIntegrity);
|
|
173
|
-
report.integrity = {
|
|
174
|
-
...(selected.metadata?.integrity ?? report.integrity ?? currentIntegrity),
|
|
175
|
-
stale: comparison.stale,
|
|
176
|
-
staleReasons: comparison.reasons,
|
|
177
|
-
};
|
|
178
|
-
if (comparison.stale && !quiet) {
|
|
179
|
-
console.error(`[supercov] stale run ${selected.id}: ${comparison.reasons.join(", ")}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return { run: selected, report };
|
|
183
|
-
}
|
|
184
|
-
function currentProjectIntegrity(root) {
|
|
185
|
-
try {
|
|
186
|
-
return createRunIntegrity(root, discoverCoverageProject(root), fileURLToPath(new URL(".", import.meta.url)));
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
function page(values, options) {
|
|
193
|
-
return values.slice(options.offset, options.offset + options.limit);
|
|
194
|
-
}
|
|
195
|
-
function output(value, options, text, pagination) {
|
|
196
|
-
process.stdout.write(options.json ? agentSuccessJson(options.command, value, pagination) : `${text}\n`);
|
|
197
|
-
}
|
|
198
|
-
function queryPagination(total, returned, options) {
|
|
199
|
-
return agentPagination(options.offset, options.limit, returned, total);
|
|
200
|
-
}
|
|
201
|
-
function pct(value) {
|
|
202
|
-
return `${value.toFixed(2)}%`;
|
|
203
|
-
}
|
|
204
|
-
function shellQuote(value) {
|
|
205
|
-
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
206
|
-
}
|
|
207
|
-
function optionKey(option) {
|
|
208
|
-
return [...new Set(option)].sort().join("\0");
|
|
209
|
-
}
|
|
210
|
-
function minimumTestObligations(report, candidates) {
|
|
211
|
-
const tests = new Map(report.tests.map((test) => [test.id, test]));
|
|
212
|
-
const testsByFile = new Map();
|
|
213
|
-
for (const test of report.tests) {
|
|
214
|
-
if (test.role !== "test" || !test.file || !candidates.has(test.id))
|
|
215
|
-
continue;
|
|
216
|
-
const ids = testsByFile.get(test.file) ?? [];
|
|
217
|
-
ids.push(test.id);
|
|
218
|
-
testsByFile.set(test.file, ids);
|
|
219
|
-
}
|
|
220
|
-
const evidenceChoices = (ids) => {
|
|
221
|
-
const options = [];
|
|
222
|
-
for (const id of ids) {
|
|
223
|
-
const test = tests.get(id);
|
|
224
|
-
if (!test)
|
|
225
|
-
continue;
|
|
226
|
-
if (test.role === "background")
|
|
227
|
-
options.push([]);
|
|
228
|
-
else if (test.role === "setup" && test.file)
|
|
229
|
-
options.push(...(testsByFile.get(test.file) ?? []).map((candidate) => [candidate]));
|
|
230
|
-
else if (candidates.has(id))
|
|
231
|
-
options.push([id]);
|
|
232
|
-
}
|
|
233
|
-
return [...new Map(options.map((option) => [optionKey(option), option])).values()];
|
|
234
|
-
};
|
|
235
|
-
const obligations = [];
|
|
236
|
-
const uniqueLines = new Map();
|
|
237
|
-
for (const line of report.lines)
|
|
238
|
-
uniqueLines.set(`${line.file}:${line.line}`, line);
|
|
239
|
-
for (const [id, line] of uniqueLines)
|
|
240
|
-
obligations.push({ id: `line:${id}`, metric: "lines", options: evidenceChoices(line.tests) });
|
|
241
|
-
for (const point of report.points)
|
|
242
|
-
obligations.push({
|
|
243
|
-
id: `${point.meta.kind}:${point.meta.id}`,
|
|
244
|
-
metric: point.meta.kind === "statement" ? "statements" : "functions",
|
|
245
|
-
options: evidenceChoices(point.tests),
|
|
246
|
-
});
|
|
247
|
-
for (const branch of report.branches)
|
|
248
|
-
for (const alternative of branch.alternatives)
|
|
249
|
-
obligations.push({
|
|
250
|
-
id: `branch:${branch.meta.id}:${alternative.id}`,
|
|
251
|
-
metric: "branches",
|
|
252
|
-
options: evidenceChoices(alternative.tests),
|
|
253
|
-
});
|
|
254
|
-
for (const decision of report.decisions) {
|
|
255
|
-
for (let condition = 0; condition < decision.meta.conditions.length; condition += 1) {
|
|
256
|
-
const options = [];
|
|
257
|
-
for (let left = 0; left < decision.vectorObservations.length; left += 1) {
|
|
258
|
-
for (let right = left + 1; right < decision.vectorObservations.length; right += 1) {
|
|
259
|
-
const first = decision.vectorObservations[left];
|
|
260
|
-
const second = decision.vectorObservations[right];
|
|
261
|
-
if (!isIndependencePair(first.vector, second.vector, condition))
|
|
262
|
-
continue;
|
|
263
|
-
for (const firstChoice of evidenceChoices(first.tests))
|
|
264
|
-
for (const secondChoice of evidenceChoices(second.tests))
|
|
265
|
-
options.push([...new Set([...firstChoice, ...secondChoice])].sort());
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
obligations.push({
|
|
269
|
-
id: `mcdc:${decision.meta.id}:${condition}`,
|
|
270
|
-
metric: "mcdc",
|
|
271
|
-
options: [...new Map(options.map((option) => [optionKey(option), option])).values()],
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return {
|
|
276
|
-
obligations,
|
|
277
|
-
expand(selected) {
|
|
278
|
-
const expanded = new Set(selected);
|
|
279
|
-
for (const test of report.tests) {
|
|
280
|
-
if (test.role === "background")
|
|
281
|
-
expanded.add(test.id);
|
|
282
|
-
else if (test.role === "setup" &&
|
|
283
|
-
test.file &&
|
|
284
|
-
(testsByFile.get(test.file) ?? []).some((id) => selected.has(id)))
|
|
285
|
-
expanded.add(test.id);
|
|
286
|
-
}
|
|
287
|
-
return expanded;
|
|
288
|
-
},
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
function obligationSatisfied(obligation, selected) {
|
|
292
|
-
return obligation.options.some((option) => option.every((test) => selected.has(test)));
|
|
293
|
-
}
|
|
294
|
-
/** Exact branch-and-bound solver; MC/DC obligations retain their witness-pair structure. */
|
|
295
|
-
export function minimumTestSet(report, target = 100, metric = "all", maxStates = 5_000) {
|
|
296
|
-
const unattributed = report.tests.filter((test) => test.role === "background" &&
|
|
297
|
-
(test.hits.length > 0 || test.decisions.some((decision) => decision.vectors.length > 0)));
|
|
298
|
-
if (unattributed.length > 0) {
|
|
299
|
-
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.");
|
|
300
|
-
}
|
|
301
|
-
const candidateTests = report.tests
|
|
302
|
-
.filter((test) => test.role === "test")
|
|
303
|
-
.map((test) => test.id)
|
|
304
|
-
.sort();
|
|
305
|
-
const candidates = new Set(candidateTests);
|
|
306
|
-
const model = minimumTestObligations(report, candidates);
|
|
307
|
-
const metrics = metric === "all"
|
|
308
|
-
? ["lines", "statements", "functions", "branches", "mcdc"]
|
|
309
|
-
: [metric];
|
|
310
|
-
const obligations = model.obligations.filter((obligation) => metrics.includes(obligation.metric));
|
|
311
|
-
const totals = new Map();
|
|
312
|
-
for (const obligation of obligations)
|
|
313
|
-
totals.set(obligation.metric, (totals.get(obligation.metric) ?? 0) + 1);
|
|
314
|
-
const skipLimits = new Map();
|
|
315
|
-
for (const selectedMetric of metrics) {
|
|
316
|
-
const total = totals.get(selectedMetric) ?? 0;
|
|
317
|
-
const required = Math.ceil((total * target) / 100);
|
|
318
|
-
skipLimits.set(selectedMetric, total - required);
|
|
319
|
-
}
|
|
320
|
-
let best = new Set(candidateTests);
|
|
321
|
-
const fullExpanded = model.expand(best);
|
|
322
|
-
const fullSummary = coverageSummaryForTests(report, fullExpanded);
|
|
323
|
-
const percentage = (selectedMetric, summary) => selectedMetric === "mcdc" ? summary.conditionCoveragePct : summary[selectedMetric].percentage;
|
|
324
|
-
const impossible = metrics.find((selectedMetric) => percentage(selectedMetric, fullSummary) + 1e-9 < target);
|
|
325
|
-
if (impossible)
|
|
326
|
-
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) } });
|
|
327
|
-
let exploredStates = 0;
|
|
328
|
-
const seen = new Set();
|
|
329
|
-
const search = (selected, skipped, skippedByMetric) => {
|
|
330
|
-
exploredStates += 1;
|
|
331
|
-
if (exploredStates > maxStates) {
|
|
332
|
-
throw new SupercovError("MINIMIZATION_COMPLEXITY_LIMIT", `Exact minimization exceeded its ${maxStates.toLocaleString()}-state safety budget. Narrow the test view with --kind or --runner, or request a different target.`, {
|
|
333
|
-
details: {
|
|
334
|
-
candidateTests: candidateTests.length,
|
|
335
|
-
obligations: obligations.length,
|
|
336
|
-
exploredStates,
|
|
337
|
-
maxStates,
|
|
338
|
-
target,
|
|
339
|
-
metric,
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
if (selected.size >= best.size)
|
|
344
|
-
return;
|
|
345
|
-
const stateKey = `${[...selected].sort().join(",")}|${[...skipped].sort().join(",")}`;
|
|
346
|
-
if (seen.has(stateKey))
|
|
347
|
-
return;
|
|
348
|
-
seen.add(stateKey);
|
|
349
|
-
const unmet = obligations.filter((obligation) => !skipped.has(obligation.id) && !obligationSatisfied(obligation, selected));
|
|
350
|
-
if (unmet.length === 0) {
|
|
351
|
-
best = new Set(selected);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
const obligation = unmet.sort((left, right) => {
|
|
355
|
-
const feasible = (value) => value.options.filter((option) => option.some((test) => !selected.has(test))).length;
|
|
356
|
-
return feasible(left) - feasible(right) || left.id.localeCompare(right.id);
|
|
357
|
-
})[0];
|
|
358
|
-
const additions = [...new Map(obligation.options
|
|
359
|
-
.map((option) => option.filter((test) => !selected.has(test)))
|
|
360
|
-
.filter((option) => option.length > 0)
|
|
361
|
-
.map((option) => [optionKey(option), option])).values()].sort((left, right) => left.length - right.length || optionKey(left).localeCompare(optionKey(right)));
|
|
362
|
-
for (const addition of additions) {
|
|
363
|
-
if (selected.size + addition.length >= best.size)
|
|
364
|
-
continue;
|
|
365
|
-
search(new Set([...selected, ...addition]), skipped, skippedByMetric);
|
|
366
|
-
}
|
|
367
|
-
const skippedCount = skippedByMetric.get(obligation.metric) ?? 0;
|
|
368
|
-
if (skippedCount < (skipLimits.get(obligation.metric) ?? 0)) {
|
|
369
|
-
const nextSkipped = new Set(skipped);
|
|
370
|
-
nextSkipped.add(obligation.id);
|
|
371
|
-
const nextCounts = new Map(skippedByMetric);
|
|
372
|
-
nextCounts.set(obligation.metric, skippedCount + 1);
|
|
373
|
-
search(selected, nextSkipped, nextCounts);
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
search(new Set(), new Set(), new Map());
|
|
377
|
-
const expanded = model.expand(best);
|
|
378
|
-
return {
|
|
379
|
-
optimal: true,
|
|
380
|
-
target,
|
|
381
|
-
metric,
|
|
382
|
-
selected: [...best].sort(),
|
|
383
|
-
expanded: [...expanded].sort(),
|
|
384
|
-
summary: coverageSummaryForTests(report, expanded),
|
|
385
|
-
exploredStates,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
function coverageCommand(runId, options, child) {
|
|
389
|
-
return [
|
|
390
|
-
"npx supercov runs",
|
|
391
|
-
shellQuote(runId),
|
|
392
|
-
"coverage",
|
|
393
|
-
child,
|
|
394
|
-
options.filter !== "all" ? `--filter ${options.filter}` : undefined,
|
|
395
|
-
options.kind ? `--kind ${shellQuote(options.kind)}` : undefined,
|
|
396
|
-
options.runner ? `--runner ${shellQuote(options.runner)}` : undefined,
|
|
397
|
-
options.metric !== "all" ? `--metric ${options.metric}` : undefined,
|
|
398
|
-
]
|
|
399
|
-
.filter(Boolean)
|
|
400
|
-
.join(" ");
|
|
401
|
-
}
|
|
402
|
-
function pageLabel(total, returned, options) {
|
|
403
|
-
const start = total === 0 || returned === 0 ? 0 : options.offset + 1;
|
|
404
|
-
const end = Math.min(options.offset + returned, total);
|
|
405
|
-
return `showing ${start}-${end} of ${total}`;
|
|
406
|
-
}
|
|
407
|
-
function nextPageCommand(base, total, returned, options) {
|
|
408
|
-
const nextOffset = options.offset + returned;
|
|
409
|
-
if (returned === 0 || nextOffset >= total)
|
|
410
|
-
return undefined;
|
|
411
|
-
return `${base} --offset ${nextOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`;
|
|
412
|
-
}
|
|
413
|
-
function selectedTestIds(report, options) {
|
|
414
|
-
if (!options.kind && !options.runner)
|
|
415
|
-
return undefined;
|
|
416
|
-
const selected = report.tests.filter((test) => (!options.kind || test.provenance.kind === options.kind) &&
|
|
417
|
-
(!options.runner || test.provenance.runner === options.runner));
|
|
418
|
-
if (selected.length === 0) {
|
|
419
|
-
const filter = [
|
|
420
|
-
options.kind ? `kind=${options.kind}` : undefined,
|
|
421
|
-
options.runner ? `runner=${options.runner}` : undefined,
|
|
422
|
-
]
|
|
423
|
-
.filter(Boolean)
|
|
424
|
-
.join(", ");
|
|
425
|
-
throw new SupercovError("TEST_FILTER_EMPTY", `No tests match ${filter}`, {
|
|
426
|
-
details: { kind: options.kind, runner: options.runner },
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
return new Set(selected.map((test) => test.id));
|
|
430
|
-
}
|
|
431
|
-
function includesSelectedTest(tests, selected) {
|
|
432
|
-
return selected ? tests.some((test) => selected.has(test)) : tests.length > 0;
|
|
433
|
-
}
|
|
434
|
-
function otherCoverage(report, testIds, selected) {
|
|
435
|
-
const tests = selected
|
|
436
|
-
? testIds
|
|
437
|
-
.filter((id) => !selected.has(id))
|
|
438
|
-
.map((id) => report.tests.find((test) => test.id === id))
|
|
439
|
-
.filter((test) => Boolean(test))
|
|
440
|
-
: [];
|
|
441
|
-
return {
|
|
442
|
-
coveredElsewhere: tests.length > 0,
|
|
443
|
-
kinds: [...new Set(tests.map((test) => test.provenance.kind))].sort(),
|
|
444
|
-
runners: [...new Set(tests.map((test) => test.provenance.runner))].sort(),
|
|
445
|
-
tests: tests.map((test) => ({ id: test.id, name: test.name })),
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
function filterDecision(decision, selected) {
|
|
449
|
-
if (!selected)
|
|
450
|
-
return decision;
|
|
451
|
-
const vectorObservations = decision.vectorObservations
|
|
452
|
-
.map((observation) => ({
|
|
453
|
-
...observation,
|
|
454
|
-
tests: observation.tests.filter((test) => selected.has(test)),
|
|
455
|
-
}))
|
|
456
|
-
.filter((observation) => observation.tests.length > 0);
|
|
457
|
-
const vectors = vectorObservations.map((observation) => observation.vector);
|
|
458
|
-
const conditions = decision.meta.conditions.map((source, index) => {
|
|
459
|
-
let witness;
|
|
460
|
-
for (let left = 0; left < vectors.length && !witness; left += 1) {
|
|
461
|
-
for (let right = left + 1; right < vectors.length; right += 1) {
|
|
462
|
-
const first = vectors[left];
|
|
463
|
-
const second = vectors[right];
|
|
464
|
-
if (isIndependencePair(first, second, index)) {
|
|
465
|
-
witness = [first, second];
|
|
466
|
-
break;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
const witnessTests = witness?.map((vector) => vectorObservations.find((observation) => observation.vector === vector)
|
|
471
|
-
?.tests ?? []);
|
|
472
|
-
return {
|
|
473
|
-
index,
|
|
474
|
-
source,
|
|
475
|
-
covered: Boolean(witness),
|
|
476
|
-
...(witness ? { witness } : {}),
|
|
477
|
-
...(witnessTests ? { witnessTests } : {}),
|
|
478
|
-
};
|
|
479
|
-
});
|
|
480
|
-
return {
|
|
481
|
-
...decision,
|
|
482
|
-
executed: vectors.length > 0,
|
|
483
|
-
covered: conditions.every((condition) => condition.covered),
|
|
484
|
-
vectors,
|
|
485
|
-
vectorObservations,
|
|
486
|
-
conditions,
|
|
487
|
-
tests: decision.tests.filter((test) => selected.has(test)),
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
function filterLabel(options) {
|
|
491
|
-
return [
|
|
492
|
-
options.filter !== "all" ? `${options.filter} attempts only` : undefined,
|
|
493
|
-
options.kind ? `kind ${options.kind}` : undefined,
|
|
494
|
-
options.runner ? `runner ${options.runner}` : undefined,
|
|
495
|
-
]
|
|
496
|
-
.filter(Boolean)
|
|
497
|
-
.join(", ");
|
|
498
|
-
}
|
|
499
|
-
function queryFilters(options) {
|
|
500
|
-
return {
|
|
501
|
-
outcome: options.filter,
|
|
502
|
-
kind: options.kind ?? null,
|
|
503
|
-
runner: options.runner ?? null,
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
function attribution(report, selected) {
|
|
507
|
-
const phases = selected
|
|
508
|
-
? report.phases.filter((phase) => selected.has(phase.test))
|
|
509
|
-
: report.phases;
|
|
510
|
-
return {
|
|
511
|
-
browserExplicit: phases.reduce((sum, phase) => sum + phase.explicitBrowserEvents, 0),
|
|
512
|
-
browserFallback: phases.reduce((sum, phase) => sum + phase.inferredBrowserEvents, 0),
|
|
513
|
-
serverExplicit: phases.reduce((sum, phase) => sum + phase.explicitServerEvents, 0),
|
|
514
|
-
serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
export function coverageDiagnostics(report, selected) {
|
|
518
|
-
const observed = attribution(report, selected);
|
|
519
|
-
const diagnostics = [];
|
|
520
|
-
// Assertion phases prove that the runner adapter observed the attempt, but
|
|
521
|
-
// they do not prove that the test executed measured application source. A
|
|
522
|
-
// contract/static-data assertion can legitimately have no coverage. Keep
|
|
523
|
-
// this visible as an ambiguity warning; only explicit corrupt transport is
|
|
524
|
-
// strong enough to claim evidence loss as an error.
|
|
525
|
-
const emptyEvidenceTests = report.tests.filter((test) => (test.role ?? "test") === "test" &&
|
|
526
|
-
(!selected || selected.has(test.id)) &&
|
|
527
|
-
test.lines.length === 0 &&
|
|
528
|
-
test.hits.length === 0 &&
|
|
529
|
-
test.decisions.length === 0 &&
|
|
530
|
-
report.phases.some((phase) => phase.test === test.id));
|
|
531
|
-
if (emptyEvidenceTests.length > 0) {
|
|
532
|
-
diagnostics.push({
|
|
533
|
-
code: "TEST_EVIDENCE_MISSING",
|
|
534
|
-
severity: "warning",
|
|
535
|
-
message: `${emptyEvidenceTests.length} test(s) recorded assertion phases but attributed zero coverage evidence; ` +
|
|
536
|
-
`this is valid for assertions over static or uninstrumented data, but may otherwise indicate missing probe transport. ` +
|
|
537
|
-
`First: ${emptyEvidenceTests[0].name}`,
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
if ((report.transport?.corruptRecords ?? 0) > 0) {
|
|
541
|
-
diagnostics.push({
|
|
542
|
-
code: "CORRUPT_EVIDENCE_RECORDS",
|
|
543
|
-
severity: "error",
|
|
544
|
-
message: `${report.transport.corruptRecords} malformed evidence record(s) in ` +
|
|
545
|
-
`${report.transport.corruptFiles} file(s) were excluded; coverage is incomplete.`,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
if ((report.transport?.remoteLaunches ?? 0) > 0 &&
|
|
549
|
-
(report.transport?.scopedServerRecords ?? 0) === 0 &&
|
|
550
|
-
observed.serverExplicit === 0 &&
|
|
551
|
-
observed.serverFallback === 0) {
|
|
552
|
-
diagnostics.push({
|
|
553
|
-
code: "REMOTE_SERVER_EVIDENCE_MISSING",
|
|
554
|
-
severity: "warning",
|
|
555
|
-
message: "Remote launches were supervised, but no server evidence returned. Coverage may describe only browser/test processes; inspect how the application server is launched.",
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
return diagnostics;
|
|
559
|
-
}
|
|
560
|
-
function gapMetricValue(gap, metric) {
|
|
561
|
-
if (metric === "all")
|
|
562
|
-
return gap.score;
|
|
563
|
-
if (metric === "lines")
|
|
564
|
-
return gap.uncoveredLines;
|
|
565
|
-
if (metric === "statements")
|
|
566
|
-
return gap.uncoveredStatements;
|
|
567
|
-
if (metric === "functions")
|
|
568
|
-
return gap.uncoveredFunctions;
|
|
569
|
-
if (metric === "branches")
|
|
570
|
-
return gap.missingBranches;
|
|
571
|
-
return gap.missingMcdcConditions;
|
|
572
|
-
}
|
|
573
|
-
function obligationMatchesMetric(obligation, metric) {
|
|
574
|
-
if (metric === "all")
|
|
575
|
-
return true;
|
|
576
|
-
const kindByMetric = {
|
|
577
|
-
lines: "line",
|
|
578
|
-
statements: "statement",
|
|
579
|
-
functions: "function",
|
|
580
|
-
branches: "branch",
|
|
581
|
-
mcdc: "mcdc",
|
|
582
|
-
};
|
|
583
|
-
return obligation.kind === kindByMetric[metric];
|
|
584
|
-
}
|
|
585
|
-
export function fileGaps(report, selected) {
|
|
586
|
-
const files = new Map();
|
|
587
|
-
const get = (file) => {
|
|
588
|
-
const existing = files.get(file);
|
|
589
|
-
if (existing)
|
|
590
|
-
return existing;
|
|
591
|
-
const created = {
|
|
592
|
-
file,
|
|
593
|
-
uncoveredLines: 0,
|
|
594
|
-
uncoveredStatements: 0,
|
|
595
|
-
uncoveredFunctions: 0,
|
|
596
|
-
missingBranches: 0,
|
|
597
|
-
missingMcdcConditions: 0,
|
|
598
|
-
measurementLimitations: 0,
|
|
599
|
-
limitationKinds: [],
|
|
600
|
-
coveredByOtherTests: {
|
|
601
|
-
lines: 0,
|
|
602
|
-
statements: 0,
|
|
603
|
-
functions: 0,
|
|
604
|
-
branches: 0,
|
|
605
|
-
mcdcConditions: 0,
|
|
606
|
-
},
|
|
607
|
-
uncoveredEverywhere: {
|
|
608
|
-
lines: 0,
|
|
609
|
-
statements: 0,
|
|
610
|
-
functions: 0,
|
|
611
|
-
branches: 0,
|
|
612
|
-
mcdcConditions: 0,
|
|
613
|
-
},
|
|
614
|
-
score: 0,
|
|
615
|
-
};
|
|
616
|
-
files.set(file, created);
|
|
617
|
-
return created;
|
|
618
|
-
};
|
|
619
|
-
const classify = (gap, dimension, coveredOverall) => {
|
|
620
|
-
if (selected && coveredOverall)
|
|
621
|
-
gap.coveredByOtherTests[dimension] += 1;
|
|
622
|
-
else
|
|
623
|
-
gap.uncoveredEverywhere[dimension] += 1;
|
|
624
|
-
};
|
|
625
|
-
for (const line of report.lines) {
|
|
626
|
-
const gap = get(line.file);
|
|
627
|
-
if (!includesSelectedTest(line.tests, selected)) {
|
|
628
|
-
gap.uncoveredLines += 1;
|
|
629
|
-
classify(gap, "lines", line.covered);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
for (const point of report.points) {
|
|
633
|
-
const gap = get(point.meta.file);
|
|
634
|
-
if (!includesSelectedTest(point.tests, selected)) {
|
|
635
|
-
if (point.meta.kind === "function")
|
|
636
|
-
gap.uncoveredFunctions += 1;
|
|
637
|
-
else
|
|
638
|
-
gap.uncoveredStatements += 1;
|
|
639
|
-
classify(gap, point.meta.kind === "function" ? "functions" : "statements", point.covered);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
for (const branch of report.branches) {
|
|
643
|
-
const gap = get(branch.meta.file);
|
|
644
|
-
for (const alternative of branch.alternatives) {
|
|
645
|
-
if (!includesSelectedTest(alternative.tests, selected)) {
|
|
646
|
-
gap.missingBranches += 1;
|
|
647
|
-
classify(gap, "branches", alternative.covered);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
for (const decision of report.decisions) {
|
|
652
|
-
const gap = get(decision.meta.file);
|
|
653
|
-
const filteredConditions = filterDecision(decision, selected).conditions;
|
|
654
|
-
for (const condition of filteredConditions) {
|
|
655
|
-
if (!condition.covered) {
|
|
656
|
-
gap.missingMcdcConditions += 1;
|
|
657
|
-
classify(gap, "mcdcConditions", decision.conditions[condition.index]?.covered ?? false);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
for (const limitation of report.limitations ?? []) {
|
|
662
|
-
const gap = get(limitation.file);
|
|
663
|
-
gap.measurementLimitations += 1;
|
|
664
|
-
if (!gap.limitationKinds.includes(limitation.kind))
|
|
665
|
-
gap.limitationKinds.push(limitation.kind);
|
|
666
|
-
}
|
|
667
|
-
for (const gap of files.values()) {
|
|
668
|
-
gap.limitationKinds.sort();
|
|
669
|
-
gap.score =
|
|
670
|
-
gap.uncoveredLines +
|
|
671
|
-
gap.uncoveredFunctions * 2 +
|
|
672
|
-
gap.missingBranches * 2 +
|
|
673
|
-
gap.missingMcdcConditions * 3 +
|
|
674
|
-
gap.measurementLimitations * 3;
|
|
675
|
-
}
|
|
676
|
-
return [...files.values()].sort((left, right) => right.score - left.score || left.file.localeCompare(right.file));
|
|
677
|
-
}
|
|
678
|
-
function findFile(report, selector) {
|
|
679
|
-
const files = [
|
|
680
|
-
...new Set([
|
|
681
|
-
...report.lines.map((line) => line.file),
|
|
682
|
-
...(report.limitations ?? []).map((limitation) => limitation.file),
|
|
683
|
-
]),
|
|
684
|
-
];
|
|
685
|
-
if (files.includes(selector))
|
|
686
|
-
return selector;
|
|
687
|
-
const matches = files.filter((file) => file.includes(selector));
|
|
688
|
-
if (matches.length === 1)
|
|
689
|
-
return matches[0];
|
|
690
|
-
if (matches.length === 0)
|
|
691
|
-
throw new SupercovError("SOURCE_NOT_FOUND", `Source file not found: ${selector}`, {
|
|
692
|
-
details: { selector },
|
|
693
|
-
});
|
|
694
|
-
throw new SupercovError("AMBIGUOUS_SELECTOR", `Ambiguous file selector: ${matches.join(", ")}`, {
|
|
695
|
-
details: { selector, matches },
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
export function coverageMeasurement(report) {
|
|
699
|
-
const limitations = report.limitations ?? [];
|
|
700
|
-
const evidenceCorruptions = report.transport?.corruptRecords ?? 0;
|
|
701
|
-
const byKind = {
|
|
702
|
-
"dynamic-code": 0,
|
|
703
|
-
"semantic-safety": 0,
|
|
704
|
-
"source-scope": 0,
|
|
705
|
-
};
|
|
706
|
-
for (const limitation of limitations)
|
|
707
|
-
byKind[limitation.kind] += 1;
|
|
708
|
-
return {
|
|
709
|
-
complete: limitations.length === 0 && evidenceCorruptions === 0,
|
|
710
|
-
limitations: limitations.length,
|
|
711
|
-
evidenceCorruptions,
|
|
712
|
-
// Every current limitation removes source from the measured denominator.
|
|
713
|
-
blocking: limitations.length + evidenceCorruptions,
|
|
714
|
-
files: new Set(limitations.map((limitation) => limitation.file)).size +
|
|
715
|
-
(report.transport?.corruptFiles ?? 0),
|
|
716
|
-
byKind,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
export function locationSelector(selector) {
|
|
720
|
-
const match = /^(.*):(\d+):\d+$/.exec(selector) ?? /^(.*):(\d+)$/.exec(selector);
|
|
721
|
-
if (!match)
|
|
722
|
-
throw new SupercovError("INVALID_ARGUMENT", "Expected <source-file>:<line>", {
|
|
723
|
-
details: { selector },
|
|
724
|
-
});
|
|
725
|
-
return { file: match[1], line: Number(match[2]) };
|
|
726
|
-
}
|
|
727
|
-
function vectorText(values, outcome) {
|
|
728
|
-
return `${values.map((value) => (value === null ? "-" : value ? "T" : "F")).join("")} -> ${outcome ? "T" : "F"}`;
|
|
729
|
-
}
|
|
730
|
-
const helpText = `Agent-oriented local coverage queries:
|
|
731
|
-
supercov runs [--limit N] [--json]
|
|
732
|
-
supercov runs <run-id> coverage [--filter all|passed|failed] [--kind e2e] [--runner playwright] [--json]
|
|
733
|
-
supercov runs <run-id> coverage kinds [--json]
|
|
734
|
-
supercov runs <run-id> coverage runners [--json]
|
|
735
|
-
supercov runs <run-id> coverage scope [--limit N] [--offset N] [--json]
|
|
736
|
-
supercov runs <run-id> coverage files [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--limit N] [--offset N] [--json]
|
|
737
|
-
supercov runs <run-id> coverage gaps [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
|
|
738
|
-
supercov runs <run-id> coverage file <source-file> [--metric all|lines|statements|functions|branches|mcdc] [--group decision] [--sort location|missing] [--kind e2e] [--limit N] [--offset N] [--json]
|
|
739
|
-
supercov runs <run-id> coverage decision <id|source-file:line> [--kind e2e] [--json]
|
|
740
|
-
supercov runs <run-id> coverage covers <source-file:line> [--kind e2e] [--json]
|
|
741
|
-
supercov runs <run-id> coverage test <id|name-fragment> [--kind e2e] [--limit N] [--json]
|
|
742
|
-
supercov runs <run-id> coverage minimize [--target 0..100] [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--limit N] [--offset N] [--json]
|
|
743
|
-
supercov diff <older-run> <newer-run> [--limit N] [--json]
|
|
744
|
-
supercov merge <run-id> <run-id> [...]
|
|
745
|
-
supercov prune [--keep N] [--dry-run]
|
|
746
|
-
supercov clean [--keep N] [--dry-run]
|
|
747
|
-
|
|
748
|
-
Use "latest" as <run-id> to query the newest local run.
|
|
749
|
-
|
|
750
|
-
Reviewed MC/DC waivers (optional ${WAIVERS_FILE} at the project root):
|
|
751
|
-
{"version":1,"waivers":[{"file":"src/x.ts","decision":"<id or source>","line":12,"condition":"<source or C2>","reason":"..."}]}
|
|
752
|
-
Waived conditions stay uncovered in every raw total and are reported separately.
|
|
753
|
-
Waive only conditions with no satisfiable independence pair. A condition that
|
|
754
|
-
some platform or environment simply never runs here is a coverage gap, not an
|
|
755
|
-
impossibility; keep it visible. Prefer omitting "line" (it breaks on edits
|
|
756
|
-
above it and is then reported as unmatched).
|
|
757
|
-
|
|
758
|
-
Create a run with:
|
|
759
|
-
supercov -- <test command>`;
|
|
760
|
-
function help(options) {
|
|
761
|
-
if (options.json) {
|
|
762
|
-
return output({
|
|
763
|
-
usage: "supercov -- <test command>",
|
|
764
|
-
runSelector: "Use latest as <run-id> to query the newest local run.",
|
|
765
|
-
queryCommands: [
|
|
766
|
-
"runs",
|
|
767
|
-
"runs <run-id> coverage",
|
|
768
|
-
"runs <run-id> coverage kinds",
|
|
769
|
-
"runs <run-id> coverage runners",
|
|
770
|
-
"runs <run-id> coverage scope",
|
|
771
|
-
"runs <run-id> coverage files",
|
|
772
|
-
"runs <run-id> coverage gaps",
|
|
773
|
-
"runs <run-id> coverage file <source-file>",
|
|
774
|
-
"runs <run-id> coverage decision <id|source-file:line>",
|
|
775
|
-
"runs <run-id> coverage covers <source-file:line>",
|
|
776
|
-
"runs <run-id> coverage test <id|name-fragment>",
|
|
777
|
-
"runs <run-id> coverage minimize",
|
|
778
|
-
"diff <older-run> <newer-run>",
|
|
779
|
-
],
|
|
780
|
-
}, options, helpText);
|
|
781
|
-
}
|
|
782
|
-
console.log(helpText);
|
|
783
|
-
}
|
|
784
|
-
/** Resolve the instance-first coverage resource syntax. */
|
|
785
|
-
export function resolveCoverageQueryInvocation(command, args) {
|
|
786
|
-
if (command !== "runs")
|
|
787
|
-
return { command, args };
|
|
788
|
-
const runId = args[0];
|
|
789
|
-
if (!runId || runId.startsWith("-")) {
|
|
790
|
-
return { command, args };
|
|
791
|
-
}
|
|
792
|
-
if (args[1] !== "coverage") {
|
|
793
|
-
// A positional run ID must never silently degrade to the run listing:
|
|
794
|
-
// an agent that omits "coverage" would otherwise get unrelated output.
|
|
795
|
-
throw new SupercovError("UNKNOWN_COMMAND", args[1] === undefined || args[1].startsWith("-")
|
|
796
|
-
? `Missing coverage query after run ${runId}. Expected: supercov runs <run-id> coverage [<query>]. Try supercov help.`
|
|
797
|
-
: `Unknown runs query: ${args[1]}. Expected: supercov runs <run-id> coverage [<query>]. Try supercov help.`, { details: { run: runId, command: args[1] ?? null } });
|
|
798
|
-
}
|
|
799
|
-
const childToken = args[2];
|
|
800
|
-
const hasChild = Boolean(childToken && !childToken.startsWith("-"));
|
|
801
|
-
const child = hasChild ? childToken : "summary";
|
|
802
|
-
const childArgs = args.slice(hasChild ? 3 : 2);
|
|
803
|
-
const coverageCommands = new Set([
|
|
804
|
-
"summary",
|
|
805
|
-
"kinds",
|
|
806
|
-
"runners",
|
|
807
|
-
"scope",
|
|
808
|
-
"files",
|
|
809
|
-
"gaps",
|
|
810
|
-
"file",
|
|
811
|
-
"decision",
|
|
812
|
-
"covers",
|
|
813
|
-
"test",
|
|
814
|
-
"minimize",
|
|
815
|
-
]);
|
|
816
|
-
if (!coverageCommands.has(child)) {
|
|
817
|
-
throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${child}. Try supercov help.`, { details: { command: child } });
|
|
818
|
-
}
|
|
819
|
-
return {
|
|
820
|
-
command: child,
|
|
821
|
-
args: ["--run", runId, ...childArgs],
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
825
|
-
const resolved = resolveCoverageQueryInvocation(command, args);
|
|
826
|
-
command = resolved.command;
|
|
827
|
-
const options = parseOptions(command, resolved.args);
|
|
828
|
-
if (command === "help")
|
|
829
|
-
return help(options);
|
|
830
|
-
const currentIntegrity = currentProjectIntegrity(root);
|
|
831
|
-
const waiverSource = readCoverageWaivers(root);
|
|
832
|
-
if (command === "runs") {
|
|
833
|
-
const availableRuns = discoverRuns(root);
|
|
834
|
-
const runs = page(availableRuns, options).map((run) => {
|
|
835
|
-
const cached = readStoredRunIndex(run);
|
|
836
|
-
const report = cached ? filteredCoverage(cached, options) : undefined;
|
|
837
|
-
return {
|
|
838
|
-
id: run.id,
|
|
839
|
-
generatedAt: report?.generatedAt ?? run.metadata?.startedAt,
|
|
840
|
-
coverageIndexed: Boolean(report),
|
|
841
|
-
lines: report?.summary.lines.percentage ?? null,
|
|
842
|
-
branches: report?.summary.branches.percentage ?? null,
|
|
843
|
-
mcdc: report?.summary.conditionCoveragePct ?? null,
|
|
844
|
-
command: run.metadata?.command,
|
|
845
|
-
durationMs: run.metadata?.durationMs,
|
|
846
|
-
timings: run.metadata?.timings,
|
|
847
|
-
testExitCode: run.metadata?.testExitCode,
|
|
848
|
-
buildReused: run.metadata?.instrumentedBuildCache?.reused,
|
|
849
|
-
rawEvidence: run.metadata?.rawEvidence,
|
|
850
|
-
...(currentIntegrity
|
|
851
|
-
? compareRunIntegrity(run.metadata?.integrity, currentIntegrity)
|
|
852
|
-
: { stale: undefined, reasons: [] }),
|
|
853
|
-
};
|
|
854
|
-
});
|
|
855
|
-
const runsBase = `npx supercov runs${options.filter !== "all" ? ` --filter ${options.filter}` : ""}`;
|
|
856
|
-
const runsNext = nextPageCommand(runsBase, availableRuns.length, runs.length, options);
|
|
857
|
-
return output({ filters: queryFilters(options), runs }, options, runs
|
|
858
|
-
.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(", ")})` : ""}`)
|
|
859
|
-
.join("\n") +
|
|
860
|
-
`\n${pageLabel(availableRuns.length, runs.length, options)}` +
|
|
861
|
-
(runsNext ? `\nnext page: ${runsNext}` : ""), queryPagination(availableRuns.length, runs.length, options));
|
|
862
|
-
}
|
|
863
|
-
if (command === "diff") {
|
|
864
|
-
const [olderSelector, newerSelector] = options.positional;
|
|
865
|
-
if (!olderSelector || !newerSelector)
|
|
866
|
-
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov diff <older-run> <newer-run>");
|
|
867
|
-
const olderSelected = selectRun(root, olderSelector, currentIntegrity, options.json);
|
|
868
|
-
const newerSelected = selectRun(root, newerSelector, currentIntegrity, options.json);
|
|
869
|
-
const older = {
|
|
870
|
-
...olderSelected,
|
|
871
|
-
report: filteredCoverage(olderSelected.report, options),
|
|
872
|
-
};
|
|
873
|
-
const newer = {
|
|
874
|
-
...newerSelected,
|
|
875
|
-
report: filteredCoverage(newerSelected.report, options),
|
|
876
|
-
};
|
|
877
|
-
const key = (file, line) => `${file}:${line}`;
|
|
878
|
-
const oldLines = new Set(older.report.lines
|
|
879
|
-
.filter((line) => line.covered)
|
|
880
|
-
.map((line) => key(line.file, line.line)));
|
|
881
|
-
const newLines = new Set(newer.report.lines
|
|
882
|
-
.filter((line) => line.covered)
|
|
883
|
-
.map((line) => key(line.file, line.line)));
|
|
884
|
-
const branchKeys = (report) => new Map(report.branches.flatMap((branch) => branch.alternatives
|
|
885
|
-
.filter((alternative) => alternative.covered)
|
|
886
|
-
.map((alternative) => [
|
|
887
|
-
`${branch.meta.id}:${alternative.id}`,
|
|
888
|
-
`${branch.meta.file}:${branch.meta.line} ${alternative.label}`,
|
|
889
|
-
])));
|
|
890
|
-
const mcdcKeys = (report) => new Map(report.decisions.flatMap((decision) => decision.conditions
|
|
891
|
-
.filter((condition) => condition.covered)
|
|
892
|
-
.map((condition) => [
|
|
893
|
-
`${decision.meta.id}:c${condition.index}`,
|
|
894
|
-
`${decision.meta.file}:${decision.meta.line} C${condition.index + 1} ${condition.source}`,
|
|
895
|
-
])));
|
|
896
|
-
const oldBranches = branchKeys(older.report);
|
|
897
|
-
const newBranches = branchKeys(newer.report);
|
|
898
|
-
const oldMcdc = mcdcKeys(older.report);
|
|
899
|
-
const newMcdc = mcdcKeys(newer.report);
|
|
900
|
-
const gainedLines = [...newLines]
|
|
901
|
-
.filter((line) => !oldLines.has(line))
|
|
902
|
-
.sort();
|
|
903
|
-
const lostLines = [...oldLines]
|
|
904
|
-
.filter((line) => !newLines.has(line))
|
|
905
|
-
.sort();
|
|
906
|
-
const gainedBranches = [...newBranches]
|
|
907
|
-
.filter(([id]) => !oldBranches.has(id))
|
|
908
|
-
.map(([, label]) => label)
|
|
909
|
-
.sort();
|
|
910
|
-
const lostBranches = [...oldBranches]
|
|
911
|
-
.filter(([id]) => !newBranches.has(id))
|
|
912
|
-
.map(([, label]) => label)
|
|
913
|
-
.sort();
|
|
914
|
-
const gainedMcdc = [...newMcdc]
|
|
915
|
-
.filter(([id]) => !oldMcdc.has(id))
|
|
916
|
-
.map(([, label]) => label)
|
|
917
|
-
.sort();
|
|
918
|
-
const lostMcdc = [...oldMcdc]
|
|
919
|
-
.filter(([id]) => !newMcdc.has(id))
|
|
920
|
-
.map(([, label]) => label)
|
|
921
|
-
.sort();
|
|
922
|
-
const result = {
|
|
923
|
-
filters: queryFilters(options),
|
|
924
|
-
older: older.run.id,
|
|
925
|
-
newer: newer.run.id,
|
|
926
|
-
delta: {
|
|
927
|
-
lines: Number((newer.report.summary.lines.percentage -
|
|
928
|
-
older.report.summary.lines.percentage).toFixed(2)),
|
|
929
|
-
branches: Number((newer.report.summary.branches.percentage -
|
|
930
|
-
older.report.summary.branches.percentage).toFixed(2)),
|
|
931
|
-
mcdc: Number((newer.report.summary.conditionCoveragePct -
|
|
932
|
-
older.report.summary.conditionCoveragePct).toFixed(2)),
|
|
933
|
-
},
|
|
934
|
-
gained: {
|
|
935
|
-
lineCount: gainedLines.length,
|
|
936
|
-
branchCount: gainedBranches.length,
|
|
937
|
-
mcdcCount: gainedMcdc.length,
|
|
938
|
-
lines: page(gainedLines, options),
|
|
939
|
-
branches: page(gainedBranches, options),
|
|
940
|
-
mcdc: page(gainedMcdc, options),
|
|
941
|
-
},
|
|
942
|
-
lost: {
|
|
943
|
-
lineCount: lostLines.length,
|
|
944
|
-
branchCount: lostBranches.length,
|
|
945
|
-
mcdcCount: lostMcdc.length,
|
|
946
|
-
lines: page(lostLines, options),
|
|
947
|
-
branches: page(lostBranches, options),
|
|
948
|
-
mcdc: page(lostMcdc, options),
|
|
949
|
-
},
|
|
950
|
-
};
|
|
951
|
-
const diffTotal = Math.max(gainedLines.length, gainedBranches.length, gainedMcdc.length, lostLines.length, lostBranches.length, lostMcdc.length);
|
|
952
|
-
const diffReturned = Math.max(result.gained.lines.length, result.gained.branches.length, result.gained.mcdc.length, result.lost.lines.length, result.lost.branches.length, result.lost.mcdc.length);
|
|
953
|
-
const diffBase = [
|
|
954
|
-
"npx supercov diff",
|
|
955
|
-
shellQuote(older.run.id),
|
|
956
|
-
shellQuote(newer.run.id),
|
|
957
|
-
options.filter !== "all" ? `--filter ${options.filter}` : undefined,
|
|
958
|
-
]
|
|
959
|
-
.filter(Boolean)
|
|
960
|
-
.join(" ");
|
|
961
|
-
const diffNext = nextPageCommand(diffBase, diffTotal, diffReturned, options);
|
|
962
|
-
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));
|
|
963
|
-
}
|
|
964
|
-
const selectedRun = selectRun(root, options.run, currentIntegrity, options.json);
|
|
965
|
-
const run = selectedRun.run;
|
|
966
|
-
const report = filteredCoverage(selectedRun.report, options);
|
|
967
|
-
const selectedTestSet = selectedTestIds(report, options);
|
|
968
|
-
const waiverEvaluation = waiverSource
|
|
969
|
-
? evaluateCoverageWaivers(report.decisions, waiverSource)
|
|
970
|
-
: undefined;
|
|
971
|
-
if (command === "summary") {
|
|
972
|
-
const summary = selectedTestSet
|
|
973
|
-
? coverageSummaryForTests(report, selectedTestSet)
|
|
974
|
-
: report.summary;
|
|
975
|
-
const gaps = fileGaps(report, selectedTestSet).filter((gap) => gap.score > 0);
|
|
976
|
-
const measurement = coverageMeasurement(report);
|
|
977
|
-
const selectedTests = report.tests.filter((test) => !selectedTestSet || selectedTestSet.has(test.id));
|
|
978
|
-
const testCount = selectedTests.filter((test) => (test.role ?? "test") === "test").length;
|
|
979
|
-
const setupCount = selectedTests.filter((test) => test.role === "setup").length;
|
|
980
|
-
const testOutcomes = Object.fromEntries(["passed", "failed", "flaky", "skipped", "timedOut", "interrupted", "unknown"].map((outcome) => [
|
|
981
|
-
outcome,
|
|
982
|
-
selectedTests.filter((test) => test.role === "test" && test.outcome === outcome).length,
|
|
983
|
-
]));
|
|
984
|
-
const diagnostics = coverageDiagnostics(report, selectedTestSet);
|
|
985
|
-
const result = {
|
|
986
|
-
run: run.id,
|
|
987
|
-
filters: queryFilters(options),
|
|
988
|
-
generatedAt: report.generatedAt,
|
|
989
|
-
valid: run.metadata?.testExitCode === 0,
|
|
990
|
-
stale: report.integrity?.stale ?? false,
|
|
991
|
-
staleReasons: report.integrity?.staleReasons ?? [],
|
|
992
|
-
structurallyComplete: summary.coverageComplete && measurement.complete,
|
|
993
|
-
complete: options.filter === "passed" &&
|
|
994
|
-
run.metadata?.testExitCode === 0 &&
|
|
995
|
-
!report.integrity?.stale &&
|
|
996
|
-
summary.coverageComplete &&
|
|
997
|
-
measurement.complete,
|
|
998
|
-
coverage: summary,
|
|
999
|
-
measurement,
|
|
1000
|
-
...(waiverEvaluation
|
|
1001
|
-
? {
|
|
1002
|
-
waivers: {
|
|
1003
|
-
file: WAIVERS_FILE,
|
|
1004
|
-
entries: waiverEvaluation.waivers.length,
|
|
1005
|
-
applied: waiverEvaluation.applied.length,
|
|
1006
|
-
contradicted: waiverEvaluation.contradicted.map((match) => ({
|
|
1007
|
-
file: match.file,
|
|
1008
|
-
line: match.line,
|
|
1009
|
-
condition: match.conditionSource,
|
|
1010
|
-
reason: match.waiver.reason,
|
|
1011
|
-
})),
|
|
1012
|
-
unmatched: waiverEvaluation.unmatched,
|
|
1013
|
-
mcdcExcludingWaived: {
|
|
1014
|
-
covered: report.summary.coveredConditions,
|
|
1015
|
-
total: report.summary.conditions -
|
|
1016
|
-
waiverEvaluation.applied.length,
|
|
1017
|
-
percentage: report.summary.conditions -
|
|
1018
|
-
waiverEvaluation.applied.length >
|
|
1019
|
-
0
|
|
1020
|
-
? (report.summary.coveredConditions /
|
|
1021
|
-
(report.summary.conditions -
|
|
1022
|
-
waiverEvaluation.applied.length)) *
|
|
1023
|
-
100
|
|
1024
|
-
: 100,
|
|
1025
|
-
},
|
|
1026
|
-
},
|
|
1027
|
-
}
|
|
1028
|
-
: {}),
|
|
1029
|
-
coverageByKind: report.coverageByKind,
|
|
1030
|
-
coverageByRunner: report.coverageByRunner,
|
|
1031
|
-
attribution: attribution(report, selectedTestSet),
|
|
1032
|
-
transport: report.transport,
|
|
1033
|
-
diagnostics,
|
|
1034
|
-
...(!selectedTestSet
|
|
1035
|
-
? {
|
|
1036
|
-
confidence: {
|
|
1037
|
-
lines: Object.fromEntries(["unexecuted", "executed", "action", "asserted"].map((level) => [
|
|
1038
|
-
level,
|
|
1039
|
-
report.lines.filter((line) => line.confidence?.level === level).length,
|
|
1040
|
-
])),
|
|
1041
|
-
assertionCoveredMcdcConditions: report.decisions.reduce((total, decision) => total +
|
|
1042
|
-
decision.conditions.filter((condition) => condition.assertionCovered).length, 0),
|
|
1043
|
-
},
|
|
1044
|
-
}
|
|
1045
|
-
: {}),
|
|
1046
|
-
filesWithGaps: gaps.length,
|
|
1047
|
-
filesWithCoverageGaps: gaps.filter((gap) => gap.uncoveredLines > 0 ||
|
|
1048
|
-
gap.uncoveredStatements > 0 ||
|
|
1049
|
-
gap.uncoveredFunctions > 0 ||
|
|
1050
|
-
gap.missingBranches > 0 ||
|
|
1051
|
-
gap.missingMcdcConditions > 0).length,
|
|
1052
|
-
filesWithMeasurementLimitations: measurement.files,
|
|
1053
|
-
tests: testCount,
|
|
1054
|
-
setups: setupCount,
|
|
1055
|
-
testOutcomes,
|
|
1056
|
-
sourceScope: report.scope
|
|
1057
|
-
? {
|
|
1058
|
-
mode: report.scope.mode,
|
|
1059
|
-
roots: report.scope.roots,
|
|
1060
|
-
included: report.scope.entries.filter((entry) => entry.status === "included").length,
|
|
1061
|
-
excluded: report.scope.entries.filter((entry) => entry.status === "excluded").length,
|
|
1062
|
-
ambiguous: report.scope.entries.filter((entry) => entry.status === "ambiguous").length,
|
|
1063
|
-
}
|
|
1064
|
-
: undefined,
|
|
1065
|
-
};
|
|
1066
|
-
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)`}${waiverEvaluation ? `\nwaivers: ${waiverEvaluation.applied.length} applied, ${waiverEvaluation.contradicted.length} contradicted, ${waiverEvaluation.unmatched.length} unmatched; MC/DC excluding waived ${pct(report.summary.conditions - waiverEvaluation.applied.length > 0 ? (report.summary.coveredConditions / (report.summary.conditions - waiverEvaluation.applied.length)) * 100 : 100)} (${report.summary.coveredConditions}/${report.summary.conditions - waiverEvaluation.applied.length})${waiverEvaluation.contradicted.map((match) => `\n contradicted (condition is covered): ${match.file}:${match.line} ${match.conditionSource}`).join("")}${waiverEvaluation.unmatched.map((waiver) => `\n unmatched (no such condition): ${waiver.file}${waiver.line !== undefined ? `:${waiver.line}` : ""} ${waiver.condition}`).join("")}` : ""}${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`);
|
|
1067
|
-
}
|
|
1068
|
-
if (command === "minimize") {
|
|
1069
|
-
const solverReport = selectedTestSet
|
|
1070
|
-
? {
|
|
1071
|
-
...report,
|
|
1072
|
-
tests: report.tests.filter((test) => selectedTestSet.has(test.id)),
|
|
1073
|
-
}
|
|
1074
|
-
: report;
|
|
1075
|
-
const minimized = minimumTestSet(solverReport, options.target, options.metric);
|
|
1076
|
-
const selectedDetails = minimized.selected.map((id) => {
|
|
1077
|
-
const test = report.tests.find((candidate) => candidate.id === id);
|
|
1078
|
-
return {
|
|
1079
|
-
id,
|
|
1080
|
-
name: test.name,
|
|
1081
|
-
file: test.file,
|
|
1082
|
-
runner: test.provenance.runner,
|
|
1083
|
-
kind: test.provenance.kind,
|
|
1084
|
-
};
|
|
1085
|
-
});
|
|
1086
|
-
const selectedPage = page(selectedDetails, options);
|
|
1087
|
-
const base = `${coverageCommand(run.id, options, "minimize")} --target ${options.target}`;
|
|
1088
|
-
const next = nextPageCommand(base, selectedDetails.length, selectedPage.length, options);
|
|
1089
|
-
return output({
|
|
1090
|
-
run: run.id,
|
|
1091
|
-
filters: queryFilters(options),
|
|
1092
|
-
...minimized,
|
|
1093
|
-
selectedCount: selectedDetails.length,
|
|
1094
|
-
totalCandidateTests: solverReport.tests.filter((test) => test.role === "test").length,
|
|
1095
|
-
tests: selectedPage,
|
|
1096
|
-
}, 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));
|
|
1097
|
-
}
|
|
1098
|
-
if (command === "scope") {
|
|
1099
|
-
if (!report.scope)
|
|
1100
|
-
throw new SupercovError("SCOPE_UNAVAILABLE", "This run does not contain a source-scope inventory.");
|
|
1101
|
-
const limitationsByFile = new Map();
|
|
1102
|
-
for (const limitation of report.limitations ?? []) {
|
|
1103
|
-
const existing = limitationsByFile.get(limitation.file) ?? [];
|
|
1104
|
-
existing.push(limitation);
|
|
1105
|
-
limitationsByFile.set(limitation.file, existing);
|
|
1106
|
-
}
|
|
1107
|
-
const ordered = report.scope.entries.map((entry) => {
|
|
1108
|
-
const limitations = limitationsByFile.get(entry.file) ?? [];
|
|
1109
|
-
return {
|
|
1110
|
-
...entry,
|
|
1111
|
-
measurementLimitations: limitations.length,
|
|
1112
|
-
limitationKinds: [...new Set(limitations.map((item) => item.kind))].sort(),
|
|
1113
|
-
};
|
|
1114
|
-
}).sort((left, right) => {
|
|
1115
|
-
const rank = { ambiguous: 0, included: 1, excluded: 2 };
|
|
1116
|
-
return rank[left.status] - rank[right.status] || left.file.localeCompare(right.file);
|
|
1117
|
-
});
|
|
1118
|
-
const selectedEntries = page(ordered, options);
|
|
1119
|
-
const base = coverageCommand(run.id, options, "scope");
|
|
1120
|
-
const next = nextPageCommand(base, ordered.length, selectedEntries.length, options);
|
|
1121
|
-
const counts = {
|
|
1122
|
-
included: ordered.filter((entry) => entry.status === "included").length,
|
|
1123
|
-
excluded: ordered.filter((entry) => entry.status === "excluded").length,
|
|
1124
|
-
ambiguous: ordered.filter((entry) => entry.status === "ambiguous").length,
|
|
1125
|
-
};
|
|
1126
|
-
return output({
|
|
1127
|
-
run: run.id,
|
|
1128
|
-
filters: queryFilters(options),
|
|
1129
|
-
mode: report.scope.mode,
|
|
1130
|
-
roots: report.scope.roots,
|
|
1131
|
-
counts,
|
|
1132
|
-
measurement: coverageMeasurement(report),
|
|
1133
|
-
entries: selectedEntries,
|
|
1134
|
-
}, 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));
|
|
1135
|
-
}
|
|
1136
|
-
if (command === "kinds" || command === "runners") {
|
|
1137
|
-
const dimension = command === "kinds" ? report.coverageByKind : report.coverageByRunner;
|
|
1138
|
-
const selectedDimension = page(dimension, options);
|
|
1139
|
-
const dimensionNext = nextPageCommand(coverageCommand(run.id, options, command), dimension.length, selectedDimension.length, options);
|
|
1140
|
-
return output({
|
|
1141
|
-
run: run.id,
|
|
1142
|
-
filters: queryFilters(options),
|
|
1143
|
-
[command]: selectedDimension,
|
|
1144
|
-
}, options, selectedDimension
|
|
1145
|
-
.map((entry) => {
|
|
1146
|
-
const name = entry.kind ?? entry.runner ?? "unknown";
|
|
1147
|
-
return `${name} ${entry.tests} test(s)${entry.setups ? ` + ${entry.setups} setup scope(s)` : ""} lines ${pct(entry.summary.lines.percentage)} branches ${pct(entry.summary.branches.percentage)} MC/DC ${pct(entry.summary.conditionCoveragePct)}`;
|
|
1148
|
-
})
|
|
1149
|
-
.join("\n") +
|
|
1150
|
-
`\n${pageLabel(dimension.length, selectedDimension.length, options)}` +
|
|
1151
|
-
(dimensionNext ? `\nnext page: ${dimensionNext}` : ""), queryPagination(dimension.length, selectedDimension.length, options));
|
|
1152
|
-
}
|
|
1153
|
-
if (command === "files" || command === "gaps") {
|
|
1154
|
-
const files = fileGaps(report, selectedTestSet);
|
|
1155
|
-
const all = files
|
|
1156
|
-
.filter((gap) => command === "files" ||
|
|
1157
|
-
gapMetricValue(gap, options.metric) > 0 ||
|
|
1158
|
-
gap.measurementLimitations > 0)
|
|
1159
|
-
.sort((left, right) => gapMetricValue(right, options.metric) - gapMetricValue(left, options.metric) ||
|
|
1160
|
-
right.measurementLimitations - left.measurementLimitations ||
|
|
1161
|
-
left.file.localeCompare(right.file));
|
|
1162
|
-
const selectedFiles = page(all, options).map((gap) => ({
|
|
1163
|
-
...gap,
|
|
1164
|
-
...(waiverEvaluation
|
|
1165
|
-
? {
|
|
1166
|
-
waivedMcdcConditions: waiverEvaluation.appliedByFile.get(gap.file) ?? 0,
|
|
1167
|
-
}
|
|
1168
|
-
: {}),
|
|
1169
|
-
}));
|
|
1170
|
-
const pageStart = all.length === 0 ? 0 : options.offset + 1;
|
|
1171
|
-
const pageEnd = Math.min(options.offset + selectedFiles.length, all.length);
|
|
1172
|
-
const nextOffset = options.offset + selectedFiles.length;
|
|
1173
|
-
const nextCommand = nextOffset < all.length
|
|
1174
|
-
? `${coverageCommand(run.id, options, command)} --offset ${nextOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
|
|
1175
|
-
: undefined;
|
|
1176
|
-
return output({
|
|
1177
|
-
run: run.id,
|
|
1178
|
-
filters: queryFilters(options),
|
|
1179
|
-
metric: options.metric,
|
|
1180
|
-
[command]: selectedFiles,
|
|
1181
|
-
}, options, selectedFiles
|
|
1182
|
-
.map((gap) => {
|
|
1183
|
-
const missing = gap.uncoveredLines +
|
|
1184
|
-
gap.uncoveredStatements +
|
|
1185
|
-
gap.uncoveredFunctions +
|
|
1186
|
-
gap.missingBranches +
|
|
1187
|
-
gap.missingMcdcConditions;
|
|
1188
|
-
const status = missing === 0
|
|
1189
|
-
? "coverage complete"
|
|
1190
|
-
: `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}${gap.waivedMcdcConditions ? ` (${gap.waivedMcdcConditions} waived)` : ""}`;
|
|
1191
|
-
const limitations = gap.measurementLimitations
|
|
1192
|
-
? ` measurement limitations ${gap.measurementLimitations} (${gap.limitationKinds.join(", ")})`
|
|
1193
|
-
: "";
|
|
1194
|
-
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)}]` : ""}`;
|
|
1195
|
-
})
|
|
1196
|
-
.join("\n") +
|
|
1197
|
-
`\nshowing ${pageStart}-${pageEnd} of ${all.length}` +
|
|
1198
|
-
(nextCommand ? `\nnext page: ${nextCommand}` : ""), queryPagination(all.length, selectedFiles.length, options));
|
|
1199
|
-
}
|
|
1200
|
-
if (command === "file") {
|
|
1201
|
-
const selector = options.positional.join(" ");
|
|
1202
|
-
if (!selector)
|
|
1203
|
-
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage file <source-file>");
|
|
1204
|
-
const file = findFile(report, selector);
|
|
1205
|
-
if (options.group === "decision") {
|
|
1206
|
-
const decisionRows = report.decisions
|
|
1207
|
-
.filter((decision) => decision.meta.file === file)
|
|
1208
|
-
.map((decision) => {
|
|
1209
|
-
const filtered = filterDecision(decision, selectedTestSet);
|
|
1210
|
-
const waived = waiverEvaluation?.waivedByDecision.get(decision.meta.id);
|
|
1211
|
-
const missing = filtered.conditions.filter((condition) => !condition.covered);
|
|
1212
|
-
const waivedMissing = missing.filter((condition) => waived?.has(condition.index));
|
|
1213
|
-
return {
|
|
1214
|
-
id: decision.meta.id,
|
|
1215
|
-
line: decision.meta.line,
|
|
1216
|
-
column: decision.meta.column,
|
|
1217
|
-
kind: decision.meta.kind,
|
|
1218
|
-
conditions: filtered.conditions.length,
|
|
1219
|
-
missingConditions: missing.length,
|
|
1220
|
-
waivedConditions: waivedMissing.length,
|
|
1221
|
-
source: decision.meta.source.replace(/\s+/g, " ").trim(),
|
|
1222
|
-
};
|
|
1223
|
-
});
|
|
1224
|
-
const withMissing = decisionRows.filter((row) => row.missingConditions > 0);
|
|
1225
|
-
const ordered = [...withMissing].sort((left, right) => options.sort === "missing"
|
|
1226
|
-
? right.missingConditions -
|
|
1227
|
-
right.waivedConditions -
|
|
1228
|
-
(left.missingConditions - left.waivedConditions) ||
|
|
1229
|
-
right.missingConditions - left.missingConditions ||
|
|
1230
|
-
left.line - right.line ||
|
|
1231
|
-
left.column - right.column
|
|
1232
|
-
: left.line - right.line ||
|
|
1233
|
-
left.column - right.column ||
|
|
1234
|
-
left.id.localeCompare(right.id));
|
|
1235
|
-
const rows = page(ordered, options);
|
|
1236
|
-
const totals = {
|
|
1237
|
-
decisions: decisionRows.length,
|
|
1238
|
-
decisionsWithMissingConditions: withMissing.length,
|
|
1239
|
-
conditions: decisionRows.reduce((sum, row) => sum + row.conditions, 0),
|
|
1240
|
-
missingConditions: decisionRows.reduce((sum, row) => sum + row.missingConditions, 0),
|
|
1241
|
-
waivedConditions: decisionRows.reduce((sum, row) => sum + row.waivedConditions, 0),
|
|
1242
|
-
};
|
|
1243
|
-
const groupedBase = `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --group decision${options.sort !== "location" ? ` --sort ${options.sort}` : ""}`;
|
|
1244
|
-
const groupedNext = nextPageCommand(groupedBase, ordered.length, rows.length, options);
|
|
1245
|
-
const snippet = (source) => source.length > 96 ? `${source.slice(0, 95)}…` : source;
|
|
1246
|
-
return output({
|
|
1247
|
-
run: run.id,
|
|
1248
|
-
filters: queryFilters(options),
|
|
1249
|
-
file,
|
|
1250
|
-
group: "decision",
|
|
1251
|
-
sort: options.sort,
|
|
1252
|
-
totals,
|
|
1253
|
-
decisions: rows,
|
|
1254
|
-
}, options, `${file} MC/DC by decision\ndecisions ${totals.decisions}, with missing conditions ${totals.decisionsWithMissingConditions}; conditions missing ${totals.missingConditions}/${totals.conditions}${totals.waivedConditions ? `, waived ${totals.waivedConditions}` : ""}\n${rows
|
|
1255
|
-
.map((row) => `${row.line}:${row.column} [${row.id}] missing ${row.missingConditions}/${row.conditions}${row.waivedConditions ? ` (${row.waivedConditions} waived)` : ""} ${snippet(row.source)}`)
|
|
1256
|
-
.join("\n")}\n${pageLabel(ordered.length, rows.length, options)} decisions with missing conditions${groupedNext ? `\nnext page: ${groupedNext}` : ""}`, queryPagination(ordered.length, rows.length, options));
|
|
1257
|
-
}
|
|
1258
|
-
const uncoveredLines = report.lines
|
|
1259
|
-
.filter((line) => line.file === file &&
|
|
1260
|
-
!includesSelectedTest(line.tests, selectedTestSet))
|
|
1261
|
-
.map((line) => ({
|
|
1262
|
-
kind: "line",
|
|
1263
|
-
line: line.line,
|
|
1264
|
-
otherCoverage: otherCoverage(report, line.tests, selectedTestSet),
|
|
1265
|
-
}));
|
|
1266
|
-
const functions = report.points
|
|
1267
|
-
.filter((point) => point.meta.file === file &&
|
|
1268
|
-
point.meta.kind === "function" &&
|
|
1269
|
-
!includesSelectedTest(point.tests, selectedTestSet))
|
|
1270
|
-
.map((point) => ({
|
|
1271
|
-
kind: "function",
|
|
1272
|
-
line: point.meta.line,
|
|
1273
|
-
column: point.meta.column,
|
|
1274
|
-
source: point.meta.label ?? point.meta.source,
|
|
1275
|
-
otherCoverage: otherCoverage(report, point.tests, selectedTestSet),
|
|
1276
|
-
}));
|
|
1277
|
-
const statements = report.points
|
|
1278
|
-
.filter((point) => point.meta.file === file &&
|
|
1279
|
-
point.meta.kind === "statement" &&
|
|
1280
|
-
!includesSelectedTest(point.tests, selectedTestSet))
|
|
1281
|
-
.map((point) => ({
|
|
1282
|
-
kind: "statement",
|
|
1283
|
-
line: point.meta.line,
|
|
1284
|
-
column: point.meta.column,
|
|
1285
|
-
source: point.meta.label ?? point.meta.source,
|
|
1286
|
-
otherCoverage: otherCoverage(report, point.tests, selectedTestSet),
|
|
1287
|
-
}));
|
|
1288
|
-
const branches = report.branches
|
|
1289
|
-
.filter((branch) => branch.meta.file === file)
|
|
1290
|
-
.flatMap((branch) => branch.alternatives
|
|
1291
|
-
.filter((alternative) => !includesSelectedTest(alternative.tests, selectedTestSet))
|
|
1292
|
-
.map((alternative) => ({
|
|
1293
|
-
kind: "branch",
|
|
1294
|
-
line: branch.meta.line,
|
|
1295
|
-
column: branch.meta.column,
|
|
1296
|
-
source: branch.meta.source,
|
|
1297
|
-
missing: alternative.label,
|
|
1298
|
-
otherCoverage: otherCoverage(report, alternative.tests, selectedTestSet),
|
|
1299
|
-
})));
|
|
1300
|
-
const mcdc = report.decisions
|
|
1301
|
-
.filter((decision) => decision.meta.file === file)
|
|
1302
|
-
.flatMap((decision) => filterDecision(decision, selectedTestSet)
|
|
1303
|
-
.conditions.filter((condition) => !condition.covered)
|
|
1304
|
-
.map((condition) => ({
|
|
1305
|
-
kind: "mcdc",
|
|
1306
|
-
id: decision.meta.id,
|
|
1307
|
-
line: decision.meta.line,
|
|
1308
|
-
column: decision.meta.column,
|
|
1309
|
-
decision: decision.meta.source,
|
|
1310
|
-
missingCondition: condition.source,
|
|
1311
|
-
...(waiverEvaluation?.waivedByDecision
|
|
1312
|
-
.get(decision.meta.id)
|
|
1313
|
-
?.has(condition.index)
|
|
1314
|
-
? {
|
|
1315
|
-
waived: true,
|
|
1316
|
-
waiverReason: waiverEvaluation.waivedByDecision
|
|
1317
|
-
.get(decision.meta.id)
|
|
1318
|
-
.get(condition.index).reason,
|
|
1319
|
-
}
|
|
1320
|
-
: {}),
|
|
1321
|
-
observedVectors: filterDecision(decision, selectedTestSet).vectorObservations.map((observation) => vectorText(observation.vector.values, observation.vector.outcome)),
|
|
1322
|
-
otherCoverage: otherCoverage(report, (decision.conditions[condition.index]?.witnessTests ?? []).flat(), selectedTestSet),
|
|
1323
|
-
})));
|
|
1324
|
-
const obligations = [
|
|
1325
|
-
...uncoveredLines,
|
|
1326
|
-
...statements,
|
|
1327
|
-
...functions,
|
|
1328
|
-
...branches,
|
|
1329
|
-
...mcdc,
|
|
1330
|
-
].filter((obligation) => obligationMatchesMetric(obligation, options.metric)).sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
|
|
1331
|
-
const allFileLimitations = (report.limitations ?? [])
|
|
1332
|
-
.filter((limitation) => limitation.file === file)
|
|
1333
|
-
.map((limitation) => ({
|
|
1334
|
-
...limitation,
|
|
1335
|
-
blocking: true,
|
|
1336
|
-
effect: "outside-measured-denominator",
|
|
1337
|
-
}))
|
|
1338
|
-
.sort((left, right) => left.line - right.line ||
|
|
1339
|
-
left.column - right.column ||
|
|
1340
|
-
left.id.localeCompare(right.id));
|
|
1341
|
-
const allFileTests = report.tests
|
|
1342
|
-
.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
|
|
1343
|
-
test.lines.some((line) => line.file === file))
|
|
1344
|
-
.map((test) => ({
|
|
1345
|
-
id: test.id,
|
|
1346
|
-
name: test.name,
|
|
1347
|
-
provenance: test.provenance,
|
|
1348
|
-
}));
|
|
1349
|
-
const tests = page(allFileTests, options);
|
|
1350
|
-
const selected = page(obligations, options);
|
|
1351
|
-
const limitations = page(allFileLimitations, options);
|
|
1352
|
-
const filePageTotal = Math.max(obligations.length, allFileTests.length, allFileLimitations.length);
|
|
1353
|
-
const filePageReturned = Math.max(selected.length, tests.length, limitations.length);
|
|
1354
|
-
const nextFileOffset = options.offset + filePageReturned;
|
|
1355
|
-
const nextFileCommand = filePageReturned > 0 && nextFileOffset < filePageTotal
|
|
1356
|
-
? `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --offset ${nextFileOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
|
|
1357
|
-
: undefined;
|
|
1358
|
-
const result = {
|
|
1359
|
-
run: run.id,
|
|
1360
|
-
filters: queryFilters(options),
|
|
1361
|
-
file,
|
|
1362
|
-
metric: options.metric,
|
|
1363
|
-
counts: {
|
|
1364
|
-
uncoveredLines: uncoveredLines.length,
|
|
1365
|
-
uncoveredStatements: statements.length,
|
|
1366
|
-
uncoveredFunctions: functions.length,
|
|
1367
|
-
missingBranches: branches.length,
|
|
1368
|
-
missingMcdcConditions: mcdc.length,
|
|
1369
|
-
waivedMcdcConditions: mcdc.filter((item) => item.waived).length,
|
|
1370
|
-
measurementLimitations: allFileLimitations.length,
|
|
1371
|
-
},
|
|
1372
|
-
tests,
|
|
1373
|
-
totalTests: allFileTests.length,
|
|
1374
|
-
totalObligations: obligations.length,
|
|
1375
|
-
obligations: selected,
|
|
1376
|
-
totalLimitations: allFileLimitations.length,
|
|
1377
|
-
limitations,
|
|
1378
|
-
};
|
|
1379
|
-
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
|
|
1380
|
-
.map((item) => item.kind === "line"
|
|
1381
|
-
? `line ${item.line}: ${item.otherCoverage.coveredElsewhere ? `covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}` : "uncovered everywhere"}`
|
|
1382
|
-
: item.kind === "statement"
|
|
1383
|
-
? `statement ${item.line}:${item.column}: ${item.source}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
|
|
1384
|
-
: item.kind === "function"
|
|
1385
|
-
? `function ${item.line}:${item.column}: ${item.source}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
|
|
1386
|
-
: item.kind === "branch"
|
|
1387
|
-
? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
|
|
1388
|
-
: `MC/DC ${item.line}:${item.column} [${item.id}]: ${item.missingCondition}${item.waived ? " [waived]" : ""}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`)
|
|
1389
|
-
.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));
|
|
1390
|
-
}
|
|
1391
|
-
if (command === "decision") {
|
|
1392
|
-
const selector = options.positional[0];
|
|
1393
|
-
if (!selector)
|
|
1394
|
-
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
|
|
1395
|
-
let matches = report.decisions.filter((decision) => decision.meta.id === selector);
|
|
1396
|
-
if (matches.length === 0 && /:\d+(?::\d+)?$/.test(selector)) {
|
|
1397
|
-
const location = locationSelector(selector);
|
|
1398
|
-
matches = report.decisions.filter((decision) => decision.meta.file === location.file &&
|
|
1399
|
-
decision.meta.line === location.line);
|
|
1400
|
-
}
|
|
1401
|
-
if (matches.length === 0)
|
|
1402
|
-
throw new SupercovError("DECISION_NOT_FOUND", `Decision not found: ${selector}`, {
|
|
1403
|
-
details: { selector },
|
|
1404
|
-
});
|
|
1405
|
-
if (matches.length > 1) {
|
|
1406
|
-
const matchingDecisions = page(matches, options).map((decision) => ({
|
|
1407
|
-
id: decision.meta.id,
|
|
1408
|
-
file: decision.meta.file,
|
|
1409
|
-
line: decision.meta.line,
|
|
1410
|
-
column: decision.meta.column,
|
|
1411
|
-
source: decision.meta.source,
|
|
1412
|
-
}));
|
|
1413
|
-
const matchesNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, matches.length, matchingDecisions.length, options);
|
|
1414
|
-
return output({
|
|
1415
|
-
run: run.id,
|
|
1416
|
-
filters: queryFilters(options),
|
|
1417
|
-
decisions: matchingDecisions,
|
|
1418
|
-
}, 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));
|
|
1419
|
-
}
|
|
1420
|
-
matches = matches.map((decision) => filterDecision(decision, selectedTestSet));
|
|
1421
|
-
const totalDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
|
|
1422
|
-
matches = matches.map((decision) => {
|
|
1423
|
-
const totals = {
|
|
1424
|
-
conditions: decision.conditions.length,
|
|
1425
|
-
vectorObservations: decision.vectorObservations.length,
|
|
1426
|
-
tests: decision.tests.length,
|
|
1427
|
-
};
|
|
1428
|
-
const waived = waiverEvaluation?.waivedByDecision.get(decision.meta.id);
|
|
1429
|
-
const vectorObservations = page(decision.vectorObservations, options);
|
|
1430
|
-
return {
|
|
1431
|
-
...decision,
|
|
1432
|
-
totals,
|
|
1433
|
-
vectors: vectorObservations.map((observation) => observation.vector),
|
|
1434
|
-
vectorObservations,
|
|
1435
|
-
conditions: page(decision.conditions, options).map((condition) => !condition.covered && waived?.has(condition.index)
|
|
1436
|
-
? {
|
|
1437
|
-
...condition,
|
|
1438
|
-
waived: true,
|
|
1439
|
-
waiverReason: waived.get(condition.index).reason,
|
|
1440
|
-
}
|
|
1441
|
-
: condition),
|
|
1442
|
-
tests: page(decision.tests, options),
|
|
1443
|
-
};
|
|
1444
|
-
});
|
|
1445
|
-
const returnedDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
|
|
1446
|
-
const decisionNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, totalDecisionEvidence, returnedDecisionEvidence, options);
|
|
1447
|
-
const result = {
|
|
1448
|
-
run: run.id,
|
|
1449
|
-
filters: queryFilters(options),
|
|
1450
|
-
paginationAppliesTo: "conditions, vectorObservations, and tests independently within each decision",
|
|
1451
|
-
decisions: matches,
|
|
1452
|
-
};
|
|
1453
|
-
return output(result, options, matches
|
|
1454
|
-
.map((decision) => `${decision.meta.id} ${decision.meta.file}:${decision.meta.line}:${decision.meta.column}\n${decision.meta.source}\n${decision.conditions
|
|
1455
|
-
.map((condition) => `C${condition.index + 1} ${condition.covered ? "covered" : condition.waived ? "MISSING (waived)" : "MISSING"}${condition.assertionCovered ? " + asserted" : ""}: ${condition.source}${condition.waiverReason ? `\n waived: ${condition.waiverReason}` : ""}`)
|
|
1456
|
-
.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"}`)
|
|
1457
|
-
.join("\n\n") +
|
|
1458
|
-
`\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
|
|
1459
|
-
(decisionNext ? `\nnext page: ${decisionNext}` : ""), queryPagination(totalDecisionEvidence, returnedDecisionEvidence, options));
|
|
1460
|
-
}
|
|
1461
|
-
if (command === "covers") {
|
|
1462
|
-
const selector = options.positional[0];
|
|
1463
|
-
if (!selector)
|
|
1464
|
-
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage covers <source-file:line>");
|
|
1465
|
-
const location = locationSelector(selector);
|
|
1466
|
-
const line = report.lines.find((candidate) => candidate.file === location.file && candidate.line === location.line);
|
|
1467
|
-
if (!line) {
|
|
1468
|
-
// "Uncovered" would be a false claim here: nothing is measured on this
|
|
1469
|
-
// exact line. Report what does anchor at it instead of a misleading no.
|
|
1470
|
-
const anchored = [
|
|
1471
|
-
...report.decisions
|
|
1472
|
-
.filter((decision) => decision.meta.file === location.file &&
|
|
1473
|
-
decision.meta.line === location.line)
|
|
1474
|
-
.map((decision) => ({
|
|
1475
|
-
kind: "decision",
|
|
1476
|
-
id: decision.meta.id,
|
|
1477
|
-
column: decision.meta.column,
|
|
1478
|
-
covered: decision.covered,
|
|
1479
|
-
coveredConditions: decision.conditions.filter((condition) => condition.covered).length,
|
|
1480
|
-
conditions: decision.conditions.length,
|
|
1481
|
-
})),
|
|
1482
|
-
...report.branches
|
|
1483
|
-
.filter((branch) => branch.meta.file === location.file &&
|
|
1484
|
-
branch.meta.line === location.line)
|
|
1485
|
-
.map((branch) => ({
|
|
1486
|
-
kind: "branch",
|
|
1487
|
-
id: branch.meta.id,
|
|
1488
|
-
column: branch.meta.column,
|
|
1489
|
-
covered: branch.alternatives.every((alternative) => alternative.covered),
|
|
1490
|
-
})),
|
|
1491
|
-
...report.points
|
|
1492
|
-
.filter((point) => point.meta.file === location.file &&
|
|
1493
|
-
point.meta.line === location.line)
|
|
1494
|
-
.map((point) => ({
|
|
1495
|
-
kind: point.meta.kind,
|
|
1496
|
-
id: point.meta.id,
|
|
1497
|
-
column: point.meta.column,
|
|
1498
|
-
covered: includesSelectedTest(point.tests, selectedTestSet),
|
|
1499
|
-
})),
|
|
1500
|
-
].sort((left, right) => left.column - right.column);
|
|
1501
|
-
const anchoredPage = page(anchored, options);
|
|
1502
|
-
return output({
|
|
1503
|
-
run: run.id,
|
|
1504
|
-
filters: queryFilters(options),
|
|
1505
|
-
location,
|
|
1506
|
-
lineObligation: false,
|
|
1507
|
-
anchored: anchoredPage,
|
|
1508
|
-
totalAnchored: anchored.length,
|
|
1509
|
-
}, options, `${location.file}:${location.line} has no line obligation${anchored.length === 0 ? "; nothing is measured at this exact line" : `; ${anchored.length} obligation(s) anchor here`}\n${anchoredPage
|
|
1510
|
-
.map((obligation) => `${obligation.kind} ${location.line}:${obligation.column} [${obligation.id}] ${obligation.covered ? "covered" : "not fully covered"}${"conditions" in obligation ? ` (${obligation.coveredConditions}/${obligation.conditions} conditions)` : ""}`)
|
|
1511
|
-
.join("\n")}${anchoredPage.length ? "\n" : ""}${pageLabel(anchored.length, anchoredPage.length, options)} anchored obligations`, queryPagination(anchored.length, anchoredPage.length, options));
|
|
1512
|
-
}
|
|
1513
|
-
const allTests = (line?.tests ?? [])
|
|
1514
|
-
.filter((id) => !selectedTestSet || selectedTestSet.has(id))
|
|
1515
|
-
.map((id) => {
|
|
1516
|
-
const test = report.tests.find((candidate) => candidate.id === id);
|
|
1517
|
-
return {
|
|
1518
|
-
id,
|
|
1519
|
-
name: test?.name ?? id,
|
|
1520
|
-
provenance: test?.provenance,
|
|
1521
|
-
};
|
|
1522
|
-
});
|
|
1523
|
-
const allPhases = (line?.phases ?? [])
|
|
1524
|
-
.map((id) => report.phases.find((candidate) => candidate.id === id))
|
|
1525
|
-
.filter((phase) => Boolean(phase) &&
|
|
1526
|
-
(!selectedTestSet || selectedTestSet.has(phase.test)))
|
|
1527
|
-
.map((phase) => ({
|
|
1528
|
-
id: phase.id,
|
|
1529
|
-
kind: phase.kind,
|
|
1530
|
-
operation: phase.operation,
|
|
1531
|
-
source: phase.source,
|
|
1532
|
-
test: phase.test,
|
|
1533
|
-
status: phase.status,
|
|
1534
|
-
causedByPhaseId: phase.causedByPhaseId,
|
|
1535
|
-
}));
|
|
1536
|
-
const tests = page(allTests, options);
|
|
1537
|
-
const phases = page(allPhases, options);
|
|
1538
|
-
const coversTotal = Math.max(allTests.length, allPhases.length);
|
|
1539
|
-
const coversReturned = Math.max(tests.length, phases.length);
|
|
1540
|
-
const coversNext = nextPageCommand(`${coverageCommand(run.id, options, "covers")} ${shellQuote(selector)}`, coversTotal, coversReturned, options);
|
|
1541
|
-
const result = {
|
|
1542
|
-
run: run.id,
|
|
1543
|
-
filters: queryFilters(options),
|
|
1544
|
-
location,
|
|
1545
|
-
covered: includesSelectedTest(line?.tests ?? [], selectedTestSet),
|
|
1546
|
-
confidence: line?.confidence,
|
|
1547
|
-
totalTests: allTests.length,
|
|
1548
|
-
totalPhases: allPhases.length,
|
|
1549
|
-
tests,
|
|
1550
|
-
phases,
|
|
1551
|
-
};
|
|
1552
|
-
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));
|
|
1553
|
-
}
|
|
1554
|
-
if (command === "test") {
|
|
1555
|
-
const selector = options.positional.join(" ").toLowerCase();
|
|
1556
|
-
if (!selector)
|
|
1557
|
-
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage test <id|name-fragment>");
|
|
1558
|
-
const matches = report.tests.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
|
|
1559
|
-
(test.id === selector || test.name.toLowerCase().includes(selector)));
|
|
1560
|
-
if (matches.length === 0)
|
|
1561
|
-
throw new SupercovError("TEST_NOT_FOUND", `Test not found: ${selector}`, {
|
|
1562
|
-
details: { selector },
|
|
1563
|
-
});
|
|
1564
|
-
const testBase = `${coverageCommand(run.id, options, "test")} ${shellQuote(options.positional.join(" "))}`;
|
|
1565
|
-
if (matches.length > 1) {
|
|
1566
|
-
const matchingTests = page(matches, options).map((test) => ({
|
|
1567
|
-
id: test.id,
|
|
1568
|
-
name: test.name,
|
|
1569
|
-
outcome: test.outcome,
|
|
1570
|
-
provenance: test.provenance,
|
|
1571
|
-
}));
|
|
1572
|
-
const matchesNext = nextPageCommand(testBase, matches.length, matchingTests.length, options);
|
|
1573
|
-
return output({
|
|
1574
|
-
run: run.id,
|
|
1575
|
-
filters: queryFilters(options),
|
|
1576
|
-
tests: matchingTests,
|
|
1577
|
-
}, 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));
|
|
1578
|
-
}
|
|
1579
|
-
const test = matches[0];
|
|
1580
|
-
const pointById = new Map(report.points.map((point) => [point.meta.id, point.meta]));
|
|
1581
|
-
const branchAlternativeById = new Map(report.branches.flatMap((branch) => branch.alternatives.map((alternative) => [
|
|
1582
|
-
alternative.id,
|
|
1583
|
-
{
|
|
1584
|
-
...branch.meta,
|
|
1585
|
-
id: alternative.id,
|
|
1586
|
-
alternative: alternative.label,
|
|
1587
|
-
},
|
|
1588
|
-
])));
|
|
1589
|
-
const allHitDetails = test.hits.map((id) => {
|
|
1590
|
-
const point = pointById.get(id);
|
|
1591
|
-
if (point)
|
|
1592
|
-
return {
|
|
1593
|
-
id: point.id,
|
|
1594
|
-
obligation: point.kind,
|
|
1595
|
-
file: point.file,
|
|
1596
|
-
line: point.line,
|
|
1597
|
-
column: point.column,
|
|
1598
|
-
label: point.label,
|
|
1599
|
-
};
|
|
1600
|
-
const branch = branchAlternativeById.get(id);
|
|
1601
|
-
if (branch)
|
|
1602
|
-
return {
|
|
1603
|
-
id: branch.id,
|
|
1604
|
-
obligation: "branch",
|
|
1605
|
-
branchKind: branch.kind,
|
|
1606
|
-
file: branch.file,
|
|
1607
|
-
line: branch.line,
|
|
1608
|
-
column: branch.column,
|
|
1609
|
-
alternative: branch.alternative,
|
|
1610
|
-
};
|
|
1611
|
-
return { id, obligation: "unknown" };
|
|
1612
|
-
});
|
|
1613
|
-
const decisionById = new Map(report.decisions.map((decision) => [decision.meta.id, decision.meta]));
|
|
1614
|
-
const allPhases = report.phases
|
|
1615
|
-
.filter((phase) => phase.test === test.id)
|
|
1616
|
-
.map((phase) => ({
|
|
1617
|
-
id: phase.id,
|
|
1618
|
-
kind: phase.kind,
|
|
1619
|
-
operation: phase.operation,
|
|
1620
|
-
source: phase.source,
|
|
1621
|
-
status: phase.status,
|
|
1622
|
-
causedByPhaseId: phase.causedByPhaseId,
|
|
1623
|
-
lines: phase.lines.length,
|
|
1624
|
-
decisions: phase.decisions.reduce((sum, decision) => sum + decision.vectors.length, 0),
|
|
1625
|
-
}));
|
|
1626
|
-
const testTotal = Math.max(test.lines.length, test.hits.length, test.decisions.length, allPhases.length);
|
|
1627
|
-
const selected = {
|
|
1628
|
-
...test,
|
|
1629
|
-
lines: page(test.lines, options),
|
|
1630
|
-
hits: page(test.hits, options),
|
|
1631
|
-
hitDetails: page(allHitDetails, options),
|
|
1632
|
-
decisions: page(test.decisions, options).map((decision) => ({
|
|
1633
|
-
...decision,
|
|
1634
|
-
meta: decisionById.get(decision.id),
|
|
1635
|
-
})),
|
|
1636
|
-
phases: page(allPhases, options),
|
|
1637
|
-
totals: {
|
|
1638
|
-
lines: test.lines.length,
|
|
1639
|
-
hits: test.hits.length,
|
|
1640
|
-
decisions: test.decisions.length,
|
|
1641
|
-
phases: allPhases.length,
|
|
1642
|
-
},
|
|
1643
|
-
};
|
|
1644
|
-
const testReturned = Math.max(selected.lines.length, selected.hits.length, selected.decisions.length, selected.phases.length);
|
|
1645
|
-
const testNext = nextPageCommand(testBase, testTotal, testReturned, options);
|
|
1646
|
-
return output({
|
|
1647
|
-
run: run.id,
|
|
1648
|
-
filters: queryFilters(options),
|
|
1649
|
-
paginationAppliesTo: "lines, hits/hitDetails, decisions, and phases independently within the test",
|
|
1650
|
-
tests: [selected],
|
|
1651
|
-
}, 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));
|
|
1652
|
-
}
|
|
1653
|
-
throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${command}. Try supercov help.`, { details: { command } });
|
|
1654
|
-
}
|
|
1655
|
-
export const coverageQueryCommands = new Set(["help", "runs", "diff"]);
|
|
1656
|
-
//# sourceMappingURL=query.js.map
|