jest-test-lineage-reporter 2.1.2 → 2.2.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 CHANGED
@@ -90,27 +90,39 @@ jest-lineage test --no-performance --no-quality
90
90
  Run mutation testing standalone (on existing lineage data).
91
91
 
92
92
  ```bash
93
- # Basic mutation testing
93
+ # Basic mutation testing (serial)
94
94
  jest-lineage mutate
95
95
 
96
96
  # With custom threshold
97
97
  jest-lineage mutate --threshold 90
98
98
 
99
+ # Parallel execution with 4 workers (faster!)
100
+ jest-lineage mutate --workers 4
101
+
102
+ # Auto-detect CPU cores and use optimal worker count
103
+ jest-lineage mutate --workers 0
104
+
99
105
  # Debug mode (create mutation files without running tests)
100
106
  jest-lineage mutate --debug --debug-dir ./mutations
101
107
  ```
102
108
 
109
+ **Performance Tip**: Use `--workers` to run mutations in parallel. Each worker processes a different file concurrently, significantly reducing execution time for projects with multiple files.
110
+
111
+ **Note**: Mutation results are saved to `.jest-lineage-mutation-results.json`. Run `jest-lineage report` after mutation testing to generate an HTML report with mutation data included.
112
+
103
113
  #### `jest-lineage report`
104
- Generate HTML report from existing lineage data.
114
+ Generate HTML report from existing lineage data and mutation results.
105
115
 
106
116
  ```bash
107
- # Generate and open report
117
+ # Generate and open report (includes mutation results if available)
108
118
  jest-lineage report --open
109
119
 
110
120
  # Custom output path
111
121
  jest-lineage report --output coverage-report.html
112
122
  ```
113
123
 
124
+ **Tip**: The report command automatically loads mutation results from `.jest-lineage-mutation-results.json` if available.
125
+
114
126
  #### `jest-lineage query <file> [line]`
115
127
  Query which tests cover specific files or lines.
116
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jest-test-lineage-reporter",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "main": "src/TestCoverageReporter.js",
5
5
  "bin": {
6
6
  "jest-lineage": "./bin/jest-lineage.js"
@@ -57,6 +57,7 @@
57
57
  "LICENSE"
58
58
  ],
59
59
  "dependencies": {
60
+ "@babel/runtime": "^7.28.4",
60
61
  "@modelcontextprotocol/sdk": "^0.5.0",
61
62
  "chalk": "^4.1.2",
62
63
  "cli-table3": "^0.6.3",
@@ -219,42 +219,99 @@ class MutationTester {
219
219
  fileResults: {},
220
220
  };
221
221
 
222
- let currentFileIndex = 0;
223
- let currentMutationIndex = 0;
222
+ // Determine worker count
223
+ const workers = this.config.workers || 1;
224
+ const shouldParallelize = workers > 1 || workers === 0;
225
+
226
+ if (shouldParallelize) {
227
+ // Parallel execution - process multiple files concurrently
228
+ const os = require('os');
229
+ const actualWorkers = workers === 0 ? Math.max(1, os.cpus().length - 1) : workers;
230
+ console.log(`\n⚔ Running mutations in parallel with ${actualWorkers} workers\n`);
231
+
232
+ const fileEntries = Object.entries(this.lineageData);
233
+ const filePromises = fileEntries.map(async ([filePath, lines], index) => {
234
+ console.log(
235
+ `\nšŸ”¬ [Worker ${(index % actualWorkers) + 1}] Testing mutations in ${filePath} (${index + 1}/${totalFiles})...`
236
+ );
224
237
 
225
- for (const [filePath, lines] of Object.entries(this.lineageData)) {
226
- currentFileIndex++;
227
- console.log(
228
- `\nšŸ”¬ Testing mutations in ${filePath} (${currentFileIndex}/${totalFiles})...`
229
- );
238
+ const fileResults = await this.testFileLines(
239
+ filePath,
240
+ lines,
241
+ 0, // Start from 0 for each file in parallel mode
242
+ totalMutationsCount
243
+ );
230
244
 
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
- );
245
+ // Log file completion summary
246
+ const fileName = filePath.split("/").pop();
247
+ const fileScore =
248
+ fileResults.totalMutations > 0
249
+ ? Math.round(
250
+ (fileResults.killedMutations / fileResults.totalMutations) * 100
251
+ )
252
+ : 0;
253
+ console.log(
254
+ `āœ… [Worker ${(index % actualWorkers) + 1}] ${fileName}: ${fileResults.totalMutations} mutations, ${fileResults.killedMutations} killed, ${fileResults.survivedMutations} survived (${fileScore}% score)`
255
+ );
256
+
257
+ return { filePath, fileResults };
258
+ });
259
+
260
+ // Process files with concurrency limit
261
+ const chunkSize = actualWorkers;
262
+ for (let i = 0; i < filePromises.length; i += chunkSize) {
263
+ const chunk = filePromises.slice(i, i + chunkSize);
264
+ const chunkResults = await Promise.all(chunk);
265
+
266
+ // Aggregate results
267
+ for (const { filePath, fileResults } of chunkResults) {
268
+ results.fileResults[filePath] = fileResults;
269
+ results.totalMutations += fileResults.totalMutations;
270
+ results.killedMutations += fileResults.killedMutations;
271
+ results.survivedMutations += fileResults.survivedMutations;
272
+ results.timeoutMutations += fileResults.timeoutMutations;
273
+ results.errorMutations += fileResults.errorMutations;
274
+ }
275
+ }
276
+ } else {
277
+ // Serial execution - process one file at a time
278
+ let currentFileIndex = 0;
279
+ let currentMutationIndex = 0;
280
+
281
+ for (const [filePath, lines] of Object.entries(this.lineageData)) {
282
+ currentFileIndex++;
283
+ console.log(
284
+ `\nšŸ”¬ Testing mutations in ${filePath} (${currentFileIndex}/${totalFiles})...`
285
+ );
286
+
287
+ const fileResults = await this.testFileLines(
288
+ filePath,
289
+ lines,
290
+ currentMutationIndex,
291
+ totalMutationsCount
292
+ );
293
+ results.fileResults[filePath] = fileResults;
294
+
295
+ results.totalMutations += fileResults.totalMutations;
296
+ results.killedMutations += fileResults.killedMutations;
297
+ results.survivedMutations += fileResults.survivedMutations;
298
+ results.timeoutMutations += fileResults.timeoutMutations;
299
+ results.errorMutations += fileResults.errorMutations;
300
+
301
+ currentMutationIndex += fileResults.totalMutations;
302
+
303
+ // Log file completion summary
304
+ const fileName = filePath.split("/").pop();
305
+ const fileScore =
306
+ fileResults.totalMutations > 0
307
+ ? Math.round(
308
+ (fileResults.killedMutations / fileResults.totalMutations) * 100
309
+ )
310
+ : 0;
311
+ console.log(
312
+ `āœ… ${fileName}: ${fileResults.totalMutations} mutations, ${fileResults.killedMutations} killed, ${fileResults.survivedMutations} survived (${fileScore}% score)`
313
+ );
314
+ }
258
315
  }
259
316
 
260
317
  // Calculate mutation score
@@ -3104,7 +3104,7 @@ class TestCoverageReporter {
3104
3104
  // Group mutations by line
3105
3105
  const mutationsByLine = {};
3106
3106
  fileData.mutations.forEach(mutation => {
3107
- const line = mutation.line || 'unknown';
3107
+ const line = mutation.lineNumber || mutation.line || 'unknown';
3108
3108
  if (!mutationsByLine[line]) {
3109
3109
  mutationsByLine[line] = [];
3110
3110
  }
@@ -55,6 +55,13 @@ async function mutateCommand(options) {
55
55
  spin.succeed('Mutation testing completed!');
56
56
  }
57
57
 
58
+ // Save results to file for HTML report
59
+ const fs = require('fs');
60
+ const path = require('path');
61
+ const resultsPath = path.join(process.cwd(), '.jest-lineage-mutation-results.json');
62
+ fs.writeFileSync(resultsPath, JSON.stringify(results, null, 2));
63
+ info(`Mutation results saved to: ${chalk.yellow(resultsPath)}`);
64
+
58
65
  // Print results
59
66
  printMutationSummary(results);
60
67
 
@@ -26,6 +26,19 @@ async function reportCommand(options) {
26
26
  // Process lineage data
27
27
  const lineageData = processLineageDataForMutation(rawData);
28
28
 
29
+ // Load mutation results if available
30
+ const fs = require('fs');
31
+ const mutationResultsPath = path.join(process.cwd(), '.jest-lineage-mutation-results.json');
32
+ let mutationResults = null;
33
+ if (fs.existsSync(mutationResultsPath)) {
34
+ try {
35
+ mutationResults = JSON.parse(fs.readFileSync(mutationResultsPath, 'utf8'));
36
+ info(`Loaded mutation results: ${chalk.cyan(mutationResults.totalMutations)} mutations tested`);
37
+ } catch (e) {
38
+ // Ignore errors loading mutation results
39
+ }
40
+ }
41
+
29
42
  // Create reporter instance with minimal config
30
43
  const reporter = new TestCoverageReporter(
31
44
  { rootDir: process.cwd() },
@@ -35,6 +48,11 @@ async function reportCommand(options) {
35
48
  // Load data into reporter
36
49
  reporter.processLineageResults(lineageData, 'unknown');
37
50
 
51
+ // Set mutation results if available
52
+ if (mutationResults) {
53
+ reporter.mutationResults = mutationResults;
54
+ }
55
+
38
56
  // Generate HTML report
39
57
  const spin = spinner('Generating HTML report...');
40
58
  spin.start();
package/src/cli/index.js CHANGED
@@ -47,6 +47,7 @@ async function run(argv) {
47
47
  .option('--data <path>', 'Path to lineage data file', '.jest-lineage-data.json')
48
48
  .option('--threshold <number>', 'Mutation score threshold (%)', '80')
49
49
  .option('--timeout <ms>', 'Timeout per mutation (ms)', '5000')
50
+ .option('--workers <number>', 'Number of parallel workers (1=serial, 0=auto)', '1')
50
51
  .option('--debug', 'Create debug mutation files instead of running tests')
51
52
  .option('--debug-dir <path>', 'Directory for debug files', './mutations-debug')
52
53
  .option('--operators <list>', 'Comma-separated mutation operators to enable')
@@ -50,6 +50,9 @@ function mapCliOptionsToConfig(cliOptions) {
50
50
  if (cliOptions.timeout !== undefined) {
51
51
  config.mutationTimeout = parseInt(cliOptions.timeout);
52
52
  }
53
+ if (cliOptions.workers !== undefined) {
54
+ config.workers = parseInt(cliOptions.workers);
55
+ }
53
56
  if (cliOptions.debug === true) {
54
57
  config.debugMutations = true;
55
58
  }
@@ -39,6 +39,12 @@ async function runJest(options = {}) {
39
39
  jestArgs.push('--coverage');
40
40
  }
41
41
 
42
+ // Run tests serially when lineage tracking is enabled to avoid race conditions
43
+ // with file writes from parallel workers
44
+ if (enableLineage && !jestArgs.includes('--runInBand') && !jestArgs.includes('--maxWorkers')) {
45
+ jestArgs.push('--runInBand');
46
+ }
47
+
42
48
  // Set environment variables for lineage tracking
43
49
  const env = {
44
50
  ...process.env,
package/src/testSetup.js CHANGED
@@ -278,7 +278,31 @@ function createTestWrapper(originalFn, testType) {
278
278
  if (process.env.JEST_LINEAGE_MUTATION !== 'true') {
279
279
  // Also store in a more persistent way for the reporter
280
280
  if (!global.__LINEAGE_PERSISTENT_DATA__) {
281
- global.__LINEAGE_PERSISTENT_DATA__ = [];
281
+ // Initialize array - load existing tests from file if merging is enabled
282
+ // This ensures data persists across test files running in separate workers
283
+ const shouldMerge = process.env.JEST_LINEAGE_MERGE !== 'false';
284
+ if (shouldMerge) {
285
+ const fs = require('fs');
286
+ const path = require('path');
287
+ const filePath = path.join(process.cwd(), '.jest-lineage-data.json');
288
+
289
+ if (fs.existsSync(filePath)) {
290
+ try {
291
+ const existingData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
292
+ // Convert coverage objects back to Maps
293
+ global.__LINEAGE_PERSISTENT_DATA__ = existingData.tests.map(test => ({
294
+ ...test,
295
+ coverage: new Map(Object.entries(test.coverage || {}))
296
+ }));
297
+ } catch (e) {
298
+ global.__LINEAGE_PERSISTENT_DATA__ = [];
299
+ }
300
+ } else {
301
+ global.__LINEAGE_PERSISTENT_DATA__ = [];
302
+ }
303
+ } else {
304
+ global.__LINEAGE_PERSISTENT_DATA__ = [];
305
+ }
282
306
  }
283
307
  global.__LINEAGE_PERSISTENT_DATA__.push(testData);
284
308
 
@@ -645,8 +669,9 @@ function writeTrackingDataToFile() {
645
669
  try {
646
670
  const filePath = path.join(process.cwd(), '.jest-lineage-data.json');
647
671
 
648
- // Check if we should merge with existing data (default: false - recreate from scratch)
649
- const shouldMerge = process.env.JEST_LINEAGE_MERGE === 'true';
672
+ // Check if we should merge with existing data (default: true for multiple test files)
673
+ // Set JEST_LINEAGE_MERGE=false to disable merging and recreate from scratch
674
+ const shouldMerge = process.env.JEST_LINEAGE_MERGE !== 'false';
650
675
 
651
676
  let existingData = { timestamp: Date.now(), tests: [] };
652
677
  if (shouldMerge && fs.existsSync(filePath)) {
@@ -717,8 +742,6 @@ function writeTrackingDataToFile() {
717
742
  }
718
743
 
719
744
  fs.writeFileSync(filePath, JSON.stringify(dataToWrite, null, 2));
720
-
721
- // console.log(`šŸ“ Wrote tracking data: ${dataToWrite.tests.length} total tests to ${filePath}`);
722
745
  } catch (error) {
723
746
  console.warn('Warning: Could not write tracking data to file:', error.message);
724
747
  }