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 +15 -3
- package/package.json +2 -1
- package/src/MutationTester.js +91 -34
- package/src/TestCoverageReporter.js +1 -1
- package/src/cli/commands/mutate.js +7 -0
- package/src/cli/commands/report.js +18 -0
- package/src/cli/index.js +1 -0
- package/src/cli/utils/config-loader.js +3 -0
- package/src/cli/utils/jest-runner.js +6 -0
- package/src/testSetup.js +28 -5
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.
|
|
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",
|
package/src/MutationTester.js
CHANGED
|
@@ -219,42 +219,99 @@ class MutationTester {
|
|
|
219
219
|
fileResults: {},
|
|
220
220
|
};
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
232
|
-
filePath
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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:
|
|
649
|
-
|
|
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
|
}
|