probatio 0.3.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.
Files changed (74) hide show
  1. package/README.md +10 -198
  2. package/bin/probatio.js +52 -0
  3. package/package.json +21 -29
  4. package/LICENSE +0 -21
  5. package/dist/cli.js +0 -721
  6. package/dist/contract.js +0 -32
  7. package/dist/flags.js +0 -87
  8. package/dist/golden/check.js +0 -188
  9. package/dist/golden/record.js +0 -395
  10. package/dist/golden/recorder.mjs +0 -78
  11. package/dist/ledger/build.js +0 -348
  12. package/dist/ledger/check.js +0 -133
  13. package/dist/matrix/report.js +0 -256
  14. package/dist/mcp.js +0 -188
  15. package/dist/memory/state.js +0 -222
  16. package/dist/mutate/affected.js +0 -173
  17. package/dist/mutate/child-lines.mjs +0 -39
  18. package/dist/mutate/coverage-map.js +0 -229
  19. package/dist/mutate/csharp-coverage.js +0 -161
  20. package/dist/mutate/fail-reporter.mjs +0 -65
  21. package/dist/mutate/find.js +0 -13
  22. package/dist/mutate/generate.js +0 -156
  23. package/dist/mutate/go-coverage.js +0 -100
  24. package/dist/mutate/ids.js +0 -22
  25. package/dist/mutate/java-coverage.js +0 -217
  26. package/dist/mutate/kill-label.js +0 -34
  27. package/dist/mutate/load-failure.mjs +0 -40
  28. package/dist/mutate/mocha-coverage.cjs +0 -77
  29. package/dist/mutate/node-batch-loader.mjs +0 -99
  30. package/dist/mutate/node-batch.mjs +0 -167
  31. package/dist/mutate/node-coverage.mjs +0 -77
  32. package/dist/mutate/operators.js +0 -181
  33. package/dist/mutate/patch.js +0 -136
  34. package/dist/mutate/precise-lines.mjs +0 -304
  35. package/dist/mutate/probatio-jacoco-run.java +0 -516
  36. package/dist/mutate/project-config.js +0 -58
  37. package/dist/mutate/py-pytest-reporter.py +0 -145
  38. package/dist/mutate/py-reporter.py +0 -79
  39. package/dist/mutate/run.js +0 -1335
  40. package/dist/mutate/rust-coverage.js +0 -144
  41. package/dist/mutate/sealed.js +0 -118
  42. package/dist/mutate/suite-decision.js +0 -130
  43. package/dist/mutate/suites.js +0 -1524
  44. package/dist/mutate/tally.js +0 -240
  45. package/dist/mutate/text-operators.js +0 -637
  46. package/dist/mutate/titles.js +0 -23
  47. package/dist/node-version.js +0 -13
  48. package/dist/swarm/check-kill.js +0 -114
  49. package/dist/swarm/queue.js +0 -68
  50. package/dist/verify/change.js +0 -340
  51. package/schemas/check-kill.schema.json +0 -93
  52. package/schemas/findings.add.schema.json +0 -53
  53. package/schemas/gap.fix.schema.json +0 -53
  54. package/schemas/gap.revert.schema.json +0 -53
  55. package/schemas/golden.check.schema.json +0 -89
  56. package/schemas/golden.compare.schema.json +0 -151
  57. package/schemas/golden.record.schema.json +0 -126
  58. package/schemas/guard.check.schema.json +0 -53
  59. package/schemas/ledger.build.schema.json +0 -132
  60. package/schemas/ledger.check.schema.json +0 -143
  61. package/schemas/matrix.report.schema.json +0 -129
  62. package/schemas/mcp.schema.json +0 -53
  63. package/schemas/mutate.generate.schema.json +0 -127
  64. package/schemas/mutate.help.schema.json +0 -53
  65. package/schemas/mutate.run.schema.json +0 -272
  66. package/schemas/mutate.sealed.schema.json +0 -53
  67. package/schemas/mutate.tally.schema.json +0 -137
  68. package/schemas/queue.claim.schema.json +0 -53
  69. package/schemas/queue.reap.schema.json +0 -71
  70. package/schemas/queue.seed.schema.json +0 -71
  71. package/schemas/schema.schema.json +0 -74
  72. package/schemas/seal.schema.json +0 -69
  73. package/schemas/status.schema.json +0 -103
  74. package/schemas/verify-change.schema.json +0 -222
@@ -1,156 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
3
- import path from "node:path";
4
- import { mutantId, makeRng, shuffle } from "./ids.js";
5
- import { findInSource, isGeneratedSource } from "./find.js";
6
- import { forwardDiff } from "./patch.js";
7
- export function generateMutants(options) {
8
- const started = Date.now();
9
- const limit = options.maxMinutes;
10
- const over = () => limit != null && Date.now() - started >= limit * 60_000;
11
- const sourceDefault = options.workingTree ? "working-tree" : "commit";
12
- if (over())
13
- return { mutants: [], violations: [], filesVisited: 0, budgetHit: true, source: sourceDefault };
14
- const srcRoot = path.resolve(options.packageDir, options.srcDir);
15
- const walked = walkSources(srcRoot, over);
16
- const files = walked.files.filter((file) => !options.skipFiles.includes(path.basename(file)));
17
- const rng = makeRng(options.seed);
18
- const violations = [];
19
- const chosen = [];
20
- let filesVisited = 0;
21
- let budgetHit = walked.stopped;
22
- let source = sourceDefault;
23
- for (const file of files) {
24
- if (over()) {
25
- budgetHit = true;
26
- break;
27
- }
28
- filesVisited += 1;
29
- const loaded = fileText(options.packageDir, file, options);
30
- if ("error" in loaded)
31
- return { mutants: [], violations: [], filesVisited, budgetHit, source: "commit", error: loaded.error };
32
- source = loaded.source;
33
- const text = loaded.text;
34
- const rel = posix(path.relative(options.packageDir, file));
35
- const found = findInSource(rel, text, options.operators ?? "core");
36
- violations.push(...found.violations);
37
- const ordered = shuffle(found.points, rng);
38
- const slice = options.perFile === null ? ordered.slice(options.skip) : ordered.slice(options.skip, options.skip + options.perFile);
39
- for (const point of slice)
40
- chosen.push({ rel, text, point });
41
- }
42
- if (violations.length > 0)
43
- return { mutants: [], violations, filesVisited, budgetHit, source };
44
- const capped = options.maxMutants === null ? chosen : chosen.slice(0, options.maxMutants);
45
- const packageReal = real(options.packageDir);
46
- const patchRoot = gitRoot(packageReal);
47
- const prefix = patchRoot ? path.relative(patchRoot, packageReal) : "";
48
- const mutantsDir = path.join(options.outDir, "mutants");
49
- rmSync(mutantsDir, { recursive: true, force: true });
50
- mkdirSync(mutantsDir, { recursive: true });
51
- const mutants = [];
52
- for (const item of capped) {
53
- const id = mutantId(item.rel, item.point.start, item.point.end, item.point.op);
54
- const mutated = item.text.slice(0, item.point.start) + item.point.replacement + item.text.slice(item.point.end);
55
- const label = prefix && !prefix.startsWith("..") ? posix(path.join(prefix, item.rel)) : item.rel;
56
- const diff = forwardDiff(label, item.text, mutated);
57
- if (!diff)
58
- continue;
59
- const patch = `${id}.patch`;
60
- writeFileSync(path.join(mutantsDir, patch), diff);
61
- mutants.push({ id, file: item.rel, line: item.point.line, op: item.point.op, patch });
62
- }
63
- writeFileSync(path.join(mutantsDir, "mutants.json"), `${JSON.stringify({ schemaVersion: 1, seed: options.seed, operators: options.operators ?? "core", mutants }, null, 2)}\n`);
64
- return { mutants, violations, filesVisited, budgetHit, source };
65
- }
66
- function fileText(packageDir, absFile, options) {
67
- if (options.workingTree)
68
- return { text: readFileSync(absFile, "utf8"), source: "working-tree" };
69
- const root = gitRoot(packageDir);
70
- if (!root)
71
- return { text: readFileSync(absFile, "utf8"), source: "working-tree" };
72
- const rel = posix(path.relative(root, real(absFile)));
73
- const commit = options.commit && options.commit.length > 0 ? options.commit : "HEAD";
74
- const shown = spawnSync("git", ["-C", root, "show", `${commit}:${rel}`], { encoding: "utf8" });
75
- if (shown.status !== 0) {
76
- const shownRel = posix(path.relative(packageDir, absFile));
77
- return { error: `${shownRel} is not in ${commit}. Pass --working-tree to read the checkout.` };
78
- }
79
- return { text: shown.stdout, source: "commit" };
80
- }
81
- function walkSources(dir, over) {
82
- if (!statExists(dir))
83
- return { files: [], stopped: false };
84
- // `--src src/text.ts` mutates that one file.
85
- if (statSync(dir).isFile())
86
- return { files: isSource(path.basename(dir)) ? [dir] : [], stopped: false };
87
- const out = [];
88
- let stopped = false;
89
- const walk = (current) => {
90
- if (over()) {
91
- stopped = true;
92
- return;
93
- }
94
- let entries;
95
- try {
96
- entries = readdirSync(current).sort();
97
- }
98
- catch {
99
- return;
100
- }
101
- for (const entry of entries) {
102
- if (over()) {
103
- stopped = true;
104
- return;
105
- }
106
- if (entry === "node_modules" || entry === ".git")
107
- continue;
108
- const full = path.join(current, entry);
109
- let info;
110
- try {
111
- info = statSync(full);
112
- }
113
- catch {
114
- continue;
115
- }
116
- if (info.isDirectory()) {
117
- walk(full);
118
- if (stopped)
119
- return;
120
- }
121
- else if (isSource(entry))
122
- out.push(full);
123
- }
124
- };
125
- walk(dir);
126
- return { files: out, stopped };
127
- }
128
- function isSource(name) {
129
- return isGeneratedSource(name);
130
- }
131
- function statExists(file) {
132
- try {
133
- statSync(file);
134
- return true;
135
- }
136
- catch {
137
- return false;
138
- }
139
- }
140
- function gitRoot(cwd) {
141
- const result = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
142
- if (result.status !== 0)
143
- return null;
144
- return real(result.stdout.trim());
145
- }
146
- function real(file) {
147
- try {
148
- return realpathSync(file);
149
- }
150
- catch {
151
- return path.resolve(file);
152
- }
153
- }
154
- function posix(file) {
155
- return file.split(path.sep).join("/");
156
- }
@@ -1,100 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import path from "node:path";
4
- /** Blocks in a Go cover profile whose count is above zero. */
5
- export function parseCoverProfile(text, modulePath) {
6
- const hits = [];
7
- const prefix = modulePath ? `${modulePath}/` : "";
8
- for (const raw of text.split("\n")) {
9
- const line = raw.trim();
10
- if (!line || line.startsWith("mode:"))
11
- continue;
12
- const match = /^(.+):(\d+)\.\d+,(\d+)\.\d+\s+\d+\s+(\d+)$/.exec(line);
13
- if (!match)
14
- continue;
15
- if (Number(match[4]) <= 0)
16
- continue;
17
- let file = match[1];
18
- if (prefix && file.startsWith(prefix))
19
- file = file.slice(prefix.length);
20
- file = file.split("\\").join("/");
21
- const start = Number(match[2]);
22
- const end = Number(match[3]);
23
- for (let number = start; number <= end; number++)
24
- hits.push({ file, line: number });
25
- }
26
- return hits;
27
- }
28
- /**
29
- * One compiled test binary per package, then one profile per test name.
30
- * A stock cover profile does not name the test, so each test gets its own.
31
- */
32
- export function writeGoCoverage(pkg, testNames, dest) {
33
- const wanted = new Set(testNames.filter((name) => name.length > 0 && !name.includes("/")));
34
- if (wanted.size === 0)
35
- return { ok: false, detail: "go coverage: no test names" };
36
- const mod = run("go", ["list", "-m"], pkg);
37
- if (mod.status !== 0)
38
- return { ok: false, detail: clip(mod.stderr || mod.stdout || mod.error?.message || "go list -m failed") };
39
- const modulePath = mod.stdout.trim().split("\n")[0]?.trim() ?? "";
40
- const listed = run("go", ["list", "-f", "{{.ImportPath}}\t{{.Dir}}\t{{len .TestGoFiles}}\t{{len .XTestGoFiles}}", "./..."], pkg);
41
- if (listed.status !== 0)
42
- return { ok: false, detail: clip(listed.stderr || listed.stdout || listed.error?.message || "go list failed") };
43
- const work = path.join(path.dirname(dest), "go-cover");
44
- mkdirSync(work, { recursive: true });
45
- const files = {};
46
- const packages = listed.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
47
- for (const row of packages) {
48
- const [importPath, dir, testFiles, xTestFiles] = row.split("\t");
49
- if (!importPath || !dir)
50
- continue;
51
- if (Number(testFiles ?? "0") + Number(xTestFiles ?? "0") <= 0)
52
- continue;
53
- const bin = path.join(work, `${safe(importPath)}.test`);
54
- const compiled = run("go", ["test", "-c", "-covermode=set", "-o", bin, importPath], pkg);
55
- if (compiled.status !== 0 || compiled.error)
56
- continue;
57
- const listedTests = run(bin, ["-test.list", "."], dir);
58
- if (listedTests.status !== 0)
59
- continue;
60
- for (const name of listedTests.stdout.split("\n").map((item) => item.trim()).filter((item) => wanted.has(item))) {
61
- const profile = path.join(work, `${safe(importPath)}-${safe(name)}.out`);
62
- run(bin, ["-test.run", `^${escapeRegExp(name)}$`, "-test.coverprofile", profile], dir);
63
- let text = "";
64
- try {
65
- text = readFileSync(profile, "utf8");
66
- }
67
- catch {
68
- continue;
69
- }
70
- for (const hit of parseCoverProfile(text, modulePath))
71
- addHit(files, hit.file, hit.line, name);
72
- }
73
- }
74
- if (Object.keys(files).length === 0)
75
- return { ok: false, detail: "go coverage: no covered lines" };
76
- writeFileSync(dest, `${JSON.stringify({ files }, null, 2)}\n`);
77
- return { ok: true, detail: `${Object.keys(files).length} files` };
78
- }
79
- function addHit(files, file, line, test) {
80
- const lines = files[file] ?? {};
81
- const key = String(line);
82
- const names = lines[key] ?? [];
83
- if (!names.includes(test))
84
- names.push(test);
85
- lines[key] = names;
86
- files[file] = lines;
87
- }
88
- function safe(value) {
89
- return value.replace(/[^A-Za-z0-9._-]+/g, "_");
90
- }
91
- function escapeRegExp(value) {
92
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
93
- }
94
- function run(bin, args, cwd) {
95
- return spawnSync(bin, args, { cwd, encoding: "utf8", timeout: 120_000 });
96
- }
97
- function clip(text) {
98
- const line = text.trim().split("\n").find((item) => item.trim()) ?? "go coverage failed";
99
- return line.slice(0, 240);
100
- }
@@ -1,22 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- /** Stable across reruns: hash of the package-relative file, the span, and the operator. */
3
- export function mutantId(file, start, end, op) {
4
- const hex = createHash("sha256").update(`${file}\0${start}\0${end}\0${op}`).digest("hex").slice(0, 12);
5
- return `m${hex}`;
6
- }
7
- /** Same sequence for the same seed. Not the Auspex experiment's floating-point shuffle. */
8
- export function makeRng(seed) {
9
- let state = seed >>> 0;
10
- return () => {
11
- state = (Math.imul(state, 1103515245) + 12345) >>> 0;
12
- return state / 4294967296;
13
- };
14
- }
15
- export function shuffle(items, rng) {
16
- const out = [...items];
17
- for (let index = out.length - 1; index > 0; index--) {
18
- const swap = Math.floor(rng() * (index + 1));
19
- [out[index], out[swap]] = [out[swap], out[index]];
20
- }
21
- return out;
22
- }
@@ -1,217 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- const JACOCO = "0.8.15";
6
- const ASM = "9.10.1";
7
- const RUNNER = fileURLToPath(new URL("./probatio-jacoco-run.java", import.meta.url));
8
- /**
9
- * One JVM, one JaCoCo dump per Surefire test method.
10
- * The default jacoco.exec file does not name the test.
11
- */
12
- export function writeJavaCoverage(pkg, testNames, dest) {
13
- const tests = testNames.map(junitMethod).filter((name) => name !== null);
14
- if (tests.length === 0)
15
- return { ok: false, detail: "java coverage: no class.method test names" };
16
- const classDirs = findClassDirs(pkg);
17
- if (classDirs.length === 0)
18
- return { ok: false, detail: "java coverage: no target/classes" };
19
- const work = path.join(path.dirname(dest), "java-cover");
20
- mkdirSync(work, { recursive: true });
21
- const jars = resolveJars(pkg, work);
22
- if ("error" in jars)
23
- return { ok: false, detail: jars.error };
24
- const source = path.join(work, "ProbatioJacocoRun.java");
25
- writeFileSync(source, readFileSync(RUNNER));
26
- const compileCp = [jars.agent, jars.junit, jars.core, jars.asm, jars.asmCommons, jars.asmTree].join(path.delimiter);
27
- const compiled = run(javaTool("javac"), ["-encoding", "UTF-8", "-cp", compileCp, "-d", work, source], pkg);
28
- if (compiled.status !== 0)
29
- return { ok: false, detail: clip(compiled.stderr || compiled.stdout || compiled.error?.message || "javac failed") };
30
- const testsFile = path.join(work, "tests.txt");
31
- writeFileSync(testsFile, `${tests.join("\n")}\n`);
32
- const projectCp = buildClasspath(pkg, work);
33
- if ("error" in projectCp)
34
- return { ok: false, detail: projectCp.error };
35
- // Surefire brings the platform launcher. A project's test classpath often has the Jupiter engine and not the launcher.
36
- const launcher = platformLauncherJar(pkg, work, projectCp.cp);
37
- if ("error" in launcher)
38
- return { ok: false, detail: launcher.error };
39
- // Our asm and JaCoCo must win over a project that still ships an older asm. That older reader rejects Java 27 class files.
40
- const cp = [work, jars.core, jars.asm, jars.asmCommons, jars.asmTree, jars.junit, launcher.jar, ...classDirs, ...testClassDirs(pkg), projectCp.cp].filter(Boolean).join(path.delimiter);
41
- const dump = path.join(work, "hits.txt");
42
- const ran = run(javaTool("java"), [`-javaagent:${jars.agent}=output=none,excludes=ProbatioJacocoRun:org.junit.*:org.apiguardian.*:org.opentest4j.*`, "-cp", cp, "ProbatioJacocoRun", classDirs.join(path.delimiter), testsFile, dump, pkg], pkg, 900_000);
43
- if (ran.error && ran.error.code === "ETIMEDOUT") {
44
- return { ok: false, detail: "java coverage: timed out" };
45
- }
46
- if (ran.status !== 0 || !existsSync(dump)) {
47
- return { ok: false, detail: clip(ran.stderr || ran.stdout || ran.error?.message || "java coverage runner failed") };
48
- }
49
- const files = hitsToMap(readFileSync(dump, "utf8"));
50
- if (Object.keys(files).length === 0)
51
- return { ok: false, detail: "java coverage: no covered lines" };
52
- writeFileSync(dest, `${JSON.stringify({ files }, null, 2)}\n`);
53
- return { ok: true, detail: `${Object.keys(files).length} files` };
54
- }
55
- export function hitsToMap(text) {
56
- const files = {};
57
- for (const raw of text.split("\n")) {
58
- const line = raw.trim();
59
- if (!line.startsWith("HIT "))
60
- continue;
61
- const parts = line.slice(4).split(" ");
62
- if (parts.length < 3)
63
- continue;
64
- const test = parts.pop() ?? "";
65
- const number = parts.pop() ?? "";
66
- const file = parts.join(" ");
67
- if (!file || !/^\d+$/.test(number) || !test)
68
- continue;
69
- const lines = files[file] ?? {};
70
- const names = lines[number] ?? [];
71
- if (!names.includes(test))
72
- names.push(test);
73
- lines[number] = names;
74
- files[file] = lines;
75
- }
76
- return files;
77
- }
78
- function junitMethod(name) {
79
- const leaf = name.split("::").pop() ?? name;
80
- const dot = leaf.lastIndexOf(".");
81
- if (dot <= 0 || dot === leaf.length - 1)
82
- return null;
83
- if (leaf.startsWith("maven-") || leaf === "javac")
84
- return null;
85
- return `${leaf.slice(0, dot)}#${leaf.slice(dot + 1)}`;
86
- }
87
- function findClassDirs(root) {
88
- const found = [];
89
- const walk = (dir, depth) => {
90
- if (depth > 4)
91
- return;
92
- let entries = [];
93
- try {
94
- entries = readdirSync(dir);
95
- }
96
- catch {
97
- return;
98
- }
99
- for (const name of entries) {
100
- if (name === "node_modules" || name === ".git" || name === "java-cover")
101
- continue;
102
- const full = path.join(dir, name);
103
- if (name === "target") {
104
- const classes = path.join(full, "classes");
105
- if (existsSync(classes))
106
- found.push(classes);
107
- continue;
108
- }
109
- if (name.startsWith("."))
110
- continue;
111
- walk(full, depth + 1);
112
- }
113
- };
114
- walk(root, 0);
115
- return found;
116
- }
117
- function testClassDirs(root) {
118
- return findClassDirs(root).map((dir) => path.join(path.dirname(dir), "test-classes")).filter((dir) => existsSync(dir));
119
- }
120
- function resolveJars(pkg, work) {
121
- const copies = [
122
- [`org.jacoco:org.jacoco.agent:${JACOCO}:jar:runtime`, "agent.jar"],
123
- [`org.jacoco:org.jacoco.core:${JACOCO}`, "core.jar"],
124
- [`org.ow2.asm:asm:${ASM}`, "asm.jar"],
125
- [`org.ow2.asm:asm-commons:${ASM}`, "asm-commons.jar"],
126
- [`org.ow2.asm:asm-tree:${ASM}`, "asm-tree.jar"],
127
- ["junit:junit:4.13.2", "junit.jar"],
128
- ];
129
- const paths = {};
130
- for (const [artifact, name] of copies) {
131
- const dest = path.join(work, name);
132
- if (!existsSync(dest)) {
133
- const before = new Set(readdirSync(work));
134
- const copied = run("mvn", ["-B", "-q", "org.apache.maven.plugins:maven-dependency-plugin:3.6.1:copy", `-Dartifact=${artifact}`, `-DoutputDirectory=${work}`], pkg);
135
- if (copied.status !== 0)
136
- return { error: clip(copied.stderr || copied.stdout || copied.error?.message || `mvn copy ${artifact} failed`) };
137
- const created = readdirSync(work).find((item) => !before.has(item) && item.endsWith(".jar"));
138
- if (!created)
139
- return { error: `java coverage: missing ${artifact}` };
140
- writeFileSync(dest, readFileSync(path.join(work, created)));
141
- }
142
- paths[name] = dest;
143
- }
144
- return {
145
- agent: paths["agent.jar"],
146
- core: paths["core.jar"],
147
- asm: paths["asm.jar"],
148
- asmCommons: paths["asm-commons.jar"],
149
- asmTree: paths["asm-tree.jar"],
150
- junit: paths["junit.jar"],
151
- };
152
- }
153
- /** Launcher jar matching the project's junit-platform-engine, or none when this suite is not Jupiter. */
154
- function platformLauncherJar(pkg, work, projectCp) {
155
- const entries = projectCp.split(path.delimiter).filter(Boolean);
156
- if (entries.some((item) => path.basename(item).startsWith("junit-platform-launcher")))
157
- return { jar: "" };
158
- const engine = entries.find((item) => path.basename(item).startsWith("junit-platform-engine-"));
159
- if (!engine)
160
- return { jar: "" };
161
- const version = path.basename(engine).replace(/^junit-platform-engine-/, "").replace(/\.jar$/, "");
162
- if (!version)
163
- return { error: "java coverage: junit-platform-engine version missing" };
164
- const dest = path.join(work, "launcher.jar");
165
- if (!existsSync(dest)) {
166
- const artifact = `org.junit.platform:junit-platform-launcher:${version}`;
167
- const before = new Set(readdirSync(work));
168
- const copied = run("mvn", ["-B", "-q", "org.apache.maven.plugins:maven-dependency-plugin:3.6.1:copy", `-Dartifact=${artifact}`, `-DoutputDirectory=${work}`], pkg);
169
- if (copied.status !== 0)
170
- return { error: clip(copied.stderr || copied.stdout || copied.error?.message || `mvn copy ${artifact} failed`) };
171
- const created = readdirSync(work).find((item) => !before.has(item) && item.endsWith(".jar"));
172
- if (!created)
173
- return { error: `java coverage: missing ${artifact}` };
174
- writeFileSync(dest, readFileSync(path.join(work, created)));
175
- }
176
- return { jar: dest };
177
- }
178
- function buildClasspath(pkg, work) {
179
- const file = path.join(work, "classpath.txt");
180
- const built = run("mvn", ["-B", "-q", "dependency:build-classpath", "-DincludeScope=test", `-Dmdep.outputFile=${file}`], pkg);
181
- if (built.status !== 0)
182
- return { error: clip(built.stderr || built.stdout || built.error?.message || "mvn classpath failed") };
183
- try {
184
- return { cp: readFileSync(file, "utf8").trim() };
185
- }
186
- catch {
187
- return { error: "java coverage: classpath file missing" };
188
- }
189
- }
190
- function javaTool(name) {
191
- const home = process.env.JAVA_HOME;
192
- if (home) {
193
- const candidate = path.join(home, "bin", name);
194
- if (existsSync(candidate) && toolWorks(candidate))
195
- return candidate;
196
- }
197
- if (toolWorks(name))
198
- return name;
199
- for (const dir of ["/opt/homebrew/opt/openjdk/bin", "/usr/local/opt/openjdk/bin"]) {
200
- const candidate = path.join(dir, name);
201
- if (existsSync(candidate) && toolWorks(candidate))
202
- return candidate;
203
- }
204
- return name;
205
- }
206
- function toolWorks(bin) {
207
- const probed = spawnSync(bin, ["-version"], { encoding: "utf8" });
208
- const text = `${probed.stdout ?? ""}${probed.stderr ?? ""}`;
209
- return probed.status === 0 && !/Unable to locate a Java Runtime/i.test(text);
210
- }
211
- function run(bin, args, cwd, timeout = 180_000) {
212
- return spawnSync(bin, args, { cwd, encoding: "utf8", timeout });
213
- }
214
- function clip(text) {
215
- const line = text.trim().split("\n").find((item) => item.trim()) ?? "java coverage failed";
216
- return line.slice(0, 240);
217
- }
@@ -1,34 +0,0 @@
1
- /** Names that mean the process failed to compile. These are not test titles. */
2
- const BUILD_NAMES = new Set([
3
- "build",
4
- "javac",
5
- "compiler",
6
- "cobc",
7
- "nasm",
8
- "clang",
9
- "cargo",
10
- "tsc",
11
- "dotnet",
12
- "swiftc",
13
- ]);
14
- /**
15
- * A named test and a compile failure are different facts.
16
- * The build next step says the build failed and does not ask for a test
17
- * named after the compiler.
18
- */
19
- export function killLabel(killedBy) {
20
- if (killedBy.length === 0)
21
- return null;
22
- if (killedBy.every((item) => BUILD_NAMES.has(bareName(item)))) {
23
- return { cause: "build", next: "Killed because the build failed. Record it as a compile kill." };
24
- }
25
- return { cause: "test", next: `Killed by ${killedBy.join(", ")}.` };
26
- }
27
- /** True when the id names a compiler, not a test. */
28
- export function isBuildName(id) {
29
- return BUILD_NAMES.has(bareName(id));
30
- }
31
- function bareName(id) {
32
- const mark = id.lastIndexOf("::");
33
- return (mark >= 0 ? id.slice(mark + 2) : id).trim().toLowerCase();
34
- }
@@ -1,40 +0,0 @@
1
- // A test file that failed before any test ran either did not link or threw while loading.
2
- // Did not link: a removed export, a missing module, a file that does not parse. No code ran.
3
- // A compiled language rejects the same change at compile time, so that mutant is unviable.
4
- // Threw while loading: the file's own top-level code ran the program and it broke. That is a kill.
5
-
6
- const STATIC_ERROR = /^(?:SyntaxError|(?:Type)?Error \[ERR_(?:MODULE_NOT_FOUND|UNKNOWN_FILE_EXTENSION|UNSUPPORTED_DIR_IMPORT|PACKAGE_PATH_NOT_EXPORTED)\]):/
7
-
8
- /**
9
- * `text` is the error as printed: a `Name: message` line, then `at` frames.
10
- * Static when the error is a link or parse error and no frame is in the project's own code.
11
- * A SyntaxError thrown by running code (JSON.parse at the top of a test) has a frame in that file.
12
- */
13
- export function staticLoadFailure(text) {
14
- const lines = String(text).split(/\r?\n/)
15
- const head = lines.findIndex((line) => STATIC_ERROR.test(line.trim()))
16
- if (head === -1) return false
17
- for (const raw of lines.slice(head + 1)) {
18
- const line = raw.trim()
19
- if (!line.startsWith("at ")) break
20
- if (projectFrame(line)) return false
21
- }
22
- return true
23
- }
24
-
25
- /** The error object a node:test event carries, printed the way node prints it. */
26
- export function errorText(error) {
27
- if (!error || typeof error !== "object") return ""
28
- const name = typeof error.name === "string" ? error.name : "Error"
29
- const code = typeof error.code === "string" ? ` [${error.code}]` : ""
30
- const message = typeof error.message === "string" ? error.message : ""
31
- const stack = typeof error.stack === "string" ? error.stack.split("\n").slice(1).join("\n") : ""
32
- return `${name}${code}: ${message}\n${stack}`
33
- }
34
-
35
- function projectFrame(frame) {
36
- if (/\bnode:/.test(frame) || frame.includes("<anonymous>")) return false
37
- const location = /(?:file:\/\/)?(\/[^\s)]+|[A-Za-z]:\\[^\s)]+):\d+:\d+\)?$/.exec(frame)
38
- if (!location) return false
39
- return !location[1].split(/[\\/]/).includes("node_modules")
40
- }
@@ -1,77 +0,0 @@
1
- // Mocha root hooks. This file is not node-coverage.mjs. Mocha does not run
2
- // node:test afterEach/after, so that collector records no Mocha title.
3
- // These hooks key inspector hits by the current Mocha title and write { files }.
4
- const { mkdirSync, writeFileSync } = require("node:fs")
5
- const path = require("node:path")
6
-
7
- const mapPath = process.env.PROBATIO_COVERAGE_MAP
8
- if (mapPath) {
9
- /** @type {Map<string, Map<number, Set<string>>>} */
10
- const byFile = new Map()
11
- /** @type {Array<{ file: string, line: number }>} */
12
- let prelude = []
13
- /** @type {{ positiveLines: () => Promise<Array<{ file: string, line: number }>> } | null} */
14
- let sampler = null
15
- /** @type {((mapPath: string) => Array<{ file: string, line: number }>) | null} */
16
- let takeChildHits = null
17
-
18
- function add(hits, name) {
19
- if (!name) return
20
- for (const hit of hits) {
21
- let lines = byFile.get(hit.file)
22
- if (!lines) {
23
- lines = new Map()
24
- byFile.set(hit.file, lines)
25
- }
26
- let names = lines.get(hit.line)
27
- if (!names) {
28
- names = new Set()
29
- lines.set(hit.line, names)
30
- }
31
- names.add(name)
32
- }
33
- }
34
-
35
- function titleOf(test) {
36
- if (!test) return ""
37
- if (typeof test.fullTitle === "function") return test.fullTitle()
38
- return typeof test.title === "string" ? test.title : ""
39
- }
40
-
41
- exports.mochaHooks = {
42
- async beforeAll() {
43
- try {
44
- const opened = await import("./precise-lines.mjs")
45
- sampler = await opened.openLineSampler((rel) => rel.endsWith("mocha-coverage.cjs") || rel.endsWith("precise-lines.mjs") || rel.endsWith("node-coverage.mjs") || rel.endsWith("child-lines.mjs"))
46
- // A node child of a Mocha test is mapped under that test, the same as under node:test.
47
- opened.exposeChildLines()
48
- takeChildHits = opened.takeChildHits
49
- prelude = await sampler.positiveLines()
50
- } catch (err) {
51
- writeFileSync(`${mapPath}.error`, `${err instanceof Error ? err.message : String(err)}\n`)
52
- }
53
- },
54
- async afterEach() {
55
- if (!sampler) return
56
- const title = titleOf(this.currentTest)
57
- try {
58
- add(prelude, title)
59
- add(await sampler.positiveLines(), title)
60
- if (takeChildHits) add(takeChildHits(mapPath), title)
61
- } catch (err) {
62
- writeFileSync(`${mapPath}.error`, `${err instanceof Error ? err.message : String(err)}\n`)
63
- }
64
- },
65
- afterAll() {
66
- if (byFile.size === 0) return
67
- const files = {}
68
- for (const [file, lines] of byFile) {
69
- const bucket = {}
70
- for (const [line, names] of lines) bucket[String(line)] = [...names].sort()
71
- files[file] = bucket
72
- }
73
- mkdirSync(path.dirname(mapPath), { recursive: true })
74
- writeFileSync(mapPath, `${JSON.stringify({ files }, null, 2)}\n`)
75
- },
76
- }
77
- }