probatio 0.0.0-stage → 0.0.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.
@@ -0,0 +1,1299 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const SKIP_DIR = new Set(["node_modules", ".git", "target", "dist", "bin", "obj", ".build", "vendor"]);
7
+ export function discoverSuite(pkg, testsDir) {
8
+ return (scriptSuite(pkg) ||
9
+ cargoSuite(pkg) ||
10
+ goSuite(pkg) ||
11
+ swiftPackageSuite(pkg) ||
12
+ mavenSuite(pkg) ||
13
+ dotnetSuite(pkg) ||
14
+ pytestSuite(pkg, testsDir) ||
15
+ unittestSuite(pkg, testsDir) ||
16
+ nodeSuite(pkg) ||
17
+ mochaSuite(pkg) ||
18
+ makeTestSuite(pkg));
19
+ }
20
+ export function commandSuite(command, files) {
21
+ return { kind: "command", files: files.length > 0 ? files : ["suite-command"], command };
22
+ }
23
+ export async function runDiscoveredSuite(pkg, files, testsDir, pattern, timeoutMs, env, hooks, extra) {
24
+ const spec = extra && "spec" in extra ? extra.spec ?? null : discoverSuite(pkg, testsDir);
25
+ const empty = {
26
+ report: null,
27
+ timedOut: false,
28
+ detail: "no suite",
29
+ command: "",
30
+ compileToken: null,
31
+ wholeSuite: false,
32
+ selectedTests: [],
33
+ };
34
+ if (!spec)
35
+ return empty;
36
+ const selected = files.length > 0 ? files : spec.files;
37
+ const child = { ...env };
38
+ if (extra?.coverageMap)
39
+ child.PROBATIO_COVERAGE_MAP = extra.coverageMap;
40
+ if (extra?.profileFile)
41
+ child.LLVM_PROFILE_FILE = extra.profileFile;
42
+ const result = await launch(pkg, spec, selected, pattern, extra?.names ?? null, timeoutMs, child, hooks);
43
+ const compileToken = result.timedOut ? null : detectCompile(spec.kind, result.stdout, result.stderr, result.code);
44
+ // Ignore a report read from disk when this run failed to compile. The previous
45
+ // suite's files are still in the worktree and are not this mutant's result.
46
+ const report = result.timedOut || compileToken ? null : adapt(spec.kind, pkg, result.stdout, result.stderr, result.code);
47
+ return {
48
+ report,
49
+ timedOut: result.timedOut,
50
+ detail: firstLine(result.stderr || result.stdout),
51
+ command: result.command,
52
+ compileToken,
53
+ wholeSuite: result.wholeSuite,
54
+ selectedTests: result.selectedTests,
55
+ };
56
+ }
57
+ function scriptSuite(pkg) {
58
+ if (!existsSync(path.join(pkg, "run_tests.sh")))
59
+ return null;
60
+ return { kind: "script", files: ["run_tests.sh"], command: "bash run_tests.sh" };
61
+ }
62
+ function cargoSuite(pkg) {
63
+ if (!existsSync(path.join(pkg, "Cargo.toml")))
64
+ return null;
65
+ const files = walk(pkg, (rel) => rel.endsWith(".rs") && !rel.startsWith("target/")).filter((rel) => {
66
+ if (rel.split("/").includes("tests") || rel.endsWith("_test.rs"))
67
+ return true;
68
+ return readFileSync(path.join(pkg, rel), "utf8").includes("#[test]");
69
+ });
70
+ if (files.length === 0)
71
+ return null;
72
+ return { kind: "cargo", files, command: "cargo test" };
73
+ }
74
+ function goSuite(pkg) {
75
+ const files = walk(pkg, (rel) => rel.endsWith("_test.go"));
76
+ if (files.length === 0)
77
+ return null;
78
+ return { kind: "go", files, command: "go test -json ./..." };
79
+ }
80
+ function swiftPackageSuite(pkg) {
81
+ if (!existsSync(path.join(pkg, "Package.swift")))
82
+ return null;
83
+ const files = walk(pkg, (rel) => rel.endsWith(".swift") && rel.split("/").includes("Tests"));
84
+ if (files.length === 0)
85
+ return null;
86
+ return { kind: "swift", files, command: "swift test" };
87
+ }
88
+ function mavenSuite(pkg) {
89
+ if (!existsSync(path.join(pkg, "pom.xml")))
90
+ return null;
91
+ const files = walk(pkg, (rel) => rel.endsWith(".java") && rel.split("/").includes("test"));
92
+ if (files.length === 0)
93
+ return null;
94
+ return { kind: "maven", files, command: "mvn -B test" };
95
+ }
96
+ function dotnetSuite(pkg) {
97
+ const inTestDir = (rel) => hasSegment(rel, "tests") || hasSegment(rel, "test");
98
+ const files = walk(pkg, (rel) => rel.endsWith(".cs") && inTestDir(rel));
99
+ if (files.length === 0)
100
+ return null;
101
+ const projects = walk(pkg, (rel) => rel.endsWith(".csproj") && inTestDir(rel));
102
+ if (projects.length === 0)
103
+ return null;
104
+ const project = [...projects].sort((a, b) => Number(isDotnetTestProject(pkg, b)) - Number(isDotnetTestProject(pkg, a)))[0];
105
+ if (!isDotnetTestProject(pkg, project) && readFileSync(path.join(pkg, project), "utf8").includes("Exe")) {
106
+ return { kind: "dotnet-exe", files, command: "dotnet run", project };
107
+ }
108
+ return { kind: "dotnet", files, command: "dotnet test", project };
109
+ }
110
+ function isDotnetTestProject(pkg, rel) {
111
+ return /Test\.Sdk|xunit|nunit|mstest/i.test(readFileSync(path.join(pkg, rel), "utf8"));
112
+ }
113
+ function pytestSuite(pkg, testsDir) {
114
+ const files = pythonTests(pkg, testsDir);
115
+ if (files.length === 0 || !isPytest(pkg, testsDir, files))
116
+ return null;
117
+ return { kind: "pytest", files, command: "python3 pytest" };
118
+ }
119
+ function unittestSuite(pkg, testsDir) {
120
+ const files = pythonTests(pkg, testsDir);
121
+ if (files.length === 0)
122
+ return null;
123
+ return { kind: "unittest", files, command: "python3 unittest" };
124
+ }
125
+ function nodeSuite(pkg) {
126
+ const files = walk(pkg, isNodeTest);
127
+ if (files.length === 0)
128
+ return null;
129
+ if (usesMocha(pkg) && !files.some((rel) => readFileSync(path.join(pkg, rel), "utf8").includes("node:test")))
130
+ return null;
131
+ return { kind: "node", files, command: "node --test" };
132
+ }
133
+ function mochaSuite(pkg) {
134
+ const files = walk(pkg, (rel) => isNodeTest(rel) || (rel.split("/").includes("test") && rel.endsWith(".ts")));
135
+ if (files.length === 0 || !usesMocha(pkg))
136
+ return null;
137
+ return { kind: "mocha", files, command: "mocha --reporter json" };
138
+ }
139
+ /** A Makefile `test` target that links the project's own COBOL tests, instead of one file. */
140
+ function makeTestSuite(pkg) {
141
+ const makefile = path.join(pkg, "Makefile");
142
+ if (!existsSync(makefile))
143
+ return null;
144
+ const text = readFileSync(makefile, "utf8");
145
+ if (!/^test:/m.test(text))
146
+ return null;
147
+ const files = walk(pkg, (rel) => /\.(cob|cbl|cobol)$/i.test(rel) && hasSegment(rel, "tests"));
148
+ if (files.length === 0)
149
+ return null;
150
+ return { kind: "make-test", files, command: "make test" };
151
+ }
152
+ function compiledSuite(pkg) {
153
+ const asm = underSuite(pkg, (rel) => rel.endsWith(".asm") || rel.endsWith(".nasm") || rel.endsWith(".yasm"));
154
+ if (asm.length > 0)
155
+ return { kind: "asm-x86_64", files: asm, command: "nasm" };
156
+ const arm = underSuite(pkg, (rel) => rel.endsWith(".s"));
157
+ if (arm.length > 0)
158
+ return { kind: "asm-aarch64", files: arm, command: "clang" };
159
+ const riscv = underSuite(pkg, (rel) => rel.endsWith(".S"));
160
+ if (riscv.length > 0)
161
+ return { kind: "asm-riscv", files: riscv, command: "riscv64-elf-as" };
162
+ const cobol = underSuite(pkg, (rel) => /\.(cob|cbl|cobol)$/i.test(rel));
163
+ if (cobol.length > 0)
164
+ return { kind: "cobol", files: cobol, command: "cobc" };
165
+ const cpp = underSuite(pkg, (rel) => /\.(cpp|cc|cxx)$/.test(rel));
166
+ if (cpp.length > 0)
167
+ return { kind: "cpp", files: cpp, command: "clang++" };
168
+ const c = underSuite(pkg, (rel) => rel.endsWith(".c"));
169
+ if (c.length > 0)
170
+ return { kind: "c", files: c, command: "clang" };
171
+ const java = underSuite(pkg, (rel) => rel.endsWith(".java"));
172
+ if (java.length > 0)
173
+ return { kind: "java", files: java, command: "javac" };
174
+ const swift = underSuite(pkg, (rel) => rel.endsWith(".swift"));
175
+ if (swift.length > 0)
176
+ return { kind: "swiftc", files: swift, command: "swiftc" };
177
+ return null;
178
+ }
179
+ function underSuite(pkg, accept) {
180
+ return walk(pkg, (rel) => accept(rel) && (hasSegment(rel, "tests") || hasSegment(rel, "test")));
181
+ }
182
+ const SOURCE_EXT = /\.(?:cobol|cbl|cob|cpp|cxx|cc|java|asm|nasm|yasm|swift|py|go|rs|js|ts|cs|c|s|S)$/;
183
+ /** Source files under tests/ or test/ that are not, by themselves, a proved runner. */
184
+ export function sourceCandidates(pkg) {
185
+ return walk(pkg, (rel) => SOURCE_EXT.test(rel) && (hasSegment(rel, "tests") || hasSegment(rel, "test")));
186
+ }
187
+ function pythonTests(pkg, testsDir) {
188
+ const roots = new Set([testsDir, "tests", "test"]);
189
+ return walk(pkg, (rel) => {
190
+ const base = path.posix.basename(rel);
191
+ if (!/^test_.*\.py$/.test(base) && !/_test\.py$/.test(base))
192
+ return false;
193
+ return rel.split("/").some((part) => roots.has(part));
194
+ });
195
+ }
196
+ function isPytest(pkg, testsDir, files) {
197
+ if (existsSync(path.join(pkg, "pytest.ini")) || existsSync(path.join(pkg, "conftest.py")))
198
+ return true;
199
+ if (existsSync(path.join(pkg, testsDir, "conftest.py")) || existsSync(path.join(pkg, "tests", "conftest.py")))
200
+ return true;
201
+ const project = path.join(pkg, "pyproject.toml");
202
+ if (existsSync(project) && readFileSync(project, "utf8").includes("pytest"))
203
+ return true;
204
+ return files.some((rel) => readFileSync(path.join(pkg, rel), "utf8").includes("pytest"));
205
+ }
206
+ function isNodeTest(rel) {
207
+ const base = path.posix.basename(rel);
208
+ if (!/\.(test|spec)\.(ts|tsx|mts|js|mjs|cjs)$/.test(base) && !/_test\.(ts|tsx|js|mjs)$/.test(base))
209
+ return false;
210
+ return hasSegment(rel, "test") || hasSegment(rel, "tests");
211
+ }
212
+ function usesMocha(pkg) {
213
+ const file = path.join(pkg, "package.json");
214
+ if (!existsSync(file))
215
+ return false;
216
+ return readFileSync(file, "utf8").includes("mocha");
217
+ }
218
+ function hasSegment(rel, name) {
219
+ return rel.split("/").some((part) => part.toLowerCase() === name.toLowerCase());
220
+ }
221
+ function tag(result, command, wholeSuite, selectedTests) {
222
+ return Promise.resolve(result).then((item) => ({ ...item, command, wholeSuite, selectedTests }));
223
+ }
224
+ function shown(bin, args) {
225
+ return [bin, ...args.map((arg) => (/\s/.test(arg) ? JSON.stringify(arg) : arg))].join(" ");
226
+ }
227
+ async function launch(pkg, spec, files, pattern, names, timeoutMs, env, hooks) {
228
+ const selection = names && names.length > 0 ? names : namesFromPattern(pattern);
229
+ const wantsNames = selection.length > 0;
230
+ if (spec.kind === "node") {
231
+ const nodePattern = pattern ?? (selection.length > 0 ? `^(${selection.map(escapeRegExp).join("|")})$` : null);
232
+ const args = [
233
+ ...(existsSync(path.join(pkg, "node_modules", ".bin", "tsx")) ? [] : ["--experimental-strip-types"]),
234
+ "--test",
235
+ ...(nodePattern ? [`--test-name-pattern=${nodePattern}`] : []),
236
+ ...files,
237
+ ];
238
+ return tag(runNode(pkg, files, nodePattern, timeoutMs, env, hooks), shown(existsSync(path.join(pkg, "node_modules", ".bin", "tsx")) ? "tsx" : "node", args), false, selection);
239
+ }
240
+ if (spec.kind === "unittest") {
241
+ const k = pattern ?? (selection.length > 0 ? selection.map((name) => name.split("::").pop() || name).join(" or ") : null);
242
+ const shownK = k && /\s/.test(k) ? JSON.stringify(k) : k;
243
+ const command = shownK ? `${spec.command} -k ${shownK}` : spec.command;
244
+ return tag(runPython(pkg, files, k, timeoutMs, env, "py-reporter.py", null), command, false, selection);
245
+ }
246
+ if (spec.kind === "pytest") {
247
+ // -k is an expression. test_simple_reject[<lambda>1] makes pytest exit 4, and that usage error is not a kill.
248
+ const plan = pytestPlan(selection);
249
+ const k = plan.nodeIds ? null : plan.pattern ?? (pattern && pytestKeywordExpr(pattern) ? pattern : null);
250
+ const shownK = k && /\s/.test(k) ? JSON.stringify(k) : k;
251
+ const command = plan.nodeIds
252
+ ? ["python3", "pytest", ...plan.nodeIds.map(quotePytestArg)].join(" ")
253
+ : shownK
254
+ ? `${spec.command} -k ${shownK}`
255
+ : spec.command;
256
+ return tag(runPython(pkg, files, k, timeoutMs, env, "py-pytest-reporter.py", plan.nodeIds), command, false, selection);
257
+ }
258
+ if (spec.kind === "cargo") {
259
+ if (selection.length > 1) {
260
+ return tag(spawnCollected("cargo", ["test", "--", "--test-threads=1"], pkg, env, timeoutMs), "cargo test -- --test-threads=1", true, selection);
261
+ }
262
+ const filter = selection.length === 1 ? selection[0] : null;
263
+ const args = filter ? ["test", filter, "--", "--test-threads=1"] : ["test", "--", "--test-threads=1"];
264
+ return tag(spawnCollected("cargo", args, pkg, env, timeoutMs), shown("cargo", args), false, selection);
265
+ }
266
+ if (spec.kind === "go") {
267
+ const filter = goRunFilter(names, pattern);
268
+ const args = ["test", "-json", "./...", ...(filter ? ["-run", filter] : [])];
269
+ return tag(spawnCollected("go", args, pkg, env, timeoutMs), shown("go", args), false, selection);
270
+ }
271
+ if (spec.kind === "swift")
272
+ return tag(runSwift(pkg, timeoutMs, env), spec.command, wantsNames, selection);
273
+ if (spec.kind === "maven") {
274
+ const tests = selection.map(surefireTest).filter((name) => name.length > 0);
275
+ const args = ["-B", "test", ...(tests.length > 0 ? [`-Dtest=${tests.join(",")}`] : [])];
276
+ const whole = wantsNames && tests.length === 0;
277
+ return tag(spawnCollected("mvn", args, pkg, javaEnv(env), timeoutMs), shown("mvn", args), whole, selection);
278
+ }
279
+ if (spec.kind === "dotnet") {
280
+ const command = wantsNames ? `${spec.command} --filter ${selection.join("|")}` : spec.command;
281
+ return tag(runDotnetTest(pkg, spec, timeoutMs, env, selection), command, false, selection);
282
+ }
283
+ if (spec.kind === "dotnet-exe")
284
+ return tag(runDotnetExe(pkg, spec, files, timeoutMs, env), spec.command, wantsNames, selection);
285
+ if (spec.kind === "mocha")
286
+ return tag(runMocha(pkg, files, timeoutMs, env), spec.command, wantsNames, selection);
287
+ // macOS `script` allocates a tty. tini's suite calls `docker run -it`, which exits immediately without one.
288
+ if (spec.kind === "script")
289
+ return tag(runScript(pkg, timeoutMs, env), spec.command, wantsNames, selection);
290
+ if (spec.kind === "c" || spec.kind === "cpp")
291
+ return tag(compileAndRun(pkg, files, spec.kind === "cpp", timeoutMs, env), spec.command, wantsNames, selection);
292
+ if (spec.kind === "java")
293
+ return tag(compileJava(pkg, files, timeoutMs, env), spec.command, wantsNames, selection);
294
+ if (spec.kind === "swiftc")
295
+ return tag(compileSwift(pkg, files, timeoutMs, env), spec.command, wantsNames, selection);
296
+ if (spec.kind === "cobol")
297
+ return tag(compileCobol(pkg, files, timeoutMs, env), spec.command, wantsNames, selection);
298
+ if (spec.kind === "make-test")
299
+ return tag(spawnCollected("make", ["test"], pkg, env, timeoutMs), "make test", wantsNames, selection);
300
+ if (spec.kind === "asm-x86_64")
301
+ return tag(assembleX86(pkg, files, timeoutMs, env), spec.command, wantsNames, selection);
302
+ if (spec.kind === "asm-aarch64")
303
+ return tag(assembleArm(pkg, files, timeoutMs, env), spec.command, wantsNames, selection);
304
+ if (spec.kind === "asm-riscv")
305
+ return tag(assembleRiscv(pkg, files, timeoutMs, env), spec.command, wantsNames, selection);
306
+ if (spec.kind === "command")
307
+ return tag(runShell(pkg, spec.command, timeoutMs, env), spec.command, wantsNames, selection);
308
+ return tag({ code: 1, stdout: "", stderr: `no runner for ${spec.kind}`, timedOut: false }, spec.command, wantsNames, selection);
309
+ }
310
+ const PYTEST_KEYWORD = /^[A-Za-z_][A-Za-z0-9_]*$/;
311
+ function pytestLeaf(name) {
312
+ return name.split("::").pop() || name;
313
+ }
314
+ /** Safe leaves stay on -k. Anything else is a node id, passed as an argument. */
315
+ function pytestPlan(selection) {
316
+ if (selection.length === 0)
317
+ return { pattern: null, nodeIds: null };
318
+ const leaves = selection.map(pytestLeaf);
319
+ if (leaves.every((leaf) => PYTEST_KEYWORD.test(leaf)))
320
+ return { pattern: leaves.join(" or "), nodeIds: null };
321
+ return { pattern: null, nodeIds: selection };
322
+ }
323
+ function pytestKeywordExpr(pattern) {
324
+ return pattern.split(" or ").every((part) => PYTEST_KEYWORD.test(part));
325
+ }
326
+ function quotePytestArg(arg) {
327
+ return /[\s<>|&;()[\]"'\\]/.test(arg) ? JSON.stringify(arg) : arg;
328
+ }
329
+ function namesFromPattern(pattern) {
330
+ if (!pattern)
331
+ return [];
332
+ const wrapped = /^\^\((.*)\)\$$/.exec(pattern);
333
+ if (!wrapped)
334
+ return [pattern];
335
+ return wrapped[1].split("|").filter((part) => part.length > 0);
336
+ }
337
+ function goRunFilter(names, pattern) {
338
+ if (pattern)
339
+ return pattern;
340
+ if (!names || names.length === 0)
341
+ return null;
342
+ return `^(${names.map(escapeRegExp).join("|")})$`;
343
+ }
344
+ function surefireTest(name) {
345
+ const leaf = name.split("::").pop() ?? name;
346
+ const dot = leaf.lastIndexOf(".");
347
+ if (dot <= 0)
348
+ return leaf;
349
+ return `${leaf.slice(0, dot)}#${leaf.slice(dot + 1)}`;
350
+ }
351
+ function escapeRegExp(value) {
352
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
353
+ }
354
+ function runShell(pkg, command, timeoutMs, env) {
355
+ const child = /\b(java|javac|mvn)\b/.test(command) ? javaEnv(env) : env;
356
+ return spawnCollected("sh", ["-c", command], pkg, child, timeoutMs);
357
+ }
358
+ async function runNode(pkg, files, pattern, timeoutMs, env, hooks) {
359
+ const tsx = path.join(pkg, "node_modules", ".bin", "tsx");
360
+ const bin = existsSync(tsx) ? tsx : process.execPath;
361
+ const prefix = existsSync(tsx) ? [] : ["--experimental-strip-types"];
362
+ const hook = env.PROBATIO_COVERAGE_MAP ? fileURLToPath(new URL("./node-coverage.mjs", import.meta.url)) : null;
363
+ const result = await spawnCollected(bin, [
364
+ ...prefix,
365
+ ...(hook ? ["--import", hook] : []),
366
+ "--test",
367
+ `--test-reporter=${hooks.reporterPath}`,
368
+ `--test-concurrency=${hooks.concurrency}`,
369
+ `--test-timeout=${hooks.testTimeoutMs}`,
370
+ ...(pattern ? [`--test-name-pattern=${pattern}`] : []),
371
+ ...files,
372
+ ], pkg, env, timeoutMs);
373
+ if (env.PROBATIO_COVERAGE_MAP)
374
+ mergeNodeCoverage(env.PROBATIO_COVERAGE_MAP);
375
+ return result;
376
+ }
377
+ function mergeNodeCoverage(mapPath) {
378
+ const dir = `${mapPath}.parts`;
379
+ const files = {};
380
+ if (existsSync(dir)) {
381
+ for (const name of readdirSync(dir)) {
382
+ if (!name.endsWith(".json"))
383
+ continue;
384
+ try {
385
+ const parsed = JSON.parse(readFileSync(path.join(dir, name), "utf8"));
386
+ for (const [file, lines] of Object.entries(parsed.files ?? {})) {
387
+ const bucket = files[file] ?? {};
388
+ for (const [line, tests] of Object.entries(lines ?? {})) {
389
+ bucket[line] = [...new Set([...(bucket[line] ?? []), ...(tests ?? [])])].sort();
390
+ }
391
+ files[file] = bucket;
392
+ }
393
+ }
394
+ catch {
395
+ // A torn shard is ignored. An empty merge does not invent a map.
396
+ }
397
+ }
398
+ rmSync(dir, { recursive: true, force: true });
399
+ }
400
+ if (Object.keys(files).length === 0) {
401
+ const error = `${mapPath}.error`;
402
+ if (existsSync(error))
403
+ writeFileSync(path.join(path.dirname(mapPath), "coverage-error.txt"), readFileSync(error));
404
+ return;
405
+ }
406
+ writeFileSync(mapPath, `${JSON.stringify({ files }, null, 2)}\n`);
407
+ }
408
+ function runPython(pkg, files, pattern, timeoutMs, env, script, nodeIds) {
409
+ const reporter = fileURLToPath(new URL(`./${script}`, import.meta.url));
410
+ const src = path.join(pkg, "src");
411
+ clearBytecode(pkg);
412
+ const next = {
413
+ ...(existsSync(src)
414
+ ? { ...env, PYTHONPATH: [src, env.PYTHONPATH].filter((item) => Boolean(item)).join(path.delimiter) }
415
+ : env),
416
+ PYTHONDONTWRITEBYTECODE: "1",
417
+ ...(env.PROBATIO_COVERAGE_MAP ? { COVERAGE_CORE: env.COVERAGE_CORE || "ctrace" } : {}),
418
+ };
419
+ const targets = nodeIds && nodeIds.length > 0 ? nodeIds : files;
420
+ return spawnCollected("python3", [reporter, ...targets, ...(pattern ? ["--pattern", pattern] : [])], pkg, next, timeoutMs);
421
+ }
422
+ async function runMocha(pkg, files, timeoutMs, env) {
423
+ const mocha = ["mocha.js", "mocha"]
424
+ .map((name) => path.join(pkg, "node_modules", "mocha", "bin", name))
425
+ .find((file) => existsSync(file));
426
+ if (!mocha)
427
+ return Promise.resolve({ code: 1, stdout: "", stderr: "mocha is not installed", timedOut: false });
428
+ const prepared = await preparePublished(pkg, env, timeoutMs);
429
+ if (prepared && (prepared.code !== 0 || prepared.timedOut))
430
+ return prepared;
431
+ const config = readMochaConfig(pkg);
432
+ const args = [mocha, "--reporter", "json", "--timeout", "20000"];
433
+ if (config) {
434
+ // The `esm` loader throws on current Node before any test runs. Drop that one require and keep the rest of the project's mocha config.
435
+ const requires = config.require.filter((name) => name !== "esm");
436
+ if (requires.length !== config.require.length) {
437
+ const dir = path.join(pkg, ".probatio-suite");
438
+ mkdirSync(dir, { recursive: true });
439
+ const override = path.join(dir, "mocha-package.json");
440
+ writeFileSync(override, `${JSON.stringify({ mocha: { extension: config.extension, require: requires, spec: config.spec } }, null, 2)}\n`);
441
+ args.push("--package", override);
442
+ }
443
+ }
444
+ else {
445
+ args.push(...files);
446
+ }
447
+ return spawnCollected(process.execPath, args, pkg, env, timeoutMs);
448
+ }
449
+ /** Rebuild published JS when tests import the package entry and `src` is newer than that entry. */
450
+ async function preparePublished(pkg, env, timeoutMs) {
451
+ const file = path.join(pkg, "package.json");
452
+ if (!existsSync(file))
453
+ return null;
454
+ let body;
455
+ try {
456
+ body = JSON.parse(readFileSync(file, "utf8"));
457
+ }
458
+ catch {
459
+ return null;
460
+ }
461
+ const entries = publishedEntries(body).map((rel) => path.join(pkg, rel));
462
+ if (entries.length === 0 || !publishedStale(pkg, entries))
463
+ return null;
464
+ const script = body.scripts?.bundle ? "bundle" : body.scripts?.build ? "build" : null;
465
+ if (!script)
466
+ return null;
467
+ return spawnCollected("npm", ["run", script, "--silent"], pkg, env, timeoutMs);
468
+ }
469
+ function publishedEntries(body) {
470
+ const raw = [];
471
+ for (const value of [body.types, body.typings, body.main, body.module]) {
472
+ if (typeof value === "string")
473
+ raw.push(value);
474
+ }
475
+ collectExportPaths(body.exports, raw);
476
+ const out = [];
477
+ for (const value of raw) {
478
+ if (!value.startsWith("."))
479
+ continue;
480
+ if (!/\.(?:js|mjs|cjs|d\.ts)$/.test(value))
481
+ continue;
482
+ const rel = value.replace(/^\.\//, "");
483
+ if (!out.includes(rel))
484
+ out.push(rel);
485
+ }
486
+ return out;
487
+ }
488
+ function collectExportPaths(value, out) {
489
+ if (typeof value === "string") {
490
+ out.push(value);
491
+ return;
492
+ }
493
+ if (!value || typeof value !== "object")
494
+ return;
495
+ for (const item of Object.values(value))
496
+ collectExportPaths(item, out);
497
+ }
498
+ function publishedStale(pkg, entries) {
499
+ if (entries.some((file) => !existsSync(file)))
500
+ return true;
501
+ const src = path.join(pkg, "src");
502
+ if (!existsSync(src))
503
+ return false;
504
+ let newest = 0;
505
+ const visit = (dir) => {
506
+ for (const name of readdirSync(dir)) {
507
+ if (name === "node_modules" || name === "dist")
508
+ continue;
509
+ const full = path.join(dir, name);
510
+ const info = statSync(full);
511
+ if (info.isDirectory())
512
+ visit(full);
513
+ else if (info.mtimeMs > newest)
514
+ newest = info.mtimeMs;
515
+ }
516
+ };
517
+ visit(src);
518
+ const oldest = Math.min(...entries.map((file) => statSync(file).mtimeMs));
519
+ return newest > oldest;
520
+ }
521
+ /** A `.pyc` from the previous mutant can outlive the patch when both share a one-second mtime. */
522
+ function clearBytecode(pkg) {
523
+ const skip = new Set(["node_modules", ".git", "dist", "target", ".build", "vendor"]);
524
+ const stack = [pkg];
525
+ while (stack.length > 0) {
526
+ const dir = stack.pop();
527
+ if (!dir)
528
+ continue;
529
+ let entries;
530
+ try {
531
+ entries = readdirSync(dir, { withFileTypes: true });
532
+ }
533
+ catch {
534
+ continue;
535
+ }
536
+ for (const entry of entries) {
537
+ if (skip.has(entry.name))
538
+ continue;
539
+ const full = path.join(dir, entry.name);
540
+ if (entry.isDirectory()) {
541
+ if (entry.name === "__pycache__")
542
+ rmSync(full, { recursive: true, force: true });
543
+ else
544
+ stack.push(full);
545
+ }
546
+ else if (entry.name.endsWith(".pyc")) {
547
+ rmSync(full, { force: true });
548
+ }
549
+ }
550
+ }
551
+ }
552
+ function runScript(pkg, timeoutMs, env) {
553
+ const next = { ...env };
554
+ const shim = writeDockerShim(pkg);
555
+ if (shim)
556
+ next.PATH = `${shim}${path.delimiter}${next.PATH ?? ""}`;
557
+ // This project's CI sets ARCH_NATIVE=1. Without it, ci/run_build.sh skips the tests.
558
+ const inner = path.join(pkg, "test", "run_inner_tests.py");
559
+ const build = path.join(pkg, "ci", "run_build.sh");
560
+ if (!next.ARCH_NATIVE && existsSync(inner) && existsSync(build) && readFileSync(build, "utf8").includes("ARCH_NATIVE")) {
561
+ next.ARCH_NATIVE = "1";
562
+ }
563
+ return spawnCollected("script", ["-q", "/dev/null", "bash", "run_tests.sh"], pkg, next, timeoutMs);
564
+ }
565
+ function writeDockerShim(pkg) {
566
+ const found = spawnSync("sh", ["-c", "command -v docker"], { encoding: "utf8" });
567
+ const real = found.stdout?.trim() ?? "";
568
+ if (found.status !== 0 || !real)
569
+ return null;
570
+ const dir = path.join(pkg, ".probatio-suite", "bin");
571
+ mkdirSync(dir, { recursive: true });
572
+ const file = path.join(dir, "docker");
573
+ writeFileSync(file, dockerShimScript(real), { mode: 0o755 });
574
+ return dir;
575
+ }
576
+ function dockerShimScript(real) {
577
+ return `#!/bin/bash
578
+ set -euo pipefail
579
+ REAL=${JSON.stringify(real)}
580
+ if [[ "\${1:-}" != "build" ]]; then
581
+ exec "$REAL" "$@"
582
+ fi
583
+ shift
584
+ args=()
585
+ context=""
586
+ while [[ $# -gt 0 ]]; do
587
+ case "$1" in
588
+ --build-arg|--file|-f|--tag|-t|--target|--platform|--iidfile|--label|--network|--output|-o|--secret|--ssh|--cache-from|--cache-to|--build-context|--ulimit|--add-host)
589
+ args+=("$1" "$2"); shift 2 ;;
590
+ --build-arg=*|--file=*|--tag=*|--target=*|--platform=*|--network=*|--output=*)
591
+ args+=("$1"); shift ;;
592
+ --)
593
+ shift; context="\${1:-}"; break ;;
594
+ -*)
595
+ args+=("$1"); shift ;;
596
+ *)
597
+ context="$1"; shift ;;
598
+ esac
599
+ done
600
+ if [[ -z "$context" ]]; then
601
+ exec "$REAL" build --pull=false "\${args[@]}"
602
+ fi
603
+ src=$(cd "$context" && pwd)
604
+ tmp=$(mktemp -d)
605
+ trap 'rm -rf "$tmp"' EXIT
606
+ cp -a "$src"/. "$tmp"/
607
+ deps="$tmp/ci/install_deps.sh"
608
+ if [[ -f "$deps" ]] && grep -q "pip install --upgrade pip" "$deps"; then
609
+ python3 - "$deps" << 'PY'
610
+ import sys
611
+ from pathlib import Path
612
+ p = Path(sys.argv[1])
613
+ text = p.read_text()
614
+ old = "python3 -m pip install --upgrade pip\\npython3 -m pip install virtualenv\\n"
615
+ new = """python3 -m pip install 'pip==20.3.4'
616
+ printf '%s\\\\n' '[global]' 'constraint = /etc/pip-constraints.txt' > /etc/pip.conf
617
+ printf '%s\\\\n' 'virtualenv==16.7.10' 'psutil==5.6.7' 'python-prctl==1.8.1' > /etc/pip-constraints.txt
618
+ python3 -m pip install 'virtualenv==16.7.10'
619
+ """
620
+ if old not in text:
621
+ raise SystemExit(0)
622
+ p.write_text(text.replace(old, new, 1))
623
+ p.chmod(0o755)
624
+ PY
625
+ fi
626
+ "$REAL" build --pull=false "\${args[@]}" "$tmp"
627
+ `;
628
+ }
629
+ function runSwift(pkg, timeoutMs, env) {
630
+ const args = ["test"];
631
+ if (declaresOwnPredicate(pkg)) {
632
+ const overlay = foundationPredicateOverlay();
633
+ if (overlay) {
634
+ args.push("-Xswiftc", "-vfsoverlay", "-Xswiftc", overlay.file, "-Xswiftc", "-module-cache-path", "-Xswiftc", overlay.cache);
635
+ }
636
+ }
637
+ return spawnCollected("swift", args, pkg, env, timeoutMs);
638
+ }
639
+ function declaresOwnPredicate(pkg) {
640
+ return walk(pkg, (rel) => rel.endsWith(".swift")).some((rel) => /struct\s+Predicate\s*</.test(readFileSync(path.join(pkg, rel), "utf8")));
641
+ }
642
+ function foundationPredicateOverlay() {
643
+ const sdk = spawnSync("xcrun", ["--sdk", "macosx", "--show-sdk-path"], { encoding: "utf8" });
644
+ const sdkPath = sdk.stdout?.trim() ?? "";
645
+ if (sdk.status !== 0 || !sdkPath)
646
+ return null;
647
+ const moduleDir = path.join(sdkPath, "System/Library/Frameworks/Foundation.framework/Modules/Foundation.swiftmodule");
648
+ if (!existsSync(moduleDir))
649
+ return null;
650
+ const interfaces = readdirSync(moduleDir).filter((name) => name.endsWith(".swiftinterface"));
651
+ if (interfaces.length === 0)
652
+ return null;
653
+ const stamp = String(statSync(path.join(moduleDir, interfaces[0])).mtimeMs);
654
+ const root = path.join(tmpdir(), "probatio-foundation-overlay");
655
+ const edited = path.join(root, "interfaces");
656
+ const file = path.join(root, "overlay.json");
657
+ const cache = path.join(root, "cache");
658
+ const stampFile = path.join(root, "stamp");
659
+ mkdirSync(cache, { recursive: true });
660
+ if (!existsSync(stampFile) || readFileSync(stampFile, "utf8") !== stamp || !existsSync(file)) {
661
+ mkdirSync(edited, { recursive: true });
662
+ const contents = [];
663
+ for (const name of readdirSync(moduleDir).sort()) {
664
+ const original = path.join(moduleDir, name);
665
+ if (!statSync(original).isFile())
666
+ continue;
667
+ let external = original;
668
+ if (name.endsWith(".swiftinterface")) {
669
+ external = path.join(edited, name);
670
+ writeFileSync(external, hideFoundationPredicate(readFileSync(original, "utf8")));
671
+ }
672
+ contents.push({ name, type: "file", "external-contents": external });
673
+ }
674
+ writeFileSync(file, `${JSON.stringify({ version: 0, roots: [{ name: moduleDir, type: "directory", contents }] }, null, 2)}\n`);
675
+ writeFileSync(stampFile, stamp);
676
+ }
677
+ return { file, cache };
678
+ }
679
+ function hideFoundationPredicate(text) {
680
+ return text
681
+ .split("public struct Predicate<each Input>")
682
+ .join("public struct HiddenPredicate<each Input>")
683
+ .split("public macro Predicate<each Input>")
684
+ .join("public macro HiddenPredicate<each Input>")
685
+ .replace(/Foundation::Predicate(?![A-Za-z])/g, "Foundation::HiddenPredicate");
686
+ }
687
+ function readMochaConfig(pkg) {
688
+ const file = path.join(pkg, "package.json");
689
+ if (!existsSync(file))
690
+ return null;
691
+ try {
692
+ const body = JSON.parse(readFileSync(file, "utf8"));
693
+ const mocha = body.mocha;
694
+ if (!mocha)
695
+ return null;
696
+ const spec = Array.isArray(mocha.spec) ? mocha.spec : undefined;
697
+ const require = Array.isArray(mocha.require) ? mocha.require : [];
698
+ if ((!spec || spec.length === 0) && require.length === 0)
699
+ return null;
700
+ return { extension: mocha.extension, require, spec };
701
+ }
702
+ catch {
703
+ return null;
704
+ }
705
+ }
706
+ async function runDotnetTest(pkg, spec, timeoutMs, env, names = null) {
707
+ const child = dotnetEnv(env);
708
+ await ensureDotnetTools(pkg, child, timeoutMs);
709
+ const built = await ensureSiblingExes(pkg, spec, child, timeoutMs);
710
+ if (built)
711
+ return built;
712
+ const args = ["test", "--nologo", "--verbosity", "detailed"];
713
+ if (spec.project)
714
+ args.push(spec.project);
715
+ if (names && names.length > 0)
716
+ args.push("--filter", names.map((name) => `FullyQualifiedName~${name}`).join("|"));
717
+ return spawnCollected("dotnet", args, pkg, child, timeoutMs);
718
+ }
719
+ async function runDotnetExe(pkg, spec, files, timeoutMs, env) {
720
+ const child = dotnetEnv(env);
721
+ await ensureDotnetTools(pkg, child, timeoutMs);
722
+ const project = spec.project ?? walk(pkg, (rel) => rel.endsWith(".csproj") && (hasSegment(rel, "tests") || hasSegment(rel, "test")))[0];
723
+ const target = project ?? path.dirname(files[0] ?? ".");
724
+ return spawnCollected("dotnet", ["run", "--project", target, "--nologo"], pkg, child, timeoutMs);
725
+ }
726
+ function dotnetEnv(env) {
727
+ if (env.DOTNET_ROLL_FORWARD)
728
+ return env;
729
+ return { ...env, DOTNET_ROLL_FORWARD: "Major" };
730
+ }
731
+ /** Build a tool dll that a test project execs in PreBuild when that dll is not a ProjectReference. */
732
+ async function ensureDotnetTools(pkg, env, timeoutMs) {
733
+ const root = repoRoot(pkg);
734
+ const projects = walk(root, (rel) => rel.endsWith(".csproj"));
735
+ const missing = new Set();
736
+ const toolRe = /([A-Za-z0-9_.-]+)[/\\]bin[/\\]\$\(Configuration\)[/\\]([A-Za-z0-9_.-]+)\.dll/g;
737
+ for (const rel of projects) {
738
+ const text = readFileSync(path.join(root, rel), "utf8");
739
+ for (const match of text.matchAll(toolRe)) {
740
+ const folder = match[1];
741
+ const asm = match[2];
742
+ const beside = path.join(root, path.dirname(rel), folder, "bin");
743
+ const top = path.join(root, folder, "bin");
744
+ if (toolDllExists(beside, asm) || toolDllExists(top, asm))
745
+ continue;
746
+ const found = projects.find((item) => path.posix.basename(item) === `${asm}.csproj` && item.split("/").includes(folder));
747
+ if (found)
748
+ missing.add(found);
749
+ }
750
+ }
751
+ for (const rel of missing) {
752
+ await spawnCollected("dotnet", ["build", rel, "-c", "Debug", "--nologo"], root, env, timeoutMs);
753
+ }
754
+ }
755
+ function toolDllExists(bin, asm) {
756
+ return ["Debug", "Release"].some((cfg) => existsSync(path.join(bin, cfg, `${asm}.dll`)));
757
+ }
758
+ /**
759
+ * Build an Exe in this package that the test project does not reference.
760
+ * Suites often start that dll by a path under bin/$(Configuration), so `dotnet test` never compiles it.
761
+ */
762
+ async function ensureSiblingExes(pkg, spec, env, timeoutMs) {
763
+ const referenced = spec.project ? referencedProjects(pkg, spec.project) : new Set();
764
+ for (const rel of walk(pkg, (item) => item.endsWith(".csproj"))) {
765
+ if (rel === spec.project || referenced.has(rel))
766
+ continue;
767
+ const text = readFileSync(path.join(pkg, rel), "utf8");
768
+ if (!/<OutputType>\s*Exe\s*<\/OutputType>/i.test(text))
769
+ continue;
770
+ const built = await spawnCollected("dotnet", ["build", rel, "-c", "Debug", "--nologo"], pkg, env, timeoutMs);
771
+ if (built.code !== 0 || built.timedOut)
772
+ return built;
773
+ }
774
+ return null;
775
+ }
776
+ function referencedProjects(pkg, projectRel) {
777
+ const text = readFileSync(path.join(pkg, projectRel), "utf8");
778
+ const dir = path.dirname(path.join(pkg, projectRel));
779
+ const out = new Set();
780
+ for (const match of text.matchAll(/<ProjectReference\b[^>]*\bInclude\s*=\s*"([^"]+)"/gi)) {
781
+ const abs = path.resolve(dir, match[1]);
782
+ out.add(path.relative(pkg, abs).split(path.sep).join("/"));
783
+ }
784
+ return out;
785
+ }
786
+ function repoRoot(dir) {
787
+ let current = dir;
788
+ while (true) {
789
+ if (existsSync(path.join(current, ".git")))
790
+ return current;
791
+ const parent = path.dirname(current);
792
+ if (parent === current)
793
+ return dir;
794
+ current = parent;
795
+ }
796
+ }
797
+ async function compileAndRun(pkg, files, cxx, timeoutMs, env) {
798
+ const out = path.join(pkg, ".probatio-suite");
799
+ mkdirSync(out, { recursive: true });
800
+ const bin = path.join(out, "suite");
801
+ const tool = cxx ? "clang++" : "clang";
802
+ const compiled = await spawnCollected(tool, [cxx ? "-std=c++17" : "-std=c11", "-o", bin, ...abs(pkg, files)], pkg, env, timeoutMs);
803
+ if (compiled.code !== 0 || compiled.timedOut)
804
+ return compileFailed(compiled);
805
+ return spawnCollected(bin, [], pkg, env, timeoutMs);
806
+ }
807
+ async function compileJava(pkg, files, timeoutMs, env) {
808
+ const runtime = javaEnv(env);
809
+ const out = path.join(pkg, ".probatio-suite");
810
+ mkdirSync(out, { recursive: true });
811
+ const javac = path.join(runtime.JAVA_HOME ?? "", "bin", "javac");
812
+ const java = path.join(runtime.JAVA_HOME ?? "", "bin", "java");
813
+ const compiler = existsSync(javac) ? javac : "javac";
814
+ const runner = existsSync(java) ? java : "java";
815
+ const compiled = await spawnCollected(compiler, ["-d", out, ...abs(pkg, files)], pkg, runtime, timeoutMs);
816
+ if (compiled.code !== 0 || compiled.timedOut)
817
+ return compileFailed(compiled);
818
+ const main = javaMain(pkg, files) ?? "Main";
819
+ return spawnCollected(runner, ["-cp", out, main], pkg, runtime, timeoutMs);
820
+ }
821
+ async function compileSwift(pkg, files, timeoutMs, env) {
822
+ const out = path.join(pkg, ".probatio-suite");
823
+ mkdirSync(out, { recursive: true });
824
+ const bin = path.join(out, "suite");
825
+ const compiled = await spawnCollected("swiftc", ["-o", bin, ...abs(pkg, files)], pkg, env, timeoutMs);
826
+ if (compiled.code !== 0 || compiled.timedOut)
827
+ return compileFailed(compiled);
828
+ return spawnCollected(bin, [], pkg, env, timeoutMs);
829
+ }
830
+ async function compileCobol(pkg, files, timeoutMs, env) {
831
+ const out = path.join(pkg, ".probatio-suite");
832
+ mkdirSync(out, { recursive: true });
833
+ const bin = path.join(out, "suite");
834
+ const source = abs(pkg, files)[0];
835
+ const free = readFileSync(source, "utf8").includes("SOURCE FORMAT FREE");
836
+ const compiled = await spawnCollected("cobc", ["-x", ...(free ? ["-free"] : []), "-o", bin, source], pkg, env, timeoutMs);
837
+ if (compiled.code !== 0 || compiled.timedOut)
838
+ return compileFailed(compiled);
839
+ return spawnCollected(bin, [], pkg, env, timeoutMs);
840
+ }
841
+ async function assembleX86(pkg, files, timeoutMs, env) {
842
+ const out = path.join(pkg, ".probatio-suite");
843
+ mkdirSync(out, { recursive: true });
844
+ const obj = path.join(out, "suite.o");
845
+ const bin = path.join(out, "suite");
846
+ const assembled = await spawnCollected("nasm", ["-f", "macho64", "-o", obj, abs(pkg, files)[0]], pkg, env, timeoutMs);
847
+ if (assembled.code !== 0 || assembled.timedOut)
848
+ return compileFailed(assembled);
849
+ const linked = await spawnCollected("clang", ["-arch", "x86_64", "-o", bin, obj], pkg, env, timeoutMs);
850
+ if (linked.code !== 0 || linked.timedOut)
851
+ return compileFailed(linked);
852
+ return spawnCollected("arch", ["-x86_64", bin], pkg, env, timeoutMs);
853
+ }
854
+ async function assembleArm(pkg, files, timeoutMs, env) {
855
+ const out = path.join(pkg, ".probatio-suite");
856
+ mkdirSync(out, { recursive: true });
857
+ const bin = path.join(out, "suite");
858
+ const assembled = await spawnCollected("clang", ["-arch", "arm64", "-o", bin, abs(pkg, files)[0]], pkg, env, timeoutMs);
859
+ if (assembled.code !== 0 || assembled.timedOut)
860
+ return compileFailed(assembled);
861
+ return spawnCollected(bin, [], pkg, env, timeoutMs);
862
+ }
863
+ async function assembleRiscv(pkg, files, timeoutMs, env) {
864
+ const out = path.join(pkg, ".probatio-suite");
865
+ mkdirSync(out, { recursive: true });
866
+ const obj = path.join(out, "suite.o");
867
+ const bin = path.join(out, "suite");
868
+ const assembler = commandExists("riscv64-elf-as") ? "riscv64-elf-as" : "riscv64-unknown-elf-as";
869
+ const linker = commandExists("riscv64-elf-ld") ? "riscv64-elf-ld" : "riscv64-unknown-elf-ld";
870
+ const assembled = await spawnCollected(assembler, ["-o", obj, abs(pkg, files)[0]], pkg, env, timeoutMs);
871
+ if (assembled.code !== 0 || assembled.timedOut)
872
+ return compileFailed(assembled);
873
+ // Homebrew QEMU on macOS ships qemu-system-riscv64 and no linux-user binary.
874
+ // The virt machine loads the kernel at 0x80000000. The fixture exits by writing
875
+ // (code << 16) | 0x5555 to the SiFive test finisher at 0x100000.
876
+ const linkScript = path.join(out, "link.ld");
877
+ writeFileSync(linkScript, ["OUTPUT_ARCH(riscv)", "ENTRY(_start)", "SECTIONS", "{", " . = 0x80000000;", " .text : { *(.text*) }", "}", ""].join("\n"));
878
+ const linked = await spawnCollected(linker, ["-nostdlib", "-T", linkScript, "-o", bin, obj], pkg, env, timeoutMs);
879
+ if (linked.code !== 0 || linked.timedOut)
880
+ return compileFailed(linked);
881
+ if (commandExists("qemu-riscv64"))
882
+ return spawnCollected("qemu-riscv64", [bin], pkg, env, timeoutMs);
883
+ if (commandExists("qemu-riscv64-static"))
884
+ return spawnCollected("qemu-riscv64-static", [bin], pkg, env, timeoutMs);
885
+ return spawnCollected("qemu-system-riscv64", ["-machine", "virt", "-nographic", "-bios", "none", "-kernel", bin, "-serial", "none", "-monitor", "none", "-display", "none"], pkg, env, timeoutMs);
886
+ }
887
+ function compileFailed(result) {
888
+ return { ...result, stderr: `PROB_COMPILE_FAILED\n${result.stderr}` };
889
+ }
890
+ /** A compiler token when this run failed before any test result exists. Null when tests ran. */
891
+ export function detectCompile(kind, stdout, stderr, code) {
892
+ if (code === 0)
893
+ return null;
894
+ const text = `${stdout}\n${stderr}`;
895
+ if (text.includes("PROB_COMPILE_FAILED"))
896
+ return compilerToken(kind);
897
+ if (kind === "maven" && /COMPILATION ERROR|Compilation failure/.test(text) && !/Tests run:\s*\d+/.test(text))
898
+ return "javac";
899
+ if (kind === "cargo" && /could not compile|error\[E\d+\]/.test(text) && !/^test \S+ \.\.\. (ok|FAILED)/m.test(text))
900
+ return "cargo";
901
+ if (kind === "swift" && /\berror:/.test(text) && !/Test Case '[^']+' (passed|failed)/.test(text))
902
+ return "swiftc";
903
+ if ((kind === "dotnet" || kind === "dotnet-exe") && /error CS\d+|error MSB\d+/.test(text) && !/^\s*(Passed|Failed)\s+\S+/m.test(text))
904
+ return "dotnet";
905
+ if (kind === "go" && /\.go:\d+:\d+:/.test(text) && !/"Test"\s*:/.test(text))
906
+ return "build";
907
+ if ((kind === "pytest" || kind === "unittest") && /SyntaxError|IndentationError/.test(text) && jsonReport(stdout) === null)
908
+ return "compiler";
909
+ if (kind === "node" && /SyntaxError/.test(text) && jsonReport(stdout) === null)
910
+ return "compiler";
911
+ if (kind === "mocha" && /error TS\d+|Unable to compile/.test(text) && !/"tests"\s*:/.test(text))
912
+ return "tsc";
913
+ if (kind === "make-test" && !/Tests run:\s*\d+/.test(text) && /error:|cobc:|syntax error/i.test(text))
914
+ return "cobc";
915
+ // A shell suite that dies in the compiler never prints a test title. That is a compile kill.
916
+ if (kind === "script" && !scriptReported(text) && /:\s*error:|fatal error:|undefined reference to|\[-Werror/.test(text))
917
+ return "clang";
918
+ return null;
919
+ }
920
+ function scriptReported(text) {
921
+ const testLine = /^(Testing (?!with FORCE_SUBREAPER\b).+|Running (?:exit code|reaping|process group|zombie|signal configuration|parent death).+|running signal test .+)$/;
922
+ return text.split(/\r?\n/).some((raw) => testLine.test(scriptLine(raw)));
923
+ }
924
+ function scriptLine(raw) {
925
+ // `script` prints ^D and a backspace before the program's own line.
926
+ const stripped = raw.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "").replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, "");
927
+ return stripped.replace(/^\^D+/, "").trim();
928
+ }
929
+ function compilerToken(kind) {
930
+ if (kind === "asm-x86_64")
931
+ return "nasm";
932
+ if (kind === "cobol" || kind === "make-test")
933
+ return "cobc";
934
+ if (kind === "java" || kind === "maven")
935
+ return "javac";
936
+ if (kind === "swift" || kind === "swiftc")
937
+ return "swiftc";
938
+ if (kind === "dotnet" || kind === "dotnet-exe")
939
+ return "dotnet";
940
+ if (kind === "cargo")
941
+ return "cargo";
942
+ if (kind === "c" || kind === "cpp" || kind === "asm-aarch64")
943
+ return "clang";
944
+ if (kind === "mocha")
945
+ return "tsc";
946
+ return "build";
947
+ }
948
+ function adapt(kind, pkg, stdout, stderr, code) {
949
+ if (stderr.includes("PROB_COMPILE_FAILED"))
950
+ return null;
951
+ if (kind === "node" || kind === "unittest" || kind === "pytest")
952
+ return jsonReport(stdout);
953
+ if (kind === "go")
954
+ return parseGo(stdout);
955
+ if (kind === "cargo")
956
+ return parseCargo(`${stdout}\n${stderr}`);
957
+ if (kind === "swift")
958
+ return parseSwift(`${stdout}\n${stderr}`, code);
959
+ if (kind === "script")
960
+ return parseScriptReport(`${stdout}\n${stderr}`, code);
961
+ if (kind === "maven")
962
+ return mavenReport(pkg, `${stdout}\n${stderr}`, code);
963
+ if (kind === "dotnet")
964
+ return parseDotnet(`${stdout}\n${stderr}`, code);
965
+ if (kind === "mocha")
966
+ return parseMocha(stdout);
967
+ if (kind === "make-test" || kind === "command") {
968
+ const text = `${stdout}\n${stderr}`;
969
+ if (/Tests run:\s*\d+/.test(text))
970
+ return parseMakeTest(text);
971
+ if (kind === "make-test")
972
+ return parseMakeTest(text);
973
+ return exitReport(kind, code);
974
+ }
975
+ return exitReport(kind, code);
976
+ }
977
+ function jsonReport(stdout) {
978
+ const lines = stdout
979
+ .split("\n")
980
+ .map((line) => line.trim())
981
+ .filter((line) => line.startsWith("{"));
982
+ if (lines.length === 0)
983
+ return null;
984
+ try {
985
+ const parsed = JSON.parse(lines[lines.length - 1]);
986
+ if (!Array.isArray(parsed.failed) || !Array.isArray(parsed.names))
987
+ return null;
988
+ if (parsed.names.length === 0)
989
+ return null;
990
+ return parsed;
991
+ }
992
+ catch {
993
+ return null;
994
+ }
995
+ }
996
+ function parseGo(stdout) {
997
+ const names = [];
998
+ const failed = [];
999
+ for (const line of stdout.split("\n")) {
1000
+ if (!line.startsWith("{"))
1001
+ continue;
1002
+ try {
1003
+ const event = JSON.parse(line);
1004
+ if (!event.Test)
1005
+ continue;
1006
+ if (event.Action === "run" && !names.includes(event.Test))
1007
+ names.push(event.Test);
1008
+ if (event.Action === "fail")
1009
+ failed.push({ name: event.Test, file: event.Package ?? "", line: 0 });
1010
+ }
1011
+ catch {
1012
+ continue;
1013
+ }
1014
+ }
1015
+ if (names.length === 0)
1016
+ return null;
1017
+ return { tests: names.length, pass: names.length - failed.length, fail: failed.length, failed, names };
1018
+ }
1019
+ function parseCargo(text) {
1020
+ const names = [];
1021
+ const failed = [];
1022
+ for (const line of text.split("\n")) {
1023
+ const match = line.match(/^test (\S+) \.\.\. (ok|FAILED)/);
1024
+ if (!match)
1025
+ continue;
1026
+ names.push(match[1]);
1027
+ if (match[2] === "FAILED")
1028
+ failed.push({ name: match[1], file: "", line: 0 });
1029
+ }
1030
+ if (names.length === 0)
1031
+ return null;
1032
+ return { tests: names.length, pass: names.length - failed.length, fail: failed.length, failed, names };
1033
+ }
1034
+ function parseScriptReport(text, code) {
1035
+ const names = [];
1036
+ const testLine = /^(Testing (?!with FORCE_SUBREAPER\b).+|Running (?:exit code|reaping|process group|zombie|signal configuration|parent death).+|running signal test .+)$/;
1037
+ for (const raw of text.split(/\r?\n/)) {
1038
+ const line = scriptLine(raw);
1039
+ if (!testLine.test(line) || names.includes(line))
1040
+ continue;
1041
+ names.push(line);
1042
+ }
1043
+ if (names.length === 0)
1044
+ return null;
1045
+ const failed = text.includes("All done, tests as expected") || code === 0 ? [] : [{ name: names[names.length - 1], file: "", line: 0 }];
1046
+ return { tests: names.length, pass: names.length - failed.length, fail: failed.length, failed, names };
1047
+ }
1048
+ function parseSwift(text, code) {
1049
+ const names = [];
1050
+ const failed = [];
1051
+ for (const match of text.matchAll(/Test Case '([^']+)' (passed|failed)/g)) {
1052
+ names.push(match[1]);
1053
+ if (match[2] === "failed")
1054
+ failed.push({ name: match[1], file: "", line: 0 });
1055
+ }
1056
+ for (const match of text.matchAll(/[✔✘]\s+([^\s(]+)/g)) {
1057
+ if (names.includes(match[1]))
1058
+ continue;
1059
+ names.push(match[1]);
1060
+ if (match[0].includes("✘"))
1061
+ failed.push({ name: match[1], file: "", line: 0 });
1062
+ }
1063
+ if (names.length === 0)
1064
+ return null;
1065
+ return { tests: names.length, pass: names.length - failed.length, fail: failed.length, failed, names };
1066
+ }
1067
+ function parseMocha(stdout) {
1068
+ const start = stdout.indexOf("{");
1069
+ if (start < 0)
1070
+ return null;
1071
+ try {
1072
+ const body = JSON.parse(stdout.slice(start));
1073
+ const tests = body.tests ?? [];
1074
+ const names = tests.map((item) => item.fullTitle || item.title || "").filter((name) => name.length > 0);
1075
+ const failed = (body.failures ?? []).map((item) => ({
1076
+ name: item.fullTitle || item.title || "",
1077
+ file: item.file ?? "",
1078
+ line: 0,
1079
+ }));
1080
+ if (names.length === 0)
1081
+ return null;
1082
+ return { tests: names.length, pass: names.length - failed.length, fail: failed.length, failed, names };
1083
+ }
1084
+ catch {
1085
+ return null;
1086
+ }
1087
+ }
1088
+ function parseMakeTest(text) {
1089
+ const run = text.match(/Tests run:\s*(\d+)/);
1090
+ if (!run)
1091
+ return null;
1092
+ const tests = Number(run[1]);
1093
+ if (!Number.isFinite(tests) || tests <= 0)
1094
+ return null;
1095
+ // Progress logs print Case: for every case. Only the Failures section is a result.
1096
+ // Tests failed: 0 is a green run even when those progress lines are present.
1097
+ const failedCountMatch = text.match(/Tests failed:\s*(\d+)/);
1098
+ const failCount = failedCountMatch ? Number(failedCountMatch[1]) : null;
1099
+ const failed = [];
1100
+ if (failCount !== 0) {
1101
+ const parts = text.split(/^Failures:\s*$/m);
1102
+ const failureLog = parts.length > 1 ? parts.slice(1).join("\n") : "";
1103
+ let suite = "";
1104
+ let unit = "";
1105
+ for (const raw of failureLog.split(/\r?\n/)) {
1106
+ const line = raw.trim();
1107
+ const suiteName = /^Suite:\s*(.*)$/.exec(line);
1108
+ if (suiteName) {
1109
+ suite = suiteName[1].trim();
1110
+ continue;
1111
+ }
1112
+ const unitName = /^Test:\s*(.*)$/.exec(line);
1113
+ if (unitName) {
1114
+ unit = unitName[1].trim();
1115
+ continue;
1116
+ }
1117
+ const caseName = /^Case:\s*(.*)$/.exec(line);
1118
+ if (!caseName)
1119
+ continue;
1120
+ const name = [suite, unit, caseName[1].trim()].filter((part) => part.length > 0).join("::");
1121
+ failed.push({ name: name || caseName[1].trim(), file: "", line: 0 });
1122
+ }
1123
+ if (failed.length === 0 && failCount !== null && failCount > 0)
1124
+ failed.push({ name: "failed", file: "", line: 0 });
1125
+ }
1126
+ const names = failed.map((item) => item.name);
1127
+ while (names.length < tests)
1128
+ names.push(`passed-${names.length + 1}`);
1129
+ return { tests, pass: Math.max(tests - failed.length, 0), fail: failed.length, failed, names };
1130
+ }
1131
+ function parseDotnet(text, code) {
1132
+ const names = [];
1133
+ const failed = [];
1134
+ for (const line of text.split("\n")) {
1135
+ const match = line.match(/^\s*(Passed|Failed)\s+(\S+)/);
1136
+ if (!match)
1137
+ continue;
1138
+ names.push(match[2]);
1139
+ if (match[1] === "Failed")
1140
+ failed.push({ name: match[2], file: "", line: 0 });
1141
+ }
1142
+ if (names.length === 0)
1143
+ return null;
1144
+ return { tests: names.length, pass: names.length - failed.length, fail: failed.length, failed, names };
1145
+ }
1146
+ function mavenReport(pkg, text, code) {
1147
+ // A compile error exits before Surefire rewrites target/surefire-reports, so the
1148
+ // previous run's XML is still a green report. That is not this mutant's result.
1149
+ if (code !== 0 && /COMPILATION ERROR|Compilation failure/.test(text) && !/Tests run:\s*\d+/.test(text)) {
1150
+ return { tests: 1, pass: 0, fail: 1, failed: [{ name: "javac", file: "", line: 0 }], names: ["javac"] };
1151
+ }
1152
+ return parseSurefire(pkg) ?? parseMavenText(text);
1153
+ }
1154
+ function parseSurefire(pkg) {
1155
+ const dir = path.join(pkg, "target", "surefire-reports");
1156
+ if (!existsSync(dir))
1157
+ return null;
1158
+ const names = [];
1159
+ const failed = [];
1160
+ for (const name of readdirSync(dir)) {
1161
+ if (!name.endsWith(".xml"))
1162
+ continue;
1163
+ const xml = readFileSync(path.join(dir, name), "utf8");
1164
+ for (const part of xml.split("<testcase ").slice(1)) {
1165
+ const test = /name="([^"]+)"/.exec(part)?.[1];
1166
+ const klass = /classname="([^"]+)"/.exec(part)?.[1] ?? "";
1167
+ if (!test)
1168
+ continue;
1169
+ const id = `${klass}.${test}`;
1170
+ names.push(id);
1171
+ if (part.includes("<failure") || part.includes("<error"))
1172
+ failed.push({ name: id, file: klass, line: 0 });
1173
+ }
1174
+ }
1175
+ if (names.length === 0)
1176
+ return null;
1177
+ return { tests: names.length, pass: names.length - failed.length, fail: failed.length, failed, names };
1178
+ }
1179
+ function parseMavenText(text) {
1180
+ const match = text.match(/Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+)/);
1181
+ if (!match)
1182
+ return null;
1183
+ const tests = Number(match[1]);
1184
+ const fail = Number(match[2]) + Number(match[3]);
1185
+ if (tests === 0)
1186
+ return null;
1187
+ const names = Array.from({ length: tests }, (_, index) => `maven-${index + 1}`);
1188
+ const failed = names.slice(0, fail).map((name) => ({ name, file: "pom.xml", line: 0 }));
1189
+ return { tests, pass: tests - fail, fail, failed, names };
1190
+ }
1191
+ function exitReport(kind, code) {
1192
+ const name = kind;
1193
+ const failed = code === 0 ? [] : [{ name, file: "", line: 0 }];
1194
+ return { tests: 1, pass: code === 0 ? 1 : 0, fail: failed.length, failed, names: [name] };
1195
+ }
1196
+ function javaMain(pkg, files) {
1197
+ for (const rel of files) {
1198
+ const text = readFileSync(path.join(pkg, rel), "utf8");
1199
+ const match = text.match(/public\s+class\s+(\w+)/);
1200
+ if (match && text.includes("static void main"))
1201
+ return match[1];
1202
+ }
1203
+ return null;
1204
+ }
1205
+ function abs(pkg, files) {
1206
+ return files.map((rel) => path.join(pkg, rel));
1207
+ }
1208
+ function walk(root, accept) {
1209
+ const out = [];
1210
+ const visit = (dir) => {
1211
+ let entries;
1212
+ try {
1213
+ entries = readdirSync(dir);
1214
+ }
1215
+ catch {
1216
+ return;
1217
+ }
1218
+ for (const entry of entries.sort()) {
1219
+ if (SKIP_DIR.has(entry))
1220
+ continue;
1221
+ const full = path.join(dir, entry);
1222
+ let info;
1223
+ try {
1224
+ info = statSync(full);
1225
+ }
1226
+ catch {
1227
+ continue;
1228
+ }
1229
+ if (info.isDirectory()) {
1230
+ visit(full);
1231
+ continue;
1232
+ }
1233
+ const rel = path.relative(root, full).split(path.sep).join("/");
1234
+ if (accept(rel))
1235
+ out.push(rel);
1236
+ }
1237
+ };
1238
+ visit(root);
1239
+ return out;
1240
+ }
1241
+ function javaEnv(env) {
1242
+ if (env.JAVA_HOME && existsSync(path.join(env.JAVA_HOME, "bin", "java")))
1243
+ return env;
1244
+ const prefix = brewPrefix("openjdk");
1245
+ if (!prefix)
1246
+ return env;
1247
+ return { ...env, JAVA_HOME: prefix, PATH: `${path.join(prefix, "bin")}${path.delimiter}${env.PATH ?? ""}` };
1248
+ }
1249
+ function brewPrefix(formula) {
1250
+ const result = spawnSync("brew", ["--prefix", formula], { encoding: "utf8" });
1251
+ if (result.status !== 0)
1252
+ return null;
1253
+ const prefix = result.stdout.trim();
1254
+ return prefix && existsSync(prefix) ? prefix : null;
1255
+ }
1256
+ function commandExists(name) {
1257
+ const result = spawnSync("sh", ["-c", `command -v ${name}`], { encoding: "utf8" });
1258
+ return result.status === 0 && result.stdout.trim().length > 0;
1259
+ }
1260
+ function firstLine(text) {
1261
+ return text.trim().split("\n").find((line) => line.trim()) ?? "";
1262
+ }
1263
+ function spawnCollected(bin, args, cwd, env, timeoutMs) {
1264
+ return new Promise((resolve) => {
1265
+ const child = spawn(bin, args, { cwd, env, detached: true, stdio: ["ignore", "pipe", "pipe"] });
1266
+ let stdout = "";
1267
+ let stderr = "";
1268
+ let settled = false;
1269
+ let timedOut = false;
1270
+ const finish = (code) => {
1271
+ if (settled)
1272
+ return;
1273
+ settled = true;
1274
+ clearTimeout(timer);
1275
+ resolve({ code, stdout, stderr, timedOut });
1276
+ };
1277
+ const timer = setTimeout(() => {
1278
+ timedOut = true;
1279
+ try {
1280
+ if (child.pid)
1281
+ process.kill(-child.pid, "SIGKILL");
1282
+ }
1283
+ catch {
1284
+ child.kill("SIGKILL");
1285
+ }
1286
+ }, timeoutMs);
1287
+ child.stdout?.on("data", (chunk) => {
1288
+ stdout += chunk;
1289
+ });
1290
+ child.stderr?.on("data", (chunk) => {
1291
+ stderr += chunk;
1292
+ });
1293
+ child.on("error", (error) => {
1294
+ stderr += error.message;
1295
+ finish(1);
1296
+ });
1297
+ child.on("close", (code) => finish(code ?? 1));
1298
+ });
1299
+ }