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/mutate/affected.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
const IMPORT_SPEC = /(?:from\s+|import\s*\(\s*|import\s+)["'](\.{1,2}\/[^"']+)["']/g;
|
|
4
|
-
/** Package-relative source file to the test files that can see a change in it. */
|
|
5
|
-
export function affectedTests(packageDir, options = {}) {
|
|
6
|
-
const srcRel = options.srcDir ?? "src";
|
|
7
|
-
const testsRel = options.testsDir ?? "tests";
|
|
8
|
-
const srcDir = path.join(packageDir, srcRel);
|
|
9
|
-
const testsDir = path.join(packageDir, testsRel);
|
|
10
|
-
const alwaysPattern = options.alwaysPattern ?? /\b(?:spawn|execFile|execSync)\b|dist\//;
|
|
11
|
-
const sources = walk(srcDir, (name) => /\.(ts|tsx|py)$/.test(name) && !name.endsWith(".d.ts"));
|
|
12
|
-
const tests = existsSync(testsDir)
|
|
13
|
-
? readdirSync(testsDir)
|
|
14
|
-
.filter((name) => /\.test\.(ts|tsx|mts|js|mjs)$/.test(name))
|
|
15
|
-
.sort()
|
|
16
|
-
: [];
|
|
17
|
-
const imports = new Map();
|
|
18
|
-
const read = (file) => {
|
|
19
|
-
const found = imports.get(file);
|
|
20
|
-
if (found)
|
|
21
|
-
return found;
|
|
22
|
-
const out = new Set();
|
|
23
|
-
imports.set(file, out);
|
|
24
|
-
if (!existsSync(file))
|
|
25
|
-
return out;
|
|
26
|
-
const text = readFileSync(file, "utf8");
|
|
27
|
-
for (const match of text.matchAll(IMPORT_SPEC)) {
|
|
28
|
-
const resolved = resolveImport(path.dirname(file), match[1]);
|
|
29
|
-
if (resolved)
|
|
30
|
-
out.add(resolved);
|
|
31
|
-
}
|
|
32
|
-
return out;
|
|
33
|
-
};
|
|
34
|
-
const closureOf = (file) => {
|
|
35
|
-
const seen = new Set();
|
|
36
|
-
const stack = [file];
|
|
37
|
-
while (stack.length) {
|
|
38
|
-
const current = stack.pop();
|
|
39
|
-
if (!current || seen.has(current))
|
|
40
|
-
continue;
|
|
41
|
-
seen.add(current);
|
|
42
|
-
for (const next of read(current))
|
|
43
|
-
stack.push(next);
|
|
44
|
-
}
|
|
45
|
-
return seen;
|
|
46
|
-
};
|
|
47
|
-
const rel = (file) => path.relative(packageDir, file).split(path.sep).join("/");
|
|
48
|
-
const byModule = new Map();
|
|
49
|
-
for (const source of sources)
|
|
50
|
-
byModule.set(rel(source), new Set());
|
|
51
|
-
const testRel = (name) => `${testsRel}/${name}`.replace(/\\/g, "/");
|
|
52
|
-
const always = [];
|
|
53
|
-
for (const test of tests) {
|
|
54
|
-
const text = readFileSync(path.join(testsDir, test), "utf8");
|
|
55
|
-
if (alwaysPattern.test(text))
|
|
56
|
-
always.push(testRel(test));
|
|
57
|
-
}
|
|
58
|
-
for (const names of byModule.values())
|
|
59
|
-
for (const test of always)
|
|
60
|
-
names.add(test);
|
|
61
|
-
for (const test of tests) {
|
|
62
|
-
const seen = closureOf(path.join(testsDir, test));
|
|
63
|
-
for (const file of seen) {
|
|
64
|
-
const key = rel(file);
|
|
65
|
-
const testsForFile = byModule.get(key);
|
|
66
|
-
if (testsForFile)
|
|
67
|
-
testsForFile.add(testRel(test));
|
|
68
|
-
}
|
|
69
|
-
const blob = [path.join(testsDir, test), ...seen].map((file) => (existsSync(file) ? readFileSync(file, "utf8") : "")).join("\n");
|
|
70
|
-
for (const source of sources) {
|
|
71
|
-
if (!source.endsWith(".py"))
|
|
72
|
-
continue;
|
|
73
|
-
if (blob.includes(path.basename(source)))
|
|
74
|
-
byModule.get(rel(source))?.add(testRel(test));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
for (const testFile of walk(testsDir, (name) => /^test_.*\.py$/.test(name) || /_test\.py$/.test(name))) {
|
|
78
|
-
const text = readFileSync(testFile, "utf8");
|
|
79
|
-
const relTest = rel(testFile);
|
|
80
|
-
for (const source of sources) {
|
|
81
|
-
if (!source.endsWith(".py"))
|
|
82
|
-
continue;
|
|
83
|
-
const moduleName = pythonModuleName(rel(source));
|
|
84
|
-
if (!moduleName)
|
|
85
|
-
continue;
|
|
86
|
-
const pattern = new RegExp(`(?<![\\w.])${escapeRegExp(moduleName)}(?![\\w.])`);
|
|
87
|
-
if (pattern.test(text))
|
|
88
|
-
byModule.get(rel(source))?.add(relTest);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return Object.fromEntries([...byModule.entries()].sort().map(([file, names]) => [file, [...names].sort()]));
|
|
92
|
-
}
|
|
93
|
-
/** Test files that import one of `changed` themselves, not through another module. */
|
|
94
|
-
export function directImporters(packageDir, changed, testsDir = "tests") {
|
|
95
|
-
const testsRoot = path.join(packageDir, testsDir);
|
|
96
|
-
if (!existsSync(testsRoot))
|
|
97
|
-
return [];
|
|
98
|
-
const wanted = new Set(changed.map((file) => path.resolve(packageDir, file)));
|
|
99
|
-
const out = [];
|
|
100
|
-
const seen = new Set();
|
|
101
|
-
const add = (rel) => {
|
|
102
|
-
if (seen.has(rel))
|
|
103
|
-
return;
|
|
104
|
-
seen.add(rel);
|
|
105
|
-
out.push(rel);
|
|
106
|
-
};
|
|
107
|
-
for (const name of readdirSync(testsRoot).filter((item) => /\.test\.(ts|tsx|mts|js|mjs)$/.test(item)).sort()) {
|
|
108
|
-
const file = path.join(testsRoot, name);
|
|
109
|
-
const text = readFileSync(file, "utf8");
|
|
110
|
-
for (const match of text.matchAll(IMPORT_SPEC)) {
|
|
111
|
-
const resolved = resolveImport(path.dirname(file), match[1]);
|
|
112
|
-
if (resolved && wanted.has(resolved)) {
|
|
113
|
-
add(`${testsDir}/${name}`.replace(/\\/g, "/"));
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const patterns = changed
|
|
119
|
-
.map((file) => pythonModuleName(file))
|
|
120
|
-
.filter((name) => name !== null)
|
|
121
|
-
.map((name) => new RegExp(`(?<![\\w.])${escapeRegExp(name)}(?![\\w.])`));
|
|
122
|
-
if (patterns.length > 0) {
|
|
123
|
-
for (const file of walk(testsRoot, (name) => /^test_.*\.py$/.test(name) || /_test\.py$/.test(name))) {
|
|
124
|
-
const text = readFileSync(file, "utf8");
|
|
125
|
-
if (!patterns.some((pattern) => pattern.test(text)))
|
|
126
|
-
continue;
|
|
127
|
-
add(path.relative(packageDir, file).split(path.sep).join("/"));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return out;
|
|
131
|
-
}
|
|
132
|
-
/** `src/pkg/mod.py` is the import name `pkg.mod`. */
|
|
133
|
-
function pythonModuleName(file) {
|
|
134
|
-
const norm = file.split(path.sep).join("/");
|
|
135
|
-
if (!norm.endsWith(".py"))
|
|
136
|
-
return null;
|
|
137
|
-
let body = norm.slice(0, -3);
|
|
138
|
-
if (body.startsWith("src/"))
|
|
139
|
-
body = body.slice(4);
|
|
140
|
-
if (body.endsWith("/__init__"))
|
|
141
|
-
body = body.slice(0, -"/__init__".length);
|
|
142
|
-
if (!body || body.startsWith("../"))
|
|
143
|
-
return null;
|
|
144
|
-
return body.split("/").join(".");
|
|
145
|
-
}
|
|
146
|
-
function escapeRegExp(value) {
|
|
147
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
148
|
-
}
|
|
149
|
-
function resolveImport(fromDir, spec) {
|
|
150
|
-
const target = path.resolve(fromDir, spec);
|
|
151
|
-
const candidates = [target, `${target}.ts`, `${target}.tsx`, `${target}.mts`, `${target}.js`, `${target}.mjs`, `${target}.py`];
|
|
152
|
-
for (const candidate of candidates) {
|
|
153
|
-
if (existsSync(candidate) && statSync(candidate).isFile())
|
|
154
|
-
return candidate;
|
|
155
|
-
}
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
158
|
-
function walk(dir, keep) {
|
|
159
|
-
if (!existsSync(dir))
|
|
160
|
-
return [];
|
|
161
|
-
const out = [];
|
|
162
|
-
const visit = (current) => {
|
|
163
|
-
for (const entry of readdirSync(current).sort()) {
|
|
164
|
-
const full = path.join(current, entry);
|
|
165
|
-
if (statSync(full).isDirectory())
|
|
166
|
-
visit(full);
|
|
167
|
-
else if (keep(entry))
|
|
168
|
-
out.push(full);
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
visit(dir);
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// Nameless line dump for a node child of a test. This is not a node:test process and it
|
|
2
|
-
// does not write a coverage shard. The parent test's afterEach reads the dump.
|
|
3
|
-
// The test-process collector puts this file in NODE_OPTIONS, so the child needs no import.
|
|
4
|
-
// The dump is written on `exit`, which also runs after process.exit(), so it is synchronous.
|
|
5
|
-
import { randomBytes } from "node:crypto"
|
|
6
|
-
import { mkdirSync, writeFileSync } from "node:fs"
|
|
7
|
-
import path from "node:path"
|
|
8
|
-
import { openSyncLineSampler } from "./precise-lines.mjs"
|
|
9
|
-
|
|
10
|
-
const mapPath = process.env.PROBATIO_COVERAGE_MAP
|
|
11
|
-
if (mapPath && !globalThis.__probatioChildLines) {
|
|
12
|
-
globalThis.__probatioChildLines = true
|
|
13
|
-
let sampler = null
|
|
14
|
-
try {
|
|
15
|
-
sampler = openSyncLineSampler(
|
|
16
|
-
(rel) => rel.endsWith("child-lines.mjs") || rel.endsWith("precise-lines.mjs"),
|
|
17
|
-
process.env.PROBATIO_PACKAGE_ROOT || process.cwd(),
|
|
18
|
-
)
|
|
19
|
-
} catch (err) {
|
|
20
|
-
writeFileSync(`${mapPath}.error`, `${err instanceof Error ? err.message : String(err)}\n`)
|
|
21
|
-
}
|
|
22
|
-
if (sampler) {
|
|
23
|
-
process.on("exit", () => {
|
|
24
|
-
// A node:test process that loaded this through an inherited NODE_OPTIONS has its own
|
|
25
|
-
// hooks. Its hits are already named. A nameless dump would land on the wrong test.
|
|
26
|
-
if (globalThis.__probatioTestHooks) return
|
|
27
|
-
try {
|
|
28
|
-
const hits = sampler.positiveLines()
|
|
29
|
-
// A child started by a mapped test writes where that test's process reads.
|
|
30
|
-
// A child that was not (a script that imports this file itself) uses the shared dir.
|
|
31
|
-
const dir = process.env.PROBATIO_CHILD_DIR || `${mapPath}.children`
|
|
32
|
-
mkdirSync(dir, { recursive: true })
|
|
33
|
-
writeFileSync(path.join(dir, `${process.pid}-${randomBytes(4).toString("hex")}.json`), JSON.stringify({ hits }))
|
|
34
|
-
} catch (err) {
|
|
35
|
-
writeFileSync(`${mapPath}.error`, `${err instanceof Error ? err.message : String(err)}\n`)
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { WHOLE_PROGRAM_TEST } from "./suite-decision.js";
|
|
5
|
-
export function readCoverageMap(file) {
|
|
6
|
-
if (!existsSync(file))
|
|
7
|
-
return null;
|
|
8
|
-
try {
|
|
9
|
-
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
10
|
-
if (!parsed || typeof parsed.files !== "object" || parsed.files === null)
|
|
11
|
-
return null;
|
|
12
|
-
return parsed;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
/** Lines the baseline executed, and the tests that hit them. No map means no decision. */
|
|
19
|
-
export function selectionFor(map, files) {
|
|
20
|
-
if (!map || Object.keys(map.files).length === 0 || files.length === 0)
|
|
21
|
-
return { state: "none" };
|
|
22
|
-
const tests = new Set();
|
|
23
|
-
let sawUncovered = false;
|
|
24
|
-
let uncovered = files[0];
|
|
25
|
-
for (const item of files) {
|
|
26
|
-
const hit = testsOnLine(map, item.file, item.line);
|
|
27
|
-
if (hit === null) {
|
|
28
|
-
sawUncovered = true;
|
|
29
|
-
uncovered = item;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
for (const name of hit)
|
|
33
|
-
tests.add(name);
|
|
34
|
-
}
|
|
35
|
-
if (tests.size === 0 && sawUncovered)
|
|
36
|
-
return { state: "uncovered", file: uncovered.file, line: uncovered.line };
|
|
37
|
-
if (tests.size === 0)
|
|
38
|
-
return { state: "none" };
|
|
39
|
-
const [file, line] = coveredAnchor(map, files);
|
|
40
|
-
return { state: "covered", file, line, tests: [...tests] };
|
|
41
|
-
}
|
|
42
|
-
function coveredAnchor(map, files) {
|
|
43
|
-
for (const item of files) {
|
|
44
|
-
if (testsOnLine(map, item.file, item.line))
|
|
45
|
-
return [item.file, item.line];
|
|
46
|
-
}
|
|
47
|
-
return [files[0].file, files[0].line];
|
|
48
|
-
}
|
|
49
|
-
function testsOnLine(map, file, line) {
|
|
50
|
-
const lines = linesFor(map, file);
|
|
51
|
-
if (!lines)
|
|
52
|
-
return null;
|
|
53
|
-
const tests = lines[String(line)];
|
|
54
|
-
if (!tests || tests.length === 0)
|
|
55
|
-
return null;
|
|
56
|
-
return tests;
|
|
57
|
-
}
|
|
58
|
-
function linesFor(map, file) {
|
|
59
|
-
const norm = file.split(path.sep).join("/");
|
|
60
|
-
if (map.files[norm])
|
|
61
|
-
return map.files[norm];
|
|
62
|
-
if (map.files[file])
|
|
63
|
-
return map.files[file];
|
|
64
|
-
// A repo-relative patch can be longer than the package-relative map key.
|
|
65
|
-
// The longest suffix wins, so span/simple.py does not steal span_handlers/simple.py.
|
|
66
|
-
let best = null;
|
|
67
|
-
let bestLen = -1;
|
|
68
|
-
const baseHits = [];
|
|
69
|
-
const base = norm.split("/").pop() ?? norm;
|
|
70
|
-
for (const [name, lines] of Object.entries(map.files)) {
|
|
71
|
-
const key = name.split(path.sep).join("/");
|
|
72
|
-
if (key === norm || norm.endsWith(`/${key}`) || key.endsWith(`/${norm}`)) {
|
|
73
|
-
if (key.length > bestLen) {
|
|
74
|
-
best = lines;
|
|
75
|
-
bestLen = key.length;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (key === base || key.endsWith(`/${base}`))
|
|
79
|
-
baseHits.push(lines);
|
|
80
|
-
}
|
|
81
|
-
if (best)
|
|
82
|
-
return best;
|
|
83
|
-
if (baseHits.length === 1)
|
|
84
|
-
return baseHits[0];
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
export function binaryFromCommand(command, pkg) {
|
|
88
|
-
const match = command.match(/(?:^|\s)-o\s+["']?([^\s"']+)["']?/);
|
|
89
|
-
if (!match)
|
|
90
|
-
return null;
|
|
91
|
-
const rel = match[1];
|
|
92
|
-
const full = path.isAbsolute(rel) ? rel : path.join(pkg, rel);
|
|
93
|
-
return existsSync(full) ? full : null;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Build a line map from LLVM profraw left by an instrumented baseline.
|
|
97
|
-
* Writes nothing when llvm-cov cannot start or no profile was produced.
|
|
98
|
-
*/
|
|
99
|
-
export function writeLlvmCoverage(pkg, profileDir, binary, dest) {
|
|
100
|
-
if (!existsSync(profileDir))
|
|
101
|
-
return { ok: false, detail: "no profile directory" };
|
|
102
|
-
const raws = readdirSync(profileDir).filter((name) => name.endsWith(".profraw")).map((name) => path.join(profileDir, name));
|
|
103
|
-
if (raws.length === 0)
|
|
104
|
-
return { ok: false, detail: "no profraw" };
|
|
105
|
-
if (!binary)
|
|
106
|
-
return { ok: false, detail: "no instrumented binary" };
|
|
107
|
-
const merged = path.join(profileDir, "baseline.profdata");
|
|
108
|
-
const exported = exportLlvmProfile(binary, raws, merged);
|
|
109
|
-
if (!exported.ok)
|
|
110
|
-
return exported;
|
|
111
|
-
const map = llvmExportToMap(exported.stdout, pkg);
|
|
112
|
-
if (!map)
|
|
113
|
-
return { ok: false, detail: "llvm-cov export was not a coverage map" };
|
|
114
|
-
writeFileSync(dest, `${JSON.stringify(map, null, 2)}\n`);
|
|
115
|
-
return { ok: true, detail: `${Object.keys(map.files).length} files` };
|
|
116
|
-
}
|
|
117
|
-
function llvmExportToMap(stdout, pkg) {
|
|
118
|
-
const start = stdout.indexOf("{");
|
|
119
|
-
if (start < 0)
|
|
120
|
-
return null;
|
|
121
|
-
let parsed;
|
|
122
|
-
try {
|
|
123
|
-
parsed = JSON.parse(stdout.slice(start));
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
const files = {};
|
|
129
|
-
const root = pkg.endsWith(path.sep) ? pkg : `${pkg}${path.sep}`;
|
|
130
|
-
for (const group of parsed.data ?? []) {
|
|
131
|
-
for (const file of group.files ?? []) {
|
|
132
|
-
if (!file.filename)
|
|
133
|
-
continue;
|
|
134
|
-
const lines = {};
|
|
135
|
-
for (const segment of file.segments ?? []) {
|
|
136
|
-
const line = segment[0];
|
|
137
|
-
const count = segment[2] ?? 0;
|
|
138
|
-
if (!line || count <= 0)
|
|
139
|
-
continue;
|
|
140
|
-
lines[String(line)] = [WHOLE_PROGRAM_TEST];
|
|
141
|
-
}
|
|
142
|
-
if (Object.keys(lines).length === 0)
|
|
143
|
-
continue;
|
|
144
|
-
let rel = file.filename;
|
|
145
|
-
if (rel.startsWith(root))
|
|
146
|
-
rel = rel.slice(root.length);
|
|
147
|
-
rel = rel.split(path.sep).join("/");
|
|
148
|
-
files[rel] = { ...(files[rel] ?? {}), ...lines };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (Object.keys(files).length === 0)
|
|
152
|
-
return null;
|
|
153
|
-
return { files };
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* clang and rustc write different profraw versions. Each pair is one LLVM install.
|
|
157
|
-
* The first pair that can merge the profile wins.
|
|
158
|
-
*/
|
|
159
|
-
export function llvmToolsPresent() {
|
|
160
|
-
return llvmToolPairs().length > 0;
|
|
161
|
-
}
|
|
162
|
-
export function exportLlvmProfile(binary, raws, merged) {
|
|
163
|
-
const pairs = llvmToolPairs();
|
|
164
|
-
if (pairs.length === 0)
|
|
165
|
-
return { ok: false, detail: "llvm tool missing (llvm-cov)" };
|
|
166
|
-
let detail = "llvm-profdata failed";
|
|
167
|
-
for (const { cov, prof } of pairs) {
|
|
168
|
-
const merge = spawnSync(prof, ["merge", "-sparse", "-o", merged, ...raws], { encoding: "utf8" });
|
|
169
|
-
if (merge.status !== 0) {
|
|
170
|
-
detail = (merge.stderr || merge.stdout || "llvm-profdata failed").trim();
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
const exported = spawnSync(cov, ["export", binary, `-instr-profile=${merged}`], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
174
|
-
if (exported.status !== 0) {
|
|
175
|
-
detail = (exported.stderr || exported.stdout || "llvm-cov failed").trim();
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
return { ok: true, stdout: exported.stdout };
|
|
179
|
-
}
|
|
180
|
-
return { ok: false, detail };
|
|
181
|
-
}
|
|
182
|
-
function llvmToolPairs() {
|
|
183
|
-
const covs = toolCandidates("llvm-cov");
|
|
184
|
-
const profs = toolCandidates("llvm-profdata");
|
|
185
|
-
const pairs = [];
|
|
186
|
-
const seen = new Set();
|
|
187
|
-
for (const cov of covs) {
|
|
188
|
-
const prof = profs.find((item) => path.dirname(item) === path.dirname(cov));
|
|
189
|
-
if (!prof)
|
|
190
|
-
continue;
|
|
191
|
-
const key = `${cov}\0${prof}`;
|
|
192
|
-
if (seen.has(key))
|
|
193
|
-
continue;
|
|
194
|
-
seen.add(key);
|
|
195
|
-
pairs.push({ cov, prof });
|
|
196
|
-
}
|
|
197
|
-
return pairs;
|
|
198
|
-
}
|
|
199
|
-
function toolCandidates(name) {
|
|
200
|
-
const out = [];
|
|
201
|
-
const add = (file) => {
|
|
202
|
-
if (!file || out.includes(file) || !existsSync(file))
|
|
203
|
-
return;
|
|
204
|
-
out.push(file);
|
|
205
|
-
};
|
|
206
|
-
const fromXcrun = spawnSync("xcrun", ["--find", name], { encoding: "utf8" });
|
|
207
|
-
if (fromXcrun.status === 0)
|
|
208
|
-
add((fromXcrun.stdout ?? "").trim());
|
|
209
|
-
const which = spawnSync("sh", ["-c", `command -v ${name}`], { encoding: "utf8" });
|
|
210
|
-
if (which.status === 0)
|
|
211
|
-
add((which.stdout ?? "").trim());
|
|
212
|
-
const rustc = spawnSync("rustc", ["--print", "sysroot"], { encoding: "utf8" });
|
|
213
|
-
if (rustc.status === 0) {
|
|
214
|
-
const root = path.join(rustc.stdout.trim(), "lib", "rustlib");
|
|
215
|
-
if (existsSync(root)) {
|
|
216
|
-
for (const host of readdirSync(root))
|
|
217
|
-
add(path.join(root, host, "bin", name));
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const usr = "/usr/lib";
|
|
221
|
-
if (existsSync(usr)) {
|
|
222
|
-
for (const dir of readdirSync(usr)) {
|
|
223
|
-
if (dir.startsWith("llvm-"))
|
|
224
|
-
add(path.join(usr, dir, "bin", name));
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
add(path.join("/usr/bin", name));
|
|
228
|
-
return out;
|
|
229
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
/**
|
|
5
|
-
* One Coverlet run per test name. The collector's report does not name the test,
|
|
6
|
-
* so each name gets its own results directory.
|
|
7
|
-
*/
|
|
8
|
-
export function writeCsharpCoverage(pkg, testNames, dest, project) {
|
|
9
|
-
const names = [...new Set(testNames.map((name) => name.trim()).filter((name) => name.length > 0))];
|
|
10
|
-
if (names.length === 0)
|
|
11
|
-
return { ok: false, detail: "csharp coverage: no test names" };
|
|
12
|
-
const work = path.join(path.dirname(dest), "csharp-cover");
|
|
13
|
-
mkdirSync(work, { recursive: true });
|
|
14
|
-
const settings = path.join(work, "coverlet.runsettings");
|
|
15
|
-
// Coverlet skips the test assembly unless asked. A one-project suite keeps the code there.
|
|
16
|
-
writeFileSync(settings, [
|
|
17
|
-
'<?xml version="1.0" encoding="utf-8"?>',
|
|
18
|
-
"<RunSettings>",
|
|
19
|
-
" <DataCollectionRunSettings>",
|
|
20
|
-
" <DataCollectors>",
|
|
21
|
-
' <DataCollector friendlyName="XPlat Code Coverage">',
|
|
22
|
-
" <Configuration>",
|
|
23
|
-
" <Format>cobertura</Format>",
|
|
24
|
-
" <IncludeTestAssembly>true</IncludeTestAssembly>",
|
|
25
|
-
" </Configuration>",
|
|
26
|
-
" </DataCollector>",
|
|
27
|
-
" </DataCollectors>",
|
|
28
|
-
" </DataCollectionRunSettings>",
|
|
29
|
-
"</RunSettings>",
|
|
30
|
-
"",
|
|
31
|
-
].join("\n"));
|
|
32
|
-
const files = {};
|
|
33
|
-
const env = dotnetEnv();
|
|
34
|
-
for (const name of names) {
|
|
35
|
-
const results = path.join(work, safe(name));
|
|
36
|
-
mkdirSync(results, { recursive: true });
|
|
37
|
-
const args = ["test", "--nologo", "--verbosity", "quiet", "--settings", settings];
|
|
38
|
-
if (project)
|
|
39
|
-
args.push(project);
|
|
40
|
-
args.push("--filter", `FullyQualifiedName~${name}`, "--collect", "XPlat Code Coverage", "--results-directory", results);
|
|
41
|
-
const ran = spawnSync("dotnet", args, { cwd: pkg, encoding: "utf8", env, timeout: 180_000, maxBuffer: 16 * 1024 * 1024 });
|
|
42
|
-
if (ran.status !== 0 || ran.error)
|
|
43
|
-
continue;
|
|
44
|
-
const report = findCobertura(results);
|
|
45
|
-
if (!report)
|
|
46
|
-
continue;
|
|
47
|
-
let text = "";
|
|
48
|
-
try {
|
|
49
|
-
text = readFileSync(report, "utf8");
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
addCobertura(files, text, pkg, name);
|
|
55
|
-
}
|
|
56
|
-
if (Object.keys(files).length === 0)
|
|
57
|
-
return { ok: false, detail: "csharp coverage: no covered lines" };
|
|
58
|
-
writeFileSync(dest, `${JSON.stringify({ files }, null, 2)}\n`);
|
|
59
|
-
return { ok: true, detail: `${Object.keys(files).length} files` };
|
|
60
|
-
}
|
|
61
|
-
function addCobertura(files, text, pkg, test) {
|
|
62
|
-
const sources = [...text.matchAll(/<source>([^<]+)<\/source>/g)].map((match) => match[1].trim());
|
|
63
|
-
for (const part of text.split(/<class\b/).slice(1)) {
|
|
64
|
-
const headEnd = part.indexOf(">");
|
|
65
|
-
if (headEnd < 0)
|
|
66
|
-
continue;
|
|
67
|
-
const filename = /filename="([^"]+)"/.exec(part.slice(0, headEnd))?.[1];
|
|
68
|
-
if (!filename)
|
|
69
|
-
continue;
|
|
70
|
-
const bodyEnd = part.indexOf("</class>");
|
|
71
|
-
const body = bodyEnd >= 0 ? part.slice(0, bodyEnd) : part;
|
|
72
|
-
const rel = coberturaRel(filename, sources, pkg);
|
|
73
|
-
if (!rel)
|
|
74
|
-
continue;
|
|
75
|
-
for (const tag of body.match(/<line\b[^>]*\/?>/g) ?? []) {
|
|
76
|
-
const number = /number="(\d+)"/.exec(tag);
|
|
77
|
-
const hits = /hits="(\d+)"/.exec(tag);
|
|
78
|
-
if (!number || !hits || Number(hits[1]) <= 0)
|
|
79
|
-
continue;
|
|
80
|
-
addHit(files, rel, Number(number[1]), test);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function coberturaRel(filename, sources, pkg) {
|
|
85
|
-
const norm = filename.split("\\").join("/");
|
|
86
|
-
const candidates = norm.startsWith("/") || /^[A-Za-z]:\//.test(norm) ? [norm] : [norm];
|
|
87
|
-
for (const source of sources) {
|
|
88
|
-
const root = source.endsWith("/") || source.endsWith("\\") ? source : `${source}/`;
|
|
89
|
-
candidates.push(path.normalize(`${root}${norm}`));
|
|
90
|
-
}
|
|
91
|
-
for (const candidate of candidates) {
|
|
92
|
-
const rel = underPkg(candidate, pkg);
|
|
93
|
-
if (rel)
|
|
94
|
-
return rel;
|
|
95
|
-
}
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
function underPkg(filename, pkg) {
|
|
99
|
-
if (!existsSync(filename))
|
|
100
|
-
return null;
|
|
101
|
-
let file = filename;
|
|
102
|
-
let root = pkg;
|
|
103
|
-
try {
|
|
104
|
-
file = realpathSync(filename);
|
|
105
|
-
root = realpathSync(pkg);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
const rel = path.relative(root, file);
|
|
111
|
-
if (!rel || rel.startsWith(".."))
|
|
112
|
-
return null;
|
|
113
|
-
return rel.split(path.sep).join("/");
|
|
114
|
-
}
|
|
115
|
-
function findCobertura(dir) {
|
|
116
|
-
const found = [];
|
|
117
|
-
const visit = (current) => {
|
|
118
|
-
let entries;
|
|
119
|
-
try {
|
|
120
|
-
entries = readdirSync(current);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
for (const entry of entries) {
|
|
126
|
-
const full = path.join(current, entry);
|
|
127
|
-
let info;
|
|
128
|
-
try {
|
|
129
|
-
info = statSync(full);
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (info.isDirectory()) {
|
|
135
|
-
visit(full);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (entry === "coverage.cobertura.xml")
|
|
139
|
-
found.push(full);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
visit(dir);
|
|
143
|
-
return found[0] ?? null;
|
|
144
|
-
}
|
|
145
|
-
function addHit(files, file, line, test) {
|
|
146
|
-
const lines = files[file] ?? {};
|
|
147
|
-
const key = String(line);
|
|
148
|
-
const names = lines[key] ?? [];
|
|
149
|
-
if (!names.includes(test))
|
|
150
|
-
names.push(test);
|
|
151
|
-
lines[key] = names;
|
|
152
|
-
files[file] = lines;
|
|
153
|
-
}
|
|
154
|
-
function dotnetEnv() {
|
|
155
|
-
if (process.env.DOTNET_ROLL_FORWARD)
|
|
156
|
-
return process.env;
|
|
157
|
-
return { ...process.env, DOTNET_ROLL_FORWARD: "Major" };
|
|
158
|
-
}
|
|
159
|
-
function safe(value) {
|
|
160
|
-
return value.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
161
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// node --test custom reporter. One JSON object on stdout: failures with the file
|
|
2
|
-
// the runner itself recorded. TAP headings are not used.
|
|
3
|
-
import { staticLoadFailure } from "./load-failure.mjs"
|
|
4
|
-
|
|
5
|
-
export default async function* reporter(source) {
|
|
6
|
-
// A file that fails to load prints its error on stderr before the file-level failure.
|
|
7
|
-
const stderr = new Map()
|
|
8
|
-
const failed = []
|
|
9
|
-
const names = []
|
|
10
|
-
let tests = 0
|
|
11
|
-
let pass = 0
|
|
12
|
-
let fail = 0
|
|
13
|
-
for await (const event of source) {
|
|
14
|
-
if (event.type === "test:stderr") {
|
|
15
|
-
const key = String(event.data?.file ?? "")
|
|
16
|
-
stderr.set(key, `${stderr.get(key) ?? ""}${String(event.data?.message ?? "")}`)
|
|
17
|
-
continue
|
|
18
|
-
}
|
|
19
|
-
if (event.type === "test:pass" || event.type === "test:fail") {
|
|
20
|
-
const data = event.data ?? {}
|
|
21
|
-
if (data.details && data.details.type === "suite") continue
|
|
22
|
-
const name = String(data.name ?? "")
|
|
23
|
-
const file = String(data.file ?? "")
|
|
24
|
-
names.push(name)
|
|
25
|
-
if (event.type === "test:fail") {
|
|
26
|
-
const failure = { name, file, line: Number(data.line ?? 0) }
|
|
27
|
-
// An open handle is reported as a timeout whose name is the file path.
|
|
28
|
-
// failureType lives on the error; the text is on error.cause, not message.
|
|
29
|
-
if (fileTimedOut(name, file, data.details?.error)) failure.fileTimeout = true
|
|
30
|
-
// The file itself failed and no test in it ran: it did not load.
|
|
31
|
-
else if (isFileName(name, file)) {
|
|
32
|
-
failure.fileLoad = true
|
|
33
|
-
// It did not link (removed export, missing module, no parse): no test code ran.
|
|
34
|
-
if (staticLoadFailure(stderr.get(file) ?? "")) failure.staticLoad = true
|
|
35
|
-
}
|
|
36
|
-
failed.push(failure)
|
|
37
|
-
}
|
|
38
|
-
} else if (event.type === "test:diagnostic") {
|
|
39
|
-
const message = String(event.data?.message ?? "")
|
|
40
|
-
const match = message.match(/^(tests|pass|fail) (\d+)$/)
|
|
41
|
-
if (!match) continue
|
|
42
|
-
const count = Number(match[2])
|
|
43
|
-
if (match[1] === "tests") tests = count
|
|
44
|
-
else if (match[1] === "pass") pass = count
|
|
45
|
-
else fail = count
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
yield JSON.stringify({ tests, pass, fail, failed, names })
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isFileName(name, file) {
|
|
52
|
-
const base = String(file).split(/[/\\]/).pop()
|
|
53
|
-
if (!base) return false
|
|
54
|
-
return name === file || name === base || name.endsWith(`/${base}`) || name.endsWith(`\\${base}`)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function fileTimedOut(name, file, error) {
|
|
58
|
-
if (!error || typeof error !== "object") return false
|
|
59
|
-
const cause = typeof error.cause === "string" ? error.cause : ""
|
|
60
|
-
const text = `${error.failureType ?? ""} ${error.message ?? ""} ${cause}`
|
|
61
|
-
if (!/testTimeoutFailure|timed out/i.test(text)) return false
|
|
62
|
-
const base = String(file).split(/[/\\]/).pop()
|
|
63
|
-
if (!base) return false
|
|
64
|
-
return name === file || name === base || name.endsWith(`/${base}`) || name.endsWith(`\\${base}`)
|
|
65
|
-
}
|
package/dist/mutate/find.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { findMutants } from "./operators.js";
|
|
2
|
-
import { findTextMutants, textKind } from "./text-operators.js";
|
|
3
|
-
/** TypeScript stays on the TypeScript parser. Every other recognized source uses its own finder. */
|
|
4
|
-
export function findInSource(file, text, operators = "core") {
|
|
5
|
-
if (textKind(file))
|
|
6
|
-
return findTextMutants(file, text, operators);
|
|
7
|
-
return findMutants(file, text, operators);
|
|
8
|
-
}
|
|
9
|
-
export function isGeneratedSource(name) {
|
|
10
|
-
if (textKind(name))
|
|
11
|
-
return true;
|
|
12
|
-
return (name.endsWith(".ts") || name.endsWith(".tsx")) && !name.endsWith(".d.ts");
|
|
13
|
-
}
|