@mutineerjs/mutineer 0.3.2 → 0.4.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.
|
@@ -193,6 +193,99 @@ describe('executePool', () => {
|
|
|
193
193
|
const content = JSON.parse(await fs.readFile(cacheFile, 'utf8'));
|
|
194
194
|
expect(content['persist-key']).toBeDefined();
|
|
195
195
|
});
|
|
196
|
+
it('escaped mutant stores snippets when lines differ', async () => {
|
|
197
|
+
const tmpFile = path.join(tmpDir, 'source.ts');
|
|
198
|
+
await fs.writeFile(tmpFile, 'const x = a + b\n');
|
|
199
|
+
const adapter = makeAdapter({
|
|
200
|
+
runMutant: vi
|
|
201
|
+
.fn()
|
|
202
|
+
.mockResolvedValue({ status: 'escaped', durationMs: 1 }),
|
|
203
|
+
});
|
|
204
|
+
const cache = {};
|
|
205
|
+
const task = makeTask({
|
|
206
|
+
key: 'snippet-key',
|
|
207
|
+
v: {
|
|
208
|
+
id: 'source.ts#0',
|
|
209
|
+
name: 'flipArith',
|
|
210
|
+
file: tmpFile,
|
|
211
|
+
code: 'const x = a - b\n',
|
|
212
|
+
line: 1,
|
|
213
|
+
col: 10,
|
|
214
|
+
tests: ['/tests/file.test.ts'],
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
await executePool({
|
|
218
|
+
tasks: [task],
|
|
219
|
+
adapter,
|
|
220
|
+
cache,
|
|
221
|
+
concurrency: 1,
|
|
222
|
+
progressMode: 'list',
|
|
223
|
+
cwd: tmpDir,
|
|
224
|
+
});
|
|
225
|
+
expect(cache['snippet-key'].originalSnippet).toBe('const x = a + b');
|
|
226
|
+
expect(cache['snippet-key'].mutatedSnippet).toBe('const x = a - b');
|
|
227
|
+
});
|
|
228
|
+
it('escaped mutant omits snippets when original and mutated lines are identical', async () => {
|
|
229
|
+
const tmpFile = path.join(tmpDir, 'source2.ts');
|
|
230
|
+
await fs.writeFile(tmpFile, 'const x = a + b\n');
|
|
231
|
+
const adapter = makeAdapter({
|
|
232
|
+
runMutant: vi
|
|
233
|
+
.fn()
|
|
234
|
+
.mockResolvedValue({ status: 'escaped', durationMs: 1 }),
|
|
235
|
+
});
|
|
236
|
+
const cache = {};
|
|
237
|
+
const task = makeTask({
|
|
238
|
+
key: 'no-snippet-key',
|
|
239
|
+
v: {
|
|
240
|
+
id: 'source2.ts#0',
|
|
241
|
+
name: 'flipArith',
|
|
242
|
+
file: tmpFile,
|
|
243
|
+
code: 'const x = a + b\n',
|
|
244
|
+
line: 1,
|
|
245
|
+
col: 10,
|
|
246
|
+
tests: ['/tests/file.test.ts'],
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
await executePool({
|
|
250
|
+
tasks: [task],
|
|
251
|
+
adapter,
|
|
252
|
+
cache,
|
|
253
|
+
concurrency: 1,
|
|
254
|
+
progressMode: 'list',
|
|
255
|
+
cwd: tmpDir,
|
|
256
|
+
});
|
|
257
|
+
expect(cache['no-snippet-key'].originalSnippet).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
it('escaped mutant omits snippets when file read fails', async () => {
|
|
260
|
+
const adapter = makeAdapter({
|
|
261
|
+
runMutant: vi
|
|
262
|
+
.fn()
|
|
263
|
+
.mockResolvedValue({ status: 'escaped', durationMs: 1 }),
|
|
264
|
+
});
|
|
265
|
+
const cache = {};
|
|
266
|
+
const task = makeTask({
|
|
267
|
+
key: 'missing-file-key',
|
|
268
|
+
v: {
|
|
269
|
+
id: 'missing.ts#0',
|
|
270
|
+
name: 'flipArith',
|
|
271
|
+
file: '/nonexistent/path/missing.ts',
|
|
272
|
+
code: 'const x = a - b\n',
|
|
273
|
+
line: 1,
|
|
274
|
+
col: 10,
|
|
275
|
+
tests: ['/tests/file.test.ts'],
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
await executePool({
|
|
279
|
+
tasks: [task],
|
|
280
|
+
adapter,
|
|
281
|
+
cache,
|
|
282
|
+
concurrency: 1,
|
|
283
|
+
progressMode: 'list',
|
|
284
|
+
cwd: tmpDir,
|
|
285
|
+
});
|
|
286
|
+
expect(cache['missing-file-key'].status).toBe('escaped');
|
|
287
|
+
expect(cache['missing-file-key'].originalSnippet).toBeUndefined();
|
|
288
|
+
});
|
|
196
289
|
it('handles adapter errors gracefully and still shuts down', async () => {
|
|
197
290
|
const adapter = makeAdapter({
|
|
198
291
|
runMutant: vi.fn().mockRejectedValue(new Error('adapter failure')),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import { render } from 'ink';
|
|
2
3
|
import { createElement } from 'react';
|
|
3
4
|
import { Progress } from '../utils/progress.js';
|
|
@@ -93,12 +94,31 @@ export async function executePool(opts) {
|
|
|
93
94
|
col: v.col,
|
|
94
95
|
}, tests);
|
|
95
96
|
const status = result.status;
|
|
97
|
+
let originalSnippet;
|
|
98
|
+
let mutatedSnippet;
|
|
99
|
+
if (status === 'escaped') {
|
|
100
|
+
try {
|
|
101
|
+
const originalLines = fs.readFileSync(v.file, 'utf8').split('\n');
|
|
102
|
+
const mutatedLines = v.code.split('\n');
|
|
103
|
+
const lineIdx = v.line - 1;
|
|
104
|
+
const orig = originalLines[lineIdx]?.trim();
|
|
105
|
+
const mutated = mutatedLines[lineIdx]?.trim();
|
|
106
|
+
if (orig !== undefined && mutated !== undefined && orig !== mutated) {
|
|
107
|
+
originalSnippet = orig;
|
|
108
|
+
mutatedSnippet = mutated;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// best-effort
|
|
113
|
+
}
|
|
114
|
+
}
|
|
96
115
|
cache[key] = {
|
|
97
116
|
status,
|
|
98
117
|
file: v.file,
|
|
99
118
|
line: v.line,
|
|
100
119
|
col: v.col,
|
|
101
120
|
mutator: v.name,
|
|
121
|
+
...(originalSnippet !== undefined && { originalSnippet, mutatedSnippet }),
|
|
102
122
|
};
|
|
103
123
|
progress.update(status);
|
|
104
124
|
}
|
package/dist/types/mutant.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface Variant extends MutantDescriptor {
|
|
|
25
25
|
export interface MutantCacheEntry extends MutantLocation {
|
|
26
26
|
readonly status: MutantStatus;
|
|
27
27
|
readonly mutator: string;
|
|
28
|
+
readonly originalSnippet?: string;
|
|
29
|
+
readonly mutatedSnippet?: string;
|
|
28
30
|
}
|
|
29
31
|
export interface MutantResult extends MutantCacheEntry {
|
|
30
32
|
readonly id: string;
|
|
@@ -43,6 +43,34 @@ describe('summary', () => {
|
|
|
43
43
|
expect(lines.some((l) => l.includes('Duration: 1.50s'))).toBe(true);
|
|
44
44
|
logSpy.mockRestore();
|
|
45
45
|
});
|
|
46
|
+
it('prints diff lines for escaped mutant with snippets', () => {
|
|
47
|
+
const cache = {
|
|
48
|
+
a: makeEntry({
|
|
49
|
+
status: 'escaped',
|
|
50
|
+
originalSnippet: 'return a + b',
|
|
51
|
+
mutatedSnippet: 'return a - b',
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
const summary = computeSummary(cache);
|
|
55
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
56
|
+
printSummary(summary, cache);
|
|
57
|
+
const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
|
|
58
|
+
expect(lines.some((l) => l.includes('- return a + b'))).toBe(true);
|
|
59
|
+
expect(lines.some((l) => l.includes('+ return a - b'))).toBe(true);
|
|
60
|
+
logSpy.mockRestore();
|
|
61
|
+
});
|
|
62
|
+
it('does not print diff lines for escaped mutant without snippets', () => {
|
|
63
|
+
const cache = {
|
|
64
|
+
a: makeEntry({ status: 'escaped' }),
|
|
65
|
+
};
|
|
66
|
+
const summary = computeSummary(cache);
|
|
67
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
68
|
+
printSummary(summary, cache);
|
|
69
|
+
const lines = logSpy.mock.calls.map((c) => stripAnsi(c.join(' ')));
|
|
70
|
+
expect(lines.some((l) => l.trimStart().startsWith('- '))).toBe(false);
|
|
71
|
+
expect(lines.some((l) => l.trimStart().startsWith('+ '))).toBe(false);
|
|
72
|
+
logSpy.mockRestore();
|
|
73
|
+
});
|
|
46
74
|
it('summarise returns summary and prints', () => {
|
|
47
75
|
const cache = { a: makeEntry({ status: 'killed' }) };
|
|
48
76
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
package/dist/utils/summary.js
CHANGED
|
@@ -74,8 +74,14 @@ export function printSummary(summary, cache, durationMs) {
|
|
|
74
74
|
}
|
|
75
75
|
if (entriesByStatus.escaped.length) {
|
|
76
76
|
console.log('\n' + chalk.red.bold('Escaped Mutants:'));
|
|
77
|
-
for (const entry of entriesByStatus.escaped)
|
|
77
|
+
for (const entry of entriesByStatus.escaped) {
|
|
78
78
|
console.log(' ' + formatRow(entry));
|
|
79
|
+
if (entry.originalSnippet !== undefined &&
|
|
80
|
+
entry.mutatedSnippet !== undefined) {
|
|
81
|
+
console.log(' ' + chalk.red('- ' + entry.originalSnippet));
|
|
82
|
+
console.log(' ' + chalk.green('+ ' + entry.mutatedSnippet));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
79
85
|
}
|
|
80
86
|
if (entriesByStatus.skipped.length) {
|
|
81
87
|
console.log('\n' + chalk.dim('Skipped Mutants:'));
|