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.
@@ -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;