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.
Files changed (74) hide show
  1. package/README.md +11 -197
  2. package/bin/probatio.js +52 -0
  3. package/package.json +22 -29
  4. package/LICENSE +0 -21
  5. package/dist/cli.js +0 -721
  6. package/dist/contract.js +0 -32
  7. package/dist/flags.js +0 -87
  8. package/dist/golden/check.js +0 -188
  9. package/dist/golden/record.js +0 -395
  10. package/dist/golden/recorder.mjs +0 -78
  11. package/dist/ledger/build.js +0 -348
  12. package/dist/ledger/check.js +0 -133
  13. package/dist/matrix/report.js +0 -256
  14. package/dist/mcp.js +0 -188
  15. package/dist/memory/state.js +0 -222
  16. package/dist/mutate/affected.js +0 -173
  17. package/dist/mutate/child-lines.mjs +0 -39
  18. package/dist/mutate/coverage-map.js +0 -229
  19. package/dist/mutate/csharp-coverage.js +0 -161
  20. package/dist/mutate/fail-reporter.mjs +0 -65
  21. package/dist/mutate/find.js +0 -13
  22. package/dist/mutate/generate.js +0 -156
  23. package/dist/mutate/go-coverage.js +0 -100
  24. package/dist/mutate/ids.js +0 -22
  25. package/dist/mutate/java-coverage.js +0 -217
  26. package/dist/mutate/kill-label.js +0 -34
  27. package/dist/mutate/load-failure.mjs +0 -40
  28. package/dist/mutate/mocha-coverage.cjs +0 -77
  29. package/dist/mutate/node-batch-loader.mjs +0 -99
  30. package/dist/mutate/node-batch.mjs +0 -167
  31. package/dist/mutate/node-coverage.mjs +0 -77
  32. package/dist/mutate/operators.js +0 -181
  33. package/dist/mutate/patch.js +0 -136
  34. package/dist/mutate/precise-lines.mjs +0 -304
  35. package/dist/mutate/probatio-jacoco-run.java +0 -516
  36. package/dist/mutate/project-config.js +0 -58
  37. package/dist/mutate/py-pytest-reporter.py +0 -145
  38. package/dist/mutate/py-reporter.py +0 -79
  39. package/dist/mutate/run.js +0 -1335
  40. package/dist/mutate/rust-coverage.js +0 -144
  41. package/dist/mutate/sealed.js +0 -118
  42. package/dist/mutate/suite-decision.js +0 -130
  43. package/dist/mutate/suites.js +0 -1524
  44. package/dist/mutate/tally.js +0 -240
  45. package/dist/mutate/text-operators.js +0 -637
  46. package/dist/mutate/titles.js +0 -23
  47. package/dist/node-version.js +0 -13
  48. package/dist/swarm/check-kill.js +0 -114
  49. package/dist/swarm/queue.js +0 -68
  50. package/dist/verify/change.js +0 -340
  51. package/schemas/check-kill.schema.json +0 -93
  52. package/schemas/findings.add.schema.json +0 -53
  53. package/schemas/gap.fix.schema.json +0 -53
  54. package/schemas/gap.revert.schema.json +0 -53
  55. package/schemas/golden.check.schema.json +0 -89
  56. package/schemas/golden.compare.schema.json +0 -151
  57. package/schemas/golden.record.schema.json +0 -126
  58. package/schemas/guard.check.schema.json +0 -53
  59. package/schemas/ledger.build.schema.json +0 -132
  60. package/schemas/ledger.check.schema.json +0 -143
  61. package/schemas/matrix.report.schema.json +0 -129
  62. package/schemas/mcp.schema.json +0 -53
  63. package/schemas/mutate.generate.schema.json +0 -127
  64. package/schemas/mutate.help.schema.json +0 -53
  65. package/schemas/mutate.run.schema.json +0 -272
  66. package/schemas/mutate.sealed.schema.json +0 -53
  67. package/schemas/mutate.tally.schema.json +0 -137
  68. package/schemas/queue.claim.schema.json +0 -53
  69. package/schemas/queue.reap.schema.json +0 -71
  70. package/schemas/queue.seed.schema.json +0 -71
  71. package/schemas/schema.schema.json +0 -74
  72. package/schemas/seal.schema.json +0 -69
  73. package/schemas/status.schema.json +0 -103
  74. package/schemas/verify-change.schema.json +0 -222
@@ -1,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
- }