jest-test-lineage-reporter 2.0.1 → 2.1.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 +252 -0
- package/bin/jest-lineage.js +20 -0
- package/package.json +14 -5
- package/src/MutationTester.js +1154 -0
- package/src/__tests__/assertion-test.test.ts +59 -0
- package/src/__tests__/calculator.test.ts +30 -0
- package/src/__tests__/depth-example.test.ts +237 -0
- package/src/__tests__/gc-pressure-example.test.ts +169 -0
- package/src/__tests__/performance-example.test.ts +83 -0
- package/src/__tests__/quality-example.test.ts +122 -0
- package/src/__tests__/survived-mutations-example.test.ts +32 -0
- package/src/__tests__/truly-weak-example.test.ts +90 -0
- package/src/__tests__/weak-test-example.test.ts +222 -0
- package/src/babel-plugin-mutation-tester.js +402 -0
- package/src/calculator.ts +12 -0
- package/src/cli/commands/analyze.js +91 -0
- package/src/cli/commands/mutate.js +89 -0
- package/src/cli/commands/query.js +107 -0
- package/src/cli/commands/report.js +65 -0
- package/src/cli/commands/test.js +56 -0
- package/src/cli/index.js +89 -0
- package/src/cli/utils/config-loader.js +114 -0
- package/src/cli/utils/data-loader.js +118 -0
- package/src/cli/utils/jest-runner.js +105 -0
- package/src/cli/utils/output-formatter.js +126 -0
- package/src/depth-example.ts +66 -0
- package/src/gc-pressure-example.ts +158 -0
- package/src/global.d.ts +7 -0
- package/src/mcp/server.js +469 -0
- package/src/performance-example.ts +82 -0
- package/src/quality-example.ts +79 -0
- package/src/survived-mutations-example.ts +19 -0
- package/src/truly-weak-example.ts +37 -0
- package/src/weak-test-example.ts +91 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Babel Plugin for Mutation Testing
|
|
3
|
+
* Creates targeted mutations for specific lines based on lineage tracking data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a mutation testing plugin that targets a specific line and mutation type
|
|
8
|
+
* @param {number} targetLine - Line number to mutate
|
|
9
|
+
* @param {string} mutationType - Type of mutation to apply
|
|
10
|
+
* @param {Object} config - Mutation configuration
|
|
11
|
+
*/
|
|
12
|
+
function createMutationPlugin(targetLine, mutationType, config = {}) {
|
|
13
|
+
return function({ types: t }, options = {}) {
|
|
14
|
+
return {
|
|
15
|
+
name: 'mutation-tester',
|
|
16
|
+
visitor: {
|
|
17
|
+
Program: {
|
|
18
|
+
enter(path, state) {
|
|
19
|
+
state.filename = state.file.opts.filename;
|
|
20
|
+
state.shouldMutate = false;
|
|
21
|
+
state.mutationApplied = false;
|
|
22
|
+
state.targetLine = targetLine;
|
|
23
|
+
state.mutationType = mutationType;
|
|
24
|
+
state.config = { ...config, ...options };
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// ARITHMETIC OPERATORS: +, -, *, /, %
|
|
29
|
+
BinaryExpression(path, state) {
|
|
30
|
+
if (!shouldApplyMutation(path, state, 'arithmetic')) return;
|
|
31
|
+
|
|
32
|
+
const operator = path.node.operator;
|
|
33
|
+
const mutations = {
|
|
34
|
+
'+': '-', // Addition to Subtraction
|
|
35
|
+
'-': '+', // Subtraction to Addition
|
|
36
|
+
'*': '/', // Multiplication to Division
|
|
37
|
+
'/': '*', // Division to Multiplication
|
|
38
|
+
'%': '*', // Modulo to Multiplication
|
|
39
|
+
'==': '!=', // Equality to Inequality
|
|
40
|
+
'!=': '==', // Inequality to Equality
|
|
41
|
+
'===': '!==', // Strict equality to strict inequality
|
|
42
|
+
'!==': '===', // Strict inequality to strict equality
|
|
43
|
+
'<': '>=', // Less than to Greater/Equal
|
|
44
|
+
'>': '<=', // Greater than to Less/Equal
|
|
45
|
+
'<=': '>', // Less/Equal to Greater
|
|
46
|
+
'>=': '<' // Greater/Equal to Less
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (mutations[operator]) {
|
|
50
|
+
path.node.operator = mutations[operator];
|
|
51
|
+
state.mutationApplied = true;
|
|
52
|
+
logMutation(state, `${operator} → ${mutations[operator]}`);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// LOGICAL OPERATORS: &&, ||
|
|
57
|
+
LogicalExpression(path, state) {
|
|
58
|
+
if (!shouldApplyMutation(path, state, 'logical')) return;
|
|
59
|
+
|
|
60
|
+
const operator = path.node.operator;
|
|
61
|
+
const mutations = {
|
|
62
|
+
'&&': '||', // AND to OR
|
|
63
|
+
'||': '&&' // OR to AND
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (mutations[operator]) {
|
|
67
|
+
path.node.operator = mutations[operator];
|
|
68
|
+
state.mutationApplied = true;
|
|
69
|
+
logMutation(state, `${operator} → ${mutations[operator]}`);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// UNARY OPERATORS: !, ++, --
|
|
74
|
+
UnaryExpression(path, state) {
|
|
75
|
+
if (!shouldApplyMutation(path, state, 'logical')) return;
|
|
76
|
+
|
|
77
|
+
if (path.node.operator === '!') {
|
|
78
|
+
// Remove negation: !condition → condition
|
|
79
|
+
path.replaceWith(path.node.argument);
|
|
80
|
+
state.mutationApplied = true;
|
|
81
|
+
logMutation(state, '! → (removed)');
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// UPDATE EXPRESSIONS: ++, --
|
|
86
|
+
UpdateExpression(path, state) {
|
|
87
|
+
if (!shouldApplyMutation(path, state, 'increments')) return;
|
|
88
|
+
|
|
89
|
+
const operator = path.node.operator;
|
|
90
|
+
const mutations = {
|
|
91
|
+
'++': '--', // Increment to Decrement
|
|
92
|
+
'--': '++' // Decrement to Increment
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (mutations[operator]) {
|
|
96
|
+
path.node.operator = mutations[operator];
|
|
97
|
+
state.mutationApplied = true;
|
|
98
|
+
logMutation(state, `${operator} → ${mutations[operator]}`);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// ASSIGNMENT OPERATORS: =, +=, -=, etc.
|
|
103
|
+
AssignmentExpression(path, state) {
|
|
104
|
+
if (!shouldApplyMutation(path, state, 'assignment')) return;
|
|
105
|
+
|
|
106
|
+
const operator = path.node.operator;
|
|
107
|
+
const mutations = {
|
|
108
|
+
'+=': '-=', // Add-assign to Subtract-assign
|
|
109
|
+
'-=': '+=', // Subtract-assign to Add-assign
|
|
110
|
+
'*=': '/=', // Multiply-assign to Divide-assign
|
|
111
|
+
'/=': '*=', // Divide-assign to Multiply-assign
|
|
112
|
+
'%=': '*=' // Modulo-assign to Multiply-assign
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (mutations[operator]) {
|
|
116
|
+
path.node.operator = mutations[operator];
|
|
117
|
+
state.mutationApplied = true;
|
|
118
|
+
logMutation(state, `${operator} → ${mutations[operator]}`);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// CONDITIONAL STATEMENTS: if, while, for
|
|
123
|
+
IfStatement(path, state) {
|
|
124
|
+
if (!shouldApplyMutation(path, state, 'conditional')) return;
|
|
125
|
+
|
|
126
|
+
// Negate condition: if (x > 0) → if (!(x > 0))
|
|
127
|
+
const { types: t } = require('@babel/core');
|
|
128
|
+
path.node.test = t.unaryExpression('!', t.parenthesizedExpression(path.node.test));
|
|
129
|
+
state.mutationApplied = true;
|
|
130
|
+
logMutation(state, 'if condition → !(condition)');
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// WHILE LOOPS
|
|
134
|
+
WhileStatement(path, state) {
|
|
135
|
+
if (!shouldApplyMutation(path, state, 'conditional')) return;
|
|
136
|
+
|
|
137
|
+
const { types: t } = require('@babel/core');
|
|
138
|
+
path.node.test = t.unaryExpression('!', t.parenthesizedExpression(path.node.test));
|
|
139
|
+
state.mutationApplied = true;
|
|
140
|
+
logMutation(state, 'while condition → !(condition)');
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// FOR LOOPS
|
|
144
|
+
ForStatement(path, state) {
|
|
145
|
+
if (!shouldApplyMutation(path, state, 'conditional')) return;
|
|
146
|
+
|
|
147
|
+
// Only mutate if there's a test condition
|
|
148
|
+
if (path.node.test) {
|
|
149
|
+
const { types: t } = require('@babel/core');
|
|
150
|
+
path.node.test = t.unaryExpression('!', t.parenthesizedExpression(path.node.test));
|
|
151
|
+
state.mutationApplied = true;
|
|
152
|
+
logMutation(state, 'for condition → !(condition)');
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// RETURN STATEMENTS
|
|
157
|
+
ReturnStatement(path, state) {
|
|
158
|
+
if (!shouldApplyMutation(path, state, 'returns')) return;
|
|
159
|
+
|
|
160
|
+
const { types: t } = require('@babel/core');
|
|
161
|
+
|
|
162
|
+
if (path.node.argument) {
|
|
163
|
+
// Apply type-safe mutations based on the argument type
|
|
164
|
+
if (t.isNumericLiteral(path.node.argument)) {
|
|
165
|
+
// For numbers, change to 0 instead of null to avoid type errors
|
|
166
|
+
const originalValue = path.node.argument.value;
|
|
167
|
+
const newValue = originalValue === 0 ? 1 : 0;
|
|
168
|
+
path.node.argument = t.numericLiteral(newValue);
|
|
169
|
+
state.mutationApplied = true;
|
|
170
|
+
logMutation(state, `return ${originalValue} → return ${newValue}`);
|
|
171
|
+
} else if (t.isBooleanLiteral(path.node.argument)) {
|
|
172
|
+
// For booleans, flip the value
|
|
173
|
+
const originalValue = path.node.argument.value;
|
|
174
|
+
path.node.argument = t.booleanLiteral(!originalValue);
|
|
175
|
+
state.mutationApplied = true;
|
|
176
|
+
logMutation(state, `return ${originalValue} → return ${!originalValue}`);
|
|
177
|
+
} else if (t.isStringLiteral(path.node.argument)) {
|
|
178
|
+
// For strings, change to empty string
|
|
179
|
+
const originalValue = path.node.argument.value;
|
|
180
|
+
path.node.argument = t.stringLiteral("");
|
|
181
|
+
state.mutationApplied = true;
|
|
182
|
+
logMutation(state, `return "${originalValue}" → return ""`);
|
|
183
|
+
} else if (t.isBinaryExpression(path.node.argument)) {
|
|
184
|
+
// For binary expressions like a + b, try to mutate the operator or operands
|
|
185
|
+
const expr = path.node.argument;
|
|
186
|
+
if (expr.operator === '+') {
|
|
187
|
+
// Change + to - for arithmetic expressions
|
|
188
|
+
expr.operator = '-';
|
|
189
|
+
state.mutationApplied = true;
|
|
190
|
+
logMutation(state, `return a + b → return a - b`);
|
|
191
|
+
} else if (expr.operator === '-') {
|
|
192
|
+
// Change - to + for arithmetic expressions
|
|
193
|
+
expr.operator = '+';
|
|
194
|
+
state.mutationApplied = true;
|
|
195
|
+
logMutation(state, `return a - b → return a + b`);
|
|
196
|
+
} else if (expr.operator === '*') {
|
|
197
|
+
// Change * to / for arithmetic expressions
|
|
198
|
+
expr.operator = '/';
|
|
199
|
+
state.mutationApplied = true;
|
|
200
|
+
logMutation(state, `return a * b → return a / b`);
|
|
201
|
+
} else if (expr.operator === '/') {
|
|
202
|
+
// Change / to * for arithmetic expressions
|
|
203
|
+
expr.operator = '*';
|
|
204
|
+
state.mutationApplied = true;
|
|
205
|
+
logMutation(state, `return a / b → return a * b`);
|
|
206
|
+
} else {
|
|
207
|
+
// For other binary expressions, skip to avoid breaking module loading
|
|
208
|
+
logMutation(state, `return expression → skipped (complex binary expression)`);
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
// For other complex expressions, skip mutation to avoid breaking module loading
|
|
212
|
+
// This is more conservative but safer
|
|
213
|
+
logMutation(state, `return expression → skipped (complex expression)`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
// LITERAL VALUES: numbers, booleans, strings
|
|
219
|
+
Literal(path, state) {
|
|
220
|
+
if (!shouldApplyMutation(path, state, 'literals')) return;
|
|
221
|
+
|
|
222
|
+
const value = path.node.value;
|
|
223
|
+
|
|
224
|
+
if (typeof value === 'number') {
|
|
225
|
+
// Mutate numbers: 5 → 0, 0 → 1, negative → positive
|
|
226
|
+
const newValue = value === 0 ? 1 : (value > 0 ? 0 : Math.abs(value));
|
|
227
|
+
path.node.value = newValue;
|
|
228
|
+
state.mutationApplied = true;
|
|
229
|
+
logMutation(state, `${value} → ${newValue}`);
|
|
230
|
+
} else if (typeof value === 'boolean') {
|
|
231
|
+
// Flip booleans: true → false, false → true
|
|
232
|
+
path.node.value = !value;
|
|
233
|
+
state.mutationApplied = true;
|
|
234
|
+
logMutation(state, `${value} → ${!value}`);
|
|
235
|
+
} else if (typeof value === 'string' && value.length > 0) {
|
|
236
|
+
// Empty strings: "hello" → ""
|
|
237
|
+
path.node.value = "";
|
|
238
|
+
state.mutationApplied = true;
|
|
239
|
+
logMutation(state, `"${value}" → ""`);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// NUMERIC LITERALS (for newer Babel versions)
|
|
244
|
+
NumericLiteral(path, state) {
|
|
245
|
+
if (!shouldApplyMutation(path, state, 'literals')) return;
|
|
246
|
+
|
|
247
|
+
const value = path.node.value;
|
|
248
|
+
const newValue = value === 0 ? 1 : (value > 0 ? 0 : Math.abs(value));
|
|
249
|
+
path.node.value = newValue;
|
|
250
|
+
state.mutationApplied = true;
|
|
251
|
+
logMutation(state, `${value} → ${newValue}`);
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
// BOOLEAN LITERALS
|
|
255
|
+
BooleanLiteral(path, state) {
|
|
256
|
+
if (!shouldApplyMutation(path, state, 'literals')) return;
|
|
257
|
+
|
|
258
|
+
const value = path.node.value;
|
|
259
|
+
path.node.value = !value;
|
|
260
|
+
state.mutationApplied = true;
|
|
261
|
+
logMutation(state, `${value} → ${!value}`);
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
// STRING LITERALS
|
|
265
|
+
StringLiteral(path, state) {
|
|
266
|
+
if (!shouldApplyMutation(path, state, 'literals')) return;
|
|
267
|
+
|
|
268
|
+
const value = path.node.value;
|
|
269
|
+
if (value.length > 0) {
|
|
270
|
+
path.node.value = "";
|
|
271
|
+
state.mutationApplied = true;
|
|
272
|
+
logMutation(state, `"${value}" → ""`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Determines if a mutation should be applied to the current node
|
|
282
|
+
*/
|
|
283
|
+
function shouldApplyMutation(path, state, operatorType) {
|
|
284
|
+
const lineNumber = path.node.loc?.start.line;
|
|
285
|
+
|
|
286
|
+
// Check if this is the target line
|
|
287
|
+
if (lineNumber !== state.targetLine) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check if this operator type is enabled
|
|
292
|
+
if (state.config.mutationOperators && !state.config.mutationOperators[operatorType]) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check if we've already applied a mutation (one per line)
|
|
297
|
+
if (state.mutationApplied) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Logs mutation information
|
|
306
|
+
*/
|
|
307
|
+
function logMutation(state, description) {
|
|
308
|
+
if (state.config.enableDebugLogging) {
|
|
309
|
+
console.log(`🧬 Mutation applied at ${state.filename}:${state.targetLine} - ${description}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Gets all possible mutations for a given line of code by analyzing the AST
|
|
315
|
+
*/
|
|
316
|
+
function getPossibleMutations(filePath, lineNumber, sourceCode) {
|
|
317
|
+
try {
|
|
318
|
+
const babel = require('@babel/core');
|
|
319
|
+
const fs = require('fs');
|
|
320
|
+
|
|
321
|
+
// Read the full file content
|
|
322
|
+
const fullFileContent = fs.readFileSync(filePath, 'utf8');
|
|
323
|
+
const lines = fullFileContent.split('\n');
|
|
324
|
+
const targetLine = lines[lineNumber - 1];
|
|
325
|
+
|
|
326
|
+
if (!targetLine) return [];
|
|
327
|
+
|
|
328
|
+
const possibleMutations = [];
|
|
329
|
+
|
|
330
|
+
// Parse the entire file to get proper AST context
|
|
331
|
+
const ast = babel.parseSync(fullFileContent, {
|
|
332
|
+
filename: filePath,
|
|
333
|
+
parserOpts: {
|
|
334
|
+
sourceType: "module",
|
|
335
|
+
allowImportExportEverywhere: true,
|
|
336
|
+
plugins: ["typescript", "jsx"],
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Traverse the AST to find nodes on the target line
|
|
341
|
+
babel.traverse(ast, {
|
|
342
|
+
enter(path) {
|
|
343
|
+
const nodeLineNumber = path.node.loc?.start.line;
|
|
344
|
+
if (nodeLineNumber !== lineNumber) return;
|
|
345
|
+
|
|
346
|
+
// Check what types of mutations are possible based on the AST nodes
|
|
347
|
+
if (path.isBinaryExpression()) {
|
|
348
|
+
const operator = path.node.operator;
|
|
349
|
+
if (['+', '-', '*', '/', '%'].includes(operator)) {
|
|
350
|
+
possibleMutations.push('arithmetic');
|
|
351
|
+
}
|
|
352
|
+
if (['==', '!=', '===', '!==', '<', '>', '<=', '>='].includes(operator)) {
|
|
353
|
+
possibleMutations.push('comparison');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (path.isLogicalExpression()) {
|
|
358
|
+
possibleMutations.push('logical');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (path.isUnaryExpression() && path.node.operator === '!') {
|
|
362
|
+
possibleMutations.push('logical');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (path.isUpdateExpression()) {
|
|
366
|
+
possibleMutations.push('increments');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (path.isAssignmentExpression() && path.node.operator !== '=') {
|
|
370
|
+
possibleMutations.push('assignment');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Only add conditional mutations for actual conditional statements
|
|
374
|
+
if (path.isIfStatement() || path.isWhileStatement() ||
|
|
375
|
+
(path.isForStatement() && path.node.test)) {
|
|
376
|
+
possibleMutations.push('conditional');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (path.isReturnStatement()) {
|
|
380
|
+
possibleMutations.push('returns');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (path.isLiteral() || path.isNumericLiteral() ||
|
|
384
|
+
path.isBooleanLiteral() || path.isStringLiteral()) {
|
|
385
|
+
possibleMutations.push('literals');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Remove duplicates
|
|
391
|
+
return [...new Set(possibleMutations)];
|
|
392
|
+
|
|
393
|
+
} catch (error) {
|
|
394
|
+
// If AST analysis fails, return empty array to skip this line
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = {
|
|
400
|
+
createMutationPlugin,
|
|
401
|
+
getPossibleMutations
|
|
402
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function add(a: number, b: number): number {
|
|
2
|
+
return a - b;
|
|
3
|
+
}
|
|
4
|
+
export function subtract(a: number, b: number): number {
|
|
5
|
+
if (!(!!(a < b))) {
|
|
6
|
+
return b - a;
|
|
7
|
+
}
|
|
8
|
+
return a - b;
|
|
9
|
+
}
|
|
10
|
+
export function multiply(a: number, b: number): number {
|
|
11
|
+
return a * b;
|
|
12
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Command
|
|
3
|
+
* Full workflow: test + mutation + report
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const testCommand = require('./test');
|
|
7
|
+
const mutateCommand = require('./mutate');
|
|
8
|
+
const reportCommand = require('./report');
|
|
9
|
+
const { lineageDataExists } = require('../utils/data-loader');
|
|
10
|
+
const { section, success, error, info } = require('../utils/output-formatter');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
|
|
13
|
+
async function analyzeCommand(options) {
|
|
14
|
+
try {
|
|
15
|
+
section('🚀 Full Analysis Workflow');
|
|
16
|
+
|
|
17
|
+
// Step 1: Run tests (unless skip-tests is specified)
|
|
18
|
+
if (!options.skipTests) {
|
|
19
|
+
info('Step 1: Running tests with lineage tracking...\n');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await testCommand([], {
|
|
23
|
+
lineage: options.lineage !== false,
|
|
24
|
+
performance: options.performance !== false,
|
|
25
|
+
quality: options.quality !== false,
|
|
26
|
+
config: options.config,
|
|
27
|
+
quiet: false
|
|
28
|
+
});
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// testCommand handles its own exit, this catch is for safety
|
|
31
|
+
error('Tests failed. Analysis stopped.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
// Verify lineage data exists
|
|
36
|
+
if (!lineageDataExists()) {
|
|
37
|
+
error('Lineage data not found. Remove --skip-tests or run jest-lineage test first.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
info('Step 1: Skipped (using existing lineage data)\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Step 2: Run mutation testing (unless skip-mutation is specified)
|
|
44
|
+
if (!options.skipMutation) {
|
|
45
|
+
console.log(); // Add spacing
|
|
46
|
+
info('Step 2: Running mutation testing...\n');
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await mutateCommand({
|
|
50
|
+
data: '.jest-lineage-data.json',
|
|
51
|
+
threshold: options.threshold,
|
|
52
|
+
timeout: options.timeout || '5000',
|
|
53
|
+
verbose: false
|
|
54
|
+
});
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// Don't fail the entire workflow if mutation testing fails
|
|
57
|
+
error('Mutation testing failed, but continuing with report generation...');
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
info('Step 2: Skipped mutation testing\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Step 3: Generate report
|
|
64
|
+
console.log(); // Add spacing
|
|
65
|
+
info('Step 3: Generating HTML report...\n');
|
|
66
|
+
|
|
67
|
+
await reportCommand({
|
|
68
|
+
data: '.jest-lineage-data.json',
|
|
69
|
+
output: options.output,
|
|
70
|
+
open: options.open
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Success summary
|
|
74
|
+
console.log();
|
|
75
|
+
section('✨ Analysis Complete');
|
|
76
|
+
success('Full analysis workflow completed successfully!');
|
|
77
|
+
console.log(chalk.gray(`\nGenerated files:`));
|
|
78
|
+
console.log(chalk.gray(` • .jest-lineage-data.json (lineage data)`));
|
|
79
|
+
console.log(chalk.gray(` • ${options.output} (HTML report)`));
|
|
80
|
+
|
|
81
|
+
process.exit(0);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
error(`Analysis workflow failed: ${err.message}`);
|
|
84
|
+
if (process.env.JEST_LINEAGE_DEBUG === 'true') {
|
|
85
|
+
console.error(err.stack);
|
|
86
|
+
}
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = analyzeCommand;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutate Command
|
|
3
|
+
* Run mutation testing on existing lineage data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MutationTester = require('../../MutationTester');
|
|
7
|
+
const { loadLineageData, processLineageDataForMutation } = require('../utils/data-loader');
|
|
8
|
+
const { loadFullConfig } = require('../utils/config-loader');
|
|
9
|
+
const { spinner, printMutationSummary, error, success, info, printLineageDataSummary } = require('../utils/output-formatter');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
async function mutateCommand(options) {
|
|
13
|
+
let mutationTester = null;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Load configuration
|
|
17
|
+
const config = loadFullConfig(options);
|
|
18
|
+
|
|
19
|
+
// Load lineage data
|
|
20
|
+
info(`Loading lineage data from: ${chalk.yellow(options.data)}`);
|
|
21
|
+
const rawData = loadLineageData(options.data);
|
|
22
|
+
|
|
23
|
+
if (!options.verbose) {
|
|
24
|
+
printLineageDataSummary(rawData);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Process data for mutation testing
|
|
28
|
+
const lineageData = processLineageDataForMutation(rawData);
|
|
29
|
+
const fileCount = Object.keys(lineageData).length;
|
|
30
|
+
const lineCount = Object.values(lineageData).reduce(
|
|
31
|
+
(sum, lines) => sum + Object.keys(lines).length,
|
|
32
|
+
0
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (fileCount === 0 || lineCount === 0) {
|
|
36
|
+
error('No coverage data found in lineage file. Run tests first.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
info(`Processing ${chalk.cyan(fileCount)} files with ${chalk.cyan(lineCount)} covered lines\n`);
|
|
41
|
+
|
|
42
|
+
// Create mutation tester
|
|
43
|
+
mutationTester = new MutationTester(config);
|
|
44
|
+
mutationTester.setLineageData(lineageData);
|
|
45
|
+
|
|
46
|
+
// Run mutation testing
|
|
47
|
+
const spin = spinner('Running mutation testing...');
|
|
48
|
+
if (!options.verbose) {
|
|
49
|
+
spin.start();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const results = await mutationTester.runMutationTesting();
|
|
53
|
+
|
|
54
|
+
if (!options.verbose) {
|
|
55
|
+
spin.succeed('Mutation testing completed!');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Print results
|
|
59
|
+
printMutationSummary(results);
|
|
60
|
+
|
|
61
|
+
// Check threshold
|
|
62
|
+
const threshold = parseInt(options.threshold) || 80;
|
|
63
|
+
if (results.mutationScore < threshold) {
|
|
64
|
+
console.log(chalk.yellow(`\n⚠️ Mutation score ${results.mutationScore.toFixed(1)}% is below threshold ${threshold}%`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
} else {
|
|
67
|
+
success(`Mutation score meets threshold (${threshold}%)`);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
error(`Mutation testing failed: ${err.message}`);
|
|
72
|
+
if (options.verbose) {
|
|
73
|
+
console.error(err.stack);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Cleanup on error
|
|
77
|
+
if (mutationTester) {
|
|
78
|
+
try {
|
|
79
|
+
await mutationTester.cleanup();
|
|
80
|
+
} catch (cleanupErr) {
|
|
81
|
+
// Ignore cleanup errors
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = mutateCommand;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Command
|
|
3
|
+
* Query test coverage for specific files/lines
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { loadLineageData, processLineageDataForMutation } = require('../utils/data-loader');
|
|
7
|
+
const { section, error, formatPath } = require('../utils/output-formatter');
|
|
8
|
+
const Table = require('cli-table3');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
async function queryCommand(file, line, options) {
|
|
13
|
+
try {
|
|
14
|
+
// Load lineage data
|
|
15
|
+
const rawData = loadLineageData(options.data);
|
|
16
|
+
const lineageData = processLineageDataForMutation(rawData);
|
|
17
|
+
|
|
18
|
+
// Normalize file path
|
|
19
|
+
const normalizedFile = path.normalize(file);
|
|
20
|
+
|
|
21
|
+
// Find matching files
|
|
22
|
+
const matchingFiles = Object.keys(lineageData).filter(f =>
|
|
23
|
+
f.includes(normalizedFile) || normalizedFile.includes(path.basename(f))
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (matchingFiles.length === 0) {
|
|
27
|
+
error(`No coverage data found for file: ${chalk.yellow(file)}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If multiple matches, show them
|
|
32
|
+
if (matchingFiles.length > 1) {
|
|
33
|
+
console.log(chalk.yellow(`Found multiple matching files:`));
|
|
34
|
+
matchingFiles.forEach(f => console.log(` - ${formatPath(f)}`));
|
|
35
|
+
console.log(chalk.yellow(`\nUsing first match: ${matchingFiles[0]}\n`));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const targetFile = matchingFiles[0];
|
|
39
|
+
const fileCoverage = lineageData[targetFile];
|
|
40
|
+
|
|
41
|
+
// If line specified, show coverage for that line
|
|
42
|
+
if (line) {
|
|
43
|
+
const lineNumber = line.toString();
|
|
44
|
+
if (!fileCoverage[lineNumber]) {
|
|
45
|
+
error(`No coverage data for line ${chalk.yellow(lineNumber)} in ${chalk.yellow(file)}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
section(`📍 Coverage for ${formatPath(targetFile)}:${lineNumber}`);
|
|
50
|
+
|
|
51
|
+
const tests = fileCoverage[lineNumber];
|
|
52
|
+
const table = new Table({
|
|
53
|
+
head: [chalk.cyan('Test Name'), chalk.cyan('File'), chalk.cyan('Exec Count')],
|
|
54
|
+
colWidths: [50, 30, 12]
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
tests.forEach(test => {
|
|
58
|
+
table.push([
|
|
59
|
+
test.testName,
|
|
60
|
+
path.basename(test.testFile),
|
|
61
|
+
test.executionCount
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log(table.toString());
|
|
66
|
+
console.log(chalk.gray(`\nTotal: ${tests.length} test(s) cover this line`));
|
|
67
|
+
} else {
|
|
68
|
+
// Show coverage for entire file
|
|
69
|
+
section(`📁 Coverage for ${formatPath(targetFile)}`);
|
|
70
|
+
|
|
71
|
+
const lines = Object.keys(fileCoverage).sort((a, b) => parseInt(a) - parseInt(b));
|
|
72
|
+
const totalTests = new Set(
|
|
73
|
+
lines.flatMap(lineNum => fileCoverage[lineNum].map(t => t.testName))
|
|
74
|
+
).size;
|
|
75
|
+
|
|
76
|
+
console.log(chalk.gray(`Lines covered: ${lines.length}`));
|
|
77
|
+
console.log(chalk.gray(`Tests covering file: ${totalTests}\n`));
|
|
78
|
+
|
|
79
|
+
// Show sample of lines
|
|
80
|
+
const sampleSize = 10;
|
|
81
|
+
const sampled = lines.slice(0, sampleSize);
|
|
82
|
+
|
|
83
|
+
sampled.forEach(lineNum => {
|
|
84
|
+
const tests = fileCoverage[lineNum];
|
|
85
|
+
console.log(chalk.cyan(`Line ${lineNum}:`));
|
|
86
|
+
tests.slice(0, 3).forEach(test => {
|
|
87
|
+
console.log(` ${chalk.gray('•')} ${test.testName} ${chalk.gray(`(${path.basename(test.testFile)})`)}`);
|
|
88
|
+
});
|
|
89
|
+
if (tests.length > 3) {
|
|
90
|
+
console.log(chalk.gray(` ... and ${tests.length - 3} more test(s)`));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (lines.length > sampleSize) {
|
|
95
|
+
console.log(chalk.gray(`\n... and ${lines.length - sampleSize} more lines`));
|
|
96
|
+
console.log(chalk.yellow(`\nTip: Specify a line number to see details: jest-lineage query ${file} <line>`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
process.exit(0);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
error(`Query failed: ${err.message}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = queryCommand;
|