@mutineerjs/mutineer 0.9.0 → 0.11.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 +52 -47
- package/dist/__tests__/index.spec.js +8 -0
- package/dist/bin/__tests__/mutineer.spec.js +7 -7
- package/dist/bin/mutineer.d.ts +1 -1
- package/dist/bin/mutineer.js +7 -4
- package/dist/core/__tests__/schemata.spec.js +62 -0
- package/dist/core/__tests__/sfc.spec.js +41 -1
- package/dist/core/schemata.js +15 -21
- package/dist/core/sfc.js +0 -4
- package/dist/core/variant-utils.js +0 -4
- package/dist/mutators/__tests__/utils.spec.js +65 -1
- package/dist/mutators/operator.js +13 -27
- package/dist/mutators/return-value.js +3 -7
- package/dist/mutators/utils.d.ts +2 -2
- package/dist/mutators/utils.js +59 -96
- package/dist/runner/__tests__/args.spec.js +8 -4
- package/dist/runner/__tests__/cache.spec.js +24 -0
- package/dist/runner/__tests__/changed.spec.js +75 -0
- package/dist/runner/__tests__/config.spec.js +50 -1
- package/dist/runner/__tests__/coverage-resolver.spec.js +88 -1
- package/dist/runner/__tests__/discover.spec.js +179 -0
- package/dist/runner/__tests__/orchestrator.spec.js +336 -11
- package/dist/runner/__tests__/pool-executor.spec.js +77 -0
- package/dist/runner/__tests__/ts-checker-worker.spec.d.ts +1 -0
- package/dist/runner/__tests__/ts-checker-worker.spec.js +66 -0
- package/dist/runner/__tests__/ts-checker.spec.js +89 -2
- package/dist/runner/args.d.ts +1 -1
- package/dist/runner/args.js +2 -2
- package/dist/runner/config.js +3 -4
- package/dist/runner/coverage-resolver.js +2 -1
- package/dist/runner/discover.js +2 -2
- package/dist/runner/jest/__tests__/adapter.spec.js +169 -0
- package/dist/runner/jest/__tests__/pool.spec.js +223 -1
- package/dist/runner/jest/adapter.js +3 -45
- package/dist/runner/jest/pool.js +4 -10
- package/dist/runner/jest/worker-runtime.js +2 -1
- package/dist/runner/orchestrator.js +8 -7
- package/dist/runner/pool-executor.js +7 -12
- package/dist/runner/shared/__tests__/strip-mutineer-args.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/strip-mutineer-args.spec.js +104 -0
- package/dist/runner/shared/__tests__/worker-script.spec.d.ts +1 -0
- package/dist/runner/shared/__tests__/worker-script.spec.js +32 -0
- package/dist/runner/shared/index.d.ts +4 -0
- package/dist/runner/shared/index.js +2 -0
- package/dist/runner/shared/pending-task.d.ts +9 -0
- package/dist/runner/shared/pending-task.js +1 -0
- package/dist/runner/shared/strip-mutineer-args.d.ts +11 -0
- package/dist/runner/shared/strip-mutineer-args.js +47 -0
- package/dist/runner/shared/worker-script.d.ts +5 -0
- package/dist/runner/shared/worker-script.js +12 -0
- package/dist/runner/ts-checker-worker.d.ts +10 -1
- package/dist/runner/ts-checker-worker.js +27 -25
- package/dist/runner/ts-checker.d.ts +6 -0
- package/dist/runner/ts-checker.js +1 -1
- package/dist/runner/vitest/__tests__/adapter.spec.js +294 -0
- package/dist/runner/vitest/__tests__/plugin.spec.js +28 -1
- package/dist/runner/vitest/__tests__/pool.spec.js +711 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +116 -1
- package/dist/runner/vitest/__tests__/worker-runtime.spec.js +81 -0
- package/dist/runner/vitest/adapter.js +14 -46
- package/dist/runner/vitest/plugin.js +1 -7
- package/dist/runner/vitest/pool.js +6 -19
- package/dist/runner/vitest/redirect-loader.js +3 -1
- package/dist/runner/vitest/worker-runtime.js +16 -1
- package/dist/runner/vitest/worker.mjs +1 -0
- package/dist/types/config.d.ts +2 -2
- package/dist/types/mutant.d.ts +3 -0
- package/dist/utils/__tests__/PoolSpinner.spec.d.ts +1 -0
- package/dist/utils/__tests__/PoolSpinner.spec.js +15 -0
- package/dist/utils/__tests__/coverage.spec.js +89 -0
- package/dist/utils/__tests__/logger.spec.js +9 -0
- package/dist/utils/__tests__/progress.spec.js +38 -0
- package/dist/utils/__tests__/summary.spec.js +70 -31
- package/dist/utils/coverage.js +3 -4
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.js +6 -0
- package/dist/utils/summary.d.ts +2 -3
- package/dist/utils/summary.js +5 -6
- package/package.json +1 -1
- package/dist/utils/CompileErrors.d.ts +0 -7
- package/dist/utils/CompileErrors.js +0 -24
- package/dist/utils/__tests__/CompileErrors.spec.js +0 -96
- /package/dist/{utils/__tests__/CompileErrors.spec.d.ts → __tests__/index.spec.d.ts} +0 -0
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* replaced by its counterpart.
|
|
7
7
|
*/
|
|
8
8
|
import { collectOperatorTargets, buildParseContext } from './utils.js';
|
|
9
|
+
function targetsToOutputs(src, targets, toOp) {
|
|
10
|
+
return targets.map((target) => ({
|
|
11
|
+
line: target.line,
|
|
12
|
+
col: target.col1,
|
|
13
|
+
code: src.slice(0, target.start) + toOp + src.slice(target.end),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
9
16
|
/**
|
|
10
17
|
* Factory to build an operator mutator using AST traversal and token analysis.
|
|
11
18
|
*
|
|
@@ -15,21 +22,14 @@ import { collectOperatorTargets, buildParseContext } from './utils.js';
|
|
|
15
22
|
* @param toOp - The operator to replace it with (e.g., '||')
|
|
16
23
|
*/
|
|
17
24
|
function makeOperatorMutator(name, description, fromOp, toOp) {
|
|
18
|
-
function targetsToOutputs(src, targets) {
|
|
19
|
-
return targets.map((target) => ({
|
|
20
|
-
line: target.line,
|
|
21
|
-
col: target.col1,
|
|
22
|
-
code: src.slice(0, target.start) + toOp + src.slice(target.end),
|
|
23
|
-
}));
|
|
24
|
-
}
|
|
25
25
|
return {
|
|
26
26
|
name,
|
|
27
27
|
description,
|
|
28
28
|
apply(src) {
|
|
29
|
-
return targetsToOutputs(src, collectOperatorTargets(src, fromOp));
|
|
29
|
+
return targetsToOutputs(src, collectOperatorTargets(src, fromOp), toOp);
|
|
30
30
|
},
|
|
31
31
|
applyWithContext(src, ctx) {
|
|
32
|
-
return targetsToOutputs(src, ctx.preCollected.operatorTargets.get(fromOp) ?? []);
|
|
32
|
+
return targetsToOutputs(src, ctx.preCollected.operatorTargets.get(fromOp) ?? [], toOp);
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
}
|
|
@@ -38,22 +38,15 @@ function makeOperatorMutator(name, description, fromOp, toOp) {
|
|
|
38
38
|
* mapKey distinguishes prefix vs postfix: 'pre++', 'post++', 'pre--', 'post--'
|
|
39
39
|
*/
|
|
40
40
|
function makeUpdateMutator(name, description, mapKey, toOp) {
|
|
41
|
-
function targetsToOutputs(src, targets) {
|
|
42
|
-
return targets.map((target) => ({
|
|
43
|
-
line: target.line,
|
|
44
|
-
col: target.col1,
|
|
45
|
-
code: src.slice(0, target.start) + toOp + src.slice(target.end),
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
48
41
|
return {
|
|
49
42
|
name,
|
|
50
43
|
description,
|
|
51
44
|
apply(src) {
|
|
52
45
|
const ctx = buildParseContext(src);
|
|
53
|
-
return targetsToOutputs(src, ctx.preCollected.updateTargets.get(mapKey) ?? []);
|
|
46
|
+
return targetsToOutputs(src, ctx.preCollected.updateTargets.get(mapKey) ?? [], toOp);
|
|
54
47
|
},
|
|
55
48
|
applyWithContext(src, ctx) {
|
|
56
|
-
return targetsToOutputs(src, ctx.preCollected.updateTargets.get(mapKey) ?? []);
|
|
49
|
+
return targetsToOutputs(src, ctx.preCollected.updateTargets.get(mapKey) ?? [], toOp);
|
|
57
50
|
},
|
|
58
51
|
};
|
|
59
52
|
}
|
|
@@ -61,22 +54,15 @@ function makeUpdateMutator(name, description, mapKey, toOp) {
|
|
|
61
54
|
* Factory for AssignmentExpression mutators (+=, -=, *=, /=).
|
|
62
55
|
*/
|
|
63
56
|
function makeAssignmentMutator(name, description, fromOp, toOp) {
|
|
64
|
-
function targetsToOutputs(src, targets) {
|
|
65
|
-
return targets.map((target) => ({
|
|
66
|
-
line: target.line,
|
|
67
|
-
col: target.col1,
|
|
68
|
-
code: src.slice(0, target.start) + toOp + src.slice(target.end),
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
57
|
return {
|
|
72
58
|
name,
|
|
73
59
|
description,
|
|
74
60
|
apply(src) {
|
|
75
61
|
const ctx = buildParseContext(src);
|
|
76
|
-
return targetsToOutputs(src, ctx.preCollected.assignmentTargets.get(fromOp) ?? []);
|
|
62
|
+
return targetsToOutputs(src, ctx.preCollected.assignmentTargets.get(fromOp) ?? [], toOp);
|
|
77
63
|
},
|
|
78
64
|
applyWithContext(src, ctx) {
|
|
79
|
-
return targetsToOutputs(src, ctx.preCollected.assignmentTargets.get(fromOp) ?? []);
|
|
65
|
+
return targetsToOutputs(src, ctx.preCollected.assignmentTargets.get(fromOp) ?? [], toOp);
|
|
80
66
|
},
|
|
81
67
|
};
|
|
82
68
|
}
|
|
@@ -27,9 +27,7 @@ function collectReturnMutations(src, ast, ignoreLines, replacer) {
|
|
|
27
27
|
const node = path.node;
|
|
28
28
|
if (!node.argument)
|
|
29
29
|
return; // bare return; — nothing to replace
|
|
30
|
-
const line = node.loc
|
|
31
|
-
if (line === undefined)
|
|
32
|
-
return;
|
|
30
|
+
const line = node.loc.start.line;
|
|
33
31
|
if (ignoreLines.has(line))
|
|
34
32
|
return;
|
|
35
33
|
const replacement = replacer(node.argument);
|
|
@@ -37,9 +35,7 @@ function collectReturnMutations(src, ast, ignoreLines, replacer) {
|
|
|
37
35
|
return;
|
|
38
36
|
const argStart = node.argument.start;
|
|
39
37
|
const argEnd = node.argument.end;
|
|
40
|
-
|
|
41
|
-
return;
|
|
42
|
-
const col = getVisualColumn(src, node.start ?? 0);
|
|
38
|
+
const col = getVisualColumn(src, node.start);
|
|
43
39
|
const code = src.slice(0, argStart) + replacement + src.slice(argEnd);
|
|
44
40
|
outputs.push({ line, col, code });
|
|
45
41
|
},
|
|
@@ -53,7 +49,7 @@ function makeReturnMutator(name, description, replacer) {
|
|
|
53
49
|
apply(src) {
|
|
54
50
|
const ast = parseSource(src);
|
|
55
51
|
const fileAst = ast;
|
|
56
|
-
const ignoreLines = buildIgnoreLines(fileAst.comments
|
|
52
|
+
const ignoreLines = buildIgnoreLines(fileAst.comments);
|
|
57
53
|
return collectReturnMutations(src, ast, ignoreLines, replacer);
|
|
58
54
|
},
|
|
59
55
|
applyWithContext(src, ctx) {
|
package/dist/mutators/utils.d.ts
CHANGED
|
@@ -112,9 +112,9 @@ export declare function collectAllTargets(src: string, ast: t.File & {
|
|
|
112
112
|
export declare function buildParseContext(src: string): ParseContext;
|
|
113
113
|
/**
|
|
114
114
|
* Collect operator targets from a pre-built ParseContext.
|
|
115
|
-
*
|
|
115
|
+
* Reads directly from preCollected targets; no additional traversal needed.
|
|
116
116
|
*/
|
|
117
|
-
export declare function collectOperatorTargetsFromContext(
|
|
117
|
+
export declare function collectOperatorTargetsFromContext(_src: string, ctx: ParseContext, opValue: string): OperatorTarget[];
|
|
118
118
|
/**
|
|
119
119
|
* Collect the operator tokens for a given operator and return their exact locations.
|
|
120
120
|
* Uses AST traversal to find BinaryExpression/LogicalExpression nodes, then maps them
|
package/dist/mutators/utils.js
CHANGED
|
@@ -131,77 +131,71 @@ export function collectAllTargets(src, ast, tokens, ignoreLines) {
|
|
|
131
131
|
const updateTargets = new Map();
|
|
132
132
|
const assignmentTargets = new Map();
|
|
133
133
|
function handleBinaryOrLogical(n) {
|
|
134
|
-
const nodeStart = n.start
|
|
135
|
-
const nodeEnd = n.end
|
|
134
|
+
const nodeStart = n.start;
|
|
135
|
+
const nodeEnd = n.end;
|
|
136
136
|
const opValue = n.operator;
|
|
137
137
|
const tok = tokens.find((tk) => tk.start >= nodeStart && tk.end <= nodeEnd && tk.value === opValue);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
operatorTargets.set(opValue, arr);
|
|
147
|
-
}
|
|
148
|
-
arr.push({
|
|
149
|
-
start: tok.start,
|
|
150
|
-
end: tok.end,
|
|
151
|
-
line,
|
|
152
|
-
col1: visualCol,
|
|
153
|
-
op: opValue,
|
|
154
|
-
});
|
|
138
|
+
const line = tok.loc.start.line;
|
|
139
|
+
if (ignoreLines.has(line))
|
|
140
|
+
return;
|
|
141
|
+
const visualCol = getVisualColumn(src, tok.start);
|
|
142
|
+
let arr = operatorTargets.get(opValue);
|
|
143
|
+
if (!arr) {
|
|
144
|
+
arr = [];
|
|
145
|
+
operatorTargets.set(opValue, arr);
|
|
155
146
|
}
|
|
147
|
+
arr.push({
|
|
148
|
+
start: tok.start,
|
|
149
|
+
end: tok.end,
|
|
150
|
+
line,
|
|
151
|
+
col1: visualCol,
|
|
152
|
+
op: opValue,
|
|
153
|
+
});
|
|
156
154
|
}
|
|
157
155
|
function handleUpdate(n) {
|
|
158
|
-
const nodeStart = n.start
|
|
159
|
-
const nodeEnd = n.end
|
|
156
|
+
const nodeStart = n.start;
|
|
157
|
+
const nodeEnd = n.end;
|
|
160
158
|
const opValue = n.operator;
|
|
161
159
|
const tok = tokens.find((tk) => tk.start >= nodeStart && tk.end <= nodeEnd && tk.value === opValue);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
updateTargets.set(mapKey, arr);
|
|
172
|
-
}
|
|
173
|
-
arr.push({
|
|
174
|
-
start: tok.start,
|
|
175
|
-
end: tok.end,
|
|
176
|
-
line,
|
|
177
|
-
col1: visualCol,
|
|
178
|
-
op: opValue,
|
|
179
|
-
});
|
|
160
|
+
const line = tok.loc.start.line;
|
|
161
|
+
if (ignoreLines.has(line))
|
|
162
|
+
return;
|
|
163
|
+
const visualCol = getVisualColumn(src, tok.start);
|
|
164
|
+
const mapKey = (n.prefix ? 'pre' : 'post') + opValue;
|
|
165
|
+
let arr = updateTargets.get(mapKey);
|
|
166
|
+
if (!arr) {
|
|
167
|
+
arr = [];
|
|
168
|
+
updateTargets.set(mapKey, arr);
|
|
180
169
|
}
|
|
170
|
+
arr.push({
|
|
171
|
+
start: tok.start,
|
|
172
|
+
end: tok.end,
|
|
173
|
+
line,
|
|
174
|
+
col1: visualCol,
|
|
175
|
+
op: opValue,
|
|
176
|
+
});
|
|
181
177
|
}
|
|
182
178
|
function handleAssignment(n) {
|
|
183
|
-
const nodeStart = n.start
|
|
184
|
-
const nodeEnd = n.end
|
|
179
|
+
const nodeStart = n.start;
|
|
180
|
+
const nodeEnd = n.end;
|
|
185
181
|
const opValue = n.operator;
|
|
186
182
|
const tok = tokens.find((tk) => tk.start >= nodeStart && tk.end <= nodeEnd && tk.value === opValue);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
assignmentTargets.set(opValue, arr);
|
|
196
|
-
}
|
|
197
|
-
arr.push({
|
|
198
|
-
start: tok.start,
|
|
199
|
-
end: tok.end,
|
|
200
|
-
line,
|
|
201
|
-
col1: visualCol,
|
|
202
|
-
op: opValue,
|
|
203
|
-
});
|
|
183
|
+
const line = tok.loc.start.line;
|
|
184
|
+
if (ignoreLines.has(line))
|
|
185
|
+
return;
|
|
186
|
+
const visualCol = getVisualColumn(src, tok.start);
|
|
187
|
+
let arr = assignmentTargets.get(opValue);
|
|
188
|
+
if (!arr) {
|
|
189
|
+
arr = [];
|
|
190
|
+
assignmentTargets.set(opValue, arr);
|
|
204
191
|
}
|
|
192
|
+
arr.push({
|
|
193
|
+
start: tok.start,
|
|
194
|
+
end: tok.end,
|
|
195
|
+
line,
|
|
196
|
+
col1: visualCol,
|
|
197
|
+
op: opValue,
|
|
198
|
+
});
|
|
205
199
|
}
|
|
206
200
|
traverse(ast, {
|
|
207
201
|
BinaryExpression(p) {
|
|
@@ -220,16 +214,12 @@ export function collectAllTargets(src, ast, tokens, ignoreLines) {
|
|
|
220
214
|
const node = p.node;
|
|
221
215
|
if (!node.argument)
|
|
222
216
|
return;
|
|
223
|
-
const line = node.loc
|
|
224
|
-
if (line === undefined)
|
|
225
|
-
return;
|
|
217
|
+
const line = node.loc.start.line;
|
|
226
218
|
if (ignoreLines.has(line))
|
|
227
219
|
return;
|
|
228
220
|
const argStart = node.argument.start;
|
|
229
221
|
const argEnd = node.argument.end;
|
|
230
|
-
|
|
231
|
-
return;
|
|
232
|
-
const col = getVisualColumn(src, node.start ?? 0);
|
|
222
|
+
const col = getVisualColumn(src, node.start);
|
|
233
223
|
returnStatements.push({
|
|
234
224
|
line,
|
|
235
225
|
col,
|
|
@@ -247,44 +237,17 @@ export function collectAllTargets(src, ast, tokens, ignoreLines) {
|
|
|
247
237
|
*/
|
|
248
238
|
export function buildParseContext(src) {
|
|
249
239
|
const ast = parseSource(src);
|
|
250
|
-
const tokens = ast.tokens
|
|
251
|
-
const ignoreLines = buildIgnoreLines(ast.comments
|
|
240
|
+
const tokens = ast.tokens;
|
|
241
|
+
const ignoreLines = buildIgnoreLines(ast.comments);
|
|
252
242
|
const preCollected = collectAllTargets(src, ast, tokens, ignoreLines);
|
|
253
243
|
return { ast, tokens, ignoreLines, preCollected };
|
|
254
244
|
}
|
|
255
245
|
/**
|
|
256
246
|
* Collect operator targets from a pre-built ParseContext.
|
|
257
|
-
*
|
|
247
|
+
* Reads directly from preCollected targets; no additional traversal needed.
|
|
258
248
|
*/
|
|
259
|
-
export function collectOperatorTargetsFromContext(
|
|
260
|
-
|
|
261
|
-
const out = [];
|
|
262
|
-
traverse(ast, {
|
|
263
|
-
enter(p) {
|
|
264
|
-
if (!isBinaryOrLogical(p.node))
|
|
265
|
-
return;
|
|
266
|
-
const n = p.node;
|
|
267
|
-
if (n.operator !== opValue)
|
|
268
|
-
return;
|
|
269
|
-
const nodeStart = n.start ?? 0;
|
|
270
|
-
const nodeEnd = n.end ?? 0;
|
|
271
|
-
const tok = tokens.find((tk) => tk.start >= nodeStart && tk.end <= nodeEnd && tk.value === opValue);
|
|
272
|
-
if (tok) {
|
|
273
|
-
const line = tok.loc.start.line;
|
|
274
|
-
if (ignoreLines.has(line))
|
|
275
|
-
return;
|
|
276
|
-
const visualCol = getVisualColumn(src, tok.start);
|
|
277
|
-
out.push({
|
|
278
|
-
start: tok.start,
|
|
279
|
-
end: tok.end,
|
|
280
|
-
line,
|
|
281
|
-
col1: visualCol,
|
|
282
|
-
op: opValue,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
});
|
|
287
|
-
return out;
|
|
249
|
+
export function collectOperatorTargetsFromContext(_src, ctx, opValue) {
|
|
250
|
+
return ctx.preCollected.operatorTargets.get(opValue) ?? [];
|
|
288
251
|
}
|
|
289
252
|
/**
|
|
290
253
|
* Collect the operator tokens for a given operator and return their exact locations.
|
|
@@ -102,6 +102,10 @@ describe('parseConcurrency', () => {
|
|
|
102
102
|
const result = parseConcurrency(['--concurrency', 'abc']);
|
|
103
103
|
expect(result).toBeGreaterThanOrEqual(1);
|
|
104
104
|
});
|
|
105
|
+
it('handles --concurrency flag with no following value', () => {
|
|
106
|
+
const result = parseConcurrency(['--concurrency']);
|
|
107
|
+
expect(result).toBeGreaterThanOrEqual(1);
|
|
108
|
+
});
|
|
105
109
|
});
|
|
106
110
|
describe('parseProgressMode', () => {
|
|
107
111
|
it('returns bar by default', () => {
|
|
@@ -125,11 +129,11 @@ describe('parseCliOptions', () => {
|
|
|
125
129
|
it('parses --changed flag', () => {
|
|
126
130
|
const opts = parseCliOptions(['--changed'], emptyCfg);
|
|
127
131
|
expect(opts.wantsChanged).toBe(true);
|
|
128
|
-
expect(opts.
|
|
132
|
+
expect(opts.wantsChangedWithImports).toBe(false);
|
|
129
133
|
});
|
|
130
|
-
it('parses --changed-with-
|
|
131
|
-
const opts = parseCliOptions(['--changed-with-
|
|
132
|
-
expect(opts.
|
|
134
|
+
it('parses --changed-with-imports flag', () => {
|
|
135
|
+
const opts = parseCliOptions(['--changed-with-imports'], emptyCfg);
|
|
136
|
+
expect(opts.wantsChangedWithImports).toBe(true);
|
|
133
137
|
});
|
|
134
138
|
it('parses --full flag', () => {
|
|
135
139
|
const opts = parseCliOptions(['--full'], emptyCfg);
|
|
@@ -137,6 +137,13 @@ describe('decodeCacheKey', () => {
|
|
|
137
137
|
expect(decoded.line).toBe(10);
|
|
138
138
|
expect(decoded.col).toBe(5);
|
|
139
139
|
});
|
|
140
|
+
it('falls back to full key when file segment is empty', () => {
|
|
141
|
+
// a:b::1,0:mutator → after parsing, restAfterFirst.slice(secondColon+1) = '' → falls back to key
|
|
142
|
+
const key = 'a:b::1,0:mutator';
|
|
143
|
+
const decoded = decodeCacheKey(key);
|
|
144
|
+
expect(decoded.file).toBe(key);
|
|
145
|
+
expect(decoded.mutator).toBe('mutator');
|
|
146
|
+
});
|
|
140
147
|
});
|
|
141
148
|
describe('keyForTests', () => {
|
|
142
149
|
it('produces deterministic keys regardless of input order', () => {
|
|
@@ -220,6 +227,23 @@ describe('readMutantCache', () => {
|
|
|
220
227
|
const result = await readMutantCache(tmpDir);
|
|
221
228
|
expect(result).toEqual({});
|
|
222
229
|
});
|
|
230
|
+
it('skips object-format entries without a status field', async () => {
|
|
231
|
+
const cache = {
|
|
232
|
+
'testsig:codesig:file.ts:1,0:flip': { someOtherField: 'foo' },
|
|
233
|
+
};
|
|
234
|
+
await fs.writeFile(path.join(tmpDir, '.mutineer-cache.json'), JSON.stringify(cache));
|
|
235
|
+
const result = await readMutantCache(tmpDir);
|
|
236
|
+
// Entry has no status, so it is skipped entirely
|
|
237
|
+
expect(result['testsig:codesig:file.ts:1,0:flip']).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
it('defaults status to skipped when status is null in object entry', async () => {
|
|
240
|
+
const cache = {
|
|
241
|
+
'testsig:codesig:file.ts:1,0:flip': { status: null },
|
|
242
|
+
};
|
|
243
|
+
await fs.writeFile(path.join(tmpDir, '.mutineer-cache.json'), JSON.stringify(cache));
|
|
244
|
+
const result = await readMutantCache(tmpDir);
|
|
245
|
+
expect(result['testsig:codesig:file.ts:1,0:flip'].status).toBe('skipped');
|
|
246
|
+
});
|
|
223
247
|
it('normalizes partial object entries with decoded fallbacks', async () => {
|
|
224
248
|
const cache = {
|
|
225
249
|
'testsig:codesig:file.ts:5,3:mut': {
|
|
@@ -274,6 +274,24 @@ describe('listChangedFiles', () => {
|
|
|
274
274
|
const result = listChangedFiles('/repo', { includeDeps: true });
|
|
275
275
|
expect(result).toContain('/repo/src/foo.ts');
|
|
276
276
|
});
|
|
277
|
+
it('handles readFileSync throwing during dep resolution', () => {
|
|
278
|
+
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
279
|
+
if (args.includes('--show-toplevel')) {
|
|
280
|
+
return { status: 0, stdout: '/repo\n' };
|
|
281
|
+
}
|
|
282
|
+
if (args.includes('main...HEAD')) {
|
|
283
|
+
return { status: 0, stdout: 'src/foo.ts\0' };
|
|
284
|
+
}
|
|
285
|
+
return { status: 0, stdout: '' };
|
|
286
|
+
});
|
|
287
|
+
// existsSync always returns true so we get past the existsSync check in resolveLocalDeps
|
|
288
|
+
existsSyncMock.mockReturnValue(true);
|
|
289
|
+
readFileSyncMock.mockImplementation(() => {
|
|
290
|
+
throw new Error('ENOENT');
|
|
291
|
+
});
|
|
292
|
+
const result = listChangedFiles('/repo', { includeDeps: true });
|
|
293
|
+
expect(result).toContain('/repo/src/foo.ts');
|
|
294
|
+
});
|
|
277
295
|
it('respects maxDepth option', () => {
|
|
278
296
|
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
279
297
|
if (args.includes('--show-toplevel')) {
|
|
@@ -292,6 +310,63 @@ describe('listChangedFiles', () => {
|
|
|
292
310
|
// maxDepth=0 means no recursion into deps
|
|
293
311
|
expect(result).toContain('/repo/src/foo.ts');
|
|
294
312
|
});
|
|
313
|
+
it('logs warning when no git repo found and quiet is not set', () => {
|
|
314
|
+
spawnSyncMock.mockReturnValue({ status: 1, stdout: '' });
|
|
315
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
316
|
+
const result = listChangedFiles('/not-a-repo');
|
|
317
|
+
expect(result).toEqual([]);
|
|
318
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
319
|
+
warnSpy.mockRestore();
|
|
320
|
+
});
|
|
321
|
+
it('handles circular dependencies without infinite recursion', () => {
|
|
322
|
+
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
323
|
+
if (args.includes('--show-toplevel'))
|
|
324
|
+
return { status: 0, stdout: '/repo\n' };
|
|
325
|
+
if (args.includes('main...HEAD'))
|
|
326
|
+
return { status: 0, stdout: 'src/a.ts\0' };
|
|
327
|
+
return { status: 0, stdout: '' };
|
|
328
|
+
});
|
|
329
|
+
readFileSyncMock.mockImplementation((p) => {
|
|
330
|
+
if (p === '/repo/src/a.ts')
|
|
331
|
+
return 'import { b } from "./b"';
|
|
332
|
+
if (p === '/repo/src/b.ts')
|
|
333
|
+
return 'import { a } from "./a"';
|
|
334
|
+
return '';
|
|
335
|
+
});
|
|
336
|
+
existsSyncMock.mockImplementation((p) => ['/repo/src/a.ts', '/repo/src/b.ts'].includes(p));
|
|
337
|
+
const result = listChangedFiles('/repo', { includeDeps: true, maxDepth: 3 });
|
|
338
|
+
expect(result).toContain('/repo/src/a.ts');
|
|
339
|
+
expect(result).toContain('/repo/src/b.ts');
|
|
340
|
+
});
|
|
341
|
+
it('skips test file dependencies', () => {
|
|
342
|
+
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
343
|
+
if (args.includes('--show-toplevel'))
|
|
344
|
+
return { status: 0, stdout: '/repo\n' };
|
|
345
|
+
if (args.includes('main...HEAD'))
|
|
346
|
+
return { status: 0, stdout: 'src/a.ts\0' };
|
|
347
|
+
return { status: 0, stdout: '' };
|
|
348
|
+
});
|
|
349
|
+
readFileSyncMock.mockReturnValue('import { x } from "./a.spec"');
|
|
350
|
+
existsSyncMock.mockImplementation((p) => ['/repo/src/a.ts', '/repo/src/a.spec.ts'].includes(p));
|
|
351
|
+
const result = listChangedFiles('/repo', { includeDeps: true });
|
|
352
|
+
expect(result).not.toContain('/repo/src/a.spec.ts');
|
|
353
|
+
});
|
|
354
|
+
it('handles absolute paths returned by git', () => {
|
|
355
|
+
// The code strips one leading slash via replace(/^\.?\//,'').
|
|
356
|
+
// A double-slash path like '//repo/src/abs.ts' still resolves as absolute after that strip.
|
|
357
|
+
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
358
|
+
if (args.includes('--show-toplevel')) {
|
|
359
|
+
return { status: 0, stdout: '/repo\n' };
|
|
360
|
+
}
|
|
361
|
+
if (args.includes('main...HEAD')) {
|
|
362
|
+
return { status: 0, stdout: '//repo/src/abs.ts\0' };
|
|
363
|
+
}
|
|
364
|
+
return { status: 0, stdout: '' };
|
|
365
|
+
});
|
|
366
|
+
existsSyncMock.mockImplementation((p) => p === '/repo/src/abs.ts');
|
|
367
|
+
const result = listChangedFiles('/repo');
|
|
368
|
+
expect(result).toContain('/repo/src/abs.ts');
|
|
369
|
+
});
|
|
295
370
|
it('only processes source files for dependency resolution', () => {
|
|
296
371
|
spawnSyncMock.mockImplementation((_cmd, args) => {
|
|
297
372
|
if (args.includes('--show-toplevel')) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -57,4 +57,53 @@ describe('loadMutineerConfig', () => {
|
|
|
57
57
|
await fs.writeFile(configFile, 'export default null');
|
|
58
58
|
await expect(loadMutineerConfig(tmpDir)).rejects.toThrow('does not export a valid configuration object');
|
|
59
59
|
});
|
|
60
|
+
it('loads a .ts config file via vite', async () => {
|
|
61
|
+
const configFile = path.join(tmpDir, 'mutineer.config.ts');
|
|
62
|
+
await fs.writeFile(configFile, 'export default { runner: "vitest" }\n');
|
|
63
|
+
const config = await loadMutineerConfig(tmpDir);
|
|
64
|
+
expect(config).toEqual({ runner: 'vitest' });
|
|
65
|
+
});
|
|
66
|
+
it('throws with helpful message when TypeScript config fails to load', async () => {
|
|
67
|
+
vi.doMock('vite', () => ({
|
|
68
|
+
loadConfigFromFile: vi
|
|
69
|
+
.fn()
|
|
70
|
+
.mockRejectedValue(new Error('vite not available')),
|
|
71
|
+
}));
|
|
72
|
+
vi.resetModules();
|
|
73
|
+
const { loadMutineerConfig: loadConfig } = await import('../config.js');
|
|
74
|
+
const configFile = path.join(tmpDir, 'mutineer.config.ts');
|
|
75
|
+
await fs.writeFile(configFile, 'export default { runner: "vitest" }');
|
|
76
|
+
await expect(loadConfig(tmpDir)).rejects.toThrow('Cannot load TypeScript config');
|
|
77
|
+
vi.doUnmock('vite');
|
|
78
|
+
});
|
|
79
|
+
it('loads a module with named exports only (no default export)', async () => {
|
|
80
|
+
const configFile = path.join(tmpDir, 'mutineer.config.mjs');
|
|
81
|
+
// No `export default` — exercises the `mod` fallback in loadModule
|
|
82
|
+
await fs.writeFile(configFile, 'export const runner = "vitest"');
|
|
83
|
+
const config = await loadMutineerConfig(tmpDir);
|
|
84
|
+
expect(config.runner).toBe('vitest');
|
|
85
|
+
});
|
|
86
|
+
it('returns empty config when Vite loadConfigFromFile returns null', async () => {
|
|
87
|
+
vi.doMock('vite', () => ({
|
|
88
|
+
loadConfigFromFile: vi.fn().mockResolvedValue(null),
|
|
89
|
+
}));
|
|
90
|
+
vi.resetModules();
|
|
91
|
+
const { loadMutineerConfig: loadConfig } = await import('../config.js');
|
|
92
|
+
const configFile = path.join(tmpDir, 'mutineer.config.ts');
|
|
93
|
+
await fs.writeFile(configFile, 'export default {}');
|
|
94
|
+
const config = await loadConfig(tmpDir);
|
|
95
|
+
expect(config).toEqual({});
|
|
96
|
+
vi.doUnmock('vite');
|
|
97
|
+
});
|
|
98
|
+
it('stringifies non-Error thrown by loadConfigFromFile', async () => {
|
|
99
|
+
vi.doMock('vite', () => ({
|
|
100
|
+
loadConfigFromFile: vi.fn().mockRejectedValue('plain string error'),
|
|
101
|
+
}));
|
|
102
|
+
vi.resetModules();
|
|
103
|
+
const { loadMutineerConfig: loadConfig } = await import('../config.js');
|
|
104
|
+
const configFile = path.join(tmpDir, 'mutineer.config.ts');
|
|
105
|
+
await fs.writeFile(configFile, 'export default {}');
|
|
106
|
+
await expect(loadConfig(tmpDir)).rejects.toThrow('plain string error');
|
|
107
|
+
vi.doUnmock('vite');
|
|
108
|
+
});
|
|
60
109
|
});
|
|
@@ -7,7 +7,7 @@ function makeOpts(overrides = {}) {
|
|
|
7
7
|
return {
|
|
8
8
|
configPath: undefined,
|
|
9
9
|
wantsChanged: false,
|
|
10
|
-
|
|
10
|
+
wantsChangedWithImports: false,
|
|
11
11
|
wantsFull: false,
|
|
12
12
|
wantsOnlyCoveredLines: false,
|
|
13
13
|
wantsPerTestCoverage: false,
|
|
@@ -112,6 +112,59 @@ describe('resolveCoverageConfig', () => {
|
|
|
112
112
|
expect(result.needsCoverageFromBaseline).toBe(true);
|
|
113
113
|
expect(result.enableCoverageForBaseline).toBe(true);
|
|
114
114
|
});
|
|
115
|
+
it('loads coverage data when coverageFilePath is provided', async () => {
|
|
116
|
+
const coverageJson = {
|
|
117
|
+
'/src/foo.ts': {
|
|
118
|
+
path: '/src/foo.ts',
|
|
119
|
+
statementMap: {
|
|
120
|
+
'0': { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
|
121
|
+
},
|
|
122
|
+
s: { '0': 1 },
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const covDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-cov-file-'));
|
|
126
|
+
const covFile = path.join(covDir, 'coverage-final.json');
|
|
127
|
+
await fs.writeFile(covFile, JSON.stringify(coverageJson));
|
|
128
|
+
try {
|
|
129
|
+
const opts = makeOpts({ coverageFilePath: covFile });
|
|
130
|
+
const result = await resolveCoverageConfig(opts, {}, makeAdapter(), []);
|
|
131
|
+
expect(result.coverageData).not.toBeNull();
|
|
132
|
+
expect(result.coverageData.coveredLines.size).toBeGreaterThan(0);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await fs.rm(covDir, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
it('logs warning when onlyCoveredLines + coverageData + no coverage provider', async () => {
|
|
139
|
+
const coverageJson = {
|
|
140
|
+
'/src/foo.ts': {
|
|
141
|
+
path: '/src/foo.ts',
|
|
142
|
+
statementMap: {
|
|
143
|
+
'0': { start: { line: 1, column: 0 }, end: { line: 1, column: 10 } },
|
|
144
|
+
},
|
|
145
|
+
s: { '0': 1 },
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const covDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-cov-warn-'));
|
|
149
|
+
const covFile = path.join(covDir, 'coverage-final.json');
|
|
150
|
+
await fs.writeFile(covFile, JSON.stringify(coverageJson));
|
|
151
|
+
try {
|
|
152
|
+
const opts = makeOpts({
|
|
153
|
+
wantsOnlyCoveredLines: true,
|
|
154
|
+
coverageFilePath: covFile,
|
|
155
|
+
});
|
|
156
|
+
const adapter = makeAdapter({
|
|
157
|
+
hasCoverageProvider: vi.fn().mockReturnValue(false),
|
|
158
|
+
});
|
|
159
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
160
|
+
await resolveCoverageConfig(opts, {}, adapter, []);
|
|
161
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('onlyCoveredLines'));
|
|
162
|
+
warnSpy.mockRestore();
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
await fs.rm(covDir, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
115
168
|
});
|
|
116
169
|
describe('loadCoverageAfterBaseline', () => {
|
|
117
170
|
let tmpDir;
|
|
@@ -179,4 +232,38 @@ describe('loadCoverageAfterBaseline', () => {
|
|
|
179
232
|
const result = await loadCoverageAfterBaseline(resolution, tmpDir);
|
|
180
233
|
expect(result.perTestCoverage).toBeNull();
|
|
181
234
|
});
|
|
235
|
+
it('loads per-test coverage when wantsPerTestCoverage is true', async () => {
|
|
236
|
+
const coverageDir = path.join(tmpDir, 'coverage');
|
|
237
|
+
await fs.mkdir(coverageDir, { recursive: true });
|
|
238
|
+
const testFile = '/test/foo.spec.ts';
|
|
239
|
+
const srcFile = '/src/foo.ts';
|
|
240
|
+
const perTestData = {
|
|
241
|
+
[testFile]: { [srcFile]: [1, 2, 3] },
|
|
242
|
+
};
|
|
243
|
+
await fs.writeFile(path.join(coverageDir, 'per-test-coverage.json'), JSON.stringify(perTestData));
|
|
244
|
+
const resolution = {
|
|
245
|
+
coverageData: null,
|
|
246
|
+
perTestCoverage: null,
|
|
247
|
+
enableCoverageForBaseline: true,
|
|
248
|
+
wantsPerTestCoverage: true,
|
|
249
|
+
needsCoverageFromBaseline: false,
|
|
250
|
+
};
|
|
251
|
+
const result = await loadCoverageAfterBaseline(resolution, tmpDir);
|
|
252
|
+
expect(result.perTestCoverage).not.toBeNull();
|
|
253
|
+
expect(result.perTestCoverage.size).toBeGreaterThan(0);
|
|
254
|
+
});
|
|
255
|
+
it('logs warning when per-test coverage data is not found', async () => {
|
|
256
|
+
const resolution = {
|
|
257
|
+
coverageData: null,
|
|
258
|
+
perTestCoverage: null,
|
|
259
|
+
enableCoverageForBaseline: true,
|
|
260
|
+
wantsPerTestCoverage: true,
|
|
261
|
+
needsCoverageFromBaseline: false,
|
|
262
|
+
};
|
|
263
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
264
|
+
const result = await loadCoverageAfterBaseline(resolution, tmpDir);
|
|
265
|
+
expect(result.perTestCoverage).toBeNull();
|
|
266
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Per-test coverage data not found'));
|
|
267
|
+
warnSpy.mockRestore();
|
|
268
|
+
});
|
|
182
269
|
});
|