supercov 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -62
- package/dist/agentJson.d.ts +44 -0
- package/dist/agentJson.d.ts.map +1 -0
- package/dist/agentJson.js +84 -0
- package/dist/agentJson.js.map +1 -0
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +1 -0
- package/dist/analyze.js.map +1 -1
- package/dist/cli.js +107 -19
- package/dist/cli.js.map +1 -1
- package/dist/directInstrumenter.d.ts +2 -2
- package/dist/directInstrumenter.d.ts.map +1 -1
- package/dist/directInstrumenter.js +18 -20
- package/dist/directInstrumenter.js.map +1 -1
- package/dist/esmInterceptor.d.ts +6 -0
- package/dist/esmInterceptor.d.ts.map +1 -0
- package/dist/esmInterceptor.js +63 -0
- package/dist/esmInterceptor.js.map +1 -0
- package/dist/evidenceArchive.d.ts +3 -1
- package/dist/evidenceArchive.d.ts.map +1 -1
- package/dist/evidenceArchive.js +31 -22
- package/dist/evidenceArchive.js.map +1 -1
- package/dist/instrumenter.d.ts.map +1 -1
- package/dist/instrumenter.js +30 -8
- package/dist/instrumenter.js.map +1 -1
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +3 -3
- package/dist/integrity.js.map +1 -1
- package/dist/launchSupervisor.d.ts +6 -0
- package/dist/launchSupervisor.d.ts.map +1 -1
- package/dist/launchSupervisor.js +78 -4
- package/dist/launchSupervisor.js.map +1 -1
- package/dist/merge.d.ts +2 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +116 -0
- package/dist/merge.js.map +1 -0
- package/dist/nodeTest.d.ts +15 -0
- package/dist/nodeTest.d.ts.map +1 -0
- package/dist/nodeTest.js +123 -0
- package/dist/nodeTest.js.map +1 -0
- package/dist/project.d.ts +6 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +36 -21
- package/dist/project.js.map +1 -1
- package/dist/query.d.ts +60 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +493 -90
- package/dist/query.js.map +1 -1
- package/dist/queryCache.d.ts +15 -0
- package/dist/queryCache.d.ts.map +1 -0
- package/dist/queryCache.js +95 -0
- package/dist/queryCache.js.map +1 -0
- package/dist/register.mjs +40 -0
- package/dist/register.mjs.map +1 -1
- package/dist/resolve-loader.d.mts +1 -0
- package/dist/resolve-loader.d.mts.map +1 -1
- package/dist/resolve-loader.mjs +43 -0
- package/dist/resolve-loader.mjs.map +1 -1
- package/dist/runAnalysis.d.ts.map +1 -1
- package/dist/runAnalysis.js +18 -0
- package/dist/runAnalysis.js.map +1 -1
- package/dist/runnerEvidence.d.ts +19 -0
- package/dist/runnerEvidence.d.ts.map +1 -0
- package/dist/runnerEvidence.js +93 -0
- package/dist/runnerEvidence.js.map +1 -0
- package/dist/runtime.d.ts +7 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +94 -11
- package/dist/runtime.js.map +1 -1
- package/dist/sourceDiscovery.d.ts +9 -0
- package/dist/sourceDiscovery.d.ts.map +1 -0
- package/dist/sourceDiscovery.js +227 -0
- package/dist/sourceDiscovery.js.map +1 -0
- package/dist/types.d.ts +15 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vitePlugin.d.ts +4 -0
- package/dist/vitePlugin.d.ts.map +1 -1
- package/dist/vitePlugin.js +13 -3
- package/dist/vitePlugin.js.map +1 -1
- package/dist/vitest.js +5 -1
- package/dist/vitest.js.map +1 -1
- package/package.json +12 -4
package/dist/query.js
CHANGED
|
@@ -3,39 +3,79 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { coverageSummaryForTests, isIndependencePair } from "./analyze.js";
|
|
5
5
|
import { EVIDENCE_ARCHIVE_SCHEMA_VERSION } from "./evidenceArchive.js";
|
|
6
|
-
import {
|
|
6
|
+
import { analyzeCoverageArchiveCached, readCoverageQueryIndex, } from "./queryCache.js";
|
|
7
7
|
import { compareRunIntegrity, createRunIntegrity } from "./integrity.js";
|
|
8
8
|
import { discoverCoverageProject } from "./project.js";
|
|
9
|
-
|
|
9
|
+
import { agentPagination, agentSuccessJson, SupercovError, } from "./agentJson.js";
|
|
10
|
+
function parseOptions(command, args) {
|
|
10
11
|
const options = {
|
|
12
|
+
command: command === "runs" || command === "diff" || command === "help"
|
|
13
|
+
? command
|
|
14
|
+
: `coverage.${command}`,
|
|
11
15
|
limit: 20,
|
|
12
16
|
offset: 0,
|
|
13
17
|
json: false,
|
|
14
18
|
filter: "all",
|
|
19
|
+
target: 100,
|
|
20
|
+
metric: "all",
|
|
15
21
|
positional: [],
|
|
16
22
|
};
|
|
17
23
|
for (let index = 0; index < args.length; index += 1) {
|
|
18
24
|
const value = args[index];
|
|
19
25
|
if (value === "--json")
|
|
20
26
|
options.json = true;
|
|
21
|
-
else if (value === "--run")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
}
|
|
27
57
|
else if (value === "--filter") {
|
|
28
58
|
const filter = args[++index]?.toLowerCase();
|
|
29
59
|
if (filter !== "all" && filter !== "passed" && filter !== "failed")
|
|
30
|
-
throw new
|
|
60
|
+
throw new SupercovError("INVALID_ARGUMENT", "--filter must be all, passed, or failed");
|
|
31
61
|
options.filter = filter;
|
|
32
62
|
}
|
|
33
|
-
else if (value === "--limit")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
|
37
75
|
else if (value.startsWith("--"))
|
|
38
|
-
throw new
|
|
76
|
+
throw new SupercovError("INVALID_ARGUMENT", `Unknown option: ${value}`, {
|
|
77
|
+
details: { option: value },
|
|
78
|
+
});
|
|
39
79
|
else
|
|
40
80
|
options.positional.push(value);
|
|
41
81
|
}
|
|
@@ -46,7 +86,7 @@ function filteredCoverage(report, options) {
|
|
|
46
86
|
return report;
|
|
47
87
|
const filtered = report.filters?.[options.filter];
|
|
48
88
|
if (!filtered) {
|
|
49
|
-
throw new
|
|
89
|
+
throw new SupercovError("FILTER_UNAVAILABLE", "This run does not contain outcome-filtered coverage. Create a new coverage run.");
|
|
50
90
|
}
|
|
51
91
|
return filtered;
|
|
52
92
|
}
|
|
@@ -80,24 +120,32 @@ function discoverRuns(root) {
|
|
|
80
120
|
}
|
|
81
121
|
return [...runs.values()].sort((left, right) => right.id.localeCompare(left.id));
|
|
82
122
|
}
|
|
83
|
-
function
|
|
84
|
-
return
|
|
123
|
+
function storedRunAnalysisOptions(run) {
|
|
124
|
+
return {
|
|
85
125
|
runId: run.id,
|
|
86
126
|
testExitCode: run.metadata?.testExitCode,
|
|
87
127
|
integrity: run.metadata?.integrity,
|
|
88
128
|
generatedAt: run.metadata?.startedAt,
|
|
89
|
-
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function analyzeStoredRun(run) {
|
|
132
|
+
return analyzeCoverageArchiveCached(run.evidencePath, storedRunAnalysisOptions(run));
|
|
90
133
|
}
|
|
91
|
-
function
|
|
134
|
+
function readStoredRunIndex(run) {
|
|
135
|
+
return readCoverageQueryIndex(run.evidencePath, storedRunAnalysisOptions(run));
|
|
136
|
+
}
|
|
137
|
+
function selectRun(root, selector, currentIntegrity, quiet = false) {
|
|
92
138
|
const runs = discoverRuns(root);
|
|
93
139
|
if (runs.length === 0)
|
|
94
|
-
throw new
|
|
140
|
+
throw new SupercovError("NO_RUNS", "No local coverage runs. Run supercov first.");
|
|
95
141
|
const selected = !selector || selector === "latest"
|
|
96
142
|
? runs[0]
|
|
97
143
|
: (runs.find((run) => run.id === selector) ??
|
|
98
144
|
runs.find((run) => run.id.startsWith(selector)));
|
|
99
145
|
if (!selected)
|
|
100
|
-
throw new
|
|
146
|
+
throw new SupercovError("RUN_NOT_FOUND", `Coverage run not found: ${selector}`, {
|
|
147
|
+
details: { selector },
|
|
148
|
+
});
|
|
101
149
|
const report = analyzeStoredRun(selected);
|
|
102
150
|
if (currentIntegrity) {
|
|
103
151
|
const comparison = compareRunIntegrity(selected.metadata?.integrity ?? report.integrity, currentIntegrity);
|
|
@@ -106,7 +154,7 @@ function selectRun(root, selector, currentIntegrity) {
|
|
|
106
154
|
stale: comparison.stale,
|
|
107
155
|
staleReasons: comparison.reasons,
|
|
108
156
|
};
|
|
109
|
-
if (comparison.stale) {
|
|
157
|
+
if (comparison.stale && !quiet) {
|
|
110
158
|
console.error(`[supercov] stale run ${selected.id}: ${comparison.reasons.join(", ")}`);
|
|
111
159
|
}
|
|
112
160
|
}
|
|
@@ -123,8 +171,11 @@ function currentProjectIntegrity(root) {
|
|
|
123
171
|
function page(values, options) {
|
|
124
172
|
return values.slice(options.offset, options.offset + options.limit);
|
|
125
173
|
}
|
|
126
|
-
function output(value, options, text) {
|
|
127
|
-
|
|
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);
|
|
128
179
|
}
|
|
129
180
|
function pct(value) {
|
|
130
181
|
return `${value.toFixed(2)}%`;
|
|
@@ -132,6 +183,175 @@ function pct(value) {
|
|
|
132
183
|
function shellQuote(value) {
|
|
133
184
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
134
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") {
|
|
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 (selected.size >= best.size)
|
|
311
|
+
return;
|
|
312
|
+
const stateKey = `${[...selected].sort().join(",")}|${[...skipped].sort().join(",")}`;
|
|
313
|
+
if (seen.has(stateKey))
|
|
314
|
+
return;
|
|
315
|
+
seen.add(stateKey);
|
|
316
|
+
const unmet = obligations.filter((obligation) => !skipped.has(obligation.id) && !obligationSatisfied(obligation, selected));
|
|
317
|
+
if (unmet.length === 0) {
|
|
318
|
+
best = new Set(selected);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const obligation = unmet.sort((left, right) => {
|
|
322
|
+
const feasible = (value) => value.options.filter((option) => option.some((test) => !selected.has(test))).length;
|
|
323
|
+
return feasible(left) - feasible(right) || left.id.localeCompare(right.id);
|
|
324
|
+
})[0];
|
|
325
|
+
const additions = [...new Map(obligation.options
|
|
326
|
+
.map((option) => option.filter((test) => !selected.has(test)))
|
|
327
|
+
.filter((option) => option.length > 0)
|
|
328
|
+
.map((option) => [optionKey(option), option])).values()].sort((left, right) => left.length - right.length || optionKey(left).localeCompare(optionKey(right)));
|
|
329
|
+
for (const addition of additions) {
|
|
330
|
+
if (selected.size + addition.length >= best.size)
|
|
331
|
+
continue;
|
|
332
|
+
search(new Set([...selected, ...addition]), skipped, skippedByMetric);
|
|
333
|
+
}
|
|
334
|
+
const skippedCount = skippedByMetric.get(obligation.metric) ?? 0;
|
|
335
|
+
if (skippedCount < (skipLimits.get(obligation.metric) ?? 0)) {
|
|
336
|
+
const nextSkipped = new Set(skipped);
|
|
337
|
+
nextSkipped.add(obligation.id);
|
|
338
|
+
const nextCounts = new Map(skippedByMetric);
|
|
339
|
+
nextCounts.set(obligation.metric, skippedCount + 1);
|
|
340
|
+
search(selected, nextSkipped, nextCounts);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
search(new Set(), new Set(), new Map());
|
|
344
|
+
const expanded = model.expand(best);
|
|
345
|
+
return {
|
|
346
|
+
optimal: true,
|
|
347
|
+
target,
|
|
348
|
+
metric,
|
|
349
|
+
selected: [...best].sort(),
|
|
350
|
+
expanded: [...expanded].sort(),
|
|
351
|
+
summary: coverageSummaryForTests(report, expanded),
|
|
352
|
+
exploredStates,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
135
355
|
function coverageCommand(runId, options, child) {
|
|
136
356
|
return [
|
|
137
357
|
"npx supercov runs",
|
|
@@ -168,7 +388,9 @@ function selectedTestIds(report, options) {
|
|
|
168
388
|
]
|
|
169
389
|
.filter(Boolean)
|
|
170
390
|
.join(", ");
|
|
171
|
-
throw new
|
|
391
|
+
throw new SupercovError("TEST_FILTER_EMPTY", `No tests match ${filter}`, {
|
|
392
|
+
details: { kind: options.kind, runner: options.runner },
|
|
393
|
+
});
|
|
172
394
|
}
|
|
173
395
|
return new Set(selected.map((test) => test.id));
|
|
174
396
|
}
|
|
@@ -240,6 +462,13 @@ function filterLabel(options) {
|
|
|
240
462
|
.filter(Boolean)
|
|
241
463
|
.join(", ");
|
|
242
464
|
}
|
|
465
|
+
function queryFilters(options) {
|
|
466
|
+
return {
|
|
467
|
+
outcome: options.filter,
|
|
468
|
+
kind: options.kind ?? null,
|
|
469
|
+
runner: options.runner ?? null,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
243
472
|
function attribution(report, selected) {
|
|
244
473
|
const phases = selected
|
|
245
474
|
? report.phases.filter((phase) => selected.has(phase.test))
|
|
@@ -251,7 +480,7 @@ function attribution(report, selected) {
|
|
|
251
480
|
serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
|
|
252
481
|
};
|
|
253
482
|
}
|
|
254
|
-
function fileGaps(report, selected) {
|
|
483
|
+
export function fileGaps(report, selected) {
|
|
255
484
|
const files = new Map();
|
|
256
485
|
const get = (file) => {
|
|
257
486
|
const existing = files.get(file);
|
|
@@ -264,6 +493,8 @@ function fileGaps(report, selected) {
|
|
|
264
493
|
uncoveredFunctions: 0,
|
|
265
494
|
missingBranches: 0,
|
|
266
495
|
missingMcdcConditions: 0,
|
|
496
|
+
measurementLimitations: 0,
|
|
497
|
+
limitationKinds: [],
|
|
267
498
|
coveredByOtherTests: {
|
|
268
499
|
lines: 0,
|
|
269
500
|
statements: 0,
|
|
@@ -325,55 +556,117 @@ function fileGaps(report, selected) {
|
|
|
325
556
|
}
|
|
326
557
|
}
|
|
327
558
|
}
|
|
559
|
+
for (const limitation of report.limitations ?? []) {
|
|
560
|
+
const gap = get(limitation.file);
|
|
561
|
+
gap.measurementLimitations += 1;
|
|
562
|
+
if (!gap.limitationKinds.includes(limitation.kind))
|
|
563
|
+
gap.limitationKinds.push(limitation.kind);
|
|
564
|
+
}
|
|
328
565
|
for (const gap of files.values()) {
|
|
566
|
+
gap.limitationKinds.sort();
|
|
329
567
|
gap.score =
|
|
330
568
|
gap.uncoveredLines +
|
|
331
569
|
gap.uncoveredFunctions * 2 +
|
|
332
570
|
gap.missingBranches * 2 +
|
|
333
|
-
gap.missingMcdcConditions * 3
|
|
571
|
+
gap.missingMcdcConditions * 3 +
|
|
572
|
+
gap.measurementLimitations * 3;
|
|
334
573
|
}
|
|
335
574
|
return [...files.values()].sort((left, right) => right.score - left.score || left.file.localeCompare(right.file));
|
|
336
575
|
}
|
|
337
576
|
function findFile(report, selector) {
|
|
338
|
-
const files = [
|
|
577
|
+
const files = [
|
|
578
|
+
...new Set([
|
|
579
|
+
...report.lines.map((line) => line.file),
|
|
580
|
+
...(report.limitations ?? []).map((limitation) => limitation.file),
|
|
581
|
+
]),
|
|
582
|
+
];
|
|
339
583
|
if (files.includes(selector))
|
|
340
584
|
return selector;
|
|
341
585
|
const matches = files.filter((file) => file.includes(selector));
|
|
342
586
|
if (matches.length === 1)
|
|
343
587
|
return matches[0];
|
|
344
588
|
if (matches.length === 0)
|
|
345
|
-
throw new
|
|
346
|
-
|
|
589
|
+
throw new SupercovError("SOURCE_NOT_FOUND", `Source file not found: ${selector}`, {
|
|
590
|
+
details: { selector },
|
|
591
|
+
});
|
|
592
|
+
throw new SupercovError("AMBIGUOUS_SELECTOR", `Ambiguous file selector: ${matches.join(", ")}`, {
|
|
593
|
+
details: { selector, matches },
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
export function coverageMeasurement(report) {
|
|
597
|
+
const limitations = report.limitations ?? [];
|
|
598
|
+
const byKind = {
|
|
599
|
+
"dynamic-code": 0,
|
|
600
|
+
"semantic-safety": 0,
|
|
601
|
+
"source-scope": 0,
|
|
602
|
+
};
|
|
603
|
+
for (const limitation of limitations)
|
|
604
|
+
byKind[limitation.kind] += 1;
|
|
605
|
+
return {
|
|
606
|
+
complete: limitations.length === 0,
|
|
607
|
+
limitations: limitations.length,
|
|
608
|
+
// Every current limitation removes source from the measured denominator.
|
|
609
|
+
blocking: limitations.length,
|
|
610
|
+
files: new Set(limitations.map((limitation) => limitation.file)).size,
|
|
611
|
+
byKind,
|
|
612
|
+
};
|
|
347
613
|
}
|
|
348
614
|
function locationSelector(selector) {
|
|
349
615
|
const match = /^(.*):(\d+)(?::\d+)?$/.exec(selector);
|
|
350
616
|
if (!match)
|
|
351
|
-
throw new
|
|
617
|
+
throw new SupercovError("INVALID_ARGUMENT", "Expected <source-file>:<line>", {
|
|
618
|
+
details: { selector },
|
|
619
|
+
});
|
|
352
620
|
return { file: match[1], line: Number(match[2]) };
|
|
353
621
|
}
|
|
354
622
|
function vectorText(values, outcome) {
|
|
355
623
|
return `${values.map((value) => (value === null ? "-" : value ? "T" : "F")).join("")} -> ${outcome ? "T" : "F"}`;
|
|
356
624
|
}
|
|
357
|
-
|
|
358
|
-
console.log(`Agent-oriented local coverage queries:
|
|
625
|
+
const helpText = `Agent-oriented local coverage queries:
|
|
359
626
|
supercov runs [--limit N] [--json]
|
|
360
627
|
supercov runs <run-id> coverage [--filter all|passed|failed] [--kind e2e] [--runner playwright] [--json]
|
|
361
628
|
supercov runs <run-id> coverage kinds [--json]
|
|
362
629
|
supercov runs <run-id> coverage runners [--json]
|
|
630
|
+
supercov runs <run-id> coverage scope [--limit N] [--offset N] [--json]
|
|
363
631
|
supercov runs <run-id> coverage files [--filter all|passed|failed] [--limit N] [--offset N] [--json]
|
|
364
632
|
supercov runs <run-id> coverage gaps [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
|
|
365
633
|
supercov runs <run-id> coverage file <source-file> [--kind e2e] [--limit N] [--offset N] [--json]
|
|
366
634
|
supercov runs <run-id> coverage decision <id|source-file:line> [--kind e2e] [--json]
|
|
367
635
|
supercov runs <run-id> coverage covers <source-file:line> [--kind e2e] [--json]
|
|
368
636
|
supercov runs <run-id> coverage test <id|name-fragment> [--kind e2e] [--limit N] [--json]
|
|
637
|
+
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]
|
|
369
638
|
supercov diff <older-run> <newer-run> [--limit N] [--json]
|
|
639
|
+
supercov merge <run-id> <run-id> [...]
|
|
370
640
|
supercov prune [--keep N] [--dry-run]
|
|
371
641
|
supercov clean [--keep N] [--dry-run]
|
|
372
642
|
|
|
373
643
|
Use "latest" as <run-id> to query the newest local run.
|
|
374
644
|
|
|
375
645
|
Create a run with:
|
|
376
|
-
supercov -- <test command
|
|
646
|
+
supercov -- <test command>`;
|
|
647
|
+
function help(options) {
|
|
648
|
+
if (options.json) {
|
|
649
|
+
return output({
|
|
650
|
+
usage: "supercov -- <test command>",
|
|
651
|
+
runSelector: "Use latest as <run-id> to query the newest local run.",
|
|
652
|
+
queryCommands: [
|
|
653
|
+
"runs",
|
|
654
|
+
"runs <run-id> coverage",
|
|
655
|
+
"runs <run-id> coverage kinds",
|
|
656
|
+
"runs <run-id> coverage runners",
|
|
657
|
+
"runs <run-id> coverage scope",
|
|
658
|
+
"runs <run-id> coverage files",
|
|
659
|
+
"runs <run-id> coverage gaps",
|
|
660
|
+
"runs <run-id> coverage file <source-file>",
|
|
661
|
+
"runs <run-id> coverage decision <id|source-file:line>",
|
|
662
|
+
"runs <run-id> coverage covers <source-file:line>",
|
|
663
|
+
"runs <run-id> coverage test <id|name-fragment>",
|
|
664
|
+
"runs <run-id> coverage minimize",
|
|
665
|
+
"diff <older-run> <newer-run>",
|
|
666
|
+
],
|
|
667
|
+
}, options, helpText);
|
|
668
|
+
}
|
|
669
|
+
console.log(helpText);
|
|
377
670
|
}
|
|
378
671
|
/** Resolve the instance-first coverage resource syntax. */
|
|
379
672
|
export function resolveCoverageQueryInvocation(command, args) {
|
|
@@ -391,15 +684,17 @@ export function resolveCoverageQueryInvocation(command, args) {
|
|
|
391
684
|
"summary",
|
|
392
685
|
"kinds",
|
|
393
686
|
"runners",
|
|
687
|
+
"scope",
|
|
394
688
|
"files",
|
|
395
689
|
"gaps",
|
|
396
690
|
"file",
|
|
397
691
|
"decision",
|
|
398
692
|
"covers",
|
|
399
693
|
"test",
|
|
694
|
+
"minimize",
|
|
400
695
|
]);
|
|
401
696
|
if (!coverageCommands.has(child)) {
|
|
402
|
-
throw new
|
|
697
|
+
throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${child}. Try supercov help.`, { details: { command: child } });
|
|
403
698
|
}
|
|
404
699
|
return {
|
|
405
700
|
command: child,
|
|
@@ -409,20 +704,22 @@ export function resolveCoverageQueryInvocation(command, args) {
|
|
|
409
704
|
export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
410
705
|
const resolved = resolveCoverageQueryInvocation(command, args);
|
|
411
706
|
command = resolved.command;
|
|
412
|
-
const options = parseOptions(resolved.args);
|
|
707
|
+
const options = parseOptions(command, resolved.args);
|
|
413
708
|
if (command === "help")
|
|
414
|
-
return help();
|
|
709
|
+
return help(options);
|
|
415
710
|
const currentIntegrity = currentProjectIntegrity(root);
|
|
416
711
|
if (command === "runs") {
|
|
417
712
|
const availableRuns = discoverRuns(root);
|
|
418
713
|
const runs = page(availableRuns, options).map((run) => {
|
|
419
|
-
const
|
|
714
|
+
const cached = readStoredRunIndex(run);
|
|
715
|
+
const report = cached ? filteredCoverage(cached, options) : undefined;
|
|
420
716
|
return {
|
|
421
717
|
id: run.id,
|
|
422
|
-
generatedAt: report.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
718
|
+
generatedAt: report?.generatedAt ?? run.metadata?.startedAt,
|
|
719
|
+
coverageIndexed: Boolean(report),
|
|
720
|
+
lines: report?.summary.lines.percentage ?? null,
|
|
721
|
+
branches: report?.summary.branches.percentage ?? null,
|
|
722
|
+
mcdc: report?.summary.conditionCoveragePct ?? null,
|
|
426
723
|
command: run.metadata?.command,
|
|
427
724
|
durationMs: run.metadata?.durationMs,
|
|
428
725
|
timings: run.metadata?.timings,
|
|
@@ -436,18 +733,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
436
733
|
});
|
|
437
734
|
const runsBase = `npx supercov runs${options.filter !== "all" ? ` --filter ${options.filter}` : ""}`;
|
|
438
735
|
const runsNext = nextPageCommand(runsBase, availableRuns.length, runs.length, options);
|
|
439
|
-
return output(runs, options, runs
|
|
440
|
-
.map((run) => `${run.id} lines ${pct(run.lines
|
|
736
|
+
return output({ filters: queryFilters(options), runs }, options, runs
|
|
737
|
+
.map((run) => `${run.id} ${run.coverageIndexed ? `lines ${pct(run.lines)} branches ${pct(run.branches)} MC/DC ${pct(run.mcdc)}` : "coverage not indexed"}${run.stale ? ` STALE (${run.reasons.join(", ")})` : ""}`)
|
|
441
738
|
.join("\n") +
|
|
442
739
|
`\n${pageLabel(availableRuns.length, runs.length, options)}` +
|
|
443
|
-
(runsNext ? `\nnext page: ${runsNext}` : ""));
|
|
740
|
+
(runsNext ? `\nnext page: ${runsNext}` : ""), queryPagination(availableRuns.length, runs.length, options));
|
|
444
741
|
}
|
|
445
742
|
if (command === "diff") {
|
|
446
743
|
const [olderSelector, newerSelector] = options.positional;
|
|
447
744
|
if (!olderSelector || !newerSelector)
|
|
448
|
-
throw new
|
|
449
|
-
const olderSelected = selectRun(root, olderSelector, currentIntegrity);
|
|
450
|
-
const newerSelected = selectRun(root, newerSelector, currentIntegrity);
|
|
745
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov diff <older-run> <newer-run>");
|
|
746
|
+
const olderSelected = selectRun(root, olderSelector, currentIntegrity, options.json);
|
|
747
|
+
const newerSelected = selectRun(root, newerSelector, currentIntegrity, options.json);
|
|
451
748
|
const older = {
|
|
452
749
|
...olderSelected,
|
|
453
750
|
report: filteredCoverage(olderSelected.report, options),
|
|
@@ -502,6 +799,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
502
799
|
.map(([, label]) => label)
|
|
503
800
|
.sort();
|
|
504
801
|
const result = {
|
|
802
|
+
filters: queryFilters(options),
|
|
505
803
|
older: older.run.id,
|
|
506
804
|
newer: newer.run.id,
|
|
507
805
|
delta: {
|
|
@@ -540,9 +838,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
540
838
|
.filter(Boolean)
|
|
541
839
|
.join(" ");
|
|
542
840
|
const diffNext = nextPageCommand(diffBase, diffTotal, diffReturned, options);
|
|
543
|
-
return output(result, options, `${older.run.id} -> ${newer.run.id}\nlines ${result.delta.lines >= 0 ? "+" : ""}${result.delta.lines}pp, branches ${result.delta.branches >= 0 ? "+" : ""}${result.delta.branches}pp, MC/DC ${result.delta.mcdc >= 0 ? "+" : ""}${result.delta.mcdc}pp\ngained: ${gainedLines.length} lines, ${gainedBranches.length} branches, ${gainedMcdc.length} MC/DC conditions\nlost: ${lostLines.length} lines, ${lostBranches.length} branches, ${lostMcdc.length} MC/DC conditions\n${result.gained.lines.map((line) => `+ line ${line}`).join("\n")}${result.gained.branches.length ? `\n${result.gained.branches.map((item) => `+ branch ${item}`).join("\n")}` : ""}${result.gained.mcdc.length ? `\n${result.gained.mcdc.map((item) => `+ MC/DC ${item}`).join("\n")}` : ""}\n${pageLabel(diffTotal, diffReturned, options)} per category${diffNext ? `\nnext page: ${diffNext}` : ""}
|
|
841
|
+
return output(result, options, `${older.run.id} -> ${newer.run.id}\nlines ${result.delta.lines >= 0 ? "+" : ""}${result.delta.lines}pp, branches ${result.delta.branches >= 0 ? "+" : ""}${result.delta.branches}pp, MC/DC ${result.delta.mcdc >= 0 ? "+" : ""}${result.delta.mcdc}pp\ngained: ${gainedLines.length} lines, ${gainedBranches.length} branches, ${gainedMcdc.length} MC/DC conditions\nlost: ${lostLines.length} lines, ${lostBranches.length} branches, ${lostMcdc.length} MC/DC conditions\n${result.gained.lines.map((line) => `+ line ${line}`).join("\n")}${result.gained.branches.length ? `\n${result.gained.branches.map((item) => `+ branch ${item}`).join("\n")}` : ""}${result.gained.mcdc.length ? `\n${result.gained.mcdc.map((item) => `+ MC/DC ${item}`).join("\n")}` : ""}\n${pageLabel(diffTotal, diffReturned, options)} per category${diffNext ? `\nnext page: ${diffNext}` : ""}`, queryPagination(diffTotal, diffReturned, options));
|
|
544
842
|
}
|
|
545
|
-
const selectedRun = selectRun(root, options.run, currentIntegrity);
|
|
843
|
+
const selectedRun = selectRun(root, options.run, currentIntegrity, options.json);
|
|
546
844
|
const run = selectedRun.run;
|
|
547
845
|
const report = filteredCoverage(selectedRun.report, options);
|
|
548
846
|
const selectedTestSet = selectedTestIds(report, options);
|
|
@@ -551,6 +849,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
551
849
|
? coverageSummaryForTests(report, selectedTestSet)
|
|
552
850
|
: report.summary;
|
|
553
851
|
const gaps = fileGaps(report, selectedTestSet).filter((gap) => gap.score > 0);
|
|
852
|
+
const measurement = coverageMeasurement(report);
|
|
554
853
|
const selectedTests = report.tests.filter((test) => !selectedTestSet || selectedTestSet.has(test.id));
|
|
555
854
|
const testCount = selectedTests.filter((test) => (test.role ?? "test") === "test").length;
|
|
556
855
|
const setupCount = selectedTests.filter((test) => test.role === "setup").length;
|
|
@@ -560,18 +859,19 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
560
859
|
]));
|
|
561
860
|
const result = {
|
|
562
861
|
run: run.id,
|
|
563
|
-
|
|
564
|
-
...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
|
|
862
|
+
filters: queryFilters(options),
|
|
565
863
|
generatedAt: report.generatedAt,
|
|
566
864
|
valid: run.metadata?.testExitCode === 0,
|
|
567
865
|
stale: report.integrity?.stale ?? false,
|
|
568
866
|
staleReasons: report.integrity?.staleReasons ?? [],
|
|
569
|
-
structurallyComplete: summary.coverageComplete,
|
|
867
|
+
structurallyComplete: summary.coverageComplete && measurement.complete,
|
|
570
868
|
complete: options.filter === "passed" &&
|
|
571
869
|
run.metadata?.testExitCode === 0 &&
|
|
572
870
|
!report.integrity?.stale &&
|
|
573
|
-
summary.coverageComplete
|
|
871
|
+
summary.coverageComplete &&
|
|
872
|
+
measurement.complete,
|
|
574
873
|
coverage: summary,
|
|
874
|
+
measurement,
|
|
575
875
|
coverageByKind: report.coverageByKind,
|
|
576
876
|
coverageByRunner: report.coverageByRunner,
|
|
577
877
|
attribution: attribution(report, selectedTestSet),
|
|
@@ -588,11 +888,94 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
588
888
|
}
|
|
589
889
|
: {}),
|
|
590
890
|
filesWithGaps: gaps.length,
|
|
891
|
+
filesWithCoverageGaps: gaps.filter((gap) => gap.uncoveredLines > 0 ||
|
|
892
|
+
gap.uncoveredStatements > 0 ||
|
|
893
|
+
gap.uncoveredFunctions > 0 ||
|
|
894
|
+
gap.missingBranches > 0 ||
|
|
895
|
+
gap.missingMcdcConditions > 0).length,
|
|
896
|
+
filesWithMeasurementLimitations: measurement.files,
|
|
591
897
|
tests: testCount,
|
|
592
898
|
setups: setupCount,
|
|
593
899
|
testOutcomes,
|
|
900
|
+
sourceScope: report.scope
|
|
901
|
+
? {
|
|
902
|
+
mode: report.scope.mode,
|
|
903
|
+
roots: report.scope.roots,
|
|
904
|
+
included: report.scope.entries.filter((entry) => entry.status === "included").length,
|
|
905
|
+
excluded: report.scope.entries.filter((entry) => entry.status === "excluded").length,
|
|
906
|
+
ambiguous: report.scope.entries.filter((entry) => entry.status === "ambiguous").length,
|
|
907
|
+
}
|
|
908
|
+
: undefined,
|
|
594
909
|
};
|
|
595
|
-
return output(result, options, `run ${run.id}${filterLabel(options) ? ` (${filterLabel(options)})` : ""}${run.metadata?.testExitCode !== 0 ? ` [INVALID: test exit ${run.metadata?.testExitCode ?? "unknown"}]` : ""}${report.integrity?.stale ? ` [STALE: ${(report.integrity.staleReasons ?? []).join(", ")}]` : ""}\nlines ${pct(summary.lines.percentage)} (${summary.lines.covered}/${summary.lines.total})\nbranches ${pct(summary.branches.percentage)} (${summary.branches.covered}/${summary.branches.total})\nMC/DC ${pct(summary.conditionCoveragePct)} (${summary.coveredConditions}/${summary.conditions})${!selectedTestSet ? `\nconfidence: ${report.lines.filter((line) => line.confidence?.level === "asserted").length} asserted lines, ${report.lines.filter((line) => line.confidence?.level === "action").length} action-linked, ${report.lines.filter((line) => line.confidence?.level === "executed").length} execution-only; ${report.decisions.reduce((total, decision) => total + decision.conditions.filter((condition) => condition.assertionCovered).length, 0)} assertion-linked MC/DC conditions` : ""}\n${testCount} test(s)${setupCount ? ` + ${setupCount} setup scope(s)` : ""}; outcomes ${Object.entries(testOutcomes).filter(([, count]) => count > 0).map(([outcome, count]) => `${outcome}=${count}`).join(", ") || "none"}; ${gaps.length} file(s) have
|
|
910
|
+
return output(result, options, `run ${run.id}${filterLabel(options) ? ` (${filterLabel(options)})` : ""}${run.metadata?.testExitCode !== 0 ? ` [INVALID: test exit ${run.metadata?.testExitCode ?? "unknown"}]` : ""}${report.integrity?.stale ? ` [STALE: ${(report.integrity.staleReasons ?? []).join(", ")}]` : ""}\nlines ${pct(summary.lines.percentage)} (${summary.lines.covered}/${summary.lines.total})\nbranches ${pct(summary.branches.percentage)} (${summary.branches.covered}/${summary.branches.total})\nMC/DC ${pct(summary.conditionCoveragePct)} (${summary.coveredConditions}/${summary.conditions})\nmeasurement: ${measurement.complete ? "complete" : `incomplete — ${measurement.blocking} blocking limitation(s) in ${measurement.files} file(s)`}${!selectedTestSet ? `\nconfidence: ${report.lines.filter((line) => line.confidence?.level === "asserted").length} asserted lines, ${report.lines.filter((line) => line.confidence?.level === "action").length} action-linked, ${report.lines.filter((line) => line.confidence?.level === "executed").length} execution-only; ${report.decisions.reduce((total, decision) => total + decision.conditions.filter((condition) => condition.assertionCovered).length, 0)} assertion-linked MC/DC conditions` : ""}\n${testCount} test(s)${setupCount ? ` + ${setupCount} setup scope(s)` : ""}; outcomes ${Object.entries(testOutcomes).filter(([, count]) => count > 0).map(([outcome, count]) => `${outcome}=${count}`).join(", ") || "none"}; ${gaps.length} file(s) have unresolved coverage or measurement gaps`);
|
|
911
|
+
}
|
|
912
|
+
if (command === "minimize") {
|
|
913
|
+
const solverReport = selectedTestSet
|
|
914
|
+
? {
|
|
915
|
+
...report,
|
|
916
|
+
tests: report.tests.filter((test) => selectedTestSet.has(test.id)),
|
|
917
|
+
}
|
|
918
|
+
: report;
|
|
919
|
+
const minimized = minimumTestSet(solverReport, options.target, options.metric);
|
|
920
|
+
const selectedDetails = minimized.selected.map((id) => {
|
|
921
|
+
const test = report.tests.find((candidate) => candidate.id === id);
|
|
922
|
+
return {
|
|
923
|
+
id,
|
|
924
|
+
name: test.name,
|
|
925
|
+
file: test.file,
|
|
926
|
+
runner: test.provenance.runner,
|
|
927
|
+
kind: test.provenance.kind,
|
|
928
|
+
};
|
|
929
|
+
});
|
|
930
|
+
const selectedPage = page(selectedDetails, options);
|
|
931
|
+
const base = `${coverageCommand(run.id, options, "minimize")} --target ${options.target}${options.metric !== "all" ? ` --metric ${options.metric}` : ""}`;
|
|
932
|
+
const next = nextPageCommand(base, selectedDetails.length, selectedPage.length, options);
|
|
933
|
+
return output({
|
|
934
|
+
run: run.id,
|
|
935
|
+
filters: queryFilters(options),
|
|
936
|
+
...minimized,
|
|
937
|
+
selectedCount: selectedDetails.length,
|
|
938
|
+
totalCandidateTests: solverReport.tests.filter((test) => test.role === "test").length,
|
|
939
|
+
tests: selectedPage,
|
|
940
|
+
}, options, `exact minimum ${selectedDetails.length}/${solverReport.tests.filter((test) => test.role === "test").length} test(s) for ${options.target}% ${options.metric === "all" ? "coverage across all measured metrics" : options.metric}; explored ${minimized.exploredStates} state(s)\nlines ${pct(minimized.summary.lines.percentage)}, statements ${pct(minimized.summary.statements.percentage)}, functions ${pct(minimized.summary.functions.percentage)}, branches ${pct(minimized.summary.branches.percentage)}, MC/DC ${pct(minimized.summary.conditionCoveragePct)}\n${selectedPage.map((test) => `${test.id} ${test.kind}/${test.runner} ${test.file ?? "unknown"} ${test.name}`).join("\n")}\n${pageLabel(selectedDetails.length, selectedPage.length, options)}${next ? `\nnext page: ${next}` : ""}`, queryPagination(selectedDetails.length, selectedPage.length, options));
|
|
941
|
+
}
|
|
942
|
+
if (command === "scope") {
|
|
943
|
+
if (!report.scope)
|
|
944
|
+
throw new SupercovError("SCOPE_UNAVAILABLE", "This run does not contain a source-scope inventory.");
|
|
945
|
+
const limitationsByFile = new Map();
|
|
946
|
+
for (const limitation of report.limitations ?? []) {
|
|
947
|
+
const existing = limitationsByFile.get(limitation.file) ?? [];
|
|
948
|
+
existing.push(limitation);
|
|
949
|
+
limitationsByFile.set(limitation.file, existing);
|
|
950
|
+
}
|
|
951
|
+
const ordered = report.scope.entries.map((entry) => {
|
|
952
|
+
const limitations = limitationsByFile.get(entry.file) ?? [];
|
|
953
|
+
return {
|
|
954
|
+
...entry,
|
|
955
|
+
measurementLimitations: limitations.length,
|
|
956
|
+
limitationKinds: [...new Set(limitations.map((item) => item.kind))].sort(),
|
|
957
|
+
};
|
|
958
|
+
}).sort((left, right) => {
|
|
959
|
+
const rank = { ambiguous: 0, included: 1, excluded: 2 };
|
|
960
|
+
return rank[left.status] - rank[right.status] || left.file.localeCompare(right.file);
|
|
961
|
+
});
|
|
962
|
+
const selectedEntries = page(ordered, options);
|
|
963
|
+
const base = coverageCommand(run.id, options, "scope");
|
|
964
|
+
const next = nextPageCommand(base, ordered.length, selectedEntries.length, options);
|
|
965
|
+
const counts = {
|
|
966
|
+
included: ordered.filter((entry) => entry.status === "included").length,
|
|
967
|
+
excluded: ordered.filter((entry) => entry.status === "excluded").length,
|
|
968
|
+
ambiguous: ordered.filter((entry) => entry.status === "ambiguous").length,
|
|
969
|
+
};
|
|
970
|
+
return output({
|
|
971
|
+
run: run.id,
|
|
972
|
+
filters: queryFilters(options),
|
|
973
|
+
mode: report.scope.mode,
|
|
974
|
+
roots: report.scope.roots,
|
|
975
|
+
counts,
|
|
976
|
+
measurement: coverageMeasurement(report),
|
|
977
|
+
entries: selectedEntries,
|
|
978
|
+
}, options, `mode ${report.scope.mode}; roots ${report.scope.roots.join(", ") || "none"}; included ${counts.included}, excluded ${counts.excluded}, ambiguous ${counts.ambiguous}; measurement ${coverageMeasurement(report).complete ? "complete" : `${coverageMeasurement(report).blocking} blocking limitation(s)`}\n${selectedEntries.map((entry) => `${entry.status.toUpperCase()} ${entry.file} ${entry.reason}${entry.measurementLimitations ? ` [measurement limitations: ${entry.measurementLimitations} ${entry.limitationKinds.join(", ")}]` : ""}${entry.packageRoot ? ` [package ${entry.packageRoot}]` : ""}`).join("\n")}\n${pageLabel(ordered.length, selectedEntries.length, options)}${next ? `\nnext page: ${next}` : ""}`, queryPagination(ordered.length, selectedEntries.length, options));
|
|
596
979
|
}
|
|
597
980
|
if (command === "kinds" || command === "runners") {
|
|
598
981
|
const dimension = command === "kinds" ? report.coverageByKind : report.coverageByRunner;
|
|
@@ -600,8 +983,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
600
983
|
const dimensionNext = nextPageCommand(coverageCommand(run.id, options, command), dimension.length, selectedDimension.length, options);
|
|
601
984
|
return output({
|
|
602
985
|
run: run.id,
|
|
603
|
-
|
|
604
|
-
offset: options.offset,
|
|
986
|
+
filters: queryFilters(options),
|
|
605
987
|
[command]: selectedDimension,
|
|
606
988
|
}, options, selectedDimension
|
|
607
989
|
.map((entry) => {
|
|
@@ -610,7 +992,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
610
992
|
})
|
|
611
993
|
.join("\n") +
|
|
612
994
|
`\n${pageLabel(dimension.length, selectedDimension.length, options)}` +
|
|
613
|
-
(dimensionNext ? `\nnext page: ${dimensionNext}` : ""));
|
|
995
|
+
(dimensionNext ? `\nnext page: ${dimensionNext}` : ""), queryPagination(dimension.length, selectedDimension.length, options));
|
|
614
996
|
}
|
|
615
997
|
if (command === "files" || command === "gaps") {
|
|
616
998
|
const files = fileGaps(report, selectedTestSet);
|
|
@@ -624,25 +1006,31 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
624
1006
|
: undefined;
|
|
625
1007
|
return output({
|
|
626
1008
|
run: run.id,
|
|
627
|
-
|
|
628
|
-
total: all.length,
|
|
629
|
-
offset: options.offset,
|
|
1009
|
+
filters: queryFilters(options),
|
|
630
1010
|
[command]: selectedFiles,
|
|
631
1011
|
}, options, selectedFiles
|
|
632
1012
|
.map((gap) => {
|
|
633
|
-
const
|
|
634
|
-
|
|
1013
|
+
const missing = gap.uncoveredLines +
|
|
1014
|
+
gap.uncoveredStatements +
|
|
1015
|
+
gap.uncoveredFunctions +
|
|
1016
|
+
gap.missingBranches +
|
|
1017
|
+
gap.missingMcdcConditions;
|
|
1018
|
+
const status = missing === 0
|
|
1019
|
+
? "coverage complete"
|
|
635
1020
|
: `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}`;
|
|
636
|
-
|
|
1021
|
+
const limitations = gap.measurementLimitations
|
|
1022
|
+
? ` measurement limitations ${gap.measurementLimitations} (${gap.limitationKinds.join(", ")})`
|
|
1023
|
+
: "";
|
|
1024
|
+
return `${gap.file} ${status}${limitations}${selectedTestSet ? ` [covered elsewhere: ${Object.values(gap.coveredByOtherTests).reduce((sum, value) => sum + value, 0)}; nowhere: ${Object.values(gap.uncoveredEverywhere).reduce((sum, value) => sum + value, 0)}]` : ""}`;
|
|
637
1025
|
})
|
|
638
1026
|
.join("\n") +
|
|
639
1027
|
`\nshowing ${pageStart}-${pageEnd} of ${all.length}` +
|
|
640
|
-
(nextCommand ? `\nnext page: ${nextCommand}` : ""));
|
|
1028
|
+
(nextCommand ? `\nnext page: ${nextCommand}` : ""), queryPagination(all.length, selectedFiles.length, options));
|
|
641
1029
|
}
|
|
642
1030
|
if (command === "file") {
|
|
643
1031
|
const selector = options.positional.join(" ");
|
|
644
1032
|
if (!selector)
|
|
645
|
-
throw new
|
|
1033
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage file <source-file>");
|
|
646
1034
|
const file = findFile(report, selector);
|
|
647
1035
|
const uncoveredLines = report.lines
|
|
648
1036
|
.filter((line) => line.file === file &&
|
|
@@ -707,6 +1095,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
707
1095
|
...branches,
|
|
708
1096
|
...mcdc,
|
|
709
1097
|
].sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
|
|
1098
|
+
const allFileLimitations = (report.limitations ?? [])
|
|
1099
|
+
.filter((limitation) => limitation.file === file)
|
|
1100
|
+
.map((limitation) => ({
|
|
1101
|
+
...limitation,
|
|
1102
|
+
blocking: true,
|
|
1103
|
+
effect: "outside-measured-denominator",
|
|
1104
|
+
}))
|
|
1105
|
+
.sort((left, right) => left.line - right.line ||
|
|
1106
|
+
left.column - right.column ||
|
|
1107
|
+
left.id.localeCompare(right.id));
|
|
710
1108
|
const allFileTests = report.tests
|
|
711
1109
|
.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
|
|
712
1110
|
test.lines.some((line) => line.file === file))
|
|
@@ -717,15 +1115,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
717
1115
|
}));
|
|
718
1116
|
const tests = page(allFileTests, options);
|
|
719
1117
|
const selected = page(obligations, options);
|
|
720
|
-
const
|
|
721
|
-
const
|
|
1118
|
+
const limitations = page(allFileLimitations, options);
|
|
1119
|
+
const filePageTotal = Math.max(obligations.length, allFileTests.length, allFileLimitations.length);
|
|
1120
|
+
const filePageReturned = Math.max(selected.length, tests.length, limitations.length);
|
|
722
1121
|
const nextFileOffset = options.offset + filePageReturned;
|
|
723
1122
|
const nextFileCommand = filePageReturned > 0 && nextFileOffset < filePageTotal
|
|
724
1123
|
? `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --offset ${nextFileOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
|
|
725
1124
|
: undefined;
|
|
726
1125
|
const result = {
|
|
727
1126
|
run: run.id,
|
|
728
|
-
|
|
1127
|
+
filters: queryFilters(options),
|
|
729
1128
|
file,
|
|
730
1129
|
counts: {
|
|
731
1130
|
uncoveredLines: uncoveredLines.length,
|
|
@@ -733,14 +1132,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
733
1132
|
uncoveredFunctions: functions.length,
|
|
734
1133
|
missingBranches: branches.length,
|
|
735
1134
|
missingMcdcConditions: mcdc.length,
|
|
1135
|
+
measurementLimitations: allFileLimitations.length,
|
|
736
1136
|
},
|
|
737
1137
|
tests,
|
|
738
1138
|
totalTests: allFileTests.length,
|
|
739
1139
|
totalObligations: obligations.length,
|
|
740
|
-
offset: options.offset,
|
|
741
1140
|
obligations: selected,
|
|
1141
|
+
totalLimitations: allFileLimitations.length,
|
|
1142
|
+
limitations,
|
|
742
1143
|
};
|
|
743
|
-
return output(result, options, `${file}\nlines ${uncoveredLines.length}, statements ${statements.length}, functions ${functions.length}, branches ${branches.length}, MC/DC ${mcdc.length}\ncovered by ${allFileTests.length} test(s)\n${selected
|
|
1144
|
+
return output(result, options, `${file}\nlines ${uncoveredLines.length}, statements ${statements.length}, functions ${functions.length}, branches ${branches.length}, MC/DC ${mcdc.length}, measurement limitations ${allFileLimitations.length}\ncovered by ${allFileTests.length} test(s)\n${selected
|
|
744
1145
|
.map((item) => item.kind === "line"
|
|
745
1146
|
? `line ${item.line}: ${item.otherCoverage.coveredElsewhere ? `covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}` : "uncovered everywhere"}`
|
|
746
1147
|
: item.kind === "statement"
|
|
@@ -750,12 +1151,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
750
1151
|
: item.kind === "branch"
|
|
751
1152
|
? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
|
|
752
1153
|
: `MC/DC ${item.line}:${item.column} [${item.id}]: ${item.missingCondition}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`)
|
|
753
|
-
.join("\n")}\n${pageLabel(filePageTotal, filePageReturned, options)} obligations/tests${nextFileCommand ? `\nnext page: ${nextFileCommand}` : ""}
|
|
1154
|
+
.join("\n")}${selected.length && limitations.length ? "\n" : ""}${limitations.map((limitation) => `LIMITATION ${limitation.kind} ${limitation.line}:${limitation.column} [${limitation.id}]\n ${limitation.reason}\n source: ${limitation.source}\n effect: outside measured denominator`).join("\n")}\n${pageLabel(filePageTotal, filePageReturned, options)} obligations/tests/limitations per category${nextFileCommand ? `\nnext page: ${nextFileCommand}` : ""}`, queryPagination(filePageTotal, filePageReturned, options));
|
|
754
1155
|
}
|
|
755
1156
|
if (command === "decision") {
|
|
756
1157
|
const selector = options.positional[0];
|
|
757
1158
|
if (!selector)
|
|
758
|
-
throw new
|
|
1159
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
|
|
759
1160
|
let matches = report.decisions.filter((decision) => decision.meta.id === selector);
|
|
760
1161
|
if (matches.length === 0 && /:\d+(?::\d+)?$/.test(selector)) {
|
|
761
1162
|
const location = locationSelector(selector);
|
|
@@ -763,7 +1164,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
763
1164
|
decision.meta.line === location.line);
|
|
764
1165
|
}
|
|
765
1166
|
if (matches.length === 0)
|
|
766
|
-
throw new
|
|
1167
|
+
throw new SupercovError("DECISION_NOT_FOUND", `Decision not found: ${selector}`, {
|
|
1168
|
+
details: { selector },
|
|
1169
|
+
});
|
|
767
1170
|
if (matches.length > 1) {
|
|
768
1171
|
const matchingDecisions = page(matches, options).map((decision) => ({
|
|
769
1172
|
id: decision.meta.id,
|
|
@@ -775,10 +1178,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
775
1178
|
const matchesNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, matches.length, matchingDecisions.length, options);
|
|
776
1179
|
return output({
|
|
777
1180
|
run: run.id,
|
|
778
|
-
|
|
779
|
-
offset: options.offset,
|
|
1181
|
+
filters: queryFilters(options),
|
|
780
1182
|
decisions: matchingDecisions,
|
|
781
|
-
}, options, `${matchingDecisions.map((decision) => `${decision.id} ${decision.file}:${decision.line}:${decision.column} ${decision.source}`).join("\n")}\n${pageLabel(matches.length, matchingDecisions.length, options)} matching decisions${matchesNext ? `\nnext page: ${matchesNext}` : ""}
|
|
1183
|
+
}, options, `${matchingDecisions.map((decision) => `${decision.id} ${decision.file}:${decision.line}:${decision.column} ${decision.source}`).join("\n")}\n${pageLabel(matches.length, matchingDecisions.length, options)} matching decisions${matchesNext ? `\nnext page: ${matchesNext}` : ""}`, queryPagination(matches.length, matchingDecisions.length, options));
|
|
782
1184
|
}
|
|
783
1185
|
matches = matches.map((decision) => filterDecision(decision, selectedTestSet));
|
|
784
1186
|
const totalDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
|
|
@@ -796,7 +1198,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
796
1198
|
const decisionNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, totalDecisionEvidence, returnedDecisionEvidence, options);
|
|
797
1199
|
const result = {
|
|
798
1200
|
run: run.id,
|
|
799
|
-
|
|
1201
|
+
filters: queryFilters(options),
|
|
800
1202
|
decisions: matches,
|
|
801
1203
|
};
|
|
802
1204
|
return output(result, options, matches
|
|
@@ -805,12 +1207,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
805
1207
|
.join("\n")}\nconfidence ${decision.confidence?.level ?? "unknown"}; asserted MC/DC ${decision.conditions.filter((condition) => condition.assertionCovered).length}/${decision.conditions.length}\nvectors:\n${decision.vectorObservations.map((observation) => ` ${vectorText(observation.vector.values, observation.vector.outcome)} tests=${observation.tests.length} confidence=${observation.confidence?.level ?? "unknown"}`).join("\n") || " none"}`)
|
|
806
1208
|
.join("\n\n") +
|
|
807
1209
|
`\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
|
|
808
|
-
(decisionNext ? `\nnext page: ${decisionNext}` : ""));
|
|
1210
|
+
(decisionNext ? `\nnext page: ${decisionNext}` : ""), queryPagination(totalDecisionEvidence, returnedDecisionEvidence, options));
|
|
809
1211
|
}
|
|
810
1212
|
if (command === "covers") {
|
|
811
1213
|
const selector = options.positional[0];
|
|
812
1214
|
if (!selector)
|
|
813
|
-
throw new
|
|
1215
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage covers <source-file:line>");
|
|
814
1216
|
const location = locationSelector(selector);
|
|
815
1217
|
const line = report.lines.find((candidate) => candidate.file === location.file && candidate.line === location.line);
|
|
816
1218
|
const allTests = (line?.tests ?? [])
|
|
@@ -843,7 +1245,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
843
1245
|
const coversNext = nextPageCommand(`${coverageCommand(run.id, options, "covers")} ${shellQuote(selector)}`, coversTotal, coversReturned, options);
|
|
844
1246
|
const result = {
|
|
845
1247
|
run: run.id,
|
|
846
|
-
|
|
1248
|
+
filters: queryFilters(options),
|
|
847
1249
|
location,
|
|
848
1250
|
covered: includesSelectedTest(line?.tests ?? [], selectedTestSet),
|
|
849
1251
|
confidence: line?.confidence,
|
|
@@ -852,16 +1254,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
852
1254
|
tests,
|
|
853
1255
|
phases,
|
|
854
1256
|
};
|
|
855
|
-
return output(result, options, `${location.file}:${location.line} ${result.covered ? "covered" : "uncovered"}; confidence ${result.confidence?.level ?? "unknown"}${result.confidence?.e2e ? "; E2E-covered" : ""}\n${tests.map((test) => `test: ${test.name} [${test.id}] (${test.provenance?.kind ?? "unknown"}/${test.provenance?.runner ?? "unknown"})`).join("\n") || "no covering tests"}\n${phases.map((phase) => `phase: ${phase.operation}${phase.status ? ` (${phase.status})` : ""}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(coversTotal, coversReturned, options)} tests/phases${coversNext ? `\nnext page: ${coversNext}` : ""}
|
|
1257
|
+
return output(result, options, `${location.file}:${location.line} ${result.covered ? "covered" : "uncovered"}; confidence ${result.confidence?.level ?? "unknown"}${result.confidence?.e2e ? "; E2E-covered" : ""}\n${tests.map((test) => `test: ${test.name} [${test.id}] (${test.provenance?.kind ?? "unknown"}/${test.provenance?.runner ?? "unknown"})`).join("\n") || "no covering tests"}\n${phases.map((phase) => `phase: ${phase.operation}${phase.status ? ` (${phase.status})` : ""}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(coversTotal, coversReturned, options)} tests/phases${coversNext ? `\nnext page: ${coversNext}` : ""}`, queryPagination(coversTotal, coversReturned, options));
|
|
856
1258
|
}
|
|
857
1259
|
if (command === "test") {
|
|
858
1260
|
const selector = options.positional.join(" ").toLowerCase();
|
|
859
1261
|
if (!selector)
|
|
860
|
-
throw new
|
|
1262
|
+
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage test <id|name-fragment>");
|
|
861
1263
|
const matches = report.tests.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
|
|
862
1264
|
(test.id === selector || test.name.toLowerCase().includes(selector)));
|
|
863
1265
|
if (matches.length === 0)
|
|
864
|
-
throw new
|
|
1266
|
+
throw new SupercovError("TEST_NOT_FOUND", `Test not found: ${selector}`, {
|
|
1267
|
+
details: { selector },
|
|
1268
|
+
});
|
|
865
1269
|
const testBase = `${coverageCommand(run.id, options, "test")} ${shellQuote(options.positional.join(" "))}`;
|
|
866
1270
|
if (matches.length > 1) {
|
|
867
1271
|
const matchingTests = page(matches, options).map((test) => ({
|
|
@@ -873,10 +1277,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
873
1277
|
const matchesNext = nextPageCommand(testBase, matches.length, matchingTests.length, options);
|
|
874
1278
|
return output({
|
|
875
1279
|
run: run.id,
|
|
876
|
-
|
|
877
|
-
offset: options.offset,
|
|
1280
|
+
filters: queryFilters(options),
|
|
878
1281
|
tests: matchingTests,
|
|
879
|
-
}, options, `${matchingTests.map((test) => `${test.name} [${test.id}] — ${test.outcome}`).join("\n")}\n${pageLabel(matches.length, matchingTests.length, options)} matching tests${matchesNext ? `\nnext page: ${matchesNext}` : ""}
|
|
1282
|
+
}, options, `${matchingTests.map((test) => `${test.name} [${test.id}] — ${test.outcome}`).join("\n")}\n${pageLabel(matches.length, matchingTests.length, options)} matching tests${matchesNext ? `\nnext page: ${matchesNext}` : ""}`, queryPagination(matches.length, matchingTests.length, options));
|
|
880
1283
|
}
|
|
881
1284
|
const test = matches[0];
|
|
882
1285
|
const allPhases = report.phases
|
|
@@ -909,11 +1312,11 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
909
1312
|
const testNext = nextPageCommand(testBase, testTotal, testReturned, options);
|
|
910
1313
|
return output({
|
|
911
1314
|
run: run.id,
|
|
912
|
-
|
|
1315
|
+
filters: queryFilters(options),
|
|
913
1316
|
tests: [selected],
|
|
914
|
-
}, options, `${selected.name}\noutcome ${selected.outcome}${selected.attempts.length ? `; ${selected.attempts.map((attempt) => `retry ${attempt.retry}=${attempt.status}`).join(", ")}` : ""}\n${selected.totals.lines} lines, ${selected.totals.hits} hits, ${selected.totals.decisions} decisions, ${selected.totals.phases} phases\n${selected.lines.map((line) => `line: ${line.file}:${line.line}`).join("\n")}${selected.lines.length && selected.phases.length ? "\n" : ""}${selected.phases.map((phase) => `${phase.kind}: ${phase.operation}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(testTotal, testReturned, options)} per evidence category${testNext ? `\nnext page: ${testNext}` : ""}
|
|
1317
|
+
}, options, `${selected.name}\noutcome ${selected.outcome}${selected.attempts.length ? `; ${selected.attempts.map((attempt) => `retry ${attempt.retry}=${attempt.status}`).join(", ")}` : ""}\n${selected.totals.lines} lines, ${selected.totals.hits} hits, ${selected.totals.decisions} decisions, ${selected.totals.phases} phases\n${selected.lines.map((line) => `line: ${line.file}:${line.line}`).join("\n")}${selected.lines.length && selected.phases.length ? "\n" : ""}${selected.phases.map((phase) => `${phase.kind}: ${phase.operation}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(testTotal, testReturned, options)} per evidence category${testNext ? `\nnext page: ${testNext}` : ""}`, queryPagination(testTotal, testReturned, options));
|
|
915
1318
|
}
|
|
916
|
-
throw new
|
|
1319
|
+
throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${command}. Try supercov help.`, { details: { command } });
|
|
917
1320
|
}
|
|
918
1321
|
export const coverageQueryCommands = new Set(["help", "runs", "diff"]);
|
|
919
1322
|
//# sourceMappingURL=query.js.map
|