probatio 0.0.0-stage → 0.0.2

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,547 @@
1
+ import path from "node:path";
2
+ const C_OPS = [
3
+ { original: "&&", op: "and-to-or", replacement: "||" },
4
+ { original: "||", op: "or-to-and", replacement: "&&" },
5
+ { original: "<=", op: "le-to-lt", replacement: "<" },
6
+ { original: ">=", op: "ge-to-gt", replacement: ">" },
7
+ { original: "==", op: "eq-to-neq", replacement: "!=" },
8
+ { original: "!=", op: "neq-to-eq", replacement: "==" },
9
+ { original: "<", op: "lt-to-le", replacement: "<=" },
10
+ { original: ">", op: "gt-to-ge", replacement: ">=" },
11
+ ];
12
+ const PY_WORDS = [
13
+ { original: "and", op: "and-to-or", replacement: "or" },
14
+ { original: "or", op: "or-to-and", replacement: "and" },
15
+ ];
16
+ const EQ_OPS = [
17
+ { original: "===", op: "eq-to-neq", replacement: "!==" },
18
+ { original: "!==", op: "neq-to-eq", replacement: "===" },
19
+ ];
20
+ const X86_OPS = [
21
+ { original: "je", op: "eq-to-neq", replacement: "jne" },
22
+ { original: "jne", op: "neq-to-eq", replacement: "je" },
23
+ { original: "jz", op: "eq-to-neq", replacement: "jnz" },
24
+ { original: "jnz", op: "neq-to-eq", replacement: "jz" },
25
+ { original: "jg", op: "gt-to-ge", replacement: "jge" },
26
+ { original: "jge", op: "ge-to-gt", replacement: "jg" },
27
+ { original: "jl", op: "lt-to-le", replacement: "jle" },
28
+ { original: "jle", op: "le-to-lt", replacement: "jl" },
29
+ { original: "ja", op: "gt-to-ge", replacement: "jae" },
30
+ { original: "jae", op: "ge-to-gt", replacement: "ja" },
31
+ { original: "jb", op: "lt-to-le", replacement: "jbe" },
32
+ { original: "jbe", op: "le-to-lt", replacement: "jb" },
33
+ { original: "and", op: "and-to-or", replacement: "or" },
34
+ { original: "or", op: "or-to-and", replacement: "and" },
35
+ ];
36
+ const ARM_OPS = [
37
+ { original: "b.eq", op: "eq-to-neq", replacement: "b.ne" },
38
+ { original: "b.ne", op: "neq-to-eq", replacement: "b.eq" },
39
+ { original: "b.gt", op: "gt-to-ge", replacement: "b.ge" },
40
+ { original: "b.ge", op: "ge-to-gt", replacement: "b.gt" },
41
+ { original: "b.lt", op: "lt-to-le", replacement: "b.le" },
42
+ { original: "b.le", op: "le-to-lt", replacement: "b.lt" },
43
+ { original: "b.hi", op: "gt-to-ge", replacement: "b.hs" },
44
+ { original: "b.hs", op: "ge-to-gt", replacement: "b.hi" },
45
+ { original: "b.lo", op: "lt-to-le", replacement: "b.ls" },
46
+ { original: "b.ls", op: "le-to-lt", replacement: "b.lo" },
47
+ { original: "cbz", op: "eq-to-neq", replacement: "cbnz" },
48
+ { original: "cbnz", op: "neq-to-eq", replacement: "cbz" },
49
+ ];
50
+ const RISCV_OPS = [
51
+ { original: "beq", op: "eq-to-neq", replacement: "bne" },
52
+ { original: "bne", op: "neq-to-eq", replacement: "beq" },
53
+ { original: "blt", op: "lt-to-le", replacement: "bge" },
54
+ { original: "bge", op: "le-to-lt", replacement: "blt" },
55
+ { original: "bltu", op: "lt-to-le", replacement: "bgeu" },
56
+ { original: "bgeu", op: "le-to-lt", replacement: "bltu" },
57
+ ];
58
+ /** Which text finder owns this path. TypeScript stays null so the TS parser is not used. */
59
+ export function textKind(file) {
60
+ switch (path.extname(file).toLowerCase()) {
61
+ case ".py":
62
+ return "python";
63
+ case ".cob":
64
+ case ".cbl":
65
+ case ".cobol":
66
+ return "cobol";
67
+ case ".rs":
68
+ return "rust";
69
+ case ".go":
70
+ return "go";
71
+ case ".java":
72
+ case ".c":
73
+ case ".h":
74
+ case ".cpp":
75
+ case ".cc":
76
+ case ".cxx":
77
+ case ".hpp":
78
+ case ".hh":
79
+ case ".hxx":
80
+ return "c";
81
+ case ".js":
82
+ case ".mjs":
83
+ case ".cjs":
84
+ return "js";
85
+ case ".cs":
86
+ return "csharp";
87
+ case ".swift":
88
+ return "swift";
89
+ case ".asm":
90
+ case ".nasm":
91
+ case ".yasm":
92
+ return "asm-x86_64";
93
+ case ".s":
94
+ case ".S":
95
+ return "asm-aarch64";
96
+ default:
97
+ return null;
98
+ }
99
+ }
100
+ /**
101
+ * Operator mutants for one non-TypeScript source. Strings and comments are skipped
102
+ * before a point is proposed, so a `/*`, `*>`, or `#` inside a string cannot hide later code.
103
+ */
104
+ export function findTextMutants(file, text) {
105
+ const kind = resolveKind(file, text);
106
+ if (!kind)
107
+ return { points: [], violations: [] };
108
+ const code = codeMask(text, kind);
109
+ const points = [];
110
+ let line = 1;
111
+ let index = 0;
112
+ while (index < text.length) {
113
+ if (text[index] === "\n") {
114
+ line += 1;
115
+ index += 1;
116
+ continue;
117
+ }
118
+ const match = code[index] ? matchAt(text, index, code, kind) : null;
119
+ if (!match) {
120
+ index += 1;
121
+ continue;
122
+ }
123
+ points.push({
124
+ file,
125
+ start: index,
126
+ end: index + match.original.length,
127
+ line,
128
+ op: match.op,
129
+ original: match.original,
130
+ replacement: match.replacement,
131
+ });
132
+ index += match.original.length;
133
+ }
134
+ return { points, violations: [] };
135
+ }
136
+ function matchAt(text, index, code, kind) {
137
+ const word = matchWord(text, index, code, kind);
138
+ if (word)
139
+ return word;
140
+ for (const op of symbolOps(kind)) {
141
+ if (!text.startsWith(op.original, index))
142
+ continue;
143
+ if (!spanIsCode(code, index, op.original.length))
144
+ continue;
145
+ if (blocked(text, index, op.original))
146
+ continue;
147
+ return op;
148
+ }
149
+ return null;
150
+ }
151
+ function resolveKind(file, text) {
152
+ const kind = textKind(file);
153
+ if (!kind)
154
+ return null;
155
+ if (kind.startsWith("asm-"))
156
+ return sniffAsm(file, text);
157
+ return kind;
158
+ }
159
+ function sniffAsm(file, text) {
160
+ const ext = path.extname(file);
161
+ const lower = ext.toLowerCase();
162
+ if (lower === ".asm" || lower === ".nasm" || lower === ".yasm")
163
+ return "asm-x86_64";
164
+ const riscv = /\b(beq|bne|blt|bge|bltu|bgeu|ecall)\b/.test(text);
165
+ const arm = /\bb\.(eq|ne|lt|le|gt|ge|hi|hs|lo|ls)\b/.test(text) || /\b(adrp|svc)\b/.test(text);
166
+ if (riscv && !arm)
167
+ return "asm-riscv";
168
+ if (arm && !riscv)
169
+ return "asm-aarch64";
170
+ // `.S` lowercases to `.s`. A capital S stays RISC-V when the body has no marker.
171
+ return lower === ".s" && ext !== ".S" ? "asm-aarch64" : "asm-riscv";
172
+ }
173
+ function symbolOps(kind) {
174
+ if (kind.startsWith("asm-"))
175
+ return [];
176
+ if (kind === "cobol")
177
+ return C_OPS.filter((op) => op.original === "<" || op.original === ">" || op.original === "<=" || op.original === ">=");
178
+ if (kind === "python")
179
+ return C_OPS.filter((op) => op.original !== "&&" && op.original !== "||");
180
+ if (kind === "js" || kind === "swift")
181
+ return [...EQ_OPS, ...C_OPS];
182
+ return C_OPS;
183
+ }
184
+ function matchWord(text, index, code, kind) {
185
+ if (kind === "python")
186
+ return matchExactWord(text, index, code, PY_WORDS, "");
187
+ if (kind === "cobol")
188
+ return matchCobolWord(text, index, code);
189
+ if (kind === "asm-x86_64")
190
+ return matchMnemonic(text, index, code, X86_OPS, true);
191
+ if (kind === "asm-aarch64")
192
+ return matchMnemonic(text, index, code, ARM_OPS, false);
193
+ if (kind === "asm-riscv")
194
+ return matchMnemonic(text, index, code, RISCV_OPS, false);
195
+ return matchExactWord(text, index, code, [
196
+ { original: "true", op: "true-to-false", replacement: "false" },
197
+ { original: "false", op: "false-to-true", replacement: "true" },
198
+ ], "");
199
+ }
200
+ function matchMnemonic(text, index, code, words, insensitive) {
201
+ const ordered = [...words].sort((left, right) => right.original.length - left.original.length);
202
+ for (const word of ordered) {
203
+ const slice = text.slice(index, index + word.original.length);
204
+ const same = insensitive ? slice.toLowerCase() === word.original : slice === word.original;
205
+ if (!same)
206
+ continue;
207
+ if (!wordEdge(text, index, index + word.original.length, "."))
208
+ continue;
209
+ if (text[index + word.original.length] === ":")
210
+ continue;
211
+ if (!spanIsCode(code, index, word.original.length))
212
+ continue;
213
+ return { original: slice, op: word.op, replacement: mirrorCase(slice, word.replacement) };
214
+ }
215
+ return null;
216
+ }
217
+ function matchExactWord(text, index, code, words, extra) {
218
+ for (const word of words) {
219
+ if (!text.startsWith(word.original, index))
220
+ continue;
221
+ if (!wordEdge(text, index, index + word.original.length, extra))
222
+ continue;
223
+ if (!spanIsCode(code, index, word.original.length))
224
+ continue;
225
+ return word;
226
+ }
227
+ return null;
228
+ }
229
+ function matchCobolWord(text, index, code) {
230
+ const words = ["AND", "OR", "TRUE", "FALSE"];
231
+ for (const word of words) {
232
+ const slice = text.slice(index, index + word.length);
233
+ if (slice.toUpperCase() !== word)
234
+ continue;
235
+ if (!wordEdge(text, index, index + word.length, "-"))
236
+ continue;
237
+ if (!spanIsCode(code, index, word.length))
238
+ continue;
239
+ if (word === "AND")
240
+ return { original: slice, op: "and-to-or", replacement: mirrorCase(slice, "OR") };
241
+ if (word === "OR")
242
+ return { original: slice, op: "or-to-and", replacement: mirrorCase(slice, "AND") };
243
+ if (word === "TRUE")
244
+ return { original: slice, op: "true-to-false", replacement: mirrorCase(slice, "FALSE") };
245
+ return { original: slice, op: "false-to-true", replacement: mirrorCase(slice, "TRUE") };
246
+ }
247
+ return null;
248
+ }
249
+ function mirrorCase(source, replacement) {
250
+ if (source === source.toUpperCase())
251
+ return replacement.toUpperCase();
252
+ if (source === source.toLowerCase())
253
+ return replacement.toLowerCase();
254
+ return replacement[0].toUpperCase() + replacement.slice(1).toLowerCase();
255
+ }
256
+ function wordEdge(text, start, end, extra) {
257
+ const ident = (ch) => ch !== undefined && (/[A-Za-z0-9_]/.test(ch) || extra.includes(ch));
258
+ return !ident(text[start - 1]) && !ident(text[end]);
259
+ }
260
+ function spanIsCode(code, start, length) {
261
+ for (let offset = 0; offset < length; offset += 1)
262
+ if (!code[start + offset])
263
+ return false;
264
+ return true;
265
+ }
266
+ function blocked(text, index, original) {
267
+ const prev = text[index - 1] ?? "";
268
+ const next = text[index + original.length] ?? "";
269
+ // `<-` is a Go channel operator. `=>` is a Rust fat arrow. Neither is a comparison.
270
+ if (original === "<" && (next === "<" || next === "=" || next === "-" || prev === "<"))
271
+ return true;
272
+ if (original === ">" && (next === ">" || next === "=" || prev === ">" || prev === "-" || prev === "="))
273
+ return true;
274
+ // Both sides must be whitespace. One side still matches `#include <stdio.h>` and `vector<int>`.
275
+ if ((original === "<" || original === ">") && !(/\s/.test(prev) && /\s/.test(next)))
276
+ return true;
277
+ // `<<=`, `>>=`, and `<=>` contain `<=` or `>=` but are not relational operators.
278
+ if (original === "<=" && (prev === "<" || next === ">" || next === "="))
279
+ return true;
280
+ if (original === ">=" && (prev === ">" || next === "="))
281
+ return true;
282
+ if ((original === "==" || original === "!=" || original === "===" || original === "!==") && next === "=")
283
+ return true;
284
+ if (original === "&&" && (next === "&" || prev === "&"))
285
+ return true;
286
+ if (original === "||" && (next === "|" || prev === "|"))
287
+ return true;
288
+ return false;
289
+ }
290
+ function codeMask(text, kind) {
291
+ const code = new Uint8Array(text.length);
292
+ let index = 0;
293
+ while (index < text.length) {
294
+ const hidden = skipHidden(text, index, kind);
295
+ if (hidden > index) {
296
+ index = hidden;
297
+ continue;
298
+ }
299
+ code[index] = 1;
300
+ index += 1;
301
+ }
302
+ return code;
303
+ }
304
+ function skipHidden(text, index, kind) {
305
+ if (kind === "python")
306
+ return skipPython(text, index);
307
+ if (kind === "cobol")
308
+ return skipCobol(text, index);
309
+ if (kind === "js")
310
+ return skipJs(text, index);
311
+ if (kind === "swift")
312
+ return skipSwift(text, index);
313
+ if (kind === "csharp")
314
+ return skipCSharp(text, index);
315
+ if (kind === "asm-x86_64")
316
+ return skipAsm(text, index, { hash: false, semi: true, slash: true });
317
+ if (kind === "asm-aarch64")
318
+ return skipAsm(text, index, { hash: false, semi: true, slash: true });
319
+ if (kind === "asm-riscv")
320
+ return skipAsm(text, index, { hash: true, semi: false, slash: true });
321
+ if (kind === "go")
322
+ return skipC(text, index, { nested: false, rust: false, raw: true });
323
+ if (kind === "rust")
324
+ return skipC(text, index, { nested: true, rust: true, raw: false });
325
+ return skipC(text, index, { nested: false, rust: false, raw: false });
326
+ }
327
+ function skipJs(text, index) {
328
+ const hidden = skipC(text, index, { nested: false, rust: false, raw: false });
329
+ if (hidden > index)
330
+ return hidden;
331
+ if (text[index] === "`")
332
+ return skipTemplate(text, index);
333
+ return index;
334
+ }
335
+ function skipSwift(text, index) {
336
+ if (text.startsWith('"""', index))
337
+ return skipQuotes(text, index, '"""', false);
338
+ return skipC(text, index, { nested: false, rust: false, raw: false });
339
+ }
340
+ function skipCSharp(text, index) {
341
+ if (text.startsWith('@"', index) || text.startsWith('$@"', index)) {
342
+ const start = text[index] === "@" ? index + 1 : index + 2;
343
+ let cursor = start + 1;
344
+ while (cursor < text.length) {
345
+ if (text.startsWith('""', cursor)) {
346
+ cursor += 2;
347
+ continue;
348
+ }
349
+ if (text[cursor] === '"')
350
+ return cursor + 1;
351
+ cursor += 1;
352
+ }
353
+ return text.length;
354
+ }
355
+ return skipC(text, index, { nested: false, rust: false, raw: false });
356
+ }
357
+ function skipTemplate(text, index) {
358
+ let cursor = index + 1;
359
+ while (cursor < text.length) {
360
+ if (text[cursor] === "\\") {
361
+ cursor += 2;
362
+ continue;
363
+ }
364
+ if (text[cursor] === "`")
365
+ return cursor + 1;
366
+ cursor += 1;
367
+ }
368
+ return text.length;
369
+ }
370
+ function skipAsm(text, index, mode) {
371
+ if (mode.slash && text.startsWith("//", index))
372
+ return eol(text, index);
373
+ if (mode.semi && text[index] === ";")
374
+ return eol(text, index);
375
+ if (mode.hash && text[index] === "#")
376
+ return eol(text, index);
377
+ if (text.startsWith("/*", index))
378
+ return skipBlock(text, index, false);
379
+ if (text[index] === '"')
380
+ return skipEscaped(text, index, '"');
381
+ if (text[index] === "'")
382
+ return skipEscaped(text, index, "'");
383
+ return index;
384
+ }
385
+ function skipPython(text, index) {
386
+ if (text[index] === "#")
387
+ return eol(text, index);
388
+ const quoted = pythonStringAt(text, index);
389
+ if (!quoted)
390
+ return index;
391
+ return skipQuotes(text, quoted.quoteAt, quoted.quote, quoted.raw);
392
+ }
393
+ function pythonStringAt(text, index) {
394
+ if (text[index] === "'" || text[index] === '"') {
395
+ const quote = text.startsWith(text[index].repeat(3), index) ? text[index].repeat(3) : text[index];
396
+ return { quoteAt: index, quote, raw: false };
397
+ }
398
+ if (index > 0 && /[A-Za-z0-9_]/.test(text[index - 1]))
399
+ return null;
400
+ if (!/[rRuUbBfF]/.test(text[index]))
401
+ return null;
402
+ let cursor = index;
403
+ let letters = "";
404
+ while (cursor < text.length && letters.length < 2 && /[rRuUbBfF]/.test(text[cursor])) {
405
+ letters += text[cursor];
406
+ cursor += 1;
407
+ }
408
+ if (text[cursor] !== "'" && text[cursor] !== '"')
409
+ return null;
410
+ if (!/^(?:[rRfFbBuU]|[rR][fFbB]|[fFbB][rR])$/.test(letters))
411
+ return null;
412
+ const quote = text.startsWith(text[cursor].repeat(3), cursor) ? text[cursor].repeat(3) : text[cursor];
413
+ return { quoteAt: cursor, quote, raw: /[rR]/.test(letters) };
414
+ }
415
+ function skipCobol(text, index) {
416
+ // Column 7 is the fixed-format indicator. `*` and `/` comment out the whole line.
417
+ if ((index === 0 || text[index - 1] === "\n") && (text[index + 6] === "*" || text[index + 6] === "/"))
418
+ return eol(text, index);
419
+ if (text.startsWith("*>", index))
420
+ return eol(text, index);
421
+ if (text[index] !== '"' && text[index] !== "'")
422
+ return index;
423
+ const quote = text[index];
424
+ let cursor = index + 1;
425
+ while (cursor < text.length) {
426
+ if (text[cursor] === "\n")
427
+ return cursor;
428
+ if (text[cursor] === quote) {
429
+ if (text[cursor + 1] === quote) {
430
+ cursor += 2;
431
+ continue;
432
+ }
433
+ return cursor + 1;
434
+ }
435
+ cursor += 1;
436
+ }
437
+ return text.length;
438
+ }
439
+ function skipC(text, index, mode) {
440
+ if (mode.rust) {
441
+ const raw = rustRawEnd(text, index);
442
+ if (raw !== null)
443
+ return raw;
444
+ }
445
+ if (text.startsWith("//", index))
446
+ return eol(text, index);
447
+ if (text.startsWith("/*", index))
448
+ return skipBlock(text, index, mode.nested);
449
+ if (text[index] === '"')
450
+ return skipEscaped(text, index, '"');
451
+ if (mode.raw && text[index] === "`") {
452
+ const end = text.indexOf("`", index + 1);
453
+ return end === -1 ? text.length : end + 1;
454
+ }
455
+ if (text[index] !== "'")
456
+ return index;
457
+ if (mode.rust)
458
+ return rustCharEnd(text, index) ?? index;
459
+ return skipEscaped(text, index, "'");
460
+ }
461
+ function skipBlock(text, index, nested) {
462
+ if (!nested) {
463
+ const end = text.indexOf("*/", index + 2);
464
+ return end === -1 ? text.length : end + 2;
465
+ }
466
+ let depth = 1;
467
+ let cursor = index + 2;
468
+ while (cursor < text.length && depth > 0) {
469
+ if (text.startsWith("/*", cursor)) {
470
+ depth += 1;
471
+ cursor += 2;
472
+ continue;
473
+ }
474
+ if (text.startsWith("*/", cursor)) {
475
+ depth -= 1;
476
+ cursor += 2;
477
+ continue;
478
+ }
479
+ cursor += 1;
480
+ }
481
+ return cursor;
482
+ }
483
+ function rustRawEnd(text, index) {
484
+ if (text[index] !== "r" && !(text[index] === "b" && text[index + 1] === "r"))
485
+ return null;
486
+ if (index > 0 && /[A-Za-z0-9_]/.test(text[index - 1]))
487
+ return null;
488
+ let cursor = text[index] === "b" ? index + 2 : index + 1;
489
+ let hashes = 0;
490
+ while (text[cursor] === "#") {
491
+ hashes += 1;
492
+ cursor += 1;
493
+ }
494
+ if (text[cursor] !== '"')
495
+ return null;
496
+ cursor += 1;
497
+ const closer = `"${"#".repeat(hashes)}`;
498
+ const end = text.indexOf(closer, cursor);
499
+ return end === -1 ? text.length : end + closer.length;
500
+ }
501
+ function rustCharEnd(text, index) {
502
+ if (text[index + 1] === "\\" && text[index + 2] === "'" && text[index + 3] === "'")
503
+ return index + 4;
504
+ if (text[index + 1] === "\\") {
505
+ let cursor = index + 2;
506
+ while (cursor < text.length && cursor < index + 12 && text[cursor] !== "'")
507
+ cursor += 1;
508
+ return text[cursor] === "'" ? cursor + 1 : null;
509
+ }
510
+ if (text[index + 2] === "'")
511
+ return index + 3;
512
+ return null;
513
+ }
514
+ function skipEscaped(text, index, quote) {
515
+ let cursor = index + 1;
516
+ while (cursor < text.length) {
517
+ if (text[cursor] === "\\") {
518
+ cursor += 2;
519
+ continue;
520
+ }
521
+ if (text[cursor] === "\n")
522
+ return cursor;
523
+ if (text[cursor] === quote)
524
+ return cursor + 1;
525
+ cursor += 1;
526
+ }
527
+ return text.length;
528
+ }
529
+ function skipQuotes(text, quoteAt, quote, raw) {
530
+ let cursor = quoteAt + quote.length;
531
+ while (cursor < text.length) {
532
+ if (!raw && text[cursor] === "\\") {
533
+ cursor += 2;
534
+ continue;
535
+ }
536
+ if (text.startsWith(quote, cursor))
537
+ return cursor + quote.length;
538
+ cursor += 1;
539
+ }
540
+ return text.length;
541
+ }
542
+ function eol(text, index) {
543
+ let cursor = index;
544
+ while (cursor < text.length && text[cursor] !== "\n")
545
+ cursor += 1;
546
+ return cursor;
547
+ }
@@ -0,0 +1,23 @@
1
+ const TITLE = /^(?:test|it)\((["'`])(.*?)\1/;
2
+ export function titlesInText(text) {
3
+ const titles = [];
4
+ for (const line of text.split("\n")) {
5
+ const match = line.match(TITLE);
6
+ if (match)
7
+ titles.push(match[2]);
8
+ }
9
+ return titles;
10
+ }
11
+ export function duplicateTitles(files) {
12
+ const seen = new Map();
13
+ for (const file of files) {
14
+ for (const title of titlesInText(file.text)) {
15
+ const list = seen.get(title) ?? [];
16
+ list.push(file.file);
17
+ seen.set(title, list);
18
+ }
19
+ }
20
+ return [...seen.entries()]
21
+ .filter(([, files]) => files.length > 1)
22
+ .map(([title, files]) => ({ title, files }));
23
+ }
@@ -0,0 +1,107 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { SCHEMA_VERSION } from "../contract.js";
4
+ import { directImporters } from "../mutate/affected.js";
5
+ import { filesInDiff } from "../mutate/patch.js";
6
+ import { killLabel } from "../mutate/kill-label.js";
7
+ import { runMutants } from "../mutate/run.js";
8
+ import { campaignSummary } from "../mutate/suite-decision.js";
9
+ /** The gap is done only when this mutant is killed and the baseline suite is green. */
10
+ export async function checkKill(id, options) {
11
+ const importers = importersOf(id, options);
12
+ if (importers.length === 0) {
13
+ return {
14
+ schemaVersion: SCHEMA_VERSION,
15
+ ok: false,
16
+ command: "check-kill",
17
+ summary: `${id} is not done. no direct importer in ${options.testsDir}`,
18
+ next: "The task is not done.",
19
+ nextCall: null,
20
+ done: false,
21
+ outcome: null,
22
+ suiteGreen: false,
23
+ };
24
+ }
25
+ const report = await runMutants({ ...options, onlyTests: importers, onlyPatch: id, affected: false });
26
+ const baselineFile = path.join(options.outDir, "baseline.json");
27
+ let suiteGreen = false;
28
+ if (existsSync(baselineFile)) {
29
+ const baseline = JSON.parse(readFileSync(baselineFile, "utf8"));
30
+ suiteGreen = baseline.ok === true;
31
+ }
32
+ // A red baseline stops inside the run. Do not read a result that names the broken test.
33
+ if (!report.ok) {
34
+ return {
35
+ schemaVersion: SCHEMA_VERSION,
36
+ ok: false,
37
+ command: "check-kill",
38
+ summary: `${id} is not done. ${report.summary}`,
39
+ next: "The task is not done.",
40
+ nextCall: null,
41
+ done: false,
42
+ outcome: null,
43
+ suiteGreen,
44
+ killedBy: [],
45
+ };
46
+ }
47
+ const resultFile = path.join(options.outDir, "results", `${id}.json`);
48
+ let outcome = null;
49
+ let killedBy = [];
50
+ if (existsSync(resultFile)) {
51
+ const result = JSON.parse(readFileSync(resultFile, "utf8"));
52
+ outcome = result.outcome ?? null;
53
+ killedBy = Array.isArray(result.killedBy) ? result.killedBy : [];
54
+ }
55
+ const done = suiteGreen && outcome === "killed";
56
+ const label = done ? killLabel(killedBy) : null;
57
+ const head = checkHead(report);
58
+ return {
59
+ schemaVersion: SCHEMA_VERSION,
60
+ ok: done,
61
+ command: "check-kill",
62
+ summary: done ? `${head} ${id} is dead and the suite is green.` : `${head} ${id} is not done.`,
63
+ next: label?.next ?? (done ? "The gap-fixing task is done." : "The task is not done."),
64
+ nextCall: null,
65
+ done,
66
+ outcome,
67
+ cause: label?.cause ?? null,
68
+ killedBy,
69
+ suiteGreen,
70
+ };
71
+ }
72
+ /** Same direct-importer set verify-change runs for a diff that touches this mutant's file. */
73
+ function importersOf(id, options) {
74
+ const patch = options.patchDirs.map((dir) => path.join(dir, `${id}.patch`)).find((file) => existsSync(file));
75
+ if (!patch)
76
+ return [];
77
+ const rels = filesInDiff(readFileSync(patch, "utf8")).map((item) => packageRelative(options.repoDir, options.packageDir, item.file));
78
+ return directImporters(options.packageDir, rels, options.testsDir);
79
+ }
80
+ function checkHead(report) {
81
+ const finished = report.killed + report.survived + report.flaky + report.timeouts + report.errors + report.noCoverage;
82
+ return campaignSummary({
83
+ noCoverage: report.noCoverage,
84
+ survived: report.survived,
85
+ killed: report.killed,
86
+ flaky: report.flaky,
87
+ timeouts: report.timeouts,
88
+ errors: report.errors,
89
+ finished,
90
+ });
91
+ }
92
+ function packageRelative(repoDir, packageDir, file) {
93
+ const norm = file.split(path.sep).join("/");
94
+ let repo = repoDir;
95
+ let pkg = packageDir;
96
+ try {
97
+ repo = realpathSync(repoDir);
98
+ pkg = realpathSync(packageDir);
99
+ }
100
+ catch {
101
+ // A missing path still has a relative prefix.
102
+ }
103
+ const prefix = path.relative(repo, pkg).split(path.sep).join("/");
104
+ if (prefix && prefix !== "." && !prefix.startsWith("..") && norm.startsWith(`${prefix}/`))
105
+ return norm.slice(prefix.length + 1);
106
+ return norm;
107
+ }