probatio 0.2.0 → 0.5.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 +10 -169
- package/bin/probatio.js +52 -0
- package/package.json +21 -25
- package/LICENSE +0 -21
- package/dist/cli.js +0 -610
- package/dist/contract.js +0 -31
- package/dist/flags.js +0 -87
- package/dist/golden/check.js +0 -188
- package/dist/ledger/build.js +0 -341
- package/dist/matrix/report.js +0 -256
- package/dist/mcp.js +0 -188
- package/dist/memory/state.js +0 -222
- package/dist/mutate/affected.js +0 -173
- package/dist/mutate/child-lines.mjs +0 -39
- package/dist/mutate/coverage-map.js +0 -229
- package/dist/mutate/csharp-coverage.js +0 -161
- package/dist/mutate/fail-reporter.mjs +0 -65
- package/dist/mutate/find.js +0 -13
- package/dist/mutate/generate.js +0 -153
- package/dist/mutate/go-coverage.js +0 -100
- package/dist/mutate/ids.js +0 -22
- package/dist/mutate/java-coverage.js +0 -217
- package/dist/mutate/kill-label.js +0 -34
- package/dist/mutate/load-failure.mjs +0 -40
- package/dist/mutate/mocha-coverage.cjs +0 -77
- package/dist/mutate/node-batch-loader.mjs +0 -99
- package/dist/mutate/node-batch.mjs +0 -167
- package/dist/mutate/node-coverage.mjs +0 -77
- package/dist/mutate/operators.js +0 -122
- package/dist/mutate/patch.js +0 -136
- package/dist/mutate/precise-lines.mjs +0 -304
- package/dist/mutate/probatio-jacoco-run.java +0 -516
- package/dist/mutate/project-config.js +0 -58
- package/dist/mutate/py-pytest-reporter.py +0 -145
- package/dist/mutate/py-reporter.py +0 -79
- package/dist/mutate/run.js +0 -1333
- package/dist/mutate/rust-coverage.js +0 -144
- package/dist/mutate/sealed.js +0 -118
- package/dist/mutate/suite-decision.js +0 -130
- package/dist/mutate/suites.js +0 -1524
- package/dist/mutate/tally.js +0 -240
- package/dist/mutate/text-operators.js +0 -547
- package/dist/mutate/titles.js +0 -23
- package/dist/node-version.js +0 -13
- package/dist/swarm/check-kill.js +0 -114
- package/dist/swarm/queue.js +0 -68
- package/dist/verify/change.js +0 -340
package/dist/mutate/tally.js
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { SCHEMA_VERSION } from "../contract.js";
|
|
4
|
-
import { isBuildName } from "./kill-label.js";
|
|
5
|
-
import { normalizeResult } from "./run.js";
|
|
6
|
-
import { leadSummary, TIMEOUT_NEXT, WHOLE_PROGRAM_TEST } from "./suite-decision.js";
|
|
7
|
-
const NOT_A_TEST = new Set(["suite", "pytest", "unittest", "command", WHOLE_PROGRAM_TEST]);
|
|
8
|
-
/**
|
|
9
|
-
* Read a finished mutate run. Name tests that killed a mutant, tests that killed
|
|
10
|
-
* nothing, and one gap per survivor. An unseen line is not a gap. This never deletes a file.
|
|
11
|
-
*/
|
|
12
|
-
export function tallyRun(outDir) {
|
|
13
|
-
const resultsDir = path.join(outDir, "results");
|
|
14
|
-
const fullPath = path.join(outDir, "tally.json");
|
|
15
|
-
if (!existsSync(resultsDir)) {
|
|
16
|
-
return {
|
|
17
|
-
schemaVersion: SCHEMA_VERSION,
|
|
18
|
-
ok: false,
|
|
19
|
-
command: "mutate.tally",
|
|
20
|
-
summary: "No mutant results in that directory.",
|
|
21
|
-
next: "Run mutate run, then tally the same --out directory.",
|
|
22
|
-
nextCall: null,
|
|
23
|
-
keep: [],
|
|
24
|
-
noKillsYet: [],
|
|
25
|
-
gaps: [],
|
|
26
|
-
rest: 0,
|
|
27
|
-
pruning: { mode: "advisory", deletedTests: 0, advice: [] },
|
|
28
|
-
full: fullPath,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
const results = readResults(resultsDir);
|
|
32
|
-
if (results.length === 0) {
|
|
33
|
-
return {
|
|
34
|
-
schemaVersion: SCHEMA_VERSION,
|
|
35
|
-
ok: false,
|
|
36
|
-
command: "mutate.tally",
|
|
37
|
-
summary: "No mutant results in that directory.",
|
|
38
|
-
next: "Run mutate run, then tally the same --out directory.",
|
|
39
|
-
nextCall: null,
|
|
40
|
-
keep: [],
|
|
41
|
-
noKillsYet: [],
|
|
42
|
-
gaps: [],
|
|
43
|
-
rest: 0,
|
|
44
|
-
pruning: { mode: "advisory", deletedTests: 0, advice: [] },
|
|
45
|
-
full: fullPath,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
const killCounts = new Map();
|
|
49
|
-
for (const result of results) {
|
|
50
|
-
if (result.outcome !== "killed")
|
|
51
|
-
continue;
|
|
52
|
-
for (const name of result.killedBy ?? []) {
|
|
53
|
-
if (!isTestName(name))
|
|
54
|
-
continue;
|
|
55
|
-
killCounts.set(name, (killCounts.get(name) ?? 0) + 1);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// One test can be spelled two ways: the kill names its file, the line map does not.
|
|
59
|
-
// The first spelling seen wins, and kill spellings are added first.
|
|
60
|
-
const seen = [];
|
|
61
|
-
const see = (name) => {
|
|
62
|
-
if (!isTestName(name))
|
|
63
|
-
return;
|
|
64
|
-
if (seen.some((known) => namesMatch(known, name)))
|
|
65
|
-
return;
|
|
66
|
-
seen.push(name);
|
|
67
|
-
};
|
|
68
|
-
for (const name of killCounts.keys())
|
|
69
|
-
see(name);
|
|
70
|
-
for (const name of coverageNames(path.join(outDir, "coverage-map.json")))
|
|
71
|
-
see(name);
|
|
72
|
-
for (const result of results) {
|
|
73
|
-
for (const name of result.selectedTests ?? [])
|
|
74
|
-
see(name);
|
|
75
|
-
}
|
|
76
|
-
const counts = new Map();
|
|
77
|
-
for (const name of seen)
|
|
78
|
-
counts.set(name, countFor(name, killCounts));
|
|
79
|
-
const keep = [...counts.entries()]
|
|
80
|
-
.filter(([, killed]) => killed > 0)
|
|
81
|
-
.map(([name]) => name)
|
|
82
|
-
.sort();
|
|
83
|
-
// One batch is not evidence for a deletion. These tests saw no mutant die in this batch, nothing more.
|
|
84
|
-
const noKillsYet = [...counts.entries()]
|
|
85
|
-
.filter(([, killed]) => killed === 0)
|
|
86
|
-
.map(([name]) => name)
|
|
87
|
-
.sort();
|
|
88
|
-
const pruning = { mode: "advisory", deletedTests: 0, advice: [] };
|
|
89
|
-
const noCoverage = results.filter((result) => result.outcome === "no coverage").length;
|
|
90
|
-
const survived = results.filter((result) => result.outcome === "survived").length;
|
|
91
|
-
const timeouts = results.filter((result) => result.outcome === "timeout").length;
|
|
92
|
-
const gaps = results
|
|
93
|
-
.filter((result) => result.outcome === "survived")
|
|
94
|
-
.map((result) => ({
|
|
95
|
-
id: result.id,
|
|
96
|
-
file: result.files?.[0]?.file ?? "",
|
|
97
|
-
line: result.files?.[0]?.line ?? 0,
|
|
98
|
-
outcome: "survived",
|
|
99
|
-
}))
|
|
100
|
-
.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.id.localeCompare(b.id));
|
|
101
|
-
const shown = gaps.slice(0, 10);
|
|
102
|
-
const summary = leadSummary(noCoverage, survived, "survived", `${keep.length} tests to keep, ${noKillsYet.length} tests saw no kill in this batch, ${gaps.length} gaps. No test file was deleted.`);
|
|
103
|
-
const notes = [];
|
|
104
|
-
if (noCoverage > 0)
|
|
105
|
-
notes.push(`${noCoverage} no coverage. That line was not seen. It is not a pass and not a gap.`);
|
|
106
|
-
if (gaps.length > 0) {
|
|
107
|
-
notes.push(`First gap is ${gaps[0].id} at ${gaps[0].file}:${gaps[0].line}. A survivor is not a pass. Add one test that fails on that mutant and passes on the code, commit it, then run nextCall.`);
|
|
108
|
-
}
|
|
109
|
-
if (timeouts > 0)
|
|
110
|
-
notes.push(TIMEOUT_NEXT);
|
|
111
|
-
if (notes.length === 0)
|
|
112
|
-
notes.push("No survivor in this run. No test file was deleted.");
|
|
113
|
-
const next = notes.join(" ");
|
|
114
|
-
const complete = {
|
|
115
|
-
schemaVersion: SCHEMA_VERSION,
|
|
116
|
-
ok: true,
|
|
117
|
-
command: "mutate.tally",
|
|
118
|
-
summary,
|
|
119
|
-
next,
|
|
120
|
-
nextCall: nextArgv(outDir, keep, gaps.length > 0),
|
|
121
|
-
keep,
|
|
122
|
-
noKillsYet,
|
|
123
|
-
gaps,
|
|
124
|
-
rest: 0,
|
|
125
|
-
pruning,
|
|
126
|
-
full: fullPath,
|
|
127
|
-
};
|
|
128
|
-
mkdirSync(outDir, { recursive: true });
|
|
129
|
-
writeFileSync(fullPath, `${JSON.stringify(complete, null, 2)}\n`);
|
|
130
|
-
return { ...complete, gaps: shown, rest: gaps.length - shown.length };
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* The command after this tally. A gap reruns the batch on HEAD once the new test is committed.
|
|
134
|
-
* Otherwise the keep ids are rescored at the same commit. Either way the out dir is fresh:
|
|
135
|
-
* the stored out dir would skip every finished mutant and echo the old result.
|
|
136
|
-
*/
|
|
137
|
-
function nextArgv(outDir, keep, hasGaps) {
|
|
138
|
-
if (!hasGaps && keep.length === 0)
|
|
139
|
-
return null;
|
|
140
|
-
let stored = {};
|
|
141
|
-
try {
|
|
142
|
-
stored = JSON.parse(readFileSync(path.join(outDir, "run.json"), "utf8"));
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
stored = {};
|
|
146
|
-
}
|
|
147
|
-
const base = stored.out || outDir;
|
|
148
|
-
const argv = ["mutate", "run"];
|
|
149
|
-
if (stored.package)
|
|
150
|
-
argv.push("--package", stored.package);
|
|
151
|
-
for (const dir of stored.patches ?? [])
|
|
152
|
-
argv.push("--patches", dir);
|
|
153
|
-
argv.push("--out", freshDir(base, hasGaps ? "after-gaps" : "rescore"));
|
|
154
|
-
if (stored.repo)
|
|
155
|
-
argv.push("--repo", stored.repo);
|
|
156
|
-
if (!hasGaps && stored.commit)
|
|
157
|
-
argv.push("--commit", stored.commit);
|
|
158
|
-
if (typeof stored.workers === "number")
|
|
159
|
-
argv.push("--workers", String(stored.workers));
|
|
160
|
-
if (stored.confirm === false)
|
|
161
|
-
argv.push("--no-confirm");
|
|
162
|
-
if (stored.testsDir && stored.testsDir !== "tests")
|
|
163
|
-
argv.push("--tests-dir", stored.testsDir);
|
|
164
|
-
if (typeof stored.suiteTimeoutMs === "number")
|
|
165
|
-
argv.push("--suite-timeout-ms", String(stored.suiteTimeoutMs));
|
|
166
|
-
if (typeof stored.testTimeoutMs === "number")
|
|
167
|
-
argv.push("--test-timeout-ms", String(stored.testTimeoutMs));
|
|
168
|
-
if (stored.direction && stored.direction !== "auto")
|
|
169
|
-
argv.push("--direction", stored.direction);
|
|
170
|
-
// --only-test refuses --suite-command, so a keep rescore uses discovery.
|
|
171
|
-
if (stored.suiteCommand && hasGaps)
|
|
172
|
-
argv.push("--suite-command", stored.suiteCommand);
|
|
173
|
-
if (!hasGaps)
|
|
174
|
-
argv.push(...keep.flatMap((id) => ["--only-test", id]));
|
|
175
|
-
return { argv };
|
|
176
|
-
}
|
|
177
|
-
/** First sibling of the out dir that does not exist yet. The same tally prints the same path. */
|
|
178
|
-
function freshDir(base, label) {
|
|
179
|
-
for (let index = 1;; index++) {
|
|
180
|
-
const candidate = index === 1 ? `${base}.${label}` : `${base}.${label}-${index}`;
|
|
181
|
-
if (!existsSync(candidate))
|
|
182
|
-
return candidate;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
function readResults(dir) {
|
|
186
|
-
const out = [];
|
|
187
|
-
for (const name of readdirSync(dir)) {
|
|
188
|
-
if (!name.endsWith(".json"))
|
|
189
|
-
continue;
|
|
190
|
-
try {
|
|
191
|
-
const parsed = JSON.parse(readFileSync(path.join(dir, name), "utf8"));
|
|
192
|
-
if (!parsed || typeof parsed.outcome !== "string")
|
|
193
|
-
continue;
|
|
194
|
-
if (!parsed.id)
|
|
195
|
-
parsed.id = name.slice(0, -".json".length);
|
|
196
|
-
out.push(normalizeResult(parsed));
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return out;
|
|
203
|
-
}
|
|
204
|
-
function coverageNames(file) {
|
|
205
|
-
if (!existsSync(file))
|
|
206
|
-
return [];
|
|
207
|
-
try {
|
|
208
|
-
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
209
|
-
const names = new Set();
|
|
210
|
-
for (const lines of Object.values(parsed.files ?? {})) {
|
|
211
|
-
for (const tests of Object.values(lines ?? {})) {
|
|
212
|
-
for (const name of tests ?? [])
|
|
213
|
-
names.add(name);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return [...names];
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
function countFor(name, kills) {
|
|
223
|
-
let total = 0;
|
|
224
|
-
for (const [killed, count] of kills) {
|
|
225
|
-
if (namesMatch(name, killed))
|
|
226
|
-
total += count;
|
|
227
|
-
}
|
|
228
|
-
return total;
|
|
229
|
-
}
|
|
230
|
-
function namesMatch(a, b) {
|
|
231
|
-
if (a === b)
|
|
232
|
-
return true;
|
|
233
|
-
return a.endsWith(`::${b}`) || b.endsWith(`::${a}`);
|
|
234
|
-
}
|
|
235
|
-
function isTestName(name) {
|
|
236
|
-
if (!name || isBuildName(name))
|
|
237
|
-
return false;
|
|
238
|
-
const leaf = name.split("::").pop() ?? name;
|
|
239
|
-
return !NOT_A_TEST.has(leaf) && !NOT_A_TEST.has(name);
|
|
240
|
-
}
|