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/analyze.ts
ADDED
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CoverageCount,
|
|
3
|
+
CoverageConfidence,
|
|
4
|
+
CoverageSummary,
|
|
5
|
+
CoverageManifest,
|
|
6
|
+
CoveragePhase,
|
|
7
|
+
CoverageRuntimeEvent,
|
|
8
|
+
McdcDecisionResult,
|
|
9
|
+
McdcDecisionSnapshot,
|
|
10
|
+
McdcRawTestResult,
|
|
11
|
+
McdcReport,
|
|
12
|
+
McdcVector,
|
|
13
|
+
TestAttemptResult,
|
|
14
|
+
TestOutcome,
|
|
15
|
+
TestProvenance,
|
|
16
|
+
} from "./types.ts";
|
|
17
|
+
|
|
18
|
+
interface MutableTestCoverage {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
file?: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
retries: Set<number>;
|
|
24
|
+
attempts: Map<number, TestAttemptResult>;
|
|
25
|
+
runnerReportedFlaky: boolean;
|
|
26
|
+
provenance: TestProvenance;
|
|
27
|
+
role: "test" | "setup" | "background";
|
|
28
|
+
hits: Set<string>;
|
|
29
|
+
decisions: Map<string, Map<string, McdcVector>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MutablePhaseCoverage {
|
|
33
|
+
phase: CoveragePhase;
|
|
34
|
+
test: string;
|
|
35
|
+
hits: Set<string>;
|
|
36
|
+
decisions: Map<string, Map<string, McdcVector>>;
|
|
37
|
+
browserEvents: number;
|
|
38
|
+
serverEvents: number;
|
|
39
|
+
explicitEvents: number;
|
|
40
|
+
inferredEvents: number;
|
|
41
|
+
explicitBrowserEvents: number;
|
|
42
|
+
inferredBrowserEvents: number;
|
|
43
|
+
explicitServerEvents: number;
|
|
44
|
+
inferredServerEvents: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function vectorKey(vector: McdcVector): string {
|
|
48
|
+
return (
|
|
49
|
+
vector.values
|
|
50
|
+
.map((value) => (value === null ? "-" : value ? "T" : "F"))
|
|
51
|
+
.join("") +
|
|
52
|
+
":" +
|
|
53
|
+
(vector.outcome ? "T" : "F")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function percentage(covered: number, total: number): number {
|
|
58
|
+
return total === 0 ? 100 : Number(((covered / total) * 100).toFixed(2));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function count(covered: number, total: number): CoverageCount {
|
|
62
|
+
return { covered, total, percentage: percentage(covered, total) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function testOutcome(test: MutableTestCoverage): TestOutcome {
|
|
66
|
+
const attempts = [...test.attempts.values()].sort(
|
|
67
|
+
(left, right) => left.retry - right.retry,
|
|
68
|
+
);
|
|
69
|
+
const terminal = attempts.at(-1);
|
|
70
|
+
if (!terminal) return "unknown";
|
|
71
|
+
if (
|
|
72
|
+
terminal.status === "passed" &&
|
|
73
|
+
(test.runnerReportedFlaky ||
|
|
74
|
+
attempts.slice(0, -1).some((attempt) => attempt.status !== "passed"))
|
|
75
|
+
) {
|
|
76
|
+
return "flaky";
|
|
77
|
+
}
|
|
78
|
+
return terminal.status;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function recordAttempt(
|
|
82
|
+
test: MutableTestCoverage,
|
|
83
|
+
raw: McdcRawTestResult,
|
|
84
|
+
): void {
|
|
85
|
+
if (raw.retry === undefined || !raw.status) return;
|
|
86
|
+
const previous = test.attempts.get(raw.retry);
|
|
87
|
+
const status =
|
|
88
|
+
raw.status === "unknown" && previous ? previous.status : raw.status;
|
|
89
|
+
const expectedStatus = raw.expectedStatus ?? previous?.expectedStatus;
|
|
90
|
+
test.attempts.set(raw.retry, {
|
|
91
|
+
retry: raw.retry,
|
|
92
|
+
status,
|
|
93
|
+
...(expectedStatus ? { expectedStatus } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Masking MC/DC pair test for short-circuit languages.
|
|
99
|
+
*
|
|
100
|
+
* The target condition must change and change the decision. Every other
|
|
101
|
+
* condition must either retain its value or be unevaluated in at least one
|
|
102
|
+
* vector (and therefore masked by the short-circuit path).
|
|
103
|
+
*/
|
|
104
|
+
export function isIndependencePair(
|
|
105
|
+
first: McdcVector,
|
|
106
|
+
second: McdcVector,
|
|
107
|
+
conditionIndex: number,
|
|
108
|
+
): boolean {
|
|
109
|
+
const firstTarget = first.values[conditionIndex];
|
|
110
|
+
const secondTarget = second.values[conditionIndex];
|
|
111
|
+
if (
|
|
112
|
+
firstTarget === null ||
|
|
113
|
+
secondTarget === null ||
|
|
114
|
+
firstTarget === secondTarget ||
|
|
115
|
+
first.outcome === second.outcome
|
|
116
|
+
) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (let index = 0; index < first.values.length; index += 1) {
|
|
121
|
+
if (index === conditionIndex) continue;
|
|
122
|
+
const left = first.values[index];
|
|
123
|
+
const right = second.values[index];
|
|
124
|
+
if (left !== null && right !== null && left !== right) return false;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function summarizeCoverage(
|
|
130
|
+
decisions: McdcDecisionResult[],
|
|
131
|
+
points: McdcReport["points"],
|
|
132
|
+
branches: McdcReport["branches"],
|
|
133
|
+
lines: McdcReport["lines"],
|
|
134
|
+
): CoverageSummary {
|
|
135
|
+
const conditions = decisions.flatMap((decision) => decision.conditions);
|
|
136
|
+
const coveredConditions = conditions.filter(
|
|
137
|
+
(condition) => condition.covered,
|
|
138
|
+
).length;
|
|
139
|
+
const statements = points.filter((point) => point.meta.kind === "statement");
|
|
140
|
+
const functions = points.filter((point) => point.meta.kind === "function");
|
|
141
|
+
const decisionAlternativeTotal = decisions.length * 2;
|
|
142
|
+
const decisionAlternativeCovered = decisions.reduce(
|
|
143
|
+
(total, decision) =>
|
|
144
|
+
total +
|
|
145
|
+
Number(decision.vectors.some((vector) => vector.outcome === false)) +
|
|
146
|
+
Number(decision.vectors.some((vector) => vector.outcome === true)),
|
|
147
|
+
0,
|
|
148
|
+
);
|
|
149
|
+
const conditionOutcomeTotal = conditions.length * 2;
|
|
150
|
+
const conditionOutcomeCovered = decisions.reduce(
|
|
151
|
+
(total, decision) =>
|
|
152
|
+
total +
|
|
153
|
+
decision.conditions.reduce(
|
|
154
|
+
(conditionTotal, condition) =>
|
|
155
|
+
conditionTotal +
|
|
156
|
+
Number(
|
|
157
|
+
decision.vectors.some(
|
|
158
|
+
(vector) => vector.values[condition.index] === false,
|
|
159
|
+
),
|
|
160
|
+
) +
|
|
161
|
+
Number(
|
|
162
|
+
decision.vectors.some(
|
|
163
|
+
(vector) => vector.values[condition.index] === true,
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
0,
|
|
167
|
+
),
|
|
168
|
+
0,
|
|
169
|
+
);
|
|
170
|
+
const genericAlternativeTotal = branches.reduce(
|
|
171
|
+
(total, branch) => total + branch.alternatives.length,
|
|
172
|
+
0,
|
|
173
|
+
);
|
|
174
|
+
const genericAlternativeCovered = branches.reduce(
|
|
175
|
+
(total, branch) =>
|
|
176
|
+
total +
|
|
177
|
+
branch.alternatives.filter((alternative) => alternative.covered).length,
|
|
178
|
+
0,
|
|
179
|
+
);
|
|
180
|
+
const valueBranches = branches.filter(
|
|
181
|
+
(branch) => branch.meta.kind === "logical-value",
|
|
182
|
+
);
|
|
183
|
+
const valueAlternativeTotal = valueBranches.reduce(
|
|
184
|
+
(total, branch) => total + branch.alternatives.length,
|
|
185
|
+
0,
|
|
186
|
+
);
|
|
187
|
+
const valueAlternativeCovered = valueBranches.reduce(
|
|
188
|
+
(total, branch) =>
|
|
189
|
+
total +
|
|
190
|
+
branch.alternatives.filter((alternative) => alternative.covered).length,
|
|
191
|
+
0,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const summary: CoverageSummary = {
|
|
195
|
+
decisions: decisions.length,
|
|
196
|
+
executedDecisions: decisions.filter((decision) => decision.executed).length,
|
|
197
|
+
coveredDecisions: decisions.filter((decision) => decision.covered).length,
|
|
198
|
+
conditions: conditions.length,
|
|
199
|
+
coveredConditions,
|
|
200
|
+
conditionCoveragePct: percentage(coveredConditions, conditions.length),
|
|
201
|
+
lines: count(lines.filter((line) => line.covered).length, lines.length),
|
|
202
|
+
statements: count(
|
|
203
|
+
statements.filter((point) => point.covered).length,
|
|
204
|
+
statements.length,
|
|
205
|
+
),
|
|
206
|
+
functions: count(
|
|
207
|
+
functions.filter((point) => point.covered).length,
|
|
208
|
+
functions.length,
|
|
209
|
+
),
|
|
210
|
+
branches: count(
|
|
211
|
+
decisionAlternativeCovered + genericAlternativeCovered,
|
|
212
|
+
decisionAlternativeTotal + genericAlternativeTotal,
|
|
213
|
+
),
|
|
214
|
+
decisionOutcomes: count(
|
|
215
|
+
decisionAlternativeCovered,
|
|
216
|
+
decisionAlternativeTotal,
|
|
217
|
+
),
|
|
218
|
+
conditionOutcomes: count(conditionOutcomeCovered, conditionOutcomeTotal),
|
|
219
|
+
valueSelections: count(valueAlternativeCovered, valueAlternativeTotal),
|
|
220
|
+
coverageComplete: false,
|
|
221
|
+
};
|
|
222
|
+
summary.coverageComplete =
|
|
223
|
+
summary.lines.percentage === 100 &&
|
|
224
|
+
summary.statements.percentage === 100 &&
|
|
225
|
+
summary.functions.percentage === 100 &&
|
|
226
|
+
summary.branches.percentage === 100 &&
|
|
227
|
+
summary.conditionOutcomes.percentage === 100 &&
|
|
228
|
+
summary.conditionCoveragePct === 100;
|
|
229
|
+
return summary;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function coverageSummaryForTestIds(
|
|
233
|
+
decisions: McdcReport["decisions"],
|
|
234
|
+
points: McdcReport["points"],
|
|
235
|
+
branches: McdcReport["branches"],
|
|
236
|
+
lines: McdcReport["lines"],
|
|
237
|
+
testIds: Set<string>,
|
|
238
|
+
): CoverageSummary {
|
|
239
|
+
const includesTest = (tests: string[]): boolean =>
|
|
240
|
+
tests.some((test) => testIds.has(test));
|
|
241
|
+
const filteredDecisions = decisions.map((decision) => {
|
|
242
|
+
const vectorObservations = decision.vectorObservations.filter(
|
|
243
|
+
(observation) => includesTest(observation.tests),
|
|
244
|
+
);
|
|
245
|
+
const vectors = vectorObservations.map((observation) => observation.vector);
|
|
246
|
+
const conditions = decision.meta.conditions.map((source, index) => ({
|
|
247
|
+
index,
|
|
248
|
+
source,
|
|
249
|
+
covered: Boolean(findWitness(vectors, index)),
|
|
250
|
+
}));
|
|
251
|
+
return {
|
|
252
|
+
...decision,
|
|
253
|
+
executed: vectors.length > 0,
|
|
254
|
+
covered: conditions.every((condition) => condition.covered),
|
|
255
|
+
vectors,
|
|
256
|
+
vectorObservations,
|
|
257
|
+
conditions,
|
|
258
|
+
tests: decision.tests.filter((test) => testIds.has(test)),
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
const filteredPoints = points.map((point) => ({
|
|
262
|
+
...point,
|
|
263
|
+
covered: includesTest(point.tests),
|
|
264
|
+
tests: point.tests.filter((test) => testIds.has(test)),
|
|
265
|
+
}));
|
|
266
|
+
const filteredBranches = branches.map((branch) => {
|
|
267
|
+
const alternatives = branch.alternatives.map((alternative) => ({
|
|
268
|
+
...alternative,
|
|
269
|
+
covered: includesTest(alternative.tests),
|
|
270
|
+
tests: alternative.tests.filter((test) => testIds.has(test)),
|
|
271
|
+
}));
|
|
272
|
+
return {
|
|
273
|
+
...branch,
|
|
274
|
+
covered: alternatives.every((alternative) => alternative.covered),
|
|
275
|
+
alternatives,
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
const filteredLines = lines.map((line) => ({
|
|
279
|
+
...line,
|
|
280
|
+
covered: includesTest(line.tests),
|
|
281
|
+
tests: line.tests.filter((test) => testIds.has(test)),
|
|
282
|
+
}));
|
|
283
|
+
return summarizeCoverage(
|
|
284
|
+
filteredDecisions,
|
|
285
|
+
filteredPoints,
|
|
286
|
+
filteredBranches,
|
|
287
|
+
filteredLines,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function coverageSummaryForTests(
|
|
292
|
+
report: McdcReport,
|
|
293
|
+
testIds: Iterable<string>,
|
|
294
|
+
): CoverageSummary {
|
|
295
|
+
return coverageSummaryForTestIds(
|
|
296
|
+
report.decisions,
|
|
297
|
+
report.points,
|
|
298
|
+
report.branches,
|
|
299
|
+
report.lines,
|
|
300
|
+
new Set(testIds),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function findWitness(
|
|
305
|
+
vectors: McdcVector[],
|
|
306
|
+
conditionIndex: number,
|
|
307
|
+
): [McdcVector, McdcVector] | undefined {
|
|
308
|
+
for (let left = 0; left < vectors.length; left += 1) {
|
|
309
|
+
for (let right = left + 1; right < vectors.length; right += 1) {
|
|
310
|
+
const first = vectors[left];
|
|
311
|
+
const second = vectors[right];
|
|
312
|
+
if (
|
|
313
|
+
first &&
|
|
314
|
+
second &&
|
|
315
|
+
isIndependencePair(first, second, conditionIndex)
|
|
316
|
+
) {
|
|
317
|
+
return [first, second];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function addTest(
|
|
325
|
+
map: Map<string, Set<string>>,
|
|
326
|
+
id: string,
|
|
327
|
+
test: string,
|
|
328
|
+
): void {
|
|
329
|
+
const tests = map.get(id) ?? new Set<string>();
|
|
330
|
+
tests.add(test);
|
|
331
|
+
map.set(id, tests);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function createMcdcReport(
|
|
335
|
+
manifest: CoverageManifest,
|
|
336
|
+
rawResults: McdcRawTestResult[],
|
|
337
|
+
): McdcReport {
|
|
338
|
+
const decisionMetadata = new Map(
|
|
339
|
+
manifest.decisions.map((entry) => [entry.id, entry]),
|
|
340
|
+
);
|
|
341
|
+
const vectorsByDecision = new Map<
|
|
342
|
+
string,
|
|
343
|
+
Map<
|
|
344
|
+
string,
|
|
345
|
+
{
|
|
346
|
+
vector: McdcVector;
|
|
347
|
+
tests: Set<string>;
|
|
348
|
+
phases: Set<string>;
|
|
349
|
+
explicitPhases: Set<string>;
|
|
350
|
+
}
|
|
351
|
+
>
|
|
352
|
+
>();
|
|
353
|
+
const testsByDecision = new Map<string, Set<string>>();
|
|
354
|
+
const testsByHit = new Map<string, Set<string>>();
|
|
355
|
+
const testsById = new Map<string, MutableTestCoverage>();
|
|
356
|
+
const phasesById = new Map<string, MutablePhaseCoverage>();
|
|
357
|
+
const phasesByHit = new Map<string, Set<string>>();
|
|
358
|
+
const explicitPhasesByHit = new Map<string, Set<string>>();
|
|
359
|
+
const phasesByDecisionVector = new Map<string, Set<string>>();
|
|
360
|
+
|
|
361
|
+
const registerTest = (raw: McdcRawTestResult): MutableTestCoverage => {
|
|
362
|
+
const id = raw.testId ?? raw.test;
|
|
363
|
+
const existing = testsById.get(id);
|
|
364
|
+
if (existing) {
|
|
365
|
+
if (raw.retry !== undefined) existing.retries.add(raw.retry);
|
|
366
|
+
recordAttempt(existing, raw);
|
|
367
|
+
existing.runnerReportedFlaky ||= raw.flaky === true;
|
|
368
|
+
return existing;
|
|
369
|
+
}
|
|
370
|
+
const test: MutableTestCoverage = {
|
|
371
|
+
id,
|
|
372
|
+
name: raw.test,
|
|
373
|
+
...(raw.testFile ? { file: raw.testFile } : {}),
|
|
374
|
+
...(raw.title ? { title: raw.title } : {}),
|
|
375
|
+
retries: new Set(raw.retry === undefined ? [] : [raw.retry]),
|
|
376
|
+
attempts: new Map(
|
|
377
|
+
raw.retry === undefined || !raw.status
|
|
378
|
+
? []
|
|
379
|
+
: [
|
|
380
|
+
[
|
|
381
|
+
raw.retry,
|
|
382
|
+
{
|
|
383
|
+
retry: raw.retry,
|
|
384
|
+
status: raw.status,
|
|
385
|
+
...(raw.expectedStatus
|
|
386
|
+
? { expectedStatus: raw.expectedStatus }
|
|
387
|
+
: {}),
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
],
|
|
391
|
+
),
|
|
392
|
+
runnerReportedFlaky: raw.flaky === true,
|
|
393
|
+
provenance: raw.provenance ?? {
|
|
394
|
+
runner: "unknown",
|
|
395
|
+
kind: "unknown",
|
|
396
|
+
source: "unknown",
|
|
397
|
+
},
|
|
398
|
+
role: raw.role ?? "test",
|
|
399
|
+
hits: new Set(),
|
|
400
|
+
decisions: new Map(),
|
|
401
|
+
};
|
|
402
|
+
recordAttempt(test, raw);
|
|
403
|
+
testsById.set(id, test);
|
|
404
|
+
return test;
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const addSnapshot = (
|
|
408
|
+
snapshot: McdcDecisionSnapshot,
|
|
409
|
+
test: MutableTestCoverage,
|
|
410
|
+
): void => {
|
|
411
|
+
decisionMetadata.set(snapshot.meta.id, snapshot.meta);
|
|
412
|
+
const vectors =
|
|
413
|
+
vectorsByDecision.get(snapshot.meta.id) ??
|
|
414
|
+
new Map<
|
|
415
|
+
string,
|
|
416
|
+
{
|
|
417
|
+
vector: McdcVector;
|
|
418
|
+
tests: Set<string>;
|
|
419
|
+
phases: Set<string>;
|
|
420
|
+
explicitPhases: Set<string>;
|
|
421
|
+
}
|
|
422
|
+
>();
|
|
423
|
+
const testVectors =
|
|
424
|
+
test.decisions.get(snapshot.meta.id) ?? new Map<string, McdcVector>();
|
|
425
|
+
for (const vector of snapshot.vectors) {
|
|
426
|
+
const key = vectorKey(vector);
|
|
427
|
+
const observation = vectors.get(key) ?? {
|
|
428
|
+
vector,
|
|
429
|
+
tests: new Set<string>(),
|
|
430
|
+
phases: new Set<string>(),
|
|
431
|
+
explicitPhases: new Set<string>(),
|
|
432
|
+
};
|
|
433
|
+
observation.tests.add(test.id);
|
|
434
|
+
vectors.set(key, observation);
|
|
435
|
+
testVectors.set(key, vector);
|
|
436
|
+
}
|
|
437
|
+
vectorsByDecision.set(snapshot.meta.id, vectors);
|
|
438
|
+
test.decisions.set(snapshot.meta.id, testVectors);
|
|
439
|
+
if (snapshot.vectors.length > 0)
|
|
440
|
+
addTest(testsByDecision, snapshot.meta.id, test.id);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const addHit = (id: string, test: MutableTestCoverage): void => {
|
|
444
|
+
addTest(testsByHit, id, test.id);
|
|
445
|
+
test.hits.add(id);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const addPhaseReference = (
|
|
449
|
+
map: Map<string, Set<string>>,
|
|
450
|
+
id: string,
|
|
451
|
+
phaseId: string,
|
|
452
|
+
): void => {
|
|
453
|
+
const phases = map.get(id) ?? new Set<string>();
|
|
454
|
+
phases.add(phaseId);
|
|
455
|
+
map.set(id, phases);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const correlatePhase = (
|
|
459
|
+
phases: CoveragePhase[],
|
|
460
|
+
event: CoverageRuntimeEvent,
|
|
461
|
+
): string | undefined => {
|
|
462
|
+
if (event.phaseId) return event.phaseId;
|
|
463
|
+
let matched: CoveragePhase | undefined;
|
|
464
|
+
for (const phase of phases) {
|
|
465
|
+
if (phase.startedAtMs > event.timestampMs) break;
|
|
466
|
+
matched = phase;
|
|
467
|
+
}
|
|
468
|
+
return matched?.id;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const addPhaseEvent = (
|
|
472
|
+
raw: McdcRawTestResult,
|
|
473
|
+
event: CoverageRuntimeEvent,
|
|
474
|
+
): void => {
|
|
475
|
+
const explicit = Boolean(event.phaseId);
|
|
476
|
+
const phaseId = correlatePhase(raw.phases ?? [], event);
|
|
477
|
+
if (!phaseId) return;
|
|
478
|
+
const phase = phasesById.get(phaseId);
|
|
479
|
+
if (!phase) return;
|
|
480
|
+
if (event.environment === "browser") {
|
|
481
|
+
phase.browserEvents += 1;
|
|
482
|
+
if (explicit) phase.explicitBrowserEvents += 1;
|
|
483
|
+
else phase.inferredBrowserEvents += 1;
|
|
484
|
+
} else {
|
|
485
|
+
phase.serverEvents += 1;
|
|
486
|
+
if (explicit) phase.explicitServerEvents += 1;
|
|
487
|
+
else phase.inferredServerEvents += 1;
|
|
488
|
+
}
|
|
489
|
+
if (explicit) phase.explicitEvents += 1;
|
|
490
|
+
else phase.inferredEvents += 1;
|
|
491
|
+
if (event.type === "hit") {
|
|
492
|
+
phase.hits.add(event.id);
|
|
493
|
+
addPhaseReference(phasesByHit, event.id, phaseId);
|
|
494
|
+
if (explicit)
|
|
495
|
+
addPhaseReference(explicitPhasesByHit, event.id, phaseId);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const vectors =
|
|
499
|
+
phase.decisions.get(event.id) ?? new Map<string, McdcVector>();
|
|
500
|
+
vectors.set(vectorKey(event.vector), event.vector);
|
|
501
|
+
phase.decisions.set(event.id, vectors);
|
|
502
|
+
addPhaseReference(
|
|
503
|
+
phasesByDecisionVector,
|
|
504
|
+
`${event.id}:${vectorKey(event.vector)}`,
|
|
505
|
+
phaseId,
|
|
506
|
+
);
|
|
507
|
+
const observation = vectorsByDecision
|
|
508
|
+
.get(event.id)
|
|
509
|
+
?.get(vectorKey(event.vector));
|
|
510
|
+
observation?.phases.add(phaseId);
|
|
511
|
+
if (explicit) observation?.explicitPhases.add(phaseId);
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
for (const raw of rawResults) {
|
|
515
|
+
const test = registerTest(raw);
|
|
516
|
+
const orderedPhases = [...(raw.phases ?? [])].sort(
|
|
517
|
+
(left, right) => left.startedAtMs - right.startedAtMs,
|
|
518
|
+
);
|
|
519
|
+
raw.phases = orderedPhases;
|
|
520
|
+
for (const phase of orderedPhases) {
|
|
521
|
+
phasesById.set(phase.id, {
|
|
522
|
+
phase,
|
|
523
|
+
test: test.id,
|
|
524
|
+
hits: new Set(),
|
|
525
|
+
decisions: new Map(),
|
|
526
|
+
browserEvents: 0,
|
|
527
|
+
serverEvents: 0,
|
|
528
|
+
explicitEvents: 0,
|
|
529
|
+
inferredEvents: 0,
|
|
530
|
+
explicitBrowserEvents: 0,
|
|
531
|
+
inferredBrowserEvents: 0,
|
|
532
|
+
explicitServerEvents: 0,
|
|
533
|
+
inferredServerEvents: 0,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
for (const runtime of raw.runtime ?? []) {
|
|
537
|
+
for (const snapshot of runtime.decisions) addSnapshot(snapshot, test);
|
|
538
|
+
for (const id of runtime.hits) addHit(id, test);
|
|
539
|
+
for (const event of runtime.events ?? []) addPhaseEvent(raw, event);
|
|
540
|
+
}
|
|
541
|
+
for (const browser of raw.browser) {
|
|
542
|
+
for (const snapshot of browser.decisions) addSnapshot(snapshot, test);
|
|
543
|
+
for (const id of browser.hits) addHit(id, test);
|
|
544
|
+
for (const event of browser.events ?? []) addPhaseEvent(raw, event);
|
|
545
|
+
}
|
|
546
|
+
for (const record of raw.server) {
|
|
547
|
+
if (record.type === "decision") {
|
|
548
|
+
addSnapshot({ meta: record.meta, vectors: [record.vector] }, test);
|
|
549
|
+
} else {
|
|
550
|
+
addHit(record.id, test);
|
|
551
|
+
}
|
|
552
|
+
if (record.timestampMs !== undefined) {
|
|
553
|
+
addPhaseEvent(raw, {
|
|
554
|
+
...record,
|
|
555
|
+
id: record.type === "decision" ? record.meta.id : record.id,
|
|
556
|
+
timestampMs: record.timestampMs,
|
|
557
|
+
environment: "server",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const assertedPhaseIds = new Set<string>();
|
|
564
|
+
for (const phase of phasesById.values()) {
|
|
565
|
+
if (phase.phase.kind !== "assertion" || phase.phase.status !== "passed")
|
|
566
|
+
continue;
|
|
567
|
+
assertedPhaseIds.add(phase.phase.id);
|
|
568
|
+
if (phase.phase.causedByPhaseId)
|
|
569
|
+
assertedPhaseIds.add(phase.phase.causedByPhaseId);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const confidenceFor = (
|
|
573
|
+
testIds: Iterable<string>,
|
|
574
|
+
phaseIds: Iterable<string>,
|
|
575
|
+
explicitPhaseIds: Iterable<string> = phaseIds,
|
|
576
|
+
): CoverageConfidence => {
|
|
577
|
+
const tests = [...new Set(testIds)].sort();
|
|
578
|
+
const phases = [...new Set(phaseIds)];
|
|
579
|
+
const assertedPhases = [...new Set(explicitPhaseIds)].filter((id) =>
|
|
580
|
+
assertedPhaseIds.has(id),
|
|
581
|
+
);
|
|
582
|
+
const assertedTests = [
|
|
583
|
+
...new Set(
|
|
584
|
+
assertedPhases
|
|
585
|
+
.map((id) => phasesById.get(id)?.test)
|
|
586
|
+
.filter((value): value is string => Boolean(value)),
|
|
587
|
+
),
|
|
588
|
+
].sort();
|
|
589
|
+
const provenances = tests
|
|
590
|
+
.map((id) => testsById.get(id)?.provenance)
|
|
591
|
+
.filter((value): value is TestProvenance => Boolean(value));
|
|
592
|
+
const roles = tests
|
|
593
|
+
.map((id) => testsById.get(id)?.role)
|
|
594
|
+
.filter((value): value is MutableTestCoverage["role"] => Boolean(value));
|
|
595
|
+
const hasAction = phases.some(
|
|
596
|
+
(id) => phasesById.get(id)?.phase.kind === "action",
|
|
597
|
+
);
|
|
598
|
+
const level: CoverageConfidence["level"] =
|
|
599
|
+
tests.length === 0
|
|
600
|
+
? "unexecuted"
|
|
601
|
+
: assertedTests.length > 0
|
|
602
|
+
? "asserted"
|
|
603
|
+
: hasAction
|
|
604
|
+
? "action"
|
|
605
|
+
: "executed";
|
|
606
|
+
const kinds = [...new Set(provenances.map((value) => value.kind))].sort();
|
|
607
|
+
return {
|
|
608
|
+
level,
|
|
609
|
+
setupOnly: roles.length > 0 && roles.every((role) => role === "setup"),
|
|
610
|
+
backgroundOnly:
|
|
611
|
+
roles.length > 0 && roles.every((role) => role === "background"),
|
|
612
|
+
asserted: assertedTests.length > 0,
|
|
613
|
+
tests,
|
|
614
|
+
assertedTests,
|
|
615
|
+
runners: [...new Set(provenances.map((value) => value.runner))].sort(),
|
|
616
|
+
kinds,
|
|
617
|
+
e2e: kinds.includes("e2e"),
|
|
618
|
+
};
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const decisions: McdcDecisionResult[] = [...decisionMetadata.values()]
|
|
622
|
+
.sort((left, right) =>
|
|
623
|
+
left.file === right.file
|
|
624
|
+
? left.line - right.line || left.column - right.column
|
|
625
|
+
: left.file.localeCompare(right.file),
|
|
626
|
+
)
|
|
627
|
+
.map((meta) => {
|
|
628
|
+
const vectorObservations = [
|
|
629
|
+
...(vectorsByDecision.get(meta.id)?.values() ?? []),
|
|
630
|
+
].map((observation) => ({
|
|
631
|
+
vector: observation.vector,
|
|
632
|
+
tests: [...observation.tests].sort(),
|
|
633
|
+
...(observation.phases.size > 0
|
|
634
|
+
? { phases: [...observation.phases].sort() }
|
|
635
|
+
: {}),
|
|
636
|
+
...(observation.explicitPhases.size > 0
|
|
637
|
+
? { explicitPhases: [...observation.explicitPhases].sort() }
|
|
638
|
+
: {}),
|
|
639
|
+
confidence: confidenceFor(
|
|
640
|
+
observation.tests,
|
|
641
|
+
observation.phases,
|
|
642
|
+
observation.explicitPhases,
|
|
643
|
+
),
|
|
644
|
+
}));
|
|
645
|
+
const vectors = vectorObservations.map(
|
|
646
|
+
(observation) => observation.vector,
|
|
647
|
+
);
|
|
648
|
+
const conditions = meta.conditions.map((source, index) => {
|
|
649
|
+
const witness = findWitness(vectors, index);
|
|
650
|
+
const witnessTests = witness?.map(
|
|
651
|
+
(vector) =>
|
|
652
|
+
vectorObservations.find(
|
|
653
|
+
(observation) =>
|
|
654
|
+
vectorKey(observation.vector) === vectorKey(vector),
|
|
655
|
+
)?.tests ?? [],
|
|
656
|
+
) as [string[], string[]] | undefined;
|
|
657
|
+
let assertionCovered = false;
|
|
658
|
+
for (let left = 0; left < vectorObservations.length; left += 1) {
|
|
659
|
+
for (let right = left + 1; right < vectorObservations.length; right += 1) {
|
|
660
|
+
const first = vectorObservations[left];
|
|
661
|
+
const second = vectorObservations[right];
|
|
662
|
+
if (
|
|
663
|
+
first &&
|
|
664
|
+
second &&
|
|
665
|
+
first.confidence.asserted &&
|
|
666
|
+
second.confidence.asserted &&
|
|
667
|
+
isIndependencePair(first.vector, second.vector, index)
|
|
668
|
+
) {
|
|
669
|
+
assertionCovered = true;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (assertionCovered) break;
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
index,
|
|
677
|
+
source,
|
|
678
|
+
covered: Boolean(witness),
|
|
679
|
+
assertionCovered,
|
|
680
|
+
...(witness ? { witness } : {}),
|
|
681
|
+
...(witnessTests ? { witnessTests } : {}),
|
|
682
|
+
};
|
|
683
|
+
});
|
|
684
|
+
return {
|
|
685
|
+
meta,
|
|
686
|
+
executed: vectors.length > 0,
|
|
687
|
+
covered: conditions.every((condition) => condition.covered),
|
|
688
|
+
vectors,
|
|
689
|
+
vectorObservations,
|
|
690
|
+
conditions,
|
|
691
|
+
tests: [...(testsByDecision.get(meta.id) ?? [])].sort(),
|
|
692
|
+
confidence: confidenceFor(
|
|
693
|
+
testsByDecision.get(meta.id) ?? [],
|
|
694
|
+
vectorObservations.flatMap((observation) => observation.phases ?? []),
|
|
695
|
+
vectorObservations.flatMap(
|
|
696
|
+
(observation) => observation.explicitPhases ?? [],
|
|
697
|
+
),
|
|
698
|
+
),
|
|
699
|
+
};
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const points = manifest.points.map((meta) => {
|
|
703
|
+
const tests = [...(testsByHit.get(meta.id) ?? [])].sort();
|
|
704
|
+
const phases = [...(phasesByHit.get(meta.id) ?? [])].sort();
|
|
705
|
+
const explicitPhases = [
|
|
706
|
+
...(explicitPhasesByHit.get(meta.id) ?? []),
|
|
707
|
+
].sort();
|
|
708
|
+
return {
|
|
709
|
+
meta,
|
|
710
|
+
covered: testsByHit.has(meta.id),
|
|
711
|
+
tests,
|
|
712
|
+
phases,
|
|
713
|
+
confidence: confidenceFor(tests, phases, explicitPhases),
|
|
714
|
+
};
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
const branches = manifest.branches.map((meta) => {
|
|
718
|
+
const alternatives = meta.alternatives.map((alternative) => {
|
|
719
|
+
const tests = [...(testsByHit.get(alternative.id) ?? [])].sort();
|
|
720
|
+
const phases = [...(phasesByHit.get(alternative.id) ?? [])].sort();
|
|
721
|
+
const explicitPhases = [
|
|
722
|
+
...(explicitPhasesByHit.get(alternative.id) ?? []),
|
|
723
|
+
].sort();
|
|
724
|
+
return {
|
|
725
|
+
...alternative,
|
|
726
|
+
covered: testsByHit.has(alternative.id),
|
|
727
|
+
tests,
|
|
728
|
+
phases,
|
|
729
|
+
confidence: confidenceFor(tests, phases, explicitPhases),
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
return {
|
|
733
|
+
meta,
|
|
734
|
+
covered: alternatives.every((alternative) => alternative.covered),
|
|
735
|
+
alternatives,
|
|
736
|
+
};
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const lineMap = new Map<
|
|
740
|
+
string,
|
|
741
|
+
{
|
|
742
|
+
file: string;
|
|
743
|
+
line: number;
|
|
744
|
+
covered: boolean;
|
|
745
|
+
tests: Set<string>;
|
|
746
|
+
phases: Set<string>;
|
|
747
|
+
explicitPhases: Set<string>;
|
|
748
|
+
}
|
|
749
|
+
>();
|
|
750
|
+
for (const point of points) {
|
|
751
|
+
const key = point.meta.file + ":" + point.meta.line;
|
|
752
|
+
const line = lineMap.get(key) ?? {
|
|
753
|
+
file: point.meta.file,
|
|
754
|
+
line: point.meta.line,
|
|
755
|
+
covered: false,
|
|
756
|
+
tests: new Set<string>(),
|
|
757
|
+
phases: new Set<string>(),
|
|
758
|
+
explicitPhases: new Set<string>(),
|
|
759
|
+
};
|
|
760
|
+
line.covered ||= point.covered;
|
|
761
|
+
for (const test of point.tests) line.tests.add(test);
|
|
762
|
+
for (const phase of point.phases) line.phases.add(phase);
|
|
763
|
+
for (const phase of explicitPhasesByHit.get(point.meta.id) ?? [])
|
|
764
|
+
line.explicitPhases.add(phase);
|
|
765
|
+
lineMap.set(key, line);
|
|
766
|
+
}
|
|
767
|
+
const lines = [...lineMap.values()]
|
|
768
|
+
.sort(
|
|
769
|
+
(left, right) =>
|
|
770
|
+
left.file.localeCompare(right.file) || left.line - right.line,
|
|
771
|
+
)
|
|
772
|
+
.map((line) => {
|
|
773
|
+
const testIds = [...line.tests].sort();
|
|
774
|
+
const provenances = testIds
|
|
775
|
+
.map((id) => testsById.get(id)?.provenance)
|
|
776
|
+
.filter((value): value is TestProvenance => Boolean(value));
|
|
777
|
+
const runners = [
|
|
778
|
+
...new Set(provenances.map((value) => value.runner)),
|
|
779
|
+
].sort();
|
|
780
|
+
const kinds = [...new Set(provenances.map((value) => value.kind))].sort();
|
|
781
|
+
return {
|
|
782
|
+
...line,
|
|
783
|
+
tests: testIds,
|
|
784
|
+
runners,
|
|
785
|
+
kinds,
|
|
786
|
+
...(kinds.length === 1 ? { exclusiveKind: kinds[0] } : {}),
|
|
787
|
+
phases: [...line.phases].sort(),
|
|
788
|
+
confidence: confidenceFor(testIds, line.phases, line.explicitPhases),
|
|
789
|
+
};
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const pointLocations = new Map(
|
|
793
|
+
manifest.points.map((point) => [
|
|
794
|
+
point.id,
|
|
795
|
+
{ file: point.file, line: point.line },
|
|
796
|
+
]),
|
|
797
|
+
);
|
|
798
|
+
const tests = [...testsById.values()]
|
|
799
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
800
|
+
.map((test) => {
|
|
801
|
+
const testLines = new Map<string, { file: string; line: number }>();
|
|
802
|
+
for (const hit of test.hits) {
|
|
803
|
+
const location = pointLocations.get(hit);
|
|
804
|
+
if (location)
|
|
805
|
+
testLines.set(`${location.file}:${location.line}`, location);
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
id: test.id,
|
|
809
|
+
name: test.name,
|
|
810
|
+
...(test.file ? { file: test.file } : {}),
|
|
811
|
+
...(test.title ? { title: test.title } : {}),
|
|
812
|
+
retries: [...test.retries].sort((left, right) => left - right),
|
|
813
|
+
attempts: [...test.attempts.values()].sort(
|
|
814
|
+
(left, right) => left.retry - right.retry,
|
|
815
|
+
),
|
|
816
|
+
outcome: testOutcome(test),
|
|
817
|
+
provenance: test.provenance,
|
|
818
|
+
role: test.role,
|
|
819
|
+
hits: [...test.hits].sort(),
|
|
820
|
+
decisions: [...test.decisions.entries()]
|
|
821
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
822
|
+
.map(([id, decisionVectors]) => ({
|
|
823
|
+
id,
|
|
824
|
+
vectors: [...decisionVectors.values()],
|
|
825
|
+
})),
|
|
826
|
+
lines: [...testLines.values()].sort(
|
|
827
|
+
(left, right) =>
|
|
828
|
+
left.file.localeCompare(right.file) || left.line - right.line,
|
|
829
|
+
),
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
const testFilesByName = new Map<
|
|
833
|
+
string,
|
|
834
|
+
{
|
|
835
|
+
tests: Set<string>;
|
|
836
|
+
runners: Set<string>;
|
|
837
|
+
kinds: Set<string>;
|
|
838
|
+
lines: Map<string, { file: string; line: number }>;
|
|
839
|
+
}
|
|
840
|
+
>();
|
|
841
|
+
for (const test of tests) {
|
|
842
|
+
const file = test.file ?? "(unknown test file)";
|
|
843
|
+
const aggregate = testFilesByName.get(file) ?? {
|
|
844
|
+
tests: new Set<string>(),
|
|
845
|
+
runners: new Set<string>(),
|
|
846
|
+
kinds: new Set<string>(),
|
|
847
|
+
lines: new Map<string, { file: string; line: number }>(),
|
|
848
|
+
};
|
|
849
|
+
aggregate.tests.add(test.id);
|
|
850
|
+
aggregate.runners.add(test.provenance.runner);
|
|
851
|
+
aggregate.kinds.add(test.provenance.kind);
|
|
852
|
+
for (const line of test.lines)
|
|
853
|
+
aggregate.lines.set(`${line.file}:${line.line}`, line);
|
|
854
|
+
testFilesByName.set(file, aggregate);
|
|
855
|
+
}
|
|
856
|
+
const testFiles = [...testFilesByName.entries()]
|
|
857
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
858
|
+
.map(([file, aggregate]) => ({
|
|
859
|
+
file,
|
|
860
|
+
tests: [...aggregate.tests].sort(),
|
|
861
|
+
runners: [...aggregate.runners].sort(),
|
|
862
|
+
kinds: [...aggregate.kinds].sort(),
|
|
863
|
+
lines: [...aggregate.lines.values()].sort(
|
|
864
|
+
(left, right) =>
|
|
865
|
+
left.file.localeCompare(right.file) || left.line - right.line,
|
|
866
|
+
),
|
|
867
|
+
}));
|
|
868
|
+
|
|
869
|
+
const phases = [...phasesById.values()]
|
|
870
|
+
.sort(
|
|
871
|
+
(left, right) =>
|
|
872
|
+
left.phase.startedAtMs - right.phase.startedAtMs ||
|
|
873
|
+
left.phase.id.localeCompare(right.phase.id),
|
|
874
|
+
)
|
|
875
|
+
.map((phase) => {
|
|
876
|
+
const phaseLines = new Map<string, { file: string; line: number }>();
|
|
877
|
+
for (const hit of phase.hits) {
|
|
878
|
+
const location = pointLocations.get(hit);
|
|
879
|
+
if (location)
|
|
880
|
+
phaseLines.set(`${location.file}:${location.line}`, location);
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
...phase.phase,
|
|
884
|
+
test: phase.test,
|
|
885
|
+
hits: [...phase.hits].sort(),
|
|
886
|
+
decisions: [...phase.decisions.entries()]
|
|
887
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
888
|
+
.map(([id, vectors]) => ({ id, vectors: [...vectors.values()] })),
|
|
889
|
+
lines: [...phaseLines.values()].sort(
|
|
890
|
+
(left, right) =>
|
|
891
|
+
left.file.localeCompare(right.file) || left.line - right.line,
|
|
892
|
+
),
|
|
893
|
+
browserEvents: phase.browserEvents,
|
|
894
|
+
serverEvents: phase.serverEvents,
|
|
895
|
+
explicitEvents: phase.explicitEvents,
|
|
896
|
+
inferredEvents: phase.inferredEvents,
|
|
897
|
+
explicitBrowserEvents: phase.explicitBrowserEvents,
|
|
898
|
+
inferredBrowserEvents: phase.inferredBrowserEvents,
|
|
899
|
+
explicitServerEvents: phase.explicitServerEvents,
|
|
900
|
+
inferredServerEvents: phase.inferredServerEvents,
|
|
901
|
+
};
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
const summary = summarizeCoverage(decisions, points, branches, lines);
|
|
905
|
+
if ((manifest.limitations?.length ?? 0) > 0) {
|
|
906
|
+
summary.coverageComplete = false;
|
|
907
|
+
summary.completenessBlocked = true;
|
|
908
|
+
}
|
|
909
|
+
const coverageByDimension = (
|
|
910
|
+
field: "kind" | "runner",
|
|
911
|
+
): Array<{
|
|
912
|
+
value: string;
|
|
913
|
+
tests: number;
|
|
914
|
+
setups: number;
|
|
915
|
+
summary: CoverageSummary;
|
|
916
|
+
}> => {
|
|
917
|
+
const values = [
|
|
918
|
+
...new Set(tests.map((test) => test.provenance[field])),
|
|
919
|
+
].sort();
|
|
920
|
+
return values.map((value) => {
|
|
921
|
+
const testIds = new Set(
|
|
922
|
+
tests
|
|
923
|
+
.filter((test) => test.provenance[field] === value)
|
|
924
|
+
.map((test) => test.id),
|
|
925
|
+
);
|
|
926
|
+
return {
|
|
927
|
+
value,
|
|
928
|
+
tests: tests.filter(
|
|
929
|
+
(test) => test.provenance[field] === value && test.role === "test",
|
|
930
|
+
).length,
|
|
931
|
+
setups: tests.filter(
|
|
932
|
+
(test) => test.provenance[field] === value && test.role === "setup",
|
|
933
|
+
).length,
|
|
934
|
+
summary: coverageSummaryForTestIds(
|
|
935
|
+
decisions,
|
|
936
|
+
points,
|
|
937
|
+
branches,
|
|
938
|
+
lines,
|
|
939
|
+
testIds,
|
|
940
|
+
),
|
|
941
|
+
};
|
|
942
|
+
});
|
|
943
|
+
};
|
|
944
|
+
const coverageByKind = coverageByDimension("kind").map(
|
|
945
|
+
({ value: kind, ...entry }) => ({ kind, ...entry }),
|
|
946
|
+
);
|
|
947
|
+
const coverageByRunner = coverageByDimension("runner").map(
|
|
948
|
+
({ value: runner, ...entry }) => ({ runner, ...entry }),
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
return {
|
|
952
|
+
generatedAt: new Date().toISOString(),
|
|
953
|
+
variant: "masking-short-circuit",
|
|
954
|
+
model: {
|
|
955
|
+
name: "coverage-completeness-v2",
|
|
956
|
+
completenessMeaning:
|
|
957
|
+
"Every obligation in the measured model was observed by at least one existing test; test assertions and product correctness are separate assumptions.",
|
|
958
|
+
measured: [
|
|
959
|
+
"executable source lines",
|
|
960
|
+
"executable statements",
|
|
961
|
+
"function entries",
|
|
962
|
+
"true and false outcomes of if, ternary, while, do/while, and classic for decisions",
|
|
963
|
+
"true and false outcomes of every atomic condition in those decisions",
|
|
964
|
+
"masking MC/DC independence for every atomic condition in those decisions",
|
|
965
|
+
"short-circuit and right-evaluated selections for &&, ||, and ?? value expressions, including JSX",
|
|
966
|
+
"short-circuit and evaluated alternatives for logical assignments and optional chains",
|
|
967
|
+
"provided and default-evaluated parameter and destructuring values",
|
|
968
|
+
"try success and catch entry",
|
|
969
|
+
"zero and entered for-in/for-of loops",
|
|
970
|
+
"entered switch cases, defaults, and implicit no-match alternatives",
|
|
971
|
+
],
|
|
972
|
+
notMeasured: [
|
|
973
|
+
"all input values or semantic input partitions",
|
|
974
|
+
"all execution paths or ordering/concurrency interleavings",
|
|
975
|
+
"destructuring defaults in classic for initializers (reported as blockers when discovered)",
|
|
976
|
+
"optional calls through super",
|
|
977
|
+
"the internal statements and decisions of runtime-generated eval/Function source",
|
|
978
|
+
"mutation score or assertion fault-detection strength",
|
|
979
|
+
],
|
|
980
|
+
},
|
|
981
|
+
limitations: manifest.limitations ?? [],
|
|
982
|
+
summary,
|
|
983
|
+
coverageByKind,
|
|
984
|
+
coverageByRunner,
|
|
985
|
+
decisions,
|
|
986
|
+
points,
|
|
987
|
+
branches,
|
|
988
|
+
tests,
|
|
989
|
+
testFiles,
|
|
990
|
+
phases,
|
|
991
|
+
lines,
|
|
992
|
+
};
|
|
993
|
+
}
|