supercov 0.0.8 → 0.0.11
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 +14 -1
- package/bin/native.js +87 -0
- package/bin/supercov.js +20 -3
- package/dist/agentJson.d.ts +2 -1
- package/dist/agentJson.d.ts.map +1 -1
- package/dist/agentJson.js +18 -0
- package/dist/agentJson.js.map +1 -1
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +9 -5
- package/dist/analyze.js.map +1 -1
- package/dist/cli.js +126 -23
- package/dist/cli.js.map +1 -1
- package/dist/directInstrumenter.d.ts.map +1 -1
- package/dist/directInstrumenter.js +39 -19
- package/dist/directInstrumenter.js.map +1 -1
- package/dist/engineEvidence.d.ts +9 -0
- package/dist/engineEvidence.d.ts.map +1 -0
- package/dist/engineEvidence.js +31 -0
- package/dist/engineEvidence.js.map +1 -0
- package/dist/engineInstrumenter.d.ts +16 -0
- package/dist/engineInstrumenter.d.ts.map +1 -0
- package/dist/engineInstrumenter.js +50 -0
- package/dist/engineInstrumenter.js.map +1 -0
- package/dist/engineProcess.d.ts +11 -0
- package/dist/engineProcess.d.ts.map +1 -0
- package/dist/engineProcess.js +59 -0
- package/dist/engineProcess.js.map +1 -0
- package/dist/evidenceArchive.d.ts +1 -0
- package/dist/evidenceArchive.d.ts.map +1 -1
- package/dist/evidenceArchive.js +59 -13
- package/dist/evidenceArchive.js.map +1 -1
- package/dist/instrumenter.d.ts +8 -1
- package/dist/instrumenter.d.ts.map +1 -1
- package/dist/instrumenter.js +585 -113
- package/dist/instrumenter.js.map +1 -1
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +20 -1
- package/dist/integrity.js.map +1 -1
- package/dist/launchSupervisor.d.ts.map +1 -1
- package/dist/launchSupervisor.js +3 -0
- package/dist/launchSupervisor.js.map +1 -1
- package/dist/merge.d.ts.map +1 -1
- package/dist/merge.js +9 -6
- package/dist/merge.js.map +1 -1
- package/dist/nodeAssert.d.ts +31 -0
- package/dist/nodeAssert.d.ts.map +1 -0
- package/dist/nodeAssert.js +30 -0
- package/dist/nodeAssert.js.map +1 -0
- package/dist/nodeAssertAdapter.d.ts +6 -0
- package/dist/nodeAssertAdapter.d.ts.map +1 -0
- package/dist/nodeAssertAdapter.js +94 -0
- package/dist/nodeAssertAdapter.js.map +1 -0
- package/dist/nodeAssertStrict.d.ts +31 -0
- package/dist/nodeAssertStrict.d.ts.map +1 -0
- package/dist/nodeAssertStrict.js +28 -0
- package/dist/nodeAssertStrict.js.map +1 -0
- package/dist/nodeAssertionInstrumenter.d.ts +8 -0
- package/dist/nodeAssertionInstrumenter.d.ts.map +1 -0
- package/dist/nodeAssertionInstrumenter.js +271 -0
- package/dist/nodeAssertionInstrumenter.js.map +1 -0
- package/dist/nodeTest.d.ts.map +1 -1
- package/dist/nodeTest.js +14 -5
- package/dist/nodeTest.js.map +1 -1
- package/dist/playwright.d.ts.map +1 -1
- package/dist/playwright.js +177 -89
- package/dist/playwright.js.map +1 -1
- package/dist/processDiagnostics.d.ts +34 -0
- package/dist/processDiagnostics.d.ts.map +1 -0
- package/dist/processDiagnostics.js +155 -0
- package/dist/processDiagnostics.js.map +1 -0
- package/dist/project.d.ts +1 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +39 -4
- package/dist/project.js.map +1 -1
- package/dist/query.d.ts +11 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +224 -11
- package/dist/query.js.map +1 -1
- package/dist/register.mjs +97 -4
- package/dist/register.mjs.map +1 -1
- package/dist/resolve-loader.d.mts.map +1 -1
- package/dist/resolve-loader.mjs +16 -2
- package/dist/resolve-loader.mjs.map +1 -1
- package/dist/runAnalysis.d.ts.map +1 -1
- package/dist/runAnalysis.js +3 -1
- package/dist/runAnalysis.js.map +1 -1
- package/dist/runnerEvidence.d.ts +3 -3
- package/dist/runnerEvidence.d.ts.map +1 -1
- package/dist/runnerEvidence.js +7 -4
- package/dist/runnerEvidence.js.map +1 -1
- package/dist/runtime.d.ts +77 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +395 -26
- package/dist/runtime.js.map +1 -1
- package/dist/runtimeIsolation.d.ts +3 -0
- package/dist/runtimeIsolation.d.ts.map +1 -0
- package/dist/runtimeIsolation.js +9 -0
- package/dist/runtimeIsolation.js.map +1 -0
- package/dist/sourceDiscovery.d.ts.map +1 -1
- package/dist/sourceDiscovery.js +7 -2
- package/dist/sourceDiscovery.js.map +1 -1
- package/dist/transport.js +2 -2
- package/dist/transport.js.map +1 -1
- package/dist/vitePlugin.d.ts.map +1 -1
- package/dist/vitePlugin.js +21 -7
- package/dist/vitePlugin.js.map +1 -1
- package/dist/vitest.js +38 -7
- package/dist/vitest.js.map +1 -1
- package/dist/waivers.d.ts +52 -0
- package/dist/waivers.d.ts.map +1 -0
- package/dist/waivers.js +121 -0
- package/dist/waivers.js.map +1 -0
- package/dist/workspace.d.ts +36 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +216 -37
- package/dist/workspace.js.map +1 -1
- package/package.json +29 -3
package/dist/query.js
CHANGED
|
@@ -5,6 +5,7 @@ import { coverageSummaryForTests, isIndependencePair } from "./analyze.js";
|
|
|
5
5
|
import { EVIDENCE_ARCHIVE_SCHEMA_VERSION } from "./evidenceArchive.js";
|
|
6
6
|
import { analyzeCoverageArchiveCached, readCoverageQueryIndex, } from "./queryCache.js";
|
|
7
7
|
import { compareRunIntegrity, createRunIntegrity } from "./integrity.js";
|
|
8
|
+
import { evaluateCoverageWaivers, readCoverageWaivers, WAIVERS_FILE, } from "./waivers.js";
|
|
8
9
|
import { discoverCoverageProject } from "./project.js";
|
|
9
10
|
import { agentPagination, agentSuccessJson, SupercovError, } from "./agentJson.js";
|
|
10
11
|
function parseOptions(command, args) {
|
|
@@ -18,6 +19,8 @@ function parseOptions(command, args) {
|
|
|
18
19
|
filter: "all",
|
|
19
20
|
target: 100,
|
|
20
21
|
metric: "all",
|
|
22
|
+
group: "none",
|
|
23
|
+
sort: "location",
|
|
21
24
|
positional: [],
|
|
22
25
|
};
|
|
23
26
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -72,6 +75,20 @@ function parseOptions(command, args) {
|
|
|
72
75
|
throw new SupercovError("INVALID_ARGUMENT", "--offset must be a non-negative integer");
|
|
73
76
|
options.offset = offset;
|
|
74
77
|
}
|
|
78
|
+
else if (value === "--group") {
|
|
79
|
+
const group = args[++index]?.toLowerCase();
|
|
80
|
+
if (group !== "decision")
|
|
81
|
+
throw new SupercovError("INVALID_ARGUMENT", "--group must be decision");
|
|
82
|
+
if (command !== "file")
|
|
83
|
+
throw new SupercovError("INVALID_ARGUMENT", "--group is only supported by: supercov runs <run-id> coverage file <source-file>");
|
|
84
|
+
options.group = "decision";
|
|
85
|
+
}
|
|
86
|
+
else if (value === "--sort") {
|
|
87
|
+
const sort = args[++index]?.toLowerCase();
|
|
88
|
+
if (sort !== "location" && sort !== "missing")
|
|
89
|
+
throw new SupercovError("INVALID_ARGUMENT", "--sort must be location or missing");
|
|
90
|
+
options.sort = sort;
|
|
91
|
+
}
|
|
75
92
|
else if (value.startsWith("--"))
|
|
76
93
|
throw new SupercovError("INVALID_ARGUMENT", `Unknown option: ${value}`, {
|
|
77
94
|
details: { option: value },
|
|
@@ -79,6 +96,10 @@ function parseOptions(command, args) {
|
|
|
79
96
|
else
|
|
80
97
|
options.positional.push(value);
|
|
81
98
|
}
|
|
99
|
+
if (options.sort !== "location" && options.group === "none")
|
|
100
|
+
throw new SupercovError("INVALID_ARGUMENT", "--sort requires --group decision");
|
|
101
|
+
if (options.group === "decision" && options.metric !== "all" && options.metric !== "mcdc")
|
|
102
|
+
throw new SupercovError("INVALID_ARGUMENT", "--group decision lists MC/DC decisions; omit --metric or use --metric mcdc");
|
|
82
103
|
return options;
|
|
83
104
|
}
|
|
84
105
|
function filteredCoverage(report, options) {
|
|
@@ -493,9 +514,29 @@ function attribution(report, selected) {
|
|
|
493
514
|
serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
|
|
494
515
|
};
|
|
495
516
|
}
|
|
496
|
-
function coverageDiagnostics(report, selected) {
|
|
517
|
+
export function coverageDiagnostics(report, selected) {
|
|
497
518
|
const observed = attribution(report, selected);
|
|
498
519
|
const diagnostics = [];
|
|
520
|
+
// Assertion phases prove that the runner adapter observed the attempt, but
|
|
521
|
+
// they do not prove that the test executed measured application source. A
|
|
522
|
+
// contract/static-data assertion can legitimately have no coverage. Keep
|
|
523
|
+
// this visible as an ambiguity warning; only explicit corrupt transport is
|
|
524
|
+
// strong enough to claim evidence loss as an error.
|
|
525
|
+
const emptyEvidenceTests = report.tests.filter((test) => (test.role ?? "test") === "test" &&
|
|
526
|
+
(!selected || selected.has(test.id)) &&
|
|
527
|
+
test.lines.length === 0 &&
|
|
528
|
+
test.hits.length === 0 &&
|
|
529
|
+
test.decisions.length === 0 &&
|
|
530
|
+
report.phases.some((phase) => phase.test === test.id));
|
|
531
|
+
if (emptyEvidenceTests.length > 0) {
|
|
532
|
+
diagnostics.push({
|
|
533
|
+
code: "TEST_EVIDENCE_MISSING",
|
|
534
|
+
severity: "warning",
|
|
535
|
+
message: `${emptyEvidenceTests.length} test(s) recorded assertion phases but attributed zero coverage evidence; ` +
|
|
536
|
+
`this is valid for assertions over static or uninstrumented data, but may otherwise indicate missing probe transport. ` +
|
|
537
|
+
`First: ${emptyEvidenceTests[0].name}`,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
499
540
|
if ((report.transport?.corruptRecords ?? 0) > 0) {
|
|
500
541
|
diagnostics.push({
|
|
501
542
|
code: "CORRUPT_EVIDENCE_RECORDS",
|
|
@@ -675,8 +716,8 @@ export function coverageMeasurement(report) {
|
|
|
675
716
|
byKind,
|
|
676
717
|
};
|
|
677
718
|
}
|
|
678
|
-
function locationSelector(selector) {
|
|
679
|
-
const match = /^(.*):(\d+)(
|
|
719
|
+
export function locationSelector(selector) {
|
|
720
|
+
const match = /^(.*):(\d+):\d+$/.exec(selector) ?? /^(.*):(\d+)$/.exec(selector);
|
|
680
721
|
if (!match)
|
|
681
722
|
throw new SupercovError("INVALID_ARGUMENT", "Expected <source-file>:<line>", {
|
|
682
723
|
details: { selector },
|
|
@@ -694,7 +735,7 @@ const helpText = `Agent-oriented local coverage queries:
|
|
|
694
735
|
supercov runs <run-id> coverage scope [--limit N] [--offset N] [--json]
|
|
695
736
|
supercov runs <run-id> coverage files [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--limit N] [--offset N] [--json]
|
|
696
737
|
supercov runs <run-id> coverage gaps [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
|
|
697
|
-
supercov runs <run-id> coverage file <source-file> [--metric all|lines|statements|functions|branches|mcdc] [--kind e2e] [--limit N] [--offset N] [--json]
|
|
738
|
+
supercov runs <run-id> coverage file <source-file> [--metric all|lines|statements|functions|branches|mcdc] [--group decision] [--sort location|missing] [--kind e2e] [--limit N] [--offset N] [--json]
|
|
698
739
|
supercov runs <run-id> coverage decision <id|source-file:line> [--kind e2e] [--json]
|
|
699
740
|
supercov runs <run-id> coverage covers <source-file:line> [--kind e2e] [--json]
|
|
700
741
|
supercov runs <run-id> coverage test <id|name-fragment> [--kind e2e] [--limit N] [--json]
|
|
@@ -706,6 +747,14 @@ const helpText = `Agent-oriented local coverage queries:
|
|
|
706
747
|
|
|
707
748
|
Use "latest" as <run-id> to query the newest local run.
|
|
708
749
|
|
|
750
|
+
Reviewed MC/DC waivers (optional ${WAIVERS_FILE} at the project root):
|
|
751
|
+
{"version":1,"waivers":[{"file":"src/x.ts","decision":"<id or source>","line":12,"condition":"<source or C2>","reason":"..."}]}
|
|
752
|
+
Waived conditions stay uncovered in every raw total and are reported separately.
|
|
753
|
+
Waive only conditions with no satisfiable independence pair. A condition that
|
|
754
|
+
some platform or environment simply never runs here is a coverage gap, not an
|
|
755
|
+
impossibility; keep it visible. Prefer omitting "line" (it breaks on edits
|
|
756
|
+
above it and is then reported as unmatched).
|
|
757
|
+
|
|
709
758
|
Create a run with:
|
|
710
759
|
supercov -- <test command>`;
|
|
711
760
|
function help(options) {
|
|
@@ -737,9 +786,16 @@ export function resolveCoverageQueryInvocation(command, args) {
|
|
|
737
786
|
if (command !== "runs")
|
|
738
787
|
return { command, args };
|
|
739
788
|
const runId = args[0];
|
|
740
|
-
if (!runId || runId.startsWith("-")
|
|
789
|
+
if (!runId || runId.startsWith("-")) {
|
|
741
790
|
return { command, args };
|
|
742
791
|
}
|
|
792
|
+
if (args[1] !== "coverage") {
|
|
793
|
+
// A positional run ID must never silently degrade to the run listing:
|
|
794
|
+
// an agent that omits "coverage" would otherwise get unrelated output.
|
|
795
|
+
throw new SupercovError("UNKNOWN_COMMAND", args[1] === undefined || args[1].startsWith("-")
|
|
796
|
+
? `Missing coverage query after run ${runId}. Expected: supercov runs <run-id> coverage [<query>]. Try supercov help.`
|
|
797
|
+
: `Unknown runs query: ${args[1]}. Expected: supercov runs <run-id> coverage [<query>]. Try supercov help.`, { details: { run: runId, command: args[1] ?? null } });
|
|
798
|
+
}
|
|
743
799
|
const childToken = args[2];
|
|
744
800
|
const hasChild = Boolean(childToken && !childToken.startsWith("-"));
|
|
745
801
|
const child = hasChild ? childToken : "summary";
|
|
@@ -772,6 +828,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
772
828
|
if (command === "help")
|
|
773
829
|
return help(options);
|
|
774
830
|
const currentIntegrity = currentProjectIntegrity(root);
|
|
831
|
+
const waiverSource = readCoverageWaivers(root);
|
|
775
832
|
if (command === "runs") {
|
|
776
833
|
const availableRuns = discoverRuns(root);
|
|
777
834
|
const runs = page(availableRuns, options).map((run) => {
|
|
@@ -908,6 +965,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
908
965
|
const run = selectedRun.run;
|
|
909
966
|
const report = filteredCoverage(selectedRun.report, options);
|
|
910
967
|
const selectedTestSet = selectedTestIds(report, options);
|
|
968
|
+
const waiverEvaluation = waiverSource
|
|
969
|
+
? evaluateCoverageWaivers(report.decisions, waiverSource)
|
|
970
|
+
: undefined;
|
|
911
971
|
if (command === "summary") {
|
|
912
972
|
const summary = selectedTestSet
|
|
913
973
|
? coverageSummaryForTests(report, selectedTestSet)
|
|
@@ -937,6 +997,35 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
937
997
|
measurement.complete,
|
|
938
998
|
coverage: summary,
|
|
939
999
|
measurement,
|
|
1000
|
+
...(waiverEvaluation
|
|
1001
|
+
? {
|
|
1002
|
+
waivers: {
|
|
1003
|
+
file: WAIVERS_FILE,
|
|
1004
|
+
entries: waiverEvaluation.waivers.length,
|
|
1005
|
+
applied: waiverEvaluation.applied.length,
|
|
1006
|
+
contradicted: waiverEvaluation.contradicted.map((match) => ({
|
|
1007
|
+
file: match.file,
|
|
1008
|
+
line: match.line,
|
|
1009
|
+
condition: match.conditionSource,
|
|
1010
|
+
reason: match.waiver.reason,
|
|
1011
|
+
})),
|
|
1012
|
+
unmatched: waiverEvaluation.unmatched,
|
|
1013
|
+
mcdcExcludingWaived: {
|
|
1014
|
+
covered: report.summary.coveredConditions,
|
|
1015
|
+
total: report.summary.conditions -
|
|
1016
|
+
waiverEvaluation.applied.length,
|
|
1017
|
+
percentage: report.summary.conditions -
|
|
1018
|
+
waiverEvaluation.applied.length >
|
|
1019
|
+
0
|
|
1020
|
+
? (report.summary.coveredConditions /
|
|
1021
|
+
(report.summary.conditions -
|
|
1022
|
+
waiverEvaluation.applied.length)) *
|
|
1023
|
+
100
|
|
1024
|
+
: 100,
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
}
|
|
1028
|
+
: {}),
|
|
940
1029
|
coverageByKind: report.coverageByKind,
|
|
941
1030
|
coverageByRunner: report.coverageByRunner,
|
|
942
1031
|
attribution: attribution(report, selectedTestSet),
|
|
@@ -974,7 +1063,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
974
1063
|
}
|
|
975
1064
|
: undefined,
|
|
976
1065
|
};
|
|
977
|
-
return output(result, options, `run ${run.id}${filterLabel(options) ? ` (${filterLabel(options)})` : ""}${run.metadata?.testExitCode !== 0 ? ` [INVALID: test exit ${run.metadata?.testExitCode ?? "unknown"}]` : ""}${report.integrity?.stale ? ` [STALE: ${(report.integrity.staleReasons ?? []).join(", ")}]` : ""}\nlines ${pct(summary.lines.percentage)} (${summary.lines.covered}/${summary.lines.total})\nbranches ${pct(summary.branches.percentage)} (${summary.branches.covered}/${summary.branches.total})\nMC/DC ${pct(summary.conditionCoveragePct)} (${summary.coveredConditions}/${summary.conditions})\nmeasurement: ${measurement.complete ? "complete" : `incomplete — ${measurement.blocking} blocking limitation(s) in ${measurement.files} file(s)`}${diagnostics.length ? `\ndiagnostic: ${diagnostics.map((item) => `${item.code}: ${item.message}`).join("; ")}` : ""}${!selectedTestSet ? `\nconfidence: ${report.lines.filter((line) => line.confidence?.level === "asserted").length} asserted lines, ${report.lines.filter((line) => line.confidence?.level === "action").length} action-linked, ${report.lines.filter((line) => line.confidence?.level === "executed").length} execution-only; ${report.decisions.reduce((total, decision) => total + decision.conditions.filter((condition) => condition.assertionCovered).length, 0)} assertion-linked MC/DC conditions` : ""}\n${testCount} test(s)${setupCount ? ` + ${setupCount} setup scope(s)` : ""}; outcomes ${Object.entries(testOutcomes).filter(([, count]) => count > 0).map(([outcome, count]) => `${outcome}=${count}`).join(", ") || "none"}; ${gaps.length} file(s) have unresolved coverage or measurement gaps`);
|
|
1066
|
+
return output(result, options, `run ${run.id}${filterLabel(options) ? ` (${filterLabel(options)})` : ""}${run.metadata?.testExitCode !== 0 ? ` [INVALID: test exit ${run.metadata?.testExitCode ?? "unknown"}]` : ""}${report.integrity?.stale ? ` [STALE: ${(report.integrity.staleReasons ?? []).join(", ")}]` : ""}\nlines ${pct(summary.lines.percentage)} (${summary.lines.covered}/${summary.lines.total})\nbranches ${pct(summary.branches.percentage)} (${summary.branches.covered}/${summary.branches.total})\nMC/DC ${pct(summary.conditionCoveragePct)} (${summary.coveredConditions}/${summary.conditions})\nmeasurement: ${measurement.complete ? "complete" : `incomplete — ${measurement.blocking} blocking limitation(s) in ${measurement.files} file(s)`}${waiverEvaluation ? `\nwaivers: ${waiverEvaluation.applied.length} applied, ${waiverEvaluation.contradicted.length} contradicted, ${waiverEvaluation.unmatched.length} unmatched; MC/DC excluding waived ${pct(report.summary.conditions - waiverEvaluation.applied.length > 0 ? (report.summary.coveredConditions / (report.summary.conditions - waiverEvaluation.applied.length)) * 100 : 100)} (${report.summary.coveredConditions}/${report.summary.conditions - waiverEvaluation.applied.length})${waiverEvaluation.contradicted.map((match) => `\n contradicted (condition is covered): ${match.file}:${match.line} ${match.conditionSource}`).join("")}${waiverEvaluation.unmatched.map((waiver) => `\n unmatched (no such condition): ${waiver.file}${waiver.line !== undefined ? `:${waiver.line}` : ""} ${waiver.condition}`).join("")}` : ""}${diagnostics.length ? `\ndiagnostic: ${diagnostics.map((item) => `${item.code}: ${item.message}`).join("; ")}` : ""}${!selectedTestSet ? `\nconfidence: ${report.lines.filter((line) => line.confidence?.level === "asserted").length} asserted lines, ${report.lines.filter((line) => line.confidence?.level === "action").length} action-linked, ${report.lines.filter((line) => line.confidence?.level === "executed").length} execution-only; ${report.decisions.reduce((total, decision) => total + decision.conditions.filter((condition) => condition.assertionCovered).length, 0)} assertion-linked MC/DC conditions` : ""}\n${testCount} test(s)${setupCount ? ` + ${setupCount} setup scope(s)` : ""}; outcomes ${Object.entries(testOutcomes).filter(([, count]) => count > 0).map(([outcome, count]) => `${outcome}=${count}`).join(", ") || "none"}; ${gaps.length} file(s) have unresolved coverage or measurement gaps`);
|
|
978
1067
|
}
|
|
979
1068
|
if (command === "minimize") {
|
|
980
1069
|
const solverReport = selectedTestSet
|
|
@@ -1070,7 +1159,14 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1070
1159
|
.sort((left, right) => gapMetricValue(right, options.metric) - gapMetricValue(left, options.metric) ||
|
|
1071
1160
|
right.measurementLimitations - left.measurementLimitations ||
|
|
1072
1161
|
left.file.localeCompare(right.file));
|
|
1073
|
-
const selectedFiles = page(all, options)
|
|
1162
|
+
const selectedFiles = page(all, options).map((gap) => ({
|
|
1163
|
+
...gap,
|
|
1164
|
+
...(waiverEvaluation
|
|
1165
|
+
? {
|
|
1166
|
+
waivedMcdcConditions: waiverEvaluation.appliedByFile.get(gap.file) ?? 0,
|
|
1167
|
+
}
|
|
1168
|
+
: {}),
|
|
1169
|
+
}));
|
|
1074
1170
|
const pageStart = all.length === 0 ? 0 : options.offset + 1;
|
|
1075
1171
|
const pageEnd = Math.min(options.offset + selectedFiles.length, all.length);
|
|
1076
1172
|
const nextOffset = options.offset + selectedFiles.length;
|
|
@@ -1091,7 +1187,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1091
1187
|
gap.missingMcdcConditions;
|
|
1092
1188
|
const status = missing === 0
|
|
1093
1189
|
? "coverage complete"
|
|
1094
|
-
: `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}`;
|
|
1190
|
+
: `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}${gap.waivedMcdcConditions ? ` (${gap.waivedMcdcConditions} waived)` : ""}`;
|
|
1095
1191
|
const limitations = gap.measurementLimitations
|
|
1096
1192
|
? ` measurement limitations ${gap.measurementLimitations} (${gap.limitationKinds.join(", ")})`
|
|
1097
1193
|
: "";
|
|
@@ -1106,6 +1202,59 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1106
1202
|
if (!selector)
|
|
1107
1203
|
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage file <source-file>");
|
|
1108
1204
|
const file = findFile(report, selector);
|
|
1205
|
+
if (options.group === "decision") {
|
|
1206
|
+
const decisionRows = report.decisions
|
|
1207
|
+
.filter((decision) => decision.meta.file === file)
|
|
1208
|
+
.map((decision) => {
|
|
1209
|
+
const filtered = filterDecision(decision, selectedTestSet);
|
|
1210
|
+
const waived = waiverEvaluation?.waivedByDecision.get(decision.meta.id);
|
|
1211
|
+
const missing = filtered.conditions.filter((condition) => !condition.covered);
|
|
1212
|
+
const waivedMissing = missing.filter((condition) => waived?.has(condition.index));
|
|
1213
|
+
return {
|
|
1214
|
+
id: decision.meta.id,
|
|
1215
|
+
line: decision.meta.line,
|
|
1216
|
+
column: decision.meta.column,
|
|
1217
|
+
kind: decision.meta.kind,
|
|
1218
|
+
conditions: filtered.conditions.length,
|
|
1219
|
+
missingConditions: missing.length,
|
|
1220
|
+
waivedConditions: waivedMissing.length,
|
|
1221
|
+
source: decision.meta.source.replace(/\s+/g, " ").trim(),
|
|
1222
|
+
};
|
|
1223
|
+
});
|
|
1224
|
+
const withMissing = decisionRows.filter((row) => row.missingConditions > 0);
|
|
1225
|
+
const ordered = [...withMissing].sort((left, right) => options.sort === "missing"
|
|
1226
|
+
? right.missingConditions -
|
|
1227
|
+
right.waivedConditions -
|
|
1228
|
+
(left.missingConditions - left.waivedConditions) ||
|
|
1229
|
+
right.missingConditions - left.missingConditions ||
|
|
1230
|
+
left.line - right.line ||
|
|
1231
|
+
left.column - right.column
|
|
1232
|
+
: left.line - right.line ||
|
|
1233
|
+
left.column - right.column ||
|
|
1234
|
+
left.id.localeCompare(right.id));
|
|
1235
|
+
const rows = page(ordered, options);
|
|
1236
|
+
const totals = {
|
|
1237
|
+
decisions: decisionRows.length,
|
|
1238
|
+
decisionsWithMissingConditions: withMissing.length,
|
|
1239
|
+
conditions: decisionRows.reduce((sum, row) => sum + row.conditions, 0),
|
|
1240
|
+
missingConditions: decisionRows.reduce((sum, row) => sum + row.missingConditions, 0),
|
|
1241
|
+
waivedConditions: decisionRows.reduce((sum, row) => sum + row.waivedConditions, 0),
|
|
1242
|
+
};
|
|
1243
|
+
const groupedBase = `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --group decision${options.sort !== "location" ? ` --sort ${options.sort}` : ""}`;
|
|
1244
|
+
const groupedNext = nextPageCommand(groupedBase, ordered.length, rows.length, options);
|
|
1245
|
+
const snippet = (source) => source.length > 96 ? `${source.slice(0, 95)}…` : source;
|
|
1246
|
+
return output({
|
|
1247
|
+
run: run.id,
|
|
1248
|
+
filters: queryFilters(options),
|
|
1249
|
+
file,
|
|
1250
|
+
group: "decision",
|
|
1251
|
+
sort: options.sort,
|
|
1252
|
+
totals,
|
|
1253
|
+
decisions: rows,
|
|
1254
|
+
}, options, `${file} MC/DC by decision\ndecisions ${totals.decisions}, with missing conditions ${totals.decisionsWithMissingConditions}; conditions missing ${totals.missingConditions}/${totals.conditions}${totals.waivedConditions ? `, waived ${totals.waivedConditions}` : ""}\n${rows
|
|
1255
|
+
.map((row) => `${row.line}:${row.column} [${row.id}] missing ${row.missingConditions}/${row.conditions}${row.waivedConditions ? ` (${row.waivedConditions} waived)` : ""} ${snippet(row.source)}`)
|
|
1256
|
+
.join("\n")}\n${pageLabel(ordered.length, rows.length, options)} decisions with missing conditions${groupedNext ? `\nnext page: ${groupedNext}` : ""}`, queryPagination(ordered.length, rows.length, options));
|
|
1257
|
+
}
|
|
1109
1258
|
const uncoveredLines = report.lines
|
|
1110
1259
|
.filter((line) => line.file === file &&
|
|
1111
1260
|
!includesSelectedTest(line.tests, selectedTestSet))
|
|
@@ -1159,6 +1308,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1159
1308
|
column: decision.meta.column,
|
|
1160
1309
|
decision: decision.meta.source,
|
|
1161
1310
|
missingCondition: condition.source,
|
|
1311
|
+
...(waiverEvaluation?.waivedByDecision
|
|
1312
|
+
.get(decision.meta.id)
|
|
1313
|
+
?.has(condition.index)
|
|
1314
|
+
? {
|
|
1315
|
+
waived: true,
|
|
1316
|
+
waiverReason: waiverEvaluation.waivedByDecision
|
|
1317
|
+
.get(decision.meta.id)
|
|
1318
|
+
.get(condition.index).reason,
|
|
1319
|
+
}
|
|
1320
|
+
: {}),
|
|
1162
1321
|
observedVectors: filterDecision(decision, selectedTestSet).vectorObservations.map((observation) => vectorText(observation.vector.values, observation.vector.outcome)),
|
|
1163
1322
|
otherCoverage: otherCoverage(report, (decision.conditions[condition.index]?.witnessTests ?? []).flat(), selectedTestSet),
|
|
1164
1323
|
})));
|
|
@@ -1207,6 +1366,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1207
1366
|
uncoveredFunctions: functions.length,
|
|
1208
1367
|
missingBranches: branches.length,
|
|
1209
1368
|
missingMcdcConditions: mcdc.length,
|
|
1369
|
+
waivedMcdcConditions: mcdc.filter((item) => item.waived).length,
|
|
1210
1370
|
measurementLimitations: allFileLimitations.length,
|
|
1211
1371
|
},
|
|
1212
1372
|
tests,
|
|
@@ -1225,7 +1385,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1225
1385
|
? `function ${item.line}:${item.column}: ${item.source}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
|
|
1226
1386
|
: item.kind === "branch"
|
|
1227
1387
|
? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
|
|
1228
|
-
: `MC/DC ${item.line}:${item.column} [${item.id}]: ${item.missingCondition}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`)
|
|
1388
|
+
: `MC/DC ${item.line}:${item.column} [${item.id}]: ${item.missingCondition}${item.waived ? " [waived]" : ""}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`)
|
|
1229
1389
|
.join("\n")}${selected.length && limitations.length ? "\n" : ""}${limitations.map((limitation) => `LIMITATION ${limitation.kind} ${limitation.line}:${limitation.column} [${limitation.id}]\n ${limitation.reason}\n source: ${limitation.source}\n effect: outside measured denominator`).join("\n")}\n${pageLabel(filePageTotal, filePageReturned, options)} obligations/tests/limitations per category${nextFileCommand ? `\nnext page: ${nextFileCommand}` : ""}`, queryPagination(filePageTotal, filePageReturned, options));
|
|
1230
1390
|
}
|
|
1231
1391
|
if (command === "decision") {
|
|
@@ -1265,13 +1425,20 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1265
1425
|
vectorObservations: decision.vectorObservations.length,
|
|
1266
1426
|
tests: decision.tests.length,
|
|
1267
1427
|
};
|
|
1428
|
+
const waived = waiverEvaluation?.waivedByDecision.get(decision.meta.id);
|
|
1268
1429
|
const vectorObservations = page(decision.vectorObservations, options);
|
|
1269
1430
|
return {
|
|
1270
1431
|
...decision,
|
|
1271
1432
|
totals,
|
|
1272
1433
|
vectors: vectorObservations.map((observation) => observation.vector),
|
|
1273
1434
|
vectorObservations,
|
|
1274
|
-
conditions: page(decision.conditions, options)
|
|
1435
|
+
conditions: page(decision.conditions, options).map((condition) => !condition.covered && waived?.has(condition.index)
|
|
1436
|
+
? {
|
|
1437
|
+
...condition,
|
|
1438
|
+
waived: true,
|
|
1439
|
+
waiverReason: waived.get(condition.index).reason,
|
|
1440
|
+
}
|
|
1441
|
+
: condition),
|
|
1275
1442
|
tests: page(decision.tests, options),
|
|
1276
1443
|
};
|
|
1277
1444
|
});
|
|
@@ -1285,7 +1452,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1285
1452
|
};
|
|
1286
1453
|
return output(result, options, matches
|
|
1287
1454
|
.map((decision) => `${decision.meta.id} ${decision.meta.file}:${decision.meta.line}:${decision.meta.column}\n${decision.meta.source}\n${decision.conditions
|
|
1288
|
-
.map((condition) => `C${condition.index + 1} ${condition.covered ? "covered" : "MISSING"}${condition.assertionCovered ? " + asserted" : ""}: ${condition.source}`)
|
|
1455
|
+
.map((condition) => `C${condition.index + 1} ${condition.covered ? "covered" : condition.waived ? "MISSING (waived)" : "MISSING"}${condition.assertionCovered ? " + asserted" : ""}: ${condition.source}${condition.waiverReason ? `\n waived: ${condition.waiverReason}` : ""}`)
|
|
1289
1456
|
.join("\n")}\nconfidence ${decision.confidence?.level ?? "unknown"}; asserted MC/DC ${decision.conditions.filter((condition) => condition.assertionCovered).length}/${decision.conditions.length}\nvectors:\n${decision.vectorObservations.map((observation) => ` ${vectorText(observation.vector.values, observation.vector.outcome)} tests=${observation.tests.length} confidence=${observation.confidence?.level ?? "unknown"}`).join("\n") || " none"}`)
|
|
1290
1457
|
.join("\n\n") +
|
|
1291
1458
|
`\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
|
|
@@ -1297,6 +1464,52 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
|
|
|
1297
1464
|
throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage covers <source-file:line>");
|
|
1298
1465
|
const location = locationSelector(selector);
|
|
1299
1466
|
const line = report.lines.find((candidate) => candidate.file === location.file && candidate.line === location.line);
|
|
1467
|
+
if (!line) {
|
|
1468
|
+
// "Uncovered" would be a false claim here: nothing is measured on this
|
|
1469
|
+
// exact line. Report what does anchor at it instead of a misleading no.
|
|
1470
|
+
const anchored = [
|
|
1471
|
+
...report.decisions
|
|
1472
|
+
.filter((decision) => decision.meta.file === location.file &&
|
|
1473
|
+
decision.meta.line === location.line)
|
|
1474
|
+
.map((decision) => ({
|
|
1475
|
+
kind: "decision",
|
|
1476
|
+
id: decision.meta.id,
|
|
1477
|
+
column: decision.meta.column,
|
|
1478
|
+
covered: decision.covered,
|
|
1479
|
+
coveredConditions: decision.conditions.filter((condition) => condition.covered).length,
|
|
1480
|
+
conditions: decision.conditions.length,
|
|
1481
|
+
})),
|
|
1482
|
+
...report.branches
|
|
1483
|
+
.filter((branch) => branch.meta.file === location.file &&
|
|
1484
|
+
branch.meta.line === location.line)
|
|
1485
|
+
.map((branch) => ({
|
|
1486
|
+
kind: "branch",
|
|
1487
|
+
id: branch.meta.id,
|
|
1488
|
+
column: branch.meta.column,
|
|
1489
|
+
covered: branch.alternatives.every((alternative) => alternative.covered),
|
|
1490
|
+
})),
|
|
1491
|
+
...report.points
|
|
1492
|
+
.filter((point) => point.meta.file === location.file &&
|
|
1493
|
+
point.meta.line === location.line)
|
|
1494
|
+
.map((point) => ({
|
|
1495
|
+
kind: point.meta.kind,
|
|
1496
|
+
id: point.meta.id,
|
|
1497
|
+
column: point.meta.column,
|
|
1498
|
+
covered: includesSelectedTest(point.tests, selectedTestSet),
|
|
1499
|
+
})),
|
|
1500
|
+
].sort((left, right) => left.column - right.column);
|
|
1501
|
+
const anchoredPage = page(anchored, options);
|
|
1502
|
+
return output({
|
|
1503
|
+
run: run.id,
|
|
1504
|
+
filters: queryFilters(options),
|
|
1505
|
+
location,
|
|
1506
|
+
lineObligation: false,
|
|
1507
|
+
anchored: anchoredPage,
|
|
1508
|
+
totalAnchored: anchored.length,
|
|
1509
|
+
}, options, `${location.file}:${location.line} has no line obligation${anchored.length === 0 ? "; nothing is measured at this exact line" : `; ${anchored.length} obligation(s) anchor here`}\n${anchoredPage
|
|
1510
|
+
.map((obligation) => `${obligation.kind} ${location.line}:${obligation.column} [${obligation.id}] ${obligation.covered ? "covered" : "not fully covered"}${"conditions" in obligation ? ` (${obligation.coveredConditions}/${obligation.conditions} conditions)` : ""}`)
|
|
1511
|
+
.join("\n")}${anchoredPage.length ? "\n" : ""}${pageLabel(anchored.length, anchoredPage.length, options)} anchored obligations`, queryPagination(anchored.length, anchoredPage.length, options));
|
|
1512
|
+
}
|
|
1300
1513
|
const allTests = (line?.tests ?? [])
|
|
1301
1514
|
.filter((id) => !selectedTestSet || selectedTestSet.has(id))
|
|
1302
1515
|
.map((id) => {
|