probatio 0.0.0-stage → 0.0.2
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 +83 -2
- package/dist/cli.js +499 -0
- package/dist/contract.js +29 -0
- package/dist/flags.js +87 -0
- package/dist/golden/check.js +188 -0
- package/dist/ledger/build.js +327 -0
- package/dist/matrix/report.js +256 -0
- package/dist/mcp.js +116 -0
- package/dist/memory/state.js +222 -0
- package/dist/mutate/affected.js +173 -0
- package/dist/mutate/coverage-map.js +172 -0
- package/dist/mutate/fail-reporter.mjs +44 -0
- package/dist/mutate/find.js +13 -0
- package/dist/mutate/generate.js +132 -0
- package/dist/mutate/go-coverage.js +100 -0
- package/dist/mutate/ids.js +22 -0
- package/dist/mutate/java-coverage.js +188 -0
- package/dist/mutate/kill-label.js +34 -0
- package/dist/mutate/node-coverage.mjs +245 -0
- package/dist/mutate/operators.js +122 -0
- package/dist/mutate/patch.js +135 -0
- package/dist/mutate/probatio-jacoco-run.java +81 -0
- package/dist/mutate/py-pytest-reporter.py +145 -0
- package/dist/mutate/py-reporter.py +79 -0
- package/dist/mutate/run.js +868 -0
- package/dist/mutate/suite-decision.js +129 -0
- package/dist/mutate/suites.js +1371 -0
- package/dist/mutate/tally.js +175 -0
- package/dist/mutate/text-operators.js +547 -0
- package/dist/mutate/titles.js +23 -0
- package/dist/swarm/check-kill.js +107 -0
- package/dist/swarm/queue.js +62 -0
- package/dist/verify/change.js +262 -0
- package/package.json +33 -4
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Pure suite decisions. The process spawn stays in the runner. */
|
|
2
|
+
export const UNKNOWN_NEXT = "The suite command is unknown. Pass --suite-command with the command that runs this package's tests.";
|
|
3
|
+
export const TIMEOUT_NEXT = "Timed out. The mutant ran longer than the baseline allows.";
|
|
4
|
+
export const WHOLE_SUITE_NEXT = "The suite command cannot take a test name, so the whole suite ran.";
|
|
5
|
+
export const TIMEOUT_MULTIPLE = 5;
|
|
6
|
+
/** cargo-mutants uses 20s. A fixture can pass a smaller positive floor. */
|
|
7
|
+
export const TIMEOUT_FLOOR_MS = 20_000;
|
|
8
|
+
/** Marker on a coverage map when the binary has no per-test names. */
|
|
9
|
+
export const WHOLE_PROGRAM_TEST = "suite";
|
|
10
|
+
export function cannotRunNext(artifact) {
|
|
11
|
+
return `The suite cannot run. ${artifact} is missing and the recipe would download it. Nothing was fetched.`;
|
|
12
|
+
}
|
|
13
|
+
export function resumeNext(ids) {
|
|
14
|
+
const listed = ids.length > 0 ? ` Not started: ${ids.join(", ")}.` : "";
|
|
15
|
+
return `Budget stopped the run.${listed} The same command resumes from the mutants already written.`;
|
|
16
|
+
}
|
|
17
|
+
export function noCoverageNext(file, line) {
|
|
18
|
+
return `No coverage for ${file}:${line}. No test executed that line.`;
|
|
19
|
+
}
|
|
20
|
+
/** Every agent-facing count starts with unseen lines, then survivors. */
|
|
21
|
+
export function leadSummary(noCoverage, survivors, survivorWord, rest) {
|
|
22
|
+
return `${noCoverage} no coverage, ${survivors} ${survivorWord}, ${rest}`;
|
|
23
|
+
}
|
|
24
|
+
export function campaignSummary(counts) {
|
|
25
|
+
return leadSummary(counts.noCoverage, counts.survived, "survived", `${counts.killed} killed, ${counts.flaky} flaky, ${counts.timeouts} timed out, ${counts.errors} errored, of ${counts.finished} finished.`);
|
|
26
|
+
}
|
|
27
|
+
export function timeoutLimitMs(baselineMs, multiple, floorMs) {
|
|
28
|
+
const floor = Math.max(1, floorMs);
|
|
29
|
+
const scaled = Math.ceil(Math.max(0, baselineMs) * Math.max(1, multiple));
|
|
30
|
+
return Math.max(floor, scaled);
|
|
31
|
+
}
|
|
32
|
+
/** Kinds that collect a per-line map on the baseline. Other kinds pay the whole suite. */
|
|
33
|
+
const LINE_MAP_KINDS = new Set(["node", "pytest", "c", "go", "maven"]);
|
|
34
|
+
/** A baseline this long, with no line map, is too expensive to repeat for a large batch. */
|
|
35
|
+
export const SLOW_BASELINE_MS = 5_000;
|
|
36
|
+
/** Pending mutants above this count, on a slow unmapped suite, are not started. */
|
|
37
|
+
export const LARGE_BATCH = 30;
|
|
38
|
+
export function hasLineMap(kind) {
|
|
39
|
+
return LINE_MAP_KINDS.has(kind);
|
|
40
|
+
}
|
|
41
|
+
export function scaleStop(input) {
|
|
42
|
+
if (hasLineMap(input.kind))
|
|
43
|
+
return { action: "run" };
|
|
44
|
+
if (input.baselineMs == null || input.baselineMs < SLOW_BASELINE_MS)
|
|
45
|
+
return { action: "run" };
|
|
46
|
+
if (input.pending <= LARGE_BATCH)
|
|
47
|
+
return { action: "run" };
|
|
48
|
+
const seconds = (input.baselineMs / 1000).toFixed(1);
|
|
49
|
+
const next = `No line map for this suite. The baseline took ${seconds}s and ${input.pending} mutants are waiting. Narrow --src or pass a patch slice. Nothing was started.`;
|
|
50
|
+
return { action: "stop", summary: next, next };
|
|
51
|
+
}
|
|
52
|
+
/** What generate tells the agent before any mutant runs. */
|
|
53
|
+
export function generateNext(count, suite) {
|
|
54
|
+
if (count === 0)
|
|
55
|
+
return "Nothing to run.";
|
|
56
|
+
if (!suite)
|
|
57
|
+
return `${count} mutants. No suite was discovered. Pass --suite-command when you run the batch.`;
|
|
58
|
+
const map = hasLineMap(suite.kind)
|
|
59
|
+
? "The first run pays that suite once, then a line map narrows each mutant."
|
|
60
|
+
: "This suite has no line map. A baseline of 5 seconds or more with more than 30 mutants stops before the first mutant.";
|
|
61
|
+
return `${count} mutants. Suite is ${suite.command}. ${map}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Unknown layout asks for a command. An explicit command is what runs.
|
|
65
|
+
* `make test` that would curl or wget a missing file does not run.
|
|
66
|
+
*/
|
|
67
|
+
export function decideSuite(input) {
|
|
68
|
+
const command = input.suiteCommand?.trim() ? input.suiteCommand.trim() : input.discovered?.command ?? "";
|
|
69
|
+
if (isMakeTest(command) && input.makefile) {
|
|
70
|
+
const artifact = missingFetchArtifact(input.makefile, input.artifactExists);
|
|
71
|
+
if (artifact) {
|
|
72
|
+
const next = cannotRunNext(artifact);
|
|
73
|
+
return { action: "cannot-run", summary: next, next, command: "make test" };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (input.suiteCommand?.trim())
|
|
77
|
+
return { action: "run" };
|
|
78
|
+
if (input.discovered)
|
|
79
|
+
return { action: "run" };
|
|
80
|
+
if (input.candidates.length > 0) {
|
|
81
|
+
return { action: "unknown", summary: "The suite command is unknown.", next: UNKNOWN_NEXT };
|
|
82
|
+
}
|
|
83
|
+
return { action: "no-tests" };
|
|
84
|
+
}
|
|
85
|
+
export function isMakeTest(command) {
|
|
86
|
+
return /^\s*make\s+test\b/.test(command);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Output paths of curl or wget recipes whose file is not on disk.
|
|
90
|
+
* Returns the base name, which is the artifact named in `next`.
|
|
91
|
+
* A Makefile that fetches for `test` is refused even when variable
|
|
92
|
+
* expansion cannot prove the edge, so a recipe cannot download a jar.
|
|
93
|
+
*/
|
|
94
|
+
export function missingFetchArtifact(makefile, exists) {
|
|
95
|
+
const lines = makefile.split(/\r?\n/);
|
|
96
|
+
for (const raw of lines) {
|
|
97
|
+
if (/^\s*#/.test(raw))
|
|
98
|
+
continue;
|
|
99
|
+
const line = raw.trim();
|
|
100
|
+
if (!/\b(curl|wget)\b/.test(line))
|
|
101
|
+
continue;
|
|
102
|
+
const artifact = artifactOf(line);
|
|
103
|
+
if (!artifact)
|
|
104
|
+
continue;
|
|
105
|
+
const rel = artifact.replace(/^\.\//, "");
|
|
106
|
+
if (exists(rel) || exists(pathBase(rel)))
|
|
107
|
+
continue;
|
|
108
|
+
return pathBase(rel);
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function artifactOf(line) {
|
|
113
|
+
const flagged = line.match(/(?:-o|--output|-O|--output-document)(?:=|\s+)["']?([^\s"']+)["']?/);
|
|
114
|
+
if (flagged && !flagged[1].startsWith("$") && !flagged[1].startsWith("-"))
|
|
115
|
+
return flagged[1];
|
|
116
|
+
const url = line.match(/https?:\/\/\S+/);
|
|
117
|
+
if (url) {
|
|
118
|
+
const base = url[0].replace(/["']$/, "").split("?")[0].split("/").pop() ?? "";
|
|
119
|
+
if (base.includes("."))
|
|
120
|
+
return base;
|
|
121
|
+
}
|
|
122
|
+
if (flagged && !flagged[1].startsWith("-"))
|
|
123
|
+
return flagged[1];
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function pathBase(rel) {
|
|
127
|
+
const parts = rel.split(/[/\\]/);
|
|
128
|
+
return parts[parts.length - 1] || rel;
|
|
129
|
+
}
|