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