jest-test-lineage-reporter 2.0.1 โ 2.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.
- package/package.json +3 -1
- package/src/MutationTester.js +1154 -0
- package/src/babel-plugin-mutation-tester.js +402 -0
|
@@ -0,0 +1,1154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation Testing Orchestrator
|
|
3
|
+
* Uses lineage tracking data to run targeted mutation tests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { spawn } = require("child_process");
|
|
9
|
+
const { createMutationPlugin } = require("./babel-plugin-mutation-tester");
|
|
10
|
+
|
|
11
|
+
class MutationTester {
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.lineageData = null;
|
|
15
|
+
this.mutationResults = new Map();
|
|
16
|
+
this.tempFiles = new Set();
|
|
17
|
+
this.debugMutationFiles = new Set(); // Track debug mutation files
|
|
18
|
+
this.originalFileContents = new Map(); // Store original file contents for restoration
|
|
19
|
+
|
|
20
|
+
// Create debug directory if debug mode is enabled
|
|
21
|
+
if (this.config.debugMutations) {
|
|
22
|
+
this.setupDebugDirectory();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Set up cleanup handlers for process interruption
|
|
26
|
+
this.setupCleanupHandlers();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Setup debug directory for mutation files
|
|
31
|
+
*/
|
|
32
|
+
setupDebugDirectory() {
|
|
33
|
+
const debugDir = this.config.debugMutationDir || "./mutations-debug";
|
|
34
|
+
if (!fs.existsSync(debugDir)) {
|
|
35
|
+
fs.mkdirSync(debugDir, { recursive: true });
|
|
36
|
+
console.log(`๐ Created debug mutation directory: ${debugDir}`);
|
|
37
|
+
} else {
|
|
38
|
+
// Clean existing debug files
|
|
39
|
+
const files = fs.readdirSync(debugDir);
|
|
40
|
+
files.forEach((file) => {
|
|
41
|
+
if (file.endsWith(".mutation.js") || file.endsWith(".mutation.ts")) {
|
|
42
|
+
fs.unlinkSync(path.join(debugDir, file));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
console.log(`๐งน Cleaned existing debug mutation files in: ${debugDir}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Setup cleanup handlers for process interruption
|
|
51
|
+
*/
|
|
52
|
+
setupCleanupHandlers() {
|
|
53
|
+
// Handle process interruption (Ctrl+C)
|
|
54
|
+
process.on("SIGINT", () => {
|
|
55
|
+
console.log("\n๐ Mutation testing interrupted. Cleaning up...");
|
|
56
|
+
this.emergencyCleanup();
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Handle process termination
|
|
61
|
+
process.on("SIGTERM", () => {
|
|
62
|
+
console.log("\n๐ Mutation testing terminated. Cleaning up...");
|
|
63
|
+
this.emergencyCleanup();
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Handle uncaught exceptions
|
|
68
|
+
process.on("uncaughtException", (error) => {
|
|
69
|
+
console.error(
|
|
70
|
+
"\nโ Uncaught exception during mutation testing:",
|
|
71
|
+
error.message
|
|
72
|
+
);
|
|
73
|
+
this.emergencyCleanup();
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load lineage tracking data from the previous test run
|
|
80
|
+
*/
|
|
81
|
+
async loadLineageData() {
|
|
82
|
+
try {
|
|
83
|
+
const lineageFile = path.join(process.cwd(), ".jest-lineage-data.json");
|
|
84
|
+
if (fs.existsSync(lineageFile)) {
|
|
85
|
+
const data = JSON.parse(fs.readFileSync(lineageFile, "utf8"));
|
|
86
|
+
this.lineageData = this.processLineageData(data);
|
|
87
|
+
console.log(
|
|
88
|
+
`๐ Loaded lineage data for ${
|
|
89
|
+
Object.keys(this.lineageData).length
|
|
90
|
+
} files`
|
|
91
|
+
);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error("โ Failed to load lineage data:", error.message);
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Set lineage data directly (used when data is passed from TestCoverageReporter)
|
|
102
|
+
*/
|
|
103
|
+
setLineageData(lineageData) {
|
|
104
|
+
this.lineageData = lineageData;
|
|
105
|
+
console.log(
|
|
106
|
+
`๐ Set lineage data for ${Object.keys(this.lineageData).length} files`
|
|
107
|
+
);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Process raw lineage data into a more usable format
|
|
113
|
+
*/
|
|
114
|
+
processLineageData(rawData) {
|
|
115
|
+
const processed = {};
|
|
116
|
+
|
|
117
|
+
if (rawData.tests) {
|
|
118
|
+
console.log(
|
|
119
|
+
`๐ Processing ${rawData.tests.length} tests for mutation testing...`
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
rawData.tests.forEach((test, testIndex) => {
|
|
123
|
+
if (test.coverage) {
|
|
124
|
+
const coverageKeys = Object.keys(test.coverage);
|
|
125
|
+
console.log(
|
|
126
|
+
` Test ${testIndex + 1}: "${test.name}" has ${
|
|
127
|
+
coverageKeys.length
|
|
128
|
+
} coverage entries`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
coverageKeys.forEach((lineKey) => {
|
|
132
|
+
// Parse line key: "file.ts:lineNumber"
|
|
133
|
+
const [filePath, lineNumber, ...suffixes] = lineKey.split(":");
|
|
134
|
+
|
|
135
|
+
// Skip metadata entries (depth, performance, meta) - only process basic line coverage
|
|
136
|
+
if (!lineNumber || suffixes.length > 0) {
|
|
137
|
+
// console.log(` Skipping metadata entry: ${lineKey}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(
|
|
142
|
+
` Processing coverage: ${lineKey} = ${test.coverage[lineKey]}`
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (!processed[filePath]) {
|
|
146
|
+
processed[filePath] = {};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!processed[filePath][lineNumber]) {
|
|
150
|
+
processed[filePath][lineNumber] = [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
processed[filePath][lineNumber].push({
|
|
154
|
+
testName: test.name,
|
|
155
|
+
testType: test.type,
|
|
156
|
+
testFile: test.testFile,
|
|
157
|
+
executionCount: test.coverage[lineKey],
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
console.log(
|
|
162
|
+
` Test ${testIndex + 1}: "${test.name}" has no coverage data`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(
|
|
169
|
+
`๐ฏ Processed lineage data for ${Object.keys(processed).length} files:`
|
|
170
|
+
);
|
|
171
|
+
Object.keys(processed).forEach((filePath) => {
|
|
172
|
+
const lineCount = Object.keys(processed[filePath]).length;
|
|
173
|
+
console.log(` ${filePath}: ${lineCount} lines`);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return processed;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Run mutation testing for all covered lines
|
|
181
|
+
*/
|
|
182
|
+
async runMutationTesting() {
|
|
183
|
+
if (!this.lineageData) {
|
|
184
|
+
console.error("โ No lineage data available. Run normal tests first.");
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log("๐งฌ Starting mutation testing...");
|
|
189
|
+
|
|
190
|
+
// Calculate total mutations for progress tracking
|
|
191
|
+
const totalFiles = Object.keys(this.lineageData).length;
|
|
192
|
+
let totalMutationsCount = 0;
|
|
193
|
+
for (const [filePath, lines] of Object.entries(this.lineageData)) {
|
|
194
|
+
for (const [lineNumber, tests] of Object.entries(lines)) {
|
|
195
|
+
const sourceCode = this.getSourceCodeLine(
|
|
196
|
+
filePath,
|
|
197
|
+
parseInt(lineNumber)
|
|
198
|
+
);
|
|
199
|
+
const mutationTypes = this.getPossibleMutationTypes(
|
|
200
|
+
sourceCode,
|
|
201
|
+
filePath,
|
|
202
|
+
parseInt(lineNumber)
|
|
203
|
+
);
|
|
204
|
+
totalMutationsCount += mutationTypes.length;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(
|
|
209
|
+
`๐ Planning to test ${totalMutationsCount} mutations across ${totalFiles} files`
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const results = {
|
|
213
|
+
totalMutations: 0,
|
|
214
|
+
killedMutations: 0,
|
|
215
|
+
survivedMutations: 0,
|
|
216
|
+
timeoutMutations: 0,
|
|
217
|
+
errorMutations: 0,
|
|
218
|
+
mutationScore: 0,
|
|
219
|
+
fileResults: {},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
let currentFileIndex = 0;
|
|
223
|
+
let currentMutationIndex = 0;
|
|
224
|
+
|
|
225
|
+
for (const [filePath, lines] of Object.entries(this.lineageData)) {
|
|
226
|
+
currentFileIndex++;
|
|
227
|
+
console.log(
|
|
228
|
+
`\n๐ฌ Testing mutations in ${filePath} (${currentFileIndex}/${totalFiles})...`
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const fileResults = await this.testFileLines(
|
|
232
|
+
filePath,
|
|
233
|
+
lines,
|
|
234
|
+
currentMutationIndex,
|
|
235
|
+
totalMutationsCount
|
|
236
|
+
);
|
|
237
|
+
results.fileResults[filePath] = fileResults;
|
|
238
|
+
|
|
239
|
+
results.totalMutations += fileResults.totalMutations;
|
|
240
|
+
results.killedMutations += fileResults.killedMutations;
|
|
241
|
+
results.survivedMutations += fileResults.survivedMutations;
|
|
242
|
+
results.timeoutMutations += fileResults.timeoutMutations;
|
|
243
|
+
results.errorMutations += fileResults.errorMutations;
|
|
244
|
+
|
|
245
|
+
currentMutationIndex += fileResults.totalMutations;
|
|
246
|
+
|
|
247
|
+
// Log file completion summary
|
|
248
|
+
const fileName = filePath.split("/").pop();
|
|
249
|
+
const fileScore =
|
|
250
|
+
fileResults.totalMutations > 0
|
|
251
|
+
? Math.round(
|
|
252
|
+
(fileResults.killedMutations / fileResults.totalMutations) * 100
|
|
253
|
+
)
|
|
254
|
+
: 0;
|
|
255
|
+
console.log(
|
|
256
|
+
`โ
${fileName}: ${fileResults.totalMutations} mutations, ${fileResults.killedMutations} killed, ${fileResults.survivedMutations} survived (${fileScore}% score)`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Calculate mutation score
|
|
261
|
+
const validMutations = results.totalMutations - results.errorMutations;
|
|
262
|
+
results.mutationScore =
|
|
263
|
+
validMutations > 0
|
|
264
|
+
? Math.round((results.killedMutations / validMutations) * 100)
|
|
265
|
+
: 0;
|
|
266
|
+
|
|
267
|
+
this.printMutationSummary(results);
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Test mutations for all lines in a specific file
|
|
273
|
+
*/
|
|
274
|
+
async testFileLines(filePath, lines, startMutationIndex, totalMutations) {
|
|
275
|
+
const fileResults = {
|
|
276
|
+
totalMutations: 0,
|
|
277
|
+
killedMutations: 0,
|
|
278
|
+
survivedMutations: 0,
|
|
279
|
+
timeoutMutations: 0,
|
|
280
|
+
errorMutations: 0,
|
|
281
|
+
lineResults: {},
|
|
282
|
+
mutations: [], // Collect all mutations for this file
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
let currentMutationIndex = startMutationIndex;
|
|
286
|
+
|
|
287
|
+
for (const [lineNumber, tests] of Object.entries(lines)) {
|
|
288
|
+
const lineResults = await this.testLineMutations(
|
|
289
|
+
filePath,
|
|
290
|
+
parseInt(lineNumber),
|
|
291
|
+
tests,
|
|
292
|
+
currentMutationIndex,
|
|
293
|
+
totalMutations
|
|
294
|
+
);
|
|
295
|
+
fileResults.lineResults[lineNumber] = lineResults;
|
|
296
|
+
|
|
297
|
+
fileResults.totalMutations += lineResults.totalMutations;
|
|
298
|
+
fileResults.killedMutations += lineResults.killedMutations;
|
|
299
|
+
fileResults.survivedMutations += lineResults.survivedMutations;
|
|
300
|
+
fileResults.timeoutMutations += lineResults.timeoutMutations;
|
|
301
|
+
fileResults.errorMutations += lineResults.errorMutations;
|
|
302
|
+
|
|
303
|
+
// Add all mutations from this line to the file's mutations array
|
|
304
|
+
fileResults.mutations.push(...lineResults.mutations);
|
|
305
|
+
|
|
306
|
+
currentMutationIndex += lineResults.totalMutations;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return fileResults;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Test mutations for a specific line
|
|
314
|
+
*/
|
|
315
|
+
async testLineMutations(
|
|
316
|
+
filePath,
|
|
317
|
+
lineNumber,
|
|
318
|
+
tests,
|
|
319
|
+
startMutationIndex,
|
|
320
|
+
totalMutations
|
|
321
|
+
) {
|
|
322
|
+
const lineResults = {
|
|
323
|
+
totalMutations: 0,
|
|
324
|
+
killedMutations: 0,
|
|
325
|
+
survivedMutations: 0,
|
|
326
|
+
timeoutMutations: 0,
|
|
327
|
+
errorMutations: 0,
|
|
328
|
+
mutations: [],
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Get the source code line to determine possible mutations
|
|
332
|
+
const sourceCode = this.getSourceCodeLine(filePath, lineNumber);
|
|
333
|
+
const mutationTypes = this.getPossibleMutationTypes(
|
|
334
|
+
sourceCode,
|
|
335
|
+
filePath,
|
|
336
|
+
lineNumber
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
let currentMutationIndex = startMutationIndex;
|
|
340
|
+
|
|
341
|
+
for (const mutationType of mutationTypes) {
|
|
342
|
+
currentMutationIndex++;
|
|
343
|
+
|
|
344
|
+
const mutationResult = await this.testSingleMutation(
|
|
345
|
+
filePath,
|
|
346
|
+
lineNumber,
|
|
347
|
+
mutationType,
|
|
348
|
+
tests,
|
|
349
|
+
currentMutationIndex,
|
|
350
|
+
totalMutations
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Skip mutations that couldn't be applied (null result)
|
|
354
|
+
if (mutationResult === null) {
|
|
355
|
+
currentMutationIndex--; // Don't count skipped mutations
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
lineResults.mutations.push(mutationResult);
|
|
360
|
+
lineResults.totalMutations++;
|
|
361
|
+
|
|
362
|
+
switch (mutationResult.status) {
|
|
363
|
+
case "killed":
|
|
364
|
+
lineResults.killedMutations++;
|
|
365
|
+
break;
|
|
366
|
+
case "survived":
|
|
367
|
+
lineResults.survivedMutations++;
|
|
368
|
+
break;
|
|
369
|
+
case "timeout":
|
|
370
|
+
lineResults.timeoutMutations++;
|
|
371
|
+
break;
|
|
372
|
+
case "error":
|
|
373
|
+
lineResults.errorMutations++;
|
|
374
|
+
break;
|
|
375
|
+
case "debug":
|
|
376
|
+
// Debug mutations don't count towards kill/survive stats
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return lineResults;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Test a single mutation
|
|
386
|
+
*/
|
|
387
|
+
async testSingleMutation(
|
|
388
|
+
filePath,
|
|
389
|
+
lineNumber,
|
|
390
|
+
mutationType,
|
|
391
|
+
tests,
|
|
392
|
+
currentMutationIndex,
|
|
393
|
+
totalMutations
|
|
394
|
+
) {
|
|
395
|
+
const mutationId = `${filePath}:${lineNumber}:${mutationType}`;
|
|
396
|
+
|
|
397
|
+
// Log progress with counter and percentage
|
|
398
|
+
const fileName = filePath.split("/").pop();
|
|
399
|
+
const percentage =
|
|
400
|
+
totalMutations > 0
|
|
401
|
+
? Math.round((currentMutationIndex / totalMutations) * 100)
|
|
402
|
+
: 0;
|
|
403
|
+
console.log(
|
|
404
|
+
`๐ง Instrumenting: ${filePath} (${currentMutationIndex}/${totalMutations} - ${percentage}%) [${fileName}:${lineNumber} ${mutationType}]`
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
// Create mutated version of the file
|
|
409
|
+
const mutatedFilePath = await this.createMutatedFile(
|
|
410
|
+
filePath,
|
|
411
|
+
lineNumber,
|
|
412
|
+
mutationType
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// Check if the mutation actually changed the code
|
|
416
|
+
const originalCodeLine = this.getSourceCodeLine(filePath, lineNumber);
|
|
417
|
+
const mutatedFileContent = fs.readFileSync(filePath, "utf8");
|
|
418
|
+
const mutatedLines = mutatedFileContent.split("\n");
|
|
419
|
+
const mutatedCodeLine = mutatedLines[lineNumber - 1] || "";
|
|
420
|
+
|
|
421
|
+
if (originalCodeLine.trim() === mutatedCodeLine.trim()) {
|
|
422
|
+
// Mutation couldn't be applied - this should have been caught during validation
|
|
423
|
+
// Silently skip this mutation and restore the file
|
|
424
|
+
this.restoreFile(filePath);
|
|
425
|
+
return null; // Return null to indicate this mutation should be skipped
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let testResult;
|
|
429
|
+
let status;
|
|
430
|
+
let testFiles = [];
|
|
431
|
+
|
|
432
|
+
if (this.config.debugMutations) {
|
|
433
|
+
// Debug mode: Don't run tests, just create mutation files for inspection
|
|
434
|
+
testResult = {
|
|
435
|
+
success: null,
|
|
436
|
+
executionTime: 0,
|
|
437
|
+
output: "Debug mode: mutation file created for manual inspection",
|
|
438
|
+
error: null,
|
|
439
|
+
};
|
|
440
|
+
status = "debug";
|
|
441
|
+
const testInfo = tests.map((test) =>
|
|
442
|
+
this.getTestFileFromTestName(test.testName)
|
|
443
|
+
);
|
|
444
|
+
testFiles = testInfo.map(info => info.testFile);
|
|
445
|
+
console.log(`๐ Debug mutation created: ${mutatedFilePath}`);
|
|
446
|
+
// In debug mode, files are preserved, so no cleanup needed
|
|
447
|
+
} else {
|
|
448
|
+
// Normal mode: Run tests and check if mutation is killed
|
|
449
|
+
const testInfo = tests.map((test) =>
|
|
450
|
+
this.getTestFileFromTestName(test.testName)
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// Extract unique test files and collect test names
|
|
454
|
+
const uniqueTestFiles = [...new Set(testInfo.map(info => info.testFile))];
|
|
455
|
+
const testNames = testInfo.map(info => info.testName);
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
testResult = await this.runTargetedTests(uniqueTestFiles, testNames);
|
|
459
|
+
status = testResult.success ? "survived" : "killed";
|
|
460
|
+
} catch (testError) {
|
|
461
|
+
console.error(
|
|
462
|
+
`โ Error running tests for mutation ${mutationId}:`,
|
|
463
|
+
testError.message
|
|
464
|
+
);
|
|
465
|
+
testResult = {
|
|
466
|
+
success: false,
|
|
467
|
+
executionTime: 0,
|
|
468
|
+
output: "",
|
|
469
|
+
error: testError.message,
|
|
470
|
+
jestArgs: testError.jestArgs || [],
|
|
471
|
+
};
|
|
472
|
+
status = "error";
|
|
473
|
+
} finally {
|
|
474
|
+
// Always clean up, even if tests failed
|
|
475
|
+
await this.cleanupMutatedFile(mutatedFilePath);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Debug logging for troubleshooting - ALWAYS show for now to debug the issue
|
|
480
|
+
console.log(`๐ Debug: ${mutationId}`);
|
|
481
|
+
console.log(` Test success: ${testResult.success}`);
|
|
482
|
+
console.log(` Status: ${status}`);
|
|
483
|
+
console.log(` Error: ${testResult.error || "none"}`);
|
|
484
|
+
if (testResult.output && testResult.output.length > 0) {
|
|
485
|
+
console.log(` Output snippet: ${testResult.output}...`);
|
|
486
|
+
}
|
|
487
|
+
if (testResult.jestArgs) {
|
|
488
|
+
console.log(` Jest args: ${testResult.jestArgs.join(" ")}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Get original and mutated code for display
|
|
492
|
+
const originalCode = this.getSourceCodeLine(filePath, lineNumber);
|
|
493
|
+
const mutatedCode = this.getMutatedCodePreview(
|
|
494
|
+
originalCode,
|
|
495
|
+
mutationType
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Check if mutation actually changed the code
|
|
499
|
+
if (originalCode.trim() === mutatedCode.trim()) {
|
|
500
|
+
console.log(
|
|
501
|
+
`โ ๏ธ Mutation failed to change code at ${filePath}:${lineNumber} (${mutationType})`
|
|
502
|
+
);
|
|
503
|
+
console.log(` Original: ${originalCode.trim()}`);
|
|
504
|
+
console.log(` Expected mutation type: ${mutationType}`);
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
id: mutationId,
|
|
508
|
+
filePath,
|
|
509
|
+
line: lineNumber,
|
|
510
|
+
lineNumber,
|
|
511
|
+
mutationType,
|
|
512
|
+
mutatorName: mutationType,
|
|
513
|
+
type: mutationType,
|
|
514
|
+
status: "error",
|
|
515
|
+
original: originalCode.trim(),
|
|
516
|
+
replacement: "MUTATION_FAILED",
|
|
517
|
+
testsRun: 0,
|
|
518
|
+
killedBy: [],
|
|
519
|
+
executionTime: 0,
|
|
520
|
+
error: "Mutation failed to change the code - no mutation was applied",
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Determine which tests killed this mutation (if any)
|
|
525
|
+
const killedBy =
|
|
526
|
+
status === "killed" ? this.getKillingTests(testResult, tests) : [];
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
id: mutationId,
|
|
530
|
+
filePath,
|
|
531
|
+
line: lineNumber,
|
|
532
|
+
lineNumber,
|
|
533
|
+
mutationType,
|
|
534
|
+
mutatorName: mutationType,
|
|
535
|
+
type: mutationType,
|
|
536
|
+
status,
|
|
537
|
+
original: originalCode.trim(),
|
|
538
|
+
replacement: mutatedCode.trim(),
|
|
539
|
+
testsRun: testFiles.length,
|
|
540
|
+
killedBy,
|
|
541
|
+
executionTime: testResult.executionTime,
|
|
542
|
+
error: testResult.error,
|
|
543
|
+
};
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error(`โ Error during mutation ${mutationId}:`, error.message);
|
|
546
|
+
if (this.config.enableDebugLogging) {
|
|
547
|
+
console.error(`Full error stack:`, error.stack);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Ensure file is restored even if an error occurs
|
|
551
|
+
try {
|
|
552
|
+
this.restoreFile(filePath);
|
|
553
|
+
} catch (restoreError) {
|
|
554
|
+
console.error(
|
|
555
|
+
`โ Failed to restore file after error:`,
|
|
556
|
+
restoreError.message
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
id: mutationId,
|
|
562
|
+
filePath,
|
|
563
|
+
lineNumber,
|
|
564
|
+
mutationType,
|
|
565
|
+
status: "error",
|
|
566
|
+
error: error.message,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Create a mutated version of a file using Babel transformer
|
|
573
|
+
*/
|
|
574
|
+
async createMutatedFile(filePath, lineNumber, mutationType) {
|
|
575
|
+
const fs = require("fs");
|
|
576
|
+
|
|
577
|
+
// Read original file
|
|
578
|
+
const originalCode = fs.readFileSync(filePath, "utf8");
|
|
579
|
+
|
|
580
|
+
// Store original content for emergency restoration
|
|
581
|
+
if (!this.originalFileContents.has(filePath)) {
|
|
582
|
+
this.originalFileContents.set(filePath, originalCode);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Use Babel transformer for AST-based mutations
|
|
586
|
+
const mutatedCode = this.applyMutationWithBabel(
|
|
587
|
+
originalCode,
|
|
588
|
+
lineNumber,
|
|
589
|
+
mutationType,
|
|
590
|
+
filePath
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
if (!mutatedCode) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Failed to apply mutation ${mutationType} at line ${lineNumber} in ${filePath}`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (this.config.debugMutations) {
|
|
600
|
+
// Debug mode: Create separate mutation files instead of overwriting originals
|
|
601
|
+
return this.createDebugMutationFile(
|
|
602
|
+
filePath,
|
|
603
|
+
lineNumber,
|
|
604
|
+
mutationType,
|
|
605
|
+
mutatedCode
|
|
606
|
+
);
|
|
607
|
+
} else {
|
|
608
|
+
// Normal mode: Temporarily replace original file
|
|
609
|
+
const backupPath = `${filePath}.backup`;
|
|
610
|
+
fs.writeFileSync(backupPath, originalCode);
|
|
611
|
+
fs.writeFileSync(filePath, mutatedCode);
|
|
612
|
+
|
|
613
|
+
this.tempFiles.add(filePath);
|
|
614
|
+
return filePath;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Create a debug mutation file (separate from original)
|
|
620
|
+
*/
|
|
621
|
+
createDebugMutationFile(
|
|
622
|
+
originalFilePath,
|
|
623
|
+
lineNumber,
|
|
624
|
+
mutationType,
|
|
625
|
+
mutatedCode
|
|
626
|
+
) {
|
|
627
|
+
const debugDir = this.config.debugMutationDir || "./mutations-debug";
|
|
628
|
+
const fileName = path.basename(originalFilePath);
|
|
629
|
+
const fileExt = path.extname(fileName);
|
|
630
|
+
const baseName = path.basename(fileName, fileExt);
|
|
631
|
+
|
|
632
|
+
// Create a unique filename for this mutation
|
|
633
|
+
const mutationFileName = `${baseName}_L${lineNumber}_${mutationType}.mutation${fileExt}`;
|
|
634
|
+
const mutationFilePath = path.join(debugDir, mutationFileName);
|
|
635
|
+
|
|
636
|
+
// Write the mutated code to the debug file
|
|
637
|
+
fs.writeFileSync(mutationFilePath, mutatedCode);
|
|
638
|
+
|
|
639
|
+
// Also create a metadata file with mutation details
|
|
640
|
+
const metadataFileName = `${baseName}_L${lineNumber}_${mutationType}.metadata.json`;
|
|
641
|
+
const metadataFilePath = path.join(debugDir, metadataFileName);
|
|
642
|
+
const metadata = {
|
|
643
|
+
originalFile: originalFilePath,
|
|
644
|
+
lineNumber: lineNumber,
|
|
645
|
+
mutationType: mutationType,
|
|
646
|
+
mutationFile: mutationFilePath,
|
|
647
|
+
timestamp: new Date().toISOString(),
|
|
648
|
+
originalLine: this.getSourceCodeLine(originalFilePath, lineNumber),
|
|
649
|
+
};
|
|
650
|
+
fs.writeFileSync(metadataFilePath, JSON.stringify(metadata, null, 2));
|
|
651
|
+
|
|
652
|
+
this.debugMutationFiles.add(mutationFilePath);
|
|
653
|
+
this.debugMutationFiles.add(metadataFilePath);
|
|
654
|
+
|
|
655
|
+
console.log(`๐ Created debug mutation file: ${mutationFileName}`);
|
|
656
|
+
return mutationFilePath;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Apply mutation using Babel transformer (AST-based approach)
|
|
661
|
+
*/
|
|
662
|
+
applyMutationWithBabel(code, lineNumber, mutationType, filePath) {
|
|
663
|
+
const babel = require("@babel/core");
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
// Create mutation plugin with specific line and mutation type
|
|
667
|
+
const mutationPlugin = createMutationPlugin(lineNumber, mutationType);
|
|
668
|
+
|
|
669
|
+
// Transform code using Babel with ONLY the mutation plugin
|
|
670
|
+
// Do NOT include lineage tracking or other plugins during mutation testing
|
|
671
|
+
const result = babel.transformSync(code, {
|
|
672
|
+
plugins: [mutationPlugin],
|
|
673
|
+
filename: filePath,
|
|
674
|
+
parserOpts: {
|
|
675
|
+
sourceType: "module",
|
|
676
|
+
allowImportExportEverywhere: true,
|
|
677
|
+
plugins: ["typescript", "jsx"],
|
|
678
|
+
},
|
|
679
|
+
// Explicitly disable all other transformations
|
|
680
|
+
presets: [],
|
|
681
|
+
// Ensure no other plugins are loaded from babel.config.js
|
|
682
|
+
babelrc: false,
|
|
683
|
+
configFile: false,
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
return result?.code || null;
|
|
687
|
+
} catch (error) {
|
|
688
|
+
console.error(
|
|
689
|
+
`Babel transformation error for ${filePath}:${lineNumber}:`,
|
|
690
|
+
error.message
|
|
691
|
+
);
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Run targeted tests for specific test files and test names
|
|
698
|
+
*/
|
|
699
|
+
async runTargetedTests(testFiles, testNames = null) {
|
|
700
|
+
return new Promise((resolve) => {
|
|
701
|
+
const startTime = Date.now();
|
|
702
|
+
|
|
703
|
+
// Build Jest command to run only specific test files and optionally specific test names
|
|
704
|
+
const jestArgs = [
|
|
705
|
+
"--testPathPatterns=" + testFiles.join("|"),
|
|
706
|
+
"--no-coverage",
|
|
707
|
+
"--bail", // Stop on first failure
|
|
708
|
+
"--no-cache", // Avoid cache issues with mutated files
|
|
709
|
+
"--forceExit", // Ensure Jest exits cleanly
|
|
710
|
+
"--runInBand", // Run tests in the main thread to avoid IPC issues
|
|
711
|
+
];
|
|
712
|
+
|
|
713
|
+
// If specific test names are provided, add testNamePattern to run only those tests
|
|
714
|
+
if (testNames && testNames.length > 0) {
|
|
715
|
+
// Escape special regex characters in test names and join with OR operator
|
|
716
|
+
const escapedTestNames = testNames.map(name =>
|
|
717
|
+
name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
718
|
+
);
|
|
719
|
+
const testNamePattern = `(${escapedTestNames.join('|')})`;
|
|
720
|
+
jestArgs.push(`--testNamePattern=${testNamePattern}`);
|
|
721
|
+
console.log(`๐ฏ Running specific tests: ${testNames.join(', ')}`);
|
|
722
|
+
} else {
|
|
723
|
+
console.log(`๐ Running all tests in files: ${testFiles.join(', ')}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const jest = spawn("jest", [...jestArgs], {
|
|
727
|
+
stdio: "pipe",
|
|
728
|
+
timeout: this.config.mutationTimeout || 5000,
|
|
729
|
+
env: {
|
|
730
|
+
...process.env,
|
|
731
|
+
NODE_ENV: "test",
|
|
732
|
+
JEST_LINEAGE_MUTATION: "false", // Disable mutation testing mode to allow normal test execution
|
|
733
|
+
JEST_LINEAGE_MUTATION_TESTING: "false", // Disable mutation testing during mutation testing
|
|
734
|
+
JEST_LINEAGE_ENABLED: "false", // Disable all lineage tracking
|
|
735
|
+
JEST_LINEAGE_TRACKING: "false", // Disable lineage tracking
|
|
736
|
+
JEST_LINEAGE_PERFORMANCE: "false", // Disable performance tracking
|
|
737
|
+
JEST_LINEAGE_QUALITY: "false", // Disable quality tracking
|
|
738
|
+
JEST_LINEAGE_MERGE: "false", // Ensure no merging with existing data
|
|
739
|
+
TS_NODE_TRANSPILE_ONLY: "true", // Disable TypeScript type checking
|
|
740
|
+
TS_NODE_TYPE_CHECK: "false", // Disable TypeScript type checking
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
let output = "";
|
|
745
|
+
jest.stdout.on("data", (data) => {
|
|
746
|
+
output += data.toString();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
jest.stderr.on("data", (data) => {
|
|
750
|
+
output += data.toString();
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
jest.on("close", (code) => {
|
|
754
|
+
const executionTime = Date.now() - startTime;
|
|
755
|
+
resolve({
|
|
756
|
+
success: code === 0,
|
|
757
|
+
executionTime,
|
|
758
|
+
output,
|
|
759
|
+
error: code !== 0 ? `Jest exited with code ${code}` : null,
|
|
760
|
+
jestArgs,
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
jest.on("error", (error) => {
|
|
765
|
+
resolve({
|
|
766
|
+
success: false,
|
|
767
|
+
executionTime: Date.now() - startTime,
|
|
768
|
+
error: error.message,
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Restore a file to its original state (synchronous)
|
|
776
|
+
*/
|
|
777
|
+
restoreFile(filePath) {
|
|
778
|
+
if (this.config.debugMutations) {
|
|
779
|
+
// In debug mode, don't restore original files
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
try {
|
|
784
|
+
const backupPath = `${filePath}.backup`;
|
|
785
|
+
if (fs.existsSync(backupPath)) {
|
|
786
|
+
// Restore from backup file
|
|
787
|
+
fs.writeFileSync(filePath, fs.readFileSync(backupPath, "utf8"));
|
|
788
|
+
fs.unlinkSync(backupPath);
|
|
789
|
+
console.log(`โ
Restored ${filePath} from backup`);
|
|
790
|
+
} else if (this.originalFileContents.has(filePath)) {
|
|
791
|
+
// Fallback: restore from stored original content
|
|
792
|
+
fs.writeFileSync(filePath, this.originalFileContents.get(filePath));
|
|
793
|
+
console.log(
|
|
794
|
+
`โ
Restored ${filePath} from memory (backup file missing)`
|
|
795
|
+
);
|
|
796
|
+
} else {
|
|
797
|
+
console.error(
|
|
798
|
+
`โ Cannot restore ${filePath}: no backup or stored content found`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
802
|
+
console.error(`โ Error restoring ${filePath}:`, error.message);
|
|
803
|
+
|
|
804
|
+
// Try fallback restoration from stored content
|
|
805
|
+
if (this.originalFileContents.has(filePath)) {
|
|
806
|
+
try {
|
|
807
|
+
fs.writeFileSync(filePath, this.originalFileContents.get(filePath));
|
|
808
|
+
console.log(`โ
Fallback restoration successful for ${filePath}`);
|
|
809
|
+
} catch (fallbackError) {
|
|
810
|
+
console.error(
|
|
811
|
+
`โ Fallback restoration failed for ${filePath}:`,
|
|
812
|
+
fallbackError.message
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
this.tempFiles.delete(filePath);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Clean up mutated file by restoring original
|
|
823
|
+
*/
|
|
824
|
+
async cleanupMutatedFile(filePath) {
|
|
825
|
+
this.restoreFile(filePath);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Get source code for a specific line from the original (unmutated) file
|
|
830
|
+
*/
|
|
831
|
+
getSourceCodeLine(filePath, lineNumber) {
|
|
832
|
+
try {
|
|
833
|
+
// Use stored original content if available (during mutation testing)
|
|
834
|
+
let sourceCode;
|
|
835
|
+
if (this.originalFileContents.has(filePath)) {
|
|
836
|
+
sourceCode = this.originalFileContents.get(filePath);
|
|
837
|
+
} else {
|
|
838
|
+
// Fallback to reading from disk (for initial analysis)
|
|
839
|
+
sourceCode = fs.readFileSync(filePath, "utf8");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const lines = sourceCode.split("\n");
|
|
843
|
+
return lines[lineNumber - 1] || "";
|
|
844
|
+
} catch (error) {
|
|
845
|
+
return "";
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Determine possible mutation types for a line of code using AST analysis
|
|
851
|
+
*/
|
|
852
|
+
getPossibleMutationTypes(sourceCode, filePath, lineNumber) {
|
|
853
|
+
if (!sourceCode || sourceCode.trim() === "") {
|
|
854
|
+
return [];
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
try {
|
|
858
|
+
const {
|
|
859
|
+
getPossibleMutations,
|
|
860
|
+
} = require("./babel-plugin-mutation-tester");
|
|
861
|
+
return getPossibleMutations(filePath, lineNumber, sourceCode);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
// If AST analysis fails, return empty array to skip this line
|
|
864
|
+
return [];
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Test if a specific mutation type can be applied to a line
|
|
870
|
+
*/
|
|
871
|
+
canApplyMutation(sourceCode, filePath, lineNumber, mutationType) {
|
|
872
|
+
try {
|
|
873
|
+
const babel = require("@babel/core");
|
|
874
|
+
const {
|
|
875
|
+
createMutationPlugin,
|
|
876
|
+
} = require("./babel-plugin-mutation-tester");
|
|
877
|
+
const fs = require("fs");
|
|
878
|
+
|
|
879
|
+
// Read the full file content for proper AST parsing
|
|
880
|
+
const fullFileContent = fs.readFileSync(filePath, "utf8");
|
|
881
|
+
|
|
882
|
+
// Create a test mutation plugin
|
|
883
|
+
const mutationPlugin = createMutationPlugin(lineNumber, mutationType);
|
|
884
|
+
|
|
885
|
+
// Try to transform the full file
|
|
886
|
+
const result = babel.transformSync(fullFileContent, {
|
|
887
|
+
plugins: [mutationPlugin],
|
|
888
|
+
filename: filePath,
|
|
889
|
+
parserOpts: {
|
|
890
|
+
sourceType: "module",
|
|
891
|
+
allowImportExportEverywhere: true,
|
|
892
|
+
plugins: ["typescript", "jsx"],
|
|
893
|
+
},
|
|
894
|
+
babelrc: false,
|
|
895
|
+
configFile: false,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// Check if the mutation was actually applied by comparing the specific line
|
|
899
|
+
if (result?.code) {
|
|
900
|
+
const originalLines = fullFileContent.split("\n");
|
|
901
|
+
const mutatedLines = result.code.split("\n");
|
|
902
|
+
const originalLine = originalLines[lineNumber - 1] || "";
|
|
903
|
+
const mutatedLine = mutatedLines[lineNumber - 1] || "";
|
|
904
|
+
return originalLine.trim() !== mutatedLine.trim();
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return false;
|
|
908
|
+
} catch (error) {
|
|
909
|
+
// If transformation fails, this mutation type can't be applied
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Extract test file path and test name from test name using lineage data
|
|
916
|
+
*/
|
|
917
|
+
getTestFileFromTestName(testName) {
|
|
918
|
+
// Search through lineage data to find the test file for this test name
|
|
919
|
+
for (const [, lines] of Object.entries(this.lineageData)) {
|
|
920
|
+
for (const [, tests] of Object.entries(lines)) {
|
|
921
|
+
for (const test of tests) {
|
|
922
|
+
if (test.testName === testName && test.testFile) {
|
|
923
|
+
return {
|
|
924
|
+
testFile: test.testFile,
|
|
925
|
+
testName: test.testName
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Fallback to calculator test if not found (for backward compatibility)
|
|
933
|
+
console.warn(
|
|
934
|
+
`โ ๏ธ Could not find test file for test "${testName}", using fallback`
|
|
935
|
+
);
|
|
936
|
+
return {
|
|
937
|
+
testFile: "src/__tests__/calculator.test.ts",
|
|
938
|
+
testName: testName
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Print mutation testing summary
|
|
944
|
+
*/
|
|
945
|
+
printMutationSummary(results) {
|
|
946
|
+
if (this.config.debugMutations) {
|
|
947
|
+
console.log("\n๐ Debug Mutation Testing Results:");
|
|
948
|
+
console.log("โ".repeat(50));
|
|
949
|
+
console.log(`๐ Total Mutations Created: ${results.totalMutations}`);
|
|
950
|
+
console.log(
|
|
951
|
+
`๐ Debug files saved to: ${
|
|
952
|
+
this.config.debugMutationDir || "./mutations-debug"
|
|
953
|
+
}`
|
|
954
|
+
);
|
|
955
|
+
console.log(
|
|
956
|
+
`๐ง Use these files to manually inspect mutations and debug issues`
|
|
957
|
+
);
|
|
958
|
+
console.log(
|
|
959
|
+
`๐ก To run actual mutation testing, set debugMutations: false in config`
|
|
960
|
+
);
|
|
961
|
+
} else {
|
|
962
|
+
console.log("\n๐งฌ Mutation Testing Results:");
|
|
963
|
+
console.log("โ".repeat(50));
|
|
964
|
+
console.log(`๐ Total Mutations: ${results.totalMutations}`);
|
|
965
|
+
console.log(`โ
Killed: ${results.killedMutations}`);
|
|
966
|
+
console.log(`๐ด Survived: ${results.survivedMutations}`);
|
|
967
|
+
console.log(`โฐ Timeout: ${results.timeoutMutations}`);
|
|
968
|
+
console.log(`โ Error: ${results.errorMutations}`);
|
|
969
|
+
console.log(`๐ฏ Mutation Score: ${results.mutationScore}%`);
|
|
970
|
+
|
|
971
|
+
if (results.mutationScore < (this.config.mutationThreshold || 80)) {
|
|
972
|
+
console.log(
|
|
973
|
+
`โ ๏ธ Mutation score below threshold (${
|
|
974
|
+
this.config.mutationThreshold || 80
|
|
975
|
+
}%)`
|
|
976
|
+
);
|
|
977
|
+
} else {
|
|
978
|
+
console.log(`๐ Mutation score meets threshold!`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Get a preview of what the mutated code would look like
|
|
985
|
+
*/
|
|
986
|
+
getMutatedCodePreview(originalCode, mutationType) {
|
|
987
|
+
try {
|
|
988
|
+
// Simple text-based mutations for preview
|
|
989
|
+
switch (mutationType) {
|
|
990
|
+
case "arithmetic":
|
|
991
|
+
return originalCode
|
|
992
|
+
.replace(/\+/g, "-")
|
|
993
|
+
.replace(/\*/g, "/")
|
|
994
|
+
.replace(/-/g, "+")
|
|
995
|
+
.replace(/\//g, "*");
|
|
996
|
+
case "logical":
|
|
997
|
+
return originalCode.replace(/&&/g, "||").replace(/\|\|/g, "&&");
|
|
998
|
+
case "conditional":
|
|
999
|
+
// For conditional mutations, we negate the entire condition
|
|
1000
|
+
// This is a simplified preview - the actual mutation is more complex
|
|
1001
|
+
if (
|
|
1002
|
+
originalCode.includes("if (") ||
|
|
1003
|
+
originalCode.includes("} else if (")
|
|
1004
|
+
) {
|
|
1005
|
+
return originalCode
|
|
1006
|
+
.replace(/if\s*\(([^)]+)\)/g, "if (!($1))")
|
|
1007
|
+
.replace(/else if\s*\(([^)]+)\)/g, "else if (!($1))");
|
|
1008
|
+
}
|
|
1009
|
+
return `${originalCode} /* condition negated */`;
|
|
1010
|
+
case "comparison":
|
|
1011
|
+
let result = originalCode;
|
|
1012
|
+
// Handle strict equality/inequality first to avoid conflicts
|
|
1013
|
+
result = result.replace(/===/g, "__TEMP_STRICT_EQ__");
|
|
1014
|
+
result = result.replace(/!==/g, "__TEMP_STRICT_NEQ__");
|
|
1015
|
+
result = result.replace(/>=/g, "__TEMP_GTE__");
|
|
1016
|
+
result = result.replace(/<=/g, "__TEMP_LTE__");
|
|
1017
|
+
|
|
1018
|
+
// Now handle simple operators
|
|
1019
|
+
result = result.replace(/>/g, "<");
|
|
1020
|
+
result = result.replace(/</g, ">");
|
|
1021
|
+
result = result.replace(/==/g, "!=");
|
|
1022
|
+
result = result.replace(/!=/g, "==");
|
|
1023
|
+
|
|
1024
|
+
// Restore complex operators with mutations
|
|
1025
|
+
result = result.replace(/__TEMP_STRICT_EQ__/g, "!==");
|
|
1026
|
+
result = result.replace(/__TEMP_STRICT_NEQ__/g, "===");
|
|
1027
|
+
result = result.replace(/__TEMP_GTE__/g, "<");
|
|
1028
|
+
result = result.replace(/__TEMP_LTE__/g, ">");
|
|
1029
|
+
|
|
1030
|
+
return result;
|
|
1031
|
+
case "returns":
|
|
1032
|
+
return originalCode.replace(/return\s+([^;]+);?/g, "return null;");
|
|
1033
|
+
case "literals":
|
|
1034
|
+
return originalCode
|
|
1035
|
+
.replace(/true/g, "false")
|
|
1036
|
+
.replace(/false/g, "true")
|
|
1037
|
+
.replace(/\d+/g, "0");
|
|
1038
|
+
default:
|
|
1039
|
+
// Don't apply invalid mutations - return original code unchanged
|
|
1040
|
+
console.warn(
|
|
1041
|
+
`โ ๏ธ Unknown mutation type '${mutationType}' - skipping mutation`
|
|
1042
|
+
);
|
|
1043
|
+
return originalCode;
|
|
1044
|
+
}
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
console.warn(
|
|
1047
|
+
`โ ๏ธ Mutation error for type '${mutationType}': ${error.message}`
|
|
1048
|
+
);
|
|
1049
|
+
return originalCode;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Determine which tests killed this mutation
|
|
1055
|
+
*/
|
|
1056
|
+
getKillingTests(testResult, tests) {
|
|
1057
|
+
// If the test failed, it means the mutation was killed
|
|
1058
|
+
if (testResult.success === false) {
|
|
1059
|
+
const killedBy = [];
|
|
1060
|
+
|
|
1061
|
+
// Try to extract test names from the output
|
|
1062
|
+
if (testResult.output) {
|
|
1063
|
+
tests.forEach((test) => {
|
|
1064
|
+
// Try multiple patterns to find test names in output
|
|
1065
|
+
const testName = test.testName;
|
|
1066
|
+
if (
|
|
1067
|
+
testResult.output.includes(testName) ||
|
|
1068
|
+
testResult.output.includes(`"${testName}"`) ||
|
|
1069
|
+
testResult.output.includes(`'${testName}'`) ||
|
|
1070
|
+
testResult.output.includes(testName.replace(/\s+/g, " "))
|
|
1071
|
+
) {
|
|
1072
|
+
killedBy.push(testName);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// If we couldn't identify specific tests, return the test names from lineage data
|
|
1078
|
+
// since we know these tests cover this line
|
|
1079
|
+
if (killedBy.length === 0 && tests.length > 0) {
|
|
1080
|
+
return tests.map((test) => test.testName);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return killedBy;
|
|
1084
|
+
}
|
|
1085
|
+
return [];
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Emergency cleanup - restore all files immediately (synchronous)
|
|
1090
|
+
*/
|
|
1091
|
+
emergencyCleanup() {
|
|
1092
|
+
console.log("๐ง Restoring original files...");
|
|
1093
|
+
|
|
1094
|
+
// Restore from backup files first
|
|
1095
|
+
for (const filePath of this.tempFiles) {
|
|
1096
|
+
try {
|
|
1097
|
+
const backupPath = `${filePath}.backup`;
|
|
1098
|
+
if (fs.existsSync(backupPath)) {
|
|
1099
|
+
fs.writeFileSync(filePath, fs.readFileSync(backupPath, "utf8"));
|
|
1100
|
+
fs.unlinkSync(backupPath);
|
|
1101
|
+
console.log(`โ
Restored: ${filePath}`);
|
|
1102
|
+
}
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
console.error(`โ Failed to restore ${filePath}:`, error.message);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Fallback: restore from stored original contents
|
|
1109
|
+
for (const [filePath, originalContent] of this.originalFileContents) {
|
|
1110
|
+
try {
|
|
1111
|
+
if (this.tempFiles.has(filePath)) {
|
|
1112
|
+
fs.writeFileSync(filePath, originalContent);
|
|
1113
|
+
console.log(`โ
Restored from memory: ${filePath}`);
|
|
1114
|
+
}
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
console.error(
|
|
1117
|
+
`โ Failed to restore from memory ${filePath}:`,
|
|
1118
|
+
error.message
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
this.tempFiles.clear();
|
|
1124
|
+
this.originalFileContents.clear();
|
|
1125
|
+
console.log("๐ฏ Emergency cleanup completed");
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Clean up all temporary files
|
|
1130
|
+
*/
|
|
1131
|
+
async cleanup() {
|
|
1132
|
+
console.log("๐งน Starting mutation testing cleanup...");
|
|
1133
|
+
|
|
1134
|
+
for (const filePath of this.tempFiles) {
|
|
1135
|
+
await this.cleanupMutatedFile(filePath);
|
|
1136
|
+
}
|
|
1137
|
+
this.tempFiles.clear();
|
|
1138
|
+
this.originalFileContents.clear();
|
|
1139
|
+
|
|
1140
|
+
// In debug mode, keep the debug files but log their location
|
|
1141
|
+
if (this.config.debugMutations && this.debugMutationFiles.size > 0) {
|
|
1142
|
+
const debugDir = this.config.debugMutationDir || "./mutations-debug";
|
|
1143
|
+
console.log(`\n๐ Debug mutation files preserved in: ${debugDir}`);
|
|
1144
|
+
console.log(` Total files created: ${this.debugMutationFiles.size}`);
|
|
1145
|
+
console.log(
|
|
1146
|
+
` Use these files to manually inspect mutations and debug issues.`
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
console.log("โ
Mutation testing cleanup completed");
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
module.exports = MutationTester;
|