probatio 0.3.0 → 0.5.1
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 +11 -197
- package/bin/probatio.js +52 -0
- package/package.json +22 -29
- package/LICENSE +0 -21
- package/dist/cli.js +0 -721
- package/dist/contract.js +0 -32
- package/dist/flags.js +0 -87
- package/dist/golden/check.js +0 -188
- package/dist/golden/record.js +0 -395
- package/dist/golden/recorder.mjs +0 -78
- package/dist/ledger/build.js +0 -348
- package/dist/ledger/check.js +0 -133
- 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 -156
- 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 -181
- 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 -1335
- 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 -637
- 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/schemas/check-kill.schema.json +0 -93
- package/schemas/findings.add.schema.json +0 -53
- package/schemas/gap.fix.schema.json +0 -53
- package/schemas/gap.revert.schema.json +0 -53
- package/schemas/golden.check.schema.json +0 -89
- package/schemas/golden.compare.schema.json +0 -151
- package/schemas/golden.record.schema.json +0 -126
- package/schemas/guard.check.schema.json +0 -53
- package/schemas/ledger.build.schema.json +0 -132
- package/schemas/ledger.check.schema.json +0 -143
- package/schemas/matrix.report.schema.json +0 -129
- package/schemas/mcp.schema.json +0 -53
- package/schemas/mutate.generate.schema.json +0 -127
- package/schemas/mutate.help.schema.json +0 -53
- package/schemas/mutate.run.schema.json +0 -272
- package/schemas/mutate.sealed.schema.json +0 -53
- package/schemas/mutate.tally.schema.json +0 -137
- package/schemas/queue.claim.schema.json +0 -53
- package/schemas/queue.reap.schema.json +0 -71
- package/schemas/queue.seed.schema.json +0 -71
- package/schemas/schema.schema.json +0 -74
- package/schemas/seal.schema.json +0 -69
- package/schemas/status.schema.json +0 -103
- package/schemas/verify-change.schema.json +0 -222
package/dist/swarm/check-kill.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { SCHEMA_VERSION } from "../contract.js";
|
|
4
|
-
import { directImporters } from "../mutate/affected.js";
|
|
5
|
-
import { filesInDiff } from "../mutate/patch.js";
|
|
6
|
-
import { killLabel } from "../mutate/kill-label.js";
|
|
7
|
-
import { normalizeResult, runMutants } from "../mutate/run.js";
|
|
8
|
-
import { campaignSummary, UNVIABLE_NEXT } from "../mutate/suite-decision.js";
|
|
9
|
-
/** The gap is done only when this mutant is killed and the baseline suite is green. */
|
|
10
|
-
export async function checkKill(id, options) {
|
|
11
|
-
const importers = importersOf(id, options);
|
|
12
|
-
if (importers.length === 0) {
|
|
13
|
-
return {
|
|
14
|
-
schemaVersion: SCHEMA_VERSION,
|
|
15
|
-
ok: false,
|
|
16
|
-
command: "check-kill",
|
|
17
|
-
summary: `${id} is not done. no direct importer in ${options.testsDir}`,
|
|
18
|
-
next: "The task is not done.",
|
|
19
|
-
nextCall: null,
|
|
20
|
-
done: false,
|
|
21
|
-
outcome: null,
|
|
22
|
-
suiteGreen: false,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const report = await runMutants({ ...options, onlyTests: importers, onlyPatch: id, affected: false });
|
|
26
|
-
const baselineFile = path.join(options.outDir, "baseline.json");
|
|
27
|
-
let suiteGreen = false;
|
|
28
|
-
if (existsSync(baselineFile)) {
|
|
29
|
-
const baseline = JSON.parse(readFileSync(baselineFile, "utf8"));
|
|
30
|
-
suiteGreen = baseline.ok === true;
|
|
31
|
-
}
|
|
32
|
-
// A red baseline stops inside the run. Do not read a result that names the broken test.
|
|
33
|
-
if (!report.ok) {
|
|
34
|
-
return {
|
|
35
|
-
schemaVersion: SCHEMA_VERSION,
|
|
36
|
-
ok: false,
|
|
37
|
-
command: "check-kill",
|
|
38
|
-
summary: `${id} is not done. ${report.summary}`,
|
|
39
|
-
next: "The task is not done.",
|
|
40
|
-
nextCall: null,
|
|
41
|
-
done: false,
|
|
42
|
-
outcome: null,
|
|
43
|
-
suiteGreen,
|
|
44
|
-
killedBy: [],
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const resultFile = path.join(options.outDir, "results", `${id}.json`);
|
|
48
|
-
let outcome = null;
|
|
49
|
-
let killedBy = [];
|
|
50
|
-
if (existsSync(resultFile)) {
|
|
51
|
-
const result = normalizeResult(JSON.parse(readFileSync(resultFile, "utf8")));
|
|
52
|
-
outcome = result.outcome ?? null;
|
|
53
|
-
killedBy = Array.isArray(result.killedBy) ? result.killedBy : [];
|
|
54
|
-
}
|
|
55
|
-
// A mutant that does not build needs no test. The task is closed, and it is not called a kill.
|
|
56
|
-
const unviableMutant = suiteGreen && outcome === "unviable";
|
|
57
|
-
const done = (suiteGreen && outcome === "killed") || unviableMutant;
|
|
58
|
-
const label = unviableMutant ? { cause: "build", next: UNVIABLE_NEXT } : done ? killLabel(killedBy) : null;
|
|
59
|
-
const head = checkHead(report);
|
|
60
|
-
return {
|
|
61
|
-
schemaVersion: SCHEMA_VERSION,
|
|
62
|
-
ok: done,
|
|
63
|
-
command: "check-kill",
|
|
64
|
-
summary: unviableMutant
|
|
65
|
-
? `${head} ${id} does not build. No test is needed.`
|
|
66
|
-
: done
|
|
67
|
-
? `${head} ${id} is dead and the suite is green.`
|
|
68
|
-
: `${head} ${id} is not done.`,
|
|
69
|
-
next: label?.next ?? (done ? "The gap-fixing task is done." : "The task is not done."),
|
|
70
|
-
nextCall: null,
|
|
71
|
-
done,
|
|
72
|
-
outcome,
|
|
73
|
-
cause: label?.cause ?? null,
|
|
74
|
-
killedBy,
|
|
75
|
-
suiteGreen,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
/** Same direct-importer set verify-change runs for a diff that touches this mutant's file. */
|
|
79
|
-
function importersOf(id, options) {
|
|
80
|
-
const patch = options.patchDirs.map((dir) => path.join(dir, `${id}.patch`)).find((file) => existsSync(file));
|
|
81
|
-
if (!patch)
|
|
82
|
-
return [];
|
|
83
|
-
const rels = filesInDiff(readFileSync(patch, "utf8")).map((item) => packageRelative(options.repoDir, options.packageDir, item.file));
|
|
84
|
-
return directImporters(options.packageDir, rels, options.testsDir);
|
|
85
|
-
}
|
|
86
|
-
function checkHead(report) {
|
|
87
|
-
const finished = report.killed + report.unviable + report.survived + report.flaky + report.timeouts + report.errors + report.noCoverage;
|
|
88
|
-
return campaignSummary({
|
|
89
|
-
noCoverage: report.noCoverage,
|
|
90
|
-
survived: report.survived,
|
|
91
|
-
killed: report.killed,
|
|
92
|
-
unviable: report.unviable,
|
|
93
|
-
flaky: report.flaky,
|
|
94
|
-
timeouts: report.timeouts,
|
|
95
|
-
errors: report.errors,
|
|
96
|
-
finished,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
function packageRelative(repoDir, packageDir, file) {
|
|
100
|
-
const norm = file.split(path.sep).join("/");
|
|
101
|
-
let repo = repoDir;
|
|
102
|
-
let pkg = packageDir;
|
|
103
|
-
try {
|
|
104
|
-
repo = realpathSync(repoDir);
|
|
105
|
-
pkg = realpathSync(packageDir);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
// A missing path still has a relative prefix.
|
|
109
|
-
}
|
|
110
|
-
const prefix = path.relative(repo, pkg).split(path.sep).join("/");
|
|
111
|
-
if (prefix && prefix !== "." && !prefix.startsWith("..") && norm.startsWith(`${prefix}/`))
|
|
112
|
-
return norm.slice(prefix.length + 1);
|
|
113
|
-
return norm;
|
|
114
|
-
}
|
package/dist/swarm/queue.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { loadSealed } from "../memory/state.js";
|
|
4
|
-
/** One path segment. Rejects slashes and `..` so a claim cannot leave the queue directory. */
|
|
5
|
-
export function safeFilePart(value) {
|
|
6
|
-
if (!/^[A-Za-z0-9._-]+$/.test(value) || value === "." || value === "..")
|
|
7
|
-
return null;
|
|
8
|
-
return value;
|
|
9
|
-
}
|
|
10
|
-
export function seedQueue(root, ids) {
|
|
11
|
-
const sealed = new Set(loadSealed(root));
|
|
12
|
-
const queue = path.join(root, "queue");
|
|
13
|
-
mkdirSync(queue, { recursive: true });
|
|
14
|
-
const written = [];
|
|
15
|
-
for (const id of [...ids].sort()) {
|
|
16
|
-
if (sealed.has(id) || !safeFilePart(id))
|
|
17
|
-
continue;
|
|
18
|
-
const dest = path.join(queue, `${id}.json`);
|
|
19
|
-
if (existsSync(dest))
|
|
20
|
-
continue;
|
|
21
|
-
writeFileSync(dest, `${JSON.stringify({ id })}\n`);
|
|
22
|
-
written.push(id);
|
|
23
|
-
}
|
|
24
|
-
return written;
|
|
25
|
-
}
|
|
26
|
-
/** Claim by atomic rename. A second claim fails because the queue file is already gone. */
|
|
27
|
-
export function claimItem(root, id, agent, now, leaseMs) {
|
|
28
|
-
if (!safeFilePart(id) || !safeFilePart(agent))
|
|
29
|
-
return { ok: false, summary: "agent and id must be a single file name.", file: null };
|
|
30
|
-
const source = path.join(root, "queue", `${id}.json`);
|
|
31
|
-
const claimed = path.join(root, "claimed");
|
|
32
|
-
const dest = path.join(claimed, `${agent}-${id}.json`);
|
|
33
|
-
if (!existsSync(source))
|
|
34
|
-
return { ok: false, summary: `${id} is not in the queue.`, file: null };
|
|
35
|
-
mkdirSync(claimed, { recursive: true });
|
|
36
|
-
// Rename first. Writing the body before the rename lets the other claim overwrite the winner.
|
|
37
|
-
try {
|
|
38
|
-
renameSync(source, dest);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return { ok: false, summary: `${id} was claimed by someone else.`, file: null };
|
|
42
|
-
}
|
|
43
|
-
const body = { id, agent, leasedUntil: now + leaseMs };
|
|
44
|
-
writeFileSync(dest, `${JSON.stringify(body)}\n`);
|
|
45
|
-
return { ok: true, summary: `${agent} claimed ${id} until ${body.leasedUntil}.`, file: dest };
|
|
46
|
-
}
|
|
47
|
-
/** Move claims whose lease has expired back to the queue. */
|
|
48
|
-
export function reapClaims(root, now) {
|
|
49
|
-
const claimed = path.join(root, "claimed");
|
|
50
|
-
const queue = path.join(root, "queue");
|
|
51
|
-
if (!existsSync(claimed))
|
|
52
|
-
return [];
|
|
53
|
-
mkdirSync(queue, { recursive: true });
|
|
54
|
-
const returned = [];
|
|
55
|
-
for (const name of readdirSync(claimed).sort()) {
|
|
56
|
-
if (!name.endsWith(".json"))
|
|
57
|
-
continue;
|
|
58
|
-
const file = path.join(claimed, name);
|
|
59
|
-
const body = JSON.parse(readFileSync(file, "utf8"));
|
|
60
|
-
if (typeof body.leasedUntil !== "number" || body.leasedUntil > now)
|
|
61
|
-
continue;
|
|
62
|
-
if (!body.id || !safeFilePart(body.id))
|
|
63
|
-
continue;
|
|
64
|
-
renameSync(file, path.join(queue, `${body.id}.json`));
|
|
65
|
-
returned.push(body.id);
|
|
66
|
-
}
|
|
67
|
-
return returned.sort();
|
|
68
|
-
}
|
package/dist/verify/change.js
DELETED
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { SCHEMA_VERSION } from "../contract.js";
|
|
4
|
-
import { asTable } from "../golden/check.js";
|
|
5
|
-
import { affectedTests, directImporters } from "../mutate/affected.js";
|
|
6
|
-
import { findInSource } from "../mutate/find.js";
|
|
7
|
-
import { mutantId } from "../mutate/ids.js";
|
|
8
|
-
import { forwardDiff, git } from "../mutate/patch.js";
|
|
9
|
-
import { runMutants } from "../mutate/run.js";
|
|
10
|
-
import { WHOLE_PROGRAM_TEST, leadSummary } from "../mutate/suite-decision.js";
|
|
11
|
-
import { discoverSuite } from "../mutate/suites.js";
|
|
12
|
-
/** Diff-scoped mutants, the tests that can see the edit, and golden contract rows that changed. */
|
|
13
|
-
export async function verifyChange(options) {
|
|
14
|
-
const repo = realpathSync(path.resolve(options.repoDir));
|
|
15
|
-
const packageDir = realpathSync(path.resolve(options.packageDir));
|
|
16
|
-
const prefix = packagePrefix(repo, packageDir);
|
|
17
|
-
// The package may sit inside a large checkout. Diff that directory only.
|
|
18
|
-
// A whole-repo unified diff can exceed the pipe and come back as a failed spawn.
|
|
19
|
-
const scope = prefix ? ["--", prefix] : [];
|
|
20
|
-
const diff = git(repo, ["diff", "-U0", "--no-renames", options.base, options.commit, ...scope]);
|
|
21
|
-
if (diff.status !== 0)
|
|
22
|
-
return fail(options.outDir, diff.stderr.trim() || "git diff failed");
|
|
23
|
-
const ranges = parseUnified(diff.stdout);
|
|
24
|
-
const names = git(repo, ["diff", "--name-only", "--no-renames", options.base, options.commit, ...scope]);
|
|
25
|
-
const changed = names.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
26
|
-
const status = git(repo, ["status", "--porcelain", ...scope]);
|
|
27
|
-
const dirty = status.stdout.trim().length > 0;
|
|
28
|
-
if (dirty && (options.base === options.commit || changed.length === 0)) {
|
|
29
|
-
return fail(options.outDir, "The working tree has uncommitted changes. verify-change did not score them. Commit the edit, or pass --base and --commit that name different commits with a diff.");
|
|
30
|
-
}
|
|
31
|
-
const goldenContract = goldenDiffs(repo, options.base, options.commit, changed);
|
|
32
|
-
const packageFiles = [];
|
|
33
|
-
const chosen = [];
|
|
34
|
-
const libImported = testsImportLib(packageDir);
|
|
35
|
-
for (const [file, lines] of ranges) {
|
|
36
|
-
const rel = toPackage(prefix, file);
|
|
37
|
-
if (!rel || !isVerifySource(rel, packageDir, libImported))
|
|
38
|
-
continue;
|
|
39
|
-
packageFiles.push(rel);
|
|
40
|
-
const text = showFile(repo, options.commit, file);
|
|
41
|
-
if (!text)
|
|
42
|
-
continue;
|
|
43
|
-
const found = findInSource(rel, text, options.operators ?? "core");
|
|
44
|
-
const lineSet = new Set(lines);
|
|
45
|
-
for (const point of found.points) {
|
|
46
|
-
if (!lineSet.has(point.line))
|
|
47
|
-
continue;
|
|
48
|
-
chosen.push({ rel, label: file, text, point });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
chosen.sort((a, b) => a.rel.localeCompare(b.rel) || a.point.line - b.point.line || a.point.op.localeCompare(b.point.op) || a.point.start - b.point.start);
|
|
52
|
-
const capped = chosen.slice(0, options.maxMutants);
|
|
53
|
-
const mutantsDir = path.join(options.outDir, "mutants");
|
|
54
|
-
mkdirSync(mutantsDir, { recursive: true });
|
|
55
|
-
const written = [];
|
|
56
|
-
const notRun = chosen.slice(capped.length).map(locate);
|
|
57
|
-
for (const item of capped) {
|
|
58
|
-
const located = locate(item);
|
|
59
|
-
const mutated = item.text.slice(0, item.point.start) + item.point.replacement + item.text.slice(item.point.end);
|
|
60
|
-
const body = forwardDiff(item.label, item.text, mutated);
|
|
61
|
-
if (!body) {
|
|
62
|
-
notRun.push(located);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
writeFileSync(path.join(mutantsDir, `${located.id}.patch`), body);
|
|
66
|
-
written.push(located);
|
|
67
|
-
}
|
|
68
|
-
writeFileSync(path.join(mutantsDir, "mutants.json"), `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, mutants: written }, null, 2)}\n`);
|
|
69
|
-
const affectedMap = affectedTests(packageDir, { testsDir: "tests" });
|
|
70
|
-
const affected = [...new Set(packageFiles.flatMap((file) => affectedMap[file] ?? []))].sort();
|
|
71
|
-
const direct = directImporters(packageDir, packageFiles, "tests");
|
|
72
|
-
const ran = options.maxTests == null ? direct : direct.slice(0, options.maxTests);
|
|
73
|
-
const notRan = direct.slice(ran.length);
|
|
74
|
-
let caught = [];
|
|
75
|
-
let missed = [];
|
|
76
|
-
let other = [];
|
|
77
|
-
let suiteCommand = "";
|
|
78
|
-
const suite = discoverSuite(packageDir, "tests");
|
|
79
|
-
if (written.length > 0 && suite && suite.files.length > 0) {
|
|
80
|
-
const report = await runMutants({
|
|
81
|
-
packageDir,
|
|
82
|
-
repoDir: repo,
|
|
83
|
-
commit: options.commit,
|
|
84
|
-
patchDirs: [mutantsDir],
|
|
85
|
-
outDir: path.join(options.outDir, "run"),
|
|
86
|
-
direction: "forward",
|
|
87
|
-
workers: 1,
|
|
88
|
-
concurrency: 1,
|
|
89
|
-
maxMinutes: options.maxMinutes,
|
|
90
|
-
maxMutants: null,
|
|
91
|
-
build: null,
|
|
92
|
-
testsDir: "tests",
|
|
93
|
-
unset: ["SOLARI_API_KEY", "AUSPEX_LIVE"],
|
|
94
|
-
suiteTimeoutMs: 600_000,
|
|
95
|
-
testTimeoutMs: 60_000,
|
|
96
|
-
confirm: false,
|
|
97
|
-
affected: false,
|
|
98
|
-
agent: null,
|
|
99
|
-
onProgress: options.onProgress,
|
|
100
|
-
});
|
|
101
|
-
suiteCommand = report.suiteCommand;
|
|
102
|
-
if (!report.ok)
|
|
103
|
-
return fail(options.outDir, report.summary, { affected, ran, notRan, goldenContract, notRun, next: report.next, suiteCommand });
|
|
104
|
-
const byId = new Map(written.map((item) => [item.id, item]));
|
|
105
|
-
const results = readOutcomes(path.join(options.outDir, "run"), written.map((item) => item.id));
|
|
106
|
-
caught = results.filter((item) => item.outcome === "killed").map((item) => byId.get(item.id)).filter((item) => item !== undefined);
|
|
107
|
-
missed = results.filter((item) => item.outcome === "survived").map((item) => byId.get(item.id)).filter((item) => item !== undefined);
|
|
108
|
-
other = results
|
|
109
|
-
.filter((item) => item.outcome !== "killed" && item.outcome !== "survived")
|
|
110
|
-
.map((item) => ({ id: item.id, outcome: item.outcome, file: byId.get(item.id)?.file ?? "", line: byId.get(item.id)?.line ?? 0 }));
|
|
111
|
-
const executed = executedNames(results);
|
|
112
|
-
const envelope = bodyOf(options.outDir, true, affected, ran, notRan, caught, missed, other, goldenContract, notRun, suiteCommand, executed);
|
|
113
|
-
writeFileSync(path.join(options.outDir, "verify.json"), `${JSON.stringify(envelope, null, 2)}\n`);
|
|
114
|
-
return envelope;
|
|
115
|
-
}
|
|
116
|
-
notRun.push(...written);
|
|
117
|
-
const envelope = bodyOf(options.outDir, true, affected, ran, notRan, caught, missed, other, goldenContract, notRun, suiteCommand, []);
|
|
118
|
-
writeFileSync(path.join(options.outDir, "verify.json"), `${JSON.stringify(envelope, null, 2)}\n`);
|
|
119
|
-
return envelope;
|
|
120
|
-
}
|
|
121
|
-
function bodyOf(outDir, ok, affected, ran, notRan, caught, missed, other, goldenContract, notRun, suiteCommand, executed) {
|
|
122
|
-
const uncovered = other.filter((item) => item.outcome === "no coverage");
|
|
123
|
-
const summary = ok
|
|
124
|
-
? leadSummary(uncovered.length, missed.length, "missed", `${caught.length} caught, ${notRun.length} not run, ${affected.length} affected tests, ${goldenContract.length} golden contract changes.`)
|
|
125
|
-
: "verify-change did not finish.";
|
|
126
|
-
const first = missed[0] ?? uncovered[0] ?? notRun[0] ?? caught[0];
|
|
127
|
-
return {
|
|
128
|
-
schemaVersion: SCHEMA_VERSION,
|
|
129
|
-
ok,
|
|
130
|
-
command: "verify-change",
|
|
131
|
-
summary,
|
|
132
|
-
next: first ? `${first.id} at ${first.file}:${first.line}` : "No diff-scoped mutant.",
|
|
133
|
-
nextCall: null,
|
|
134
|
-
affected,
|
|
135
|
-
ran,
|
|
136
|
-
notRan,
|
|
137
|
-
suiteCommand,
|
|
138
|
-
executed,
|
|
139
|
-
confirmed: false,
|
|
140
|
-
caught,
|
|
141
|
-
missed,
|
|
142
|
-
other,
|
|
143
|
-
notRun,
|
|
144
|
-
goldenContract,
|
|
145
|
-
rest: notRun.length,
|
|
146
|
-
full: path.join(outDir, "verify.json"),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
function locate(item) {
|
|
150
|
-
return {
|
|
151
|
-
id: mutantId(item.rel, item.point.start, item.point.end, item.point.op),
|
|
152
|
-
file: item.rel,
|
|
153
|
-
line: item.point.line,
|
|
154
|
-
op: item.point.op,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function fail(outDir, summary, extra) {
|
|
158
|
-
mkdirSync(outDir, { recursive: true });
|
|
159
|
-
const notRun = extra?.notRun ?? [];
|
|
160
|
-
return {
|
|
161
|
-
schemaVersion: SCHEMA_VERSION,
|
|
162
|
-
ok: false,
|
|
163
|
-
command: "verify-change",
|
|
164
|
-
summary,
|
|
165
|
-
next: extra?.next ?? "Fix that and run the same command again.",
|
|
166
|
-
nextCall: null,
|
|
167
|
-
affected: extra?.affected ?? [],
|
|
168
|
-
ran: extra?.ran ?? [],
|
|
169
|
-
notRan: extra?.notRan ?? [],
|
|
170
|
-
suiteCommand: extra?.suiteCommand ?? "",
|
|
171
|
-
executed: [],
|
|
172
|
-
confirmed: false,
|
|
173
|
-
caught: [],
|
|
174
|
-
missed: [],
|
|
175
|
-
other: [],
|
|
176
|
-
notRun,
|
|
177
|
-
goldenContract: extra?.goldenContract ?? [],
|
|
178
|
-
rest: notRun.length,
|
|
179
|
-
full: path.join(outDir, "verify.json"),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
function readOutcomes(outDir, ids) {
|
|
183
|
-
const file = path.join(outDir, "results");
|
|
184
|
-
const out = [];
|
|
185
|
-
for (const id of ids) {
|
|
186
|
-
try {
|
|
187
|
-
const result = JSON.parse(readFileSync(path.join(file, `${id}.json`), "utf8"));
|
|
188
|
-
out.push({ id, outcome: result.outcome, selectedTests: result.selectedTests ?? [] });
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
out.push({ id, outcome: "missing", selectedTests: [] });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return out;
|
|
195
|
-
}
|
|
196
|
-
function executedNames(results) {
|
|
197
|
-
return [...new Set(results.flatMap((item) => item.selectedTests))].filter((name) => name.length > 0 && name !== WHOLE_PROGRAM_TEST).sort();
|
|
198
|
-
}
|
|
199
|
-
function goldenDiffs(repo, base, commit, files) {
|
|
200
|
-
const found = [];
|
|
201
|
-
for (const file of files) {
|
|
202
|
-
if (!file.endsWith(".golden.json") && !file.includes("/golden/"))
|
|
203
|
-
continue;
|
|
204
|
-
if (!file.endsWith(".json"))
|
|
205
|
-
continue;
|
|
206
|
-
const before = asTable(showFile(repo, base, file));
|
|
207
|
-
const after = asTable(showFile(repo, commit, file));
|
|
208
|
-
const ids = [...new Set([...Object.keys(before.rows), ...Object.keys(after.rows)])].sort();
|
|
209
|
-
for (const row of ids) {
|
|
210
|
-
const left = contractOf(before.rows[row]);
|
|
211
|
-
const right = contractOf(after.rows[row]);
|
|
212
|
-
if (JSON.stringify(left) === JSON.stringify(right))
|
|
213
|
-
continue;
|
|
214
|
-
const fields = [...new Set([...Object.keys(left), ...Object.keys(right)])].filter((key) => JSON.stringify(left[key]) !== JSON.stringify(right[key])).sort();
|
|
215
|
-
found.push({ file, row, fields });
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return found;
|
|
219
|
-
}
|
|
220
|
-
function contractOf(row) {
|
|
221
|
-
if (!row)
|
|
222
|
-
return {};
|
|
223
|
-
const out = {};
|
|
224
|
-
for (const key of Object.keys(row).sort()) {
|
|
225
|
-
if (key === "next" || key === "nextLead")
|
|
226
|
-
continue;
|
|
227
|
-
out[key] = row[key];
|
|
228
|
-
}
|
|
229
|
-
return out;
|
|
230
|
-
}
|
|
231
|
-
function parseUnified(diff) {
|
|
232
|
-
const lines = new Map();
|
|
233
|
-
let file = null;
|
|
234
|
-
for (const line of diff.split(/\r?\n/)) {
|
|
235
|
-
const plus = line.match(/^\+\+\+ ([^\t]+)/);
|
|
236
|
-
if (plus) {
|
|
237
|
-
let name = plus[1];
|
|
238
|
-
if (name === "/dev/null") {
|
|
239
|
-
file = null;
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (name.startsWith("a/") || name.startsWith("b/"))
|
|
243
|
-
name = name.slice(2);
|
|
244
|
-
file = name;
|
|
245
|
-
if (!lines.has(file))
|
|
246
|
-
lines.set(file, []);
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
250
|
-
if (!hunk || !file)
|
|
251
|
-
continue;
|
|
252
|
-
const start = Number(hunk[1]);
|
|
253
|
-
const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
|
|
254
|
-
const list = lines.get(file) ?? [];
|
|
255
|
-
for (let offset = 0; offset < count; offset++)
|
|
256
|
-
list.push(start + offset);
|
|
257
|
-
lines.set(file, list);
|
|
258
|
-
}
|
|
259
|
-
return lines;
|
|
260
|
-
}
|
|
261
|
-
function showFile(repo, commit, file) {
|
|
262
|
-
const result = git(repo, ["show", `${commit}:${file}`]);
|
|
263
|
-
return result.status === 0 ? result.stdout : "";
|
|
264
|
-
}
|
|
265
|
-
function packagePrefix(repo, packageDir) {
|
|
266
|
-
const rel = path.relative(repo, packageDir);
|
|
267
|
-
if (rel.startsWith(".."))
|
|
268
|
-
throw new Error("package is outside the repository");
|
|
269
|
-
return rel.split(path.sep).join("/");
|
|
270
|
-
}
|
|
271
|
-
function toPackage(prefix, file) {
|
|
272
|
-
if (!prefix)
|
|
273
|
-
return file;
|
|
274
|
-
if (!file.startsWith(`${prefix}/`))
|
|
275
|
-
return null;
|
|
276
|
-
return file.slice(prefix.length + 1);
|
|
277
|
-
}
|
|
278
|
-
const SKIPPED_SOURCE = new Set(["node_modules", "dist", "docs", "tests", "test"]);
|
|
279
|
-
/** src/, a lib/ tree the tests import, or a Go file beside go.mod. Tests, docs, dist, and node_modules stay out. */
|
|
280
|
-
function isVerifySource(rel, packageDir, libImported) {
|
|
281
|
-
if (rel.endsWith(".d.ts"))
|
|
282
|
-
return false;
|
|
283
|
-
const parts = rel.split("/");
|
|
284
|
-
if (parts.some((part) => SKIPPED_SOURCE.has(part)))
|
|
285
|
-
return false;
|
|
286
|
-
if (rel.startsWith("src/"))
|
|
287
|
-
return true;
|
|
288
|
-
if (libImported && rel.startsWith("lib/"))
|
|
289
|
-
return true;
|
|
290
|
-
if (!rel.includes("/") && rel.endsWith(".go") && !rel.endsWith("_test.go") && existsSync(path.join(packageDir, "go.mod")))
|
|
291
|
-
return true;
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
function testsImportLib(packageDir) {
|
|
295
|
-
const pattern = /(?:require\s*\(\s*|from\s+)['"](?:@[^'"]+\/)?(?:\.\.\/|\.\/)*lib\//;
|
|
296
|
-
for (const dirName of ["tests", "test"]) {
|
|
297
|
-
const dir = path.join(packageDir, dirName);
|
|
298
|
-
if (!existsSync(dir))
|
|
299
|
-
continue;
|
|
300
|
-
if (walkText(dir).some((text) => pattern.test(text)))
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
function walkText(dir) {
|
|
306
|
-
const out = [];
|
|
307
|
-
const visit = (current) => {
|
|
308
|
-
let entries;
|
|
309
|
-
try {
|
|
310
|
-
entries = readdirSync(current);
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
for (const name of entries) {
|
|
316
|
-
if (name === "node_modules" || name === "dist")
|
|
317
|
-
continue;
|
|
318
|
-
const full = path.join(current, name);
|
|
319
|
-
let info;
|
|
320
|
-
try {
|
|
321
|
-
info = statSync(full);
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
if (info.isDirectory())
|
|
327
|
-
visit(full);
|
|
328
|
-
else if (info.isFile() && info.size < 1_000_000) {
|
|
329
|
-
try {
|
|
330
|
-
out.push(readFileSync(full, "utf8"));
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
// An unreadable test file does not count as an import of lib/.
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
visit(dir);
|
|
339
|
-
return out;
|
|
340
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://github.com/IronAdamant/Probatio/blob/main/schemas/check-kill.schema.json",
|
|
4
|
-
"title": "probatio check-kill",
|
|
5
|
-
"description": "Done only when the mutant is killed (or does not build) and the suite is still green. Every command prints one JSON object. schemaVersion changes when a field is renamed, removed, or changes meaning. A new field does not change it.",
|
|
6
|
-
"type": "object",
|
|
7
|
-
"required": [
|
|
8
|
-
"schemaVersion",
|
|
9
|
-
"ok",
|
|
10
|
-
"command",
|
|
11
|
-
"summary",
|
|
12
|
-
"next",
|
|
13
|
-
"nextCall"
|
|
14
|
-
],
|
|
15
|
-
"properties": {
|
|
16
|
-
"schemaVersion": {
|
|
17
|
-
"const": 2
|
|
18
|
-
},
|
|
19
|
-
"ok": {
|
|
20
|
-
"type": "boolean"
|
|
21
|
-
},
|
|
22
|
-
"command": {
|
|
23
|
-
"const": "check-kill"
|
|
24
|
-
},
|
|
25
|
-
"summary": {
|
|
26
|
-
"type": "string"
|
|
27
|
-
},
|
|
28
|
-
"next": {
|
|
29
|
-
"type": "string"
|
|
30
|
-
},
|
|
31
|
-
"nextCall": {
|
|
32
|
-
"oneOf": [
|
|
33
|
-
{
|
|
34
|
-
"type": "null"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"type": "object",
|
|
38
|
-
"properties": {
|
|
39
|
-
"argv": {
|
|
40
|
-
"type": "array",
|
|
41
|
-
"items": {
|
|
42
|
-
"type": "string"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"required": [
|
|
47
|
-
"argv"
|
|
48
|
-
]
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
},
|
|
52
|
-
"done": {
|
|
53
|
-
"type": "boolean"
|
|
54
|
-
},
|
|
55
|
-
"outcome": {
|
|
56
|
-
"type": [
|
|
57
|
-
"string",
|
|
58
|
-
"null"
|
|
59
|
-
]
|
|
60
|
-
},
|
|
61
|
-
"cause": {
|
|
62
|
-
"enum": [
|
|
63
|
-
null,
|
|
64
|
-
"test",
|
|
65
|
-
"build"
|
|
66
|
-
]
|
|
67
|
-
},
|
|
68
|
-
"killedBy": {
|
|
69
|
-
"type": "array",
|
|
70
|
-
"items": {
|
|
71
|
-
"type": "string"
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
"suiteGreen": {
|
|
75
|
-
"type": "boolean"
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
"if": {
|
|
79
|
-
"properties": {
|
|
80
|
-
"ok": {
|
|
81
|
-
"const": true
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
"then": {
|
|
86
|
-
"required": [
|
|
87
|
-
"done",
|
|
88
|
-
"outcome",
|
|
89
|
-
"killedBy",
|
|
90
|
-
"suiteGreen"
|
|
91
|
-
]
|
|
92
|
-
}
|
|
93
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://github.com/IronAdamant/Probatio/blob/main/schemas/findings.add.schema.json",
|
|
4
|
-
"title": "probatio findings.add",
|
|
5
|
-
"description": "Appends a decision to the findings log. Every command prints one JSON object. schemaVersion changes when a field is renamed, removed, or changes meaning. A new field does not change it.",
|
|
6
|
-
"type": "object",
|
|
7
|
-
"required": [
|
|
8
|
-
"schemaVersion",
|
|
9
|
-
"ok",
|
|
10
|
-
"command",
|
|
11
|
-
"summary",
|
|
12
|
-
"next",
|
|
13
|
-
"nextCall"
|
|
14
|
-
],
|
|
15
|
-
"properties": {
|
|
16
|
-
"schemaVersion": {
|
|
17
|
-
"const": 2
|
|
18
|
-
},
|
|
19
|
-
"ok": {
|
|
20
|
-
"type": "boolean"
|
|
21
|
-
},
|
|
22
|
-
"command": {
|
|
23
|
-
"const": "findings.add"
|
|
24
|
-
},
|
|
25
|
-
"summary": {
|
|
26
|
-
"type": "string"
|
|
27
|
-
},
|
|
28
|
-
"next": {
|
|
29
|
-
"type": "string"
|
|
30
|
-
},
|
|
31
|
-
"nextCall": {
|
|
32
|
-
"oneOf": [
|
|
33
|
-
{
|
|
34
|
-
"type": "null"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"type": "object",
|
|
38
|
-
"properties": {
|
|
39
|
-
"argv": {
|
|
40
|
-
"type": "array",
|
|
41
|
-
"items": {
|
|
42
|
-
"type": "string"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"required": [
|
|
47
|
-
"argv"
|
|
48
|
-
]
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|