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.
- package/README.md +10 -198
- package/bin/probatio.js +52 -0
- package/package.json +21 -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
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
// Inspector line samples shared by the node:test collector, the Mocha collector,
|
|
2
|
-
// and a plain-script child. This file has no test hooks and writes no shard.
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs"
|
|
4
|
-
import { Session as SyncSession } from "node:inspector"
|
|
5
|
-
import { Session } from "node:inspector/promises"
|
|
6
|
-
import { findSourceMap } from "node:module"
|
|
7
|
-
import path from "node:path"
|
|
8
|
-
import { fileURLToPath } from "node:url"
|
|
9
|
-
|
|
10
|
-
const B64 = new Int32Array(128)
|
|
11
|
-
for (let i = 0; i < 64; i++) B64["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charCodeAt(i)] = i
|
|
12
|
-
|
|
13
|
-
function decodeSegments(row) {
|
|
14
|
-
const segments = []
|
|
15
|
-
let i = 0
|
|
16
|
-
while (i < row.length) {
|
|
17
|
-
const fields = []
|
|
18
|
-
while (i < row.length && row.charCodeAt(i) !== 44) {
|
|
19
|
-
let value = 0
|
|
20
|
-
let shift = 0
|
|
21
|
-
let digit = 0
|
|
22
|
-
do {
|
|
23
|
-
digit = B64[row.charCodeAt(i++)]
|
|
24
|
-
value |= (digit & 31) << shift
|
|
25
|
-
shift += 5
|
|
26
|
-
} while (digit & 32)
|
|
27
|
-
fields.push(value & 1 ? -(value >> 1) : value >> 1)
|
|
28
|
-
}
|
|
29
|
-
if (row.charCodeAt(i) === 44) i++
|
|
30
|
-
if (fields.length > 0) segments.push(fields)
|
|
31
|
-
}
|
|
32
|
-
return segments
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Offsets to source lines, shared by the async sampler (test processes) and the sync one (children).
|
|
37
|
-
* Paths are relative to `root`. node_modules and paths outside root are left out.
|
|
38
|
-
* @param {(rel: string) => boolean} [skipRel]
|
|
39
|
-
* @param {string} [root]
|
|
40
|
-
*/
|
|
41
|
-
function lineConverter(skipRel, root = process.cwd()) {
|
|
42
|
-
const indexCache = new Map()
|
|
43
|
-
|
|
44
|
-
function relPath(url) {
|
|
45
|
-
if (!url || url.startsWith("node:")) return null
|
|
46
|
-
let abs
|
|
47
|
-
try {
|
|
48
|
-
abs = fileURLToPath(url.split("?")[0].split("#")[0])
|
|
49
|
-
} catch {
|
|
50
|
-
return null
|
|
51
|
-
}
|
|
52
|
-
const rel = path.relative(root, abs).split(path.sep).join("/")
|
|
53
|
-
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return null
|
|
54
|
-
if (rel.split("/").includes("node_modules")) return null
|
|
55
|
-
if (skipRel && skipRel(rel)) return null
|
|
56
|
-
return rel
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function indexFor(scriptId, text, sm) {
|
|
60
|
-
if (indexCache.has(scriptId)) return indexCache.get(scriptId)
|
|
61
|
-
const mapped = sm && sm.payload && typeof sm.payload.mappings === "string" ? linesFromMappings(text, sm.payload.mappings) : null
|
|
62
|
-
const lines = mapped && hasLine(mapped) ? mapped : linesFromNewlines(text, sm)
|
|
63
|
-
indexCache.set(scriptId, lines)
|
|
64
|
-
return lines
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function hasLine(lines) {
|
|
68
|
-
for (let i = 0; i < lines.length; i++) if (lines[i] > 0) return true
|
|
69
|
-
return false
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function linesFromNewlines(text, sm) {
|
|
73
|
-
const lines = new Int32Array(text.length + 1)
|
|
74
|
-
if (!sm) {
|
|
75
|
-
let line = 1
|
|
76
|
-
for (let i = 0; i < text.length; i++) {
|
|
77
|
-
lines[i] = line
|
|
78
|
-
if (text.charCodeAt(i) === 10) line++
|
|
79
|
-
}
|
|
80
|
-
return lines
|
|
81
|
-
}
|
|
82
|
-
let genLine = 0
|
|
83
|
-
let col = 0
|
|
84
|
-
for (let i = 0; i < text.length; i++) {
|
|
85
|
-
const entry = sm.findEntry(genLine, col)
|
|
86
|
-
if (entry && Number.isInteger(entry.originalLine) && entry.originalLine >= 0) lines[i] = entry.originalLine + 1
|
|
87
|
-
if (text.charCodeAt(i) === 10) {
|
|
88
|
-
genLine++
|
|
89
|
-
col = 0
|
|
90
|
-
} else {
|
|
91
|
-
col++
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return lines
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function linesFromMappings(text, mappings) {
|
|
98
|
-
const lines = new Int32Array(text.length + 1)
|
|
99
|
-
const starts = [0]
|
|
100
|
-
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1)
|
|
101
|
-
let carryLine = 0
|
|
102
|
-
const rows = mappings.split(";")
|
|
103
|
-
for (let genLine = 0; genLine < rows.length && genLine < starts.length; genLine++) {
|
|
104
|
-
const start = starts[genLine]
|
|
105
|
-
const next = genLine + 1 < starts.length ? starts[genLine + 1] : text.length
|
|
106
|
-
let col = 0
|
|
107
|
-
let cursor = start
|
|
108
|
-
let active = -1
|
|
109
|
-
for (const fields of decodeSegments(rows[genLine])) {
|
|
110
|
-
col += fields[0]
|
|
111
|
-
const at = Math.min(next, start + Math.max(0, col))
|
|
112
|
-
if (active >= 0) {
|
|
113
|
-
for (let i = cursor; i < at; i++) lines[i] = active + 1
|
|
114
|
-
}
|
|
115
|
-
cursor = at
|
|
116
|
-
if (fields.length >= 4) {
|
|
117
|
-
carryLine += fields[2]
|
|
118
|
-
active = carryLine
|
|
119
|
-
} else {
|
|
120
|
-
active = -1
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (active >= 0) {
|
|
124
|
-
for (let i = cursor; i < next; i++) lines[i] = active + 1
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return lines
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function applyRanges(text, script, lines) {
|
|
131
|
-
const ranges = []
|
|
132
|
-
for (const fn of script.functions || []) {
|
|
133
|
-
for (const range of fn.ranges || []) ranges.push(range)
|
|
134
|
-
}
|
|
135
|
-
ranges.sort((a, b) => a.startOffset - b.startOffset || b.endOffset - a.endOffset)
|
|
136
|
-
const offsets = new Int32Array(text.length)
|
|
137
|
-
for (const range of ranges) {
|
|
138
|
-
const end = Math.min(text.length, range.endOffset)
|
|
139
|
-
for (let i = Math.max(0, range.startOffset); i < end; i++) offsets[i] = range.count
|
|
140
|
-
}
|
|
141
|
-
const counts = new Map()
|
|
142
|
-
for (let i = 0; i < text.length; i++) {
|
|
143
|
-
const line = lines[i]
|
|
144
|
-
if (line <= 0 || offsets[i] <= 0) continue
|
|
145
|
-
if (offsets[i] > (counts.get(line) || 0)) counts.set(line, offsets[i])
|
|
146
|
-
}
|
|
147
|
-
return counts
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Lines with a positive count in one script, or null when the script is not ours. */
|
|
151
|
-
function scriptLines(script, text) {
|
|
152
|
-
const file = relPath(script.url)
|
|
153
|
-
if (!file || !text) return null
|
|
154
|
-
const counts = applyRanges(text, script, indexFor(script.scriptId, text, findSourceMap(script.url)))
|
|
155
|
-
const found = []
|
|
156
|
-
for (const [line, count] of counts) {
|
|
157
|
-
if (count > 0) found.push({ file, line })
|
|
158
|
-
}
|
|
159
|
-
return found
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return { relPath, scriptLines }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @param {(rel: string) => boolean} [skipRel]
|
|
167
|
-
* @returns {Promise<{ positiveLines: () => Promise<Array<{ file: string, line: number }>>, reset: () => Promise<void> }>}
|
|
168
|
-
*/
|
|
169
|
-
export async function openLineSampler(skipRel) {
|
|
170
|
-
const session = new Session()
|
|
171
|
-
session.connect()
|
|
172
|
-
await session.post("Profiler.enable")
|
|
173
|
-
await session.post("Debugger.enable")
|
|
174
|
-
await session.post("Profiler.startPreciseCoverage", { callCount: true, detailed: true })
|
|
175
|
-
|
|
176
|
-
const textCache = new Map()
|
|
177
|
-
const convert = lineConverter(skipRel)
|
|
178
|
-
|
|
179
|
-
async function scriptText(script) {
|
|
180
|
-
if (textCache.has(script.scriptId)) return textCache.get(script.scriptId)
|
|
181
|
-
let text = ""
|
|
182
|
-
try {
|
|
183
|
-
const got = await session.post("Debugger.getScriptSource", { scriptId: script.scriptId })
|
|
184
|
-
text = got.scriptSource || ""
|
|
185
|
-
} catch {
|
|
186
|
-
text = ""
|
|
187
|
-
}
|
|
188
|
-
textCache.set(script.scriptId, text)
|
|
189
|
-
return text
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function positiveLines() {
|
|
193
|
-
const cov = await session.post("Profiler.takePreciseCoverage")
|
|
194
|
-
const found = []
|
|
195
|
-
for (const script of cov.result || []) {
|
|
196
|
-
if (!convert.relPath(script.url)) continue
|
|
197
|
-
const lines = convert.scriptLines(script, await scriptText(script))
|
|
198
|
-
if (lines) found.push(...lines)
|
|
199
|
-
}
|
|
200
|
-
return found
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
positiveLines,
|
|
205
|
-
reset: async () => {
|
|
206
|
-
await session.post("Profiler.takePreciseCoverage")
|
|
207
|
-
},
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* The same sampler with synchronous calls, so an `exit` handler can read it.
|
|
213
|
-
* `exit` runs after process.exit(), where a promise never settles.
|
|
214
|
-
* @param {(rel: string) => boolean} [skipRel]
|
|
215
|
-
* @param {string} [root]
|
|
216
|
-
*/
|
|
217
|
-
export function openSyncLineSampler(skipRel, root) {
|
|
218
|
-
const session = new SyncSession()
|
|
219
|
-
session.connect()
|
|
220
|
-
const post = (method, params) => {
|
|
221
|
-
let result
|
|
222
|
-
let failure = null
|
|
223
|
-
let done = false
|
|
224
|
-
session.post(method, params, (err, value) => {
|
|
225
|
-
failure = err
|
|
226
|
-
result = value
|
|
227
|
-
done = true
|
|
228
|
-
})
|
|
229
|
-
if (!done) throw new Error(`inspector ${method} did not answer synchronously`)
|
|
230
|
-
if (failure) throw failure
|
|
231
|
-
return result
|
|
232
|
-
}
|
|
233
|
-
post("Profiler.enable")
|
|
234
|
-
post("Debugger.enable")
|
|
235
|
-
post("Profiler.startPreciseCoverage", { callCount: true, detailed: true })
|
|
236
|
-
const convert = lineConverter(skipRel, root)
|
|
237
|
-
return {
|
|
238
|
-
positiveLines() {
|
|
239
|
-
const cov = post("Profiler.takePreciseCoverage")
|
|
240
|
-
const found = []
|
|
241
|
-
for (const script of cov.result || []) {
|
|
242
|
-
if (!convert.relPath(script.url)) continue
|
|
243
|
-
let text = ""
|
|
244
|
-
try {
|
|
245
|
-
text = post("Debugger.getScriptSource", { scriptId: script.scriptId }).scriptSource || ""
|
|
246
|
-
} catch {
|
|
247
|
-
text = ""
|
|
248
|
-
}
|
|
249
|
-
const lines = convert.scriptLines(script, text)
|
|
250
|
-
if (lines) found.push(...lines)
|
|
251
|
-
}
|
|
252
|
-
return found
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Called inside a test process that collects a line map. Every node child it starts
|
|
259
|
-
* loads child-lines.mjs through NODE_OPTIONS, with no import in the child's own code.
|
|
260
|
-
* The marker tells child-lines that this process already has test hooks.
|
|
261
|
-
*/
|
|
262
|
-
export function exposeChildLines() {
|
|
263
|
-
globalThis.__probatioTestHooks = true
|
|
264
|
-
// One dump dir per test process. Test files can run in parallel, and a child's lines
|
|
265
|
-
// belong to the test in the process that started it, not to a test in another file.
|
|
266
|
-
const mapPath = process.env.PROBATIO_COVERAGE_MAP
|
|
267
|
-
if (mapPath) process.env.PROBATIO_CHILD_DIR = childDir(mapPath)
|
|
268
|
-
const childLines = new URL("./child-lines.mjs", import.meta.url).href
|
|
269
|
-
const flag = `--import=${childLines}`
|
|
270
|
-
const current = process.env.NODE_OPTIONS || ""
|
|
271
|
-
if (!current.includes(flag)) process.env.NODE_OPTIONS = current ? `${current} ${flag}` : flag
|
|
272
|
-
if (!process.env.PROBATIO_PACKAGE_ROOT) process.env.PROBATIO_PACKAGE_ROOT = process.cwd()
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function childDir(mapPath) {
|
|
276
|
-
return path.join(`${mapPath}.children`, String(process.pid))
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Line dumps the children of the current test left. Each one is read once. */
|
|
280
|
-
export function takeChildHits(mapPath) {
|
|
281
|
-
const dir = childDir(mapPath)
|
|
282
|
-
if (!existsSync(dir)) return []
|
|
283
|
-
const hits = []
|
|
284
|
-
for (const name of readdirSync(dir)) {
|
|
285
|
-
if (!name.endsWith(".json")) continue
|
|
286
|
-
const full = path.join(dir, name)
|
|
287
|
-
try {
|
|
288
|
-
const parsed = JSON.parse(readFileSync(full, "utf8"))
|
|
289
|
-
const list = parsed && Array.isArray(parsed.hits) ? parsed.hits : []
|
|
290
|
-
for (const hit of list) {
|
|
291
|
-
if (!hit || typeof hit.file !== "string" || !Number.isInteger(hit.line)) continue
|
|
292
|
-
hits.push({ file: hit.file, line: hit.line })
|
|
293
|
-
}
|
|
294
|
-
} catch {
|
|
295
|
-
// A torn dump is ignored. The parent does not invent a line.
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
unlinkSync(full)
|
|
299
|
-
} catch {
|
|
300
|
-
// The next test must not inherit this dump.
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return hits
|
|
304
|
-
}
|