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