jest-test-lineage-reporter 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -0
- package/bin/jest-lineage.js +20 -0
- package/package.json +14 -7
- package/src/__tests__/assertion-test.test.ts +59 -0
- package/src/__tests__/calculator.test.ts +30 -0
- package/src/__tests__/depth-example.test.ts +237 -0
- package/src/__tests__/gc-pressure-example.test.ts +169 -0
- package/src/__tests__/performance-example.test.ts +83 -0
- package/src/__tests__/quality-example.test.ts +122 -0
- package/src/__tests__/survived-mutations-example.test.ts +32 -0
- package/src/__tests__/truly-weak-example.test.ts +90 -0
- package/src/__tests__/weak-test-example.test.ts +222 -0
- package/src/calculator.ts +12 -0
- package/src/cli/commands/analyze.js +91 -0
- package/src/cli/commands/mutate.js +89 -0
- package/src/cli/commands/query.js +107 -0
- package/src/cli/commands/report.js +65 -0
- package/src/cli/commands/test.js +56 -0
- package/src/cli/index.js +89 -0
- package/src/cli/utils/config-loader.js +114 -0
- package/src/cli/utils/data-loader.js +118 -0
- package/src/cli/utils/jest-runner.js +105 -0
- package/src/cli/utils/output-formatter.js +126 -0
- package/src/depth-example.ts +66 -0
- package/src/gc-pressure-example.ts +158 -0
- package/src/global.d.ts +7 -0
- package/src/mcp/server.js +469 -0
- package/src/performance-example.ts +82 -0
- package/src/quality-example.ts +79 -0
- package/src/survived-mutations-example.ts +19 -0
- package/src/truly-weak-example.ts +37 -0
- package/src/weak-test-example.ts +91 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {
|
|
2
|
+
simpleAdd,
|
|
3
|
+
complexLogic,
|
|
4
|
+
boundaryCheck,
|
|
5
|
+
logicalOperations,
|
|
6
|
+
mathOperations,
|
|
7
|
+
stringOperations,
|
|
8
|
+
incrementOperations,
|
|
9
|
+
subtleBugFunction,
|
|
10
|
+
reallyWeakFunction,
|
|
11
|
+
guaranteedSurvivedMutations
|
|
12
|
+
} from '../weak-test-example';
|
|
13
|
+
|
|
14
|
+
describe('Weak Test Examples - Some mutations will survive', () => {
|
|
15
|
+
|
|
16
|
+
// GOOD TEST - should kill mutations
|
|
17
|
+
describe('simpleAdd', () => {
|
|
18
|
+
it('should add two positive numbers', () => {
|
|
19
|
+
expect(simpleAdd(2, 3)).toBe(5);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should add negative numbers', () => {
|
|
23
|
+
expect(simpleAdd(-2, -3)).toBe(-5);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// WEAK TEST - missing edge cases, mutations may survive
|
|
28
|
+
describe('complexLogic', () => {
|
|
29
|
+
it('should handle basic case', () => {
|
|
30
|
+
expect(complexLogic(5, 3)).toBe(8); // Only tests the x + y path
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle large numbers but with weak assertion', () => {
|
|
34
|
+
const result = complexLogic(15, 5);
|
|
35
|
+
expect(typeof result).toBe('number'); // Very weak! Doesn't check actual value
|
|
36
|
+
// This covers x > 10 path but doesn't verify x * 2 logic
|
|
37
|
+
// Mutations like * to / will survive!
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle negative numbers but with weak assertion', () => {
|
|
41
|
+
const result = complexLogic(-5, 10);
|
|
42
|
+
expect(result).toBeGreaterThanOrEqual(0); // Weak! Doesn't check exact value
|
|
43
|
+
// This covers x < 0 path but doesn't verify return 0 logic
|
|
44
|
+
// Mutations like return 0 to return 1 will survive!
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// WEAK TEST - doesn't test boundary conditions properly
|
|
49
|
+
describe('boundaryCheck', () => {
|
|
50
|
+
it('should return medium for normal values', () => {
|
|
51
|
+
expect(boundaryCheck(50)).toBe("medium");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle high values but with weak assertion', () => {
|
|
55
|
+
const result = boundaryCheck(150);
|
|
56
|
+
expect(typeof result).toBe('string'); // Weak! Doesn't check actual value
|
|
57
|
+
// This covers >= 100 path but doesn't verify "high" result
|
|
58
|
+
// Mutations like >= to > will survive!
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle low values but with weak assertion', () => {
|
|
62
|
+
const result = boundaryCheck(-10);
|
|
63
|
+
expect(result.length).toBeGreaterThan(0); // Weak! Doesn't check actual value
|
|
64
|
+
// This covers <= 0 path but doesn't verify "low" result
|
|
65
|
+
// Mutations like <= to < will survive!
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// WEAK TEST - doesn't test all logical combinations
|
|
70
|
+
describe('logicalOperations', () => {
|
|
71
|
+
it('should handle true and true', () => {
|
|
72
|
+
expect(logicalOperations(true, true)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle other combinations but with weak assertions', () => {
|
|
76
|
+
const result1 = logicalOperations(true, false);
|
|
77
|
+
const result2 = logicalOperations(false, true);
|
|
78
|
+
const result3 = logicalOperations(false, false);
|
|
79
|
+
|
|
80
|
+
// Very weak assertions - just check they return booleans!
|
|
81
|
+
expect(typeof result1).toBe('boolean');
|
|
82
|
+
expect(typeof result2).toBe('boolean');
|
|
83
|
+
expect(typeof result3).toBe('boolean');
|
|
84
|
+
|
|
85
|
+
// This covers all logical paths but doesn't verify correct logic
|
|
86
|
+
// Mutations like && to ||, || to &&, !a to a will survive!
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// WEAK TEST - doesn't verify the actual math
|
|
91
|
+
describe('mathOperations', () => {
|
|
92
|
+
it('should return a number', () => {
|
|
93
|
+
const result = mathOperations(10);
|
|
94
|
+
expect(typeof result).toBe('number'); // Very weak assertion!
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should not throw errors', () => {
|
|
98
|
+
// This test covers the lines but doesn't validate the math at all!
|
|
99
|
+
expect(() => {
|
|
100
|
+
mathOperations(5);
|
|
101
|
+
mathOperations(0);
|
|
102
|
+
mathOperations(-3);
|
|
103
|
+
}).not.toThrow();
|
|
104
|
+
|
|
105
|
+
// This covers all the math operation lines but mutations will survive
|
|
106
|
+
// because we're not checking the actual results!
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// WEAK TEST - incomplete string testing
|
|
111
|
+
describe('stringOperations', () => {
|
|
112
|
+
it('should handle non-empty strings', () => {
|
|
113
|
+
const result = stringOperations("hello");
|
|
114
|
+
expect(typeof result).toBe('string'); // Weak assertion
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Missing tests for empty string, boundary lengths
|
|
118
|
+
// String comparison mutations will survive
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// WEAK TEST - doesn't verify increment behavior
|
|
122
|
+
describe('incrementOperations', () => {
|
|
123
|
+
it('should return a number', () => {
|
|
124
|
+
const result = incrementOperations(5);
|
|
125
|
+
expect(result).toBeGreaterThan(0); // Very weak assertion
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle edge cases but with wrong expectations', () => {
|
|
129
|
+
// This test covers the lines but has completely wrong logic!
|
|
130
|
+
const result1 = incrementOperations(10);
|
|
131
|
+
const result2 = incrementOperations(15);
|
|
132
|
+
|
|
133
|
+
// These assertions are so generic that mutations will survive
|
|
134
|
+
expect(result1).toBeDefined();
|
|
135
|
+
expect(result2).toBeDefined();
|
|
136
|
+
expect(typeof result1).toBe('number');
|
|
137
|
+
expect(typeof result2).toBe('number');
|
|
138
|
+
|
|
139
|
+
// This covers the increment/decrement logic but doesn't validate it!
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// TERRIBLE TEST - covers lines but doesn't validate behavior at all
|
|
144
|
+
describe('subtleBugFunction', () => {
|
|
145
|
+
it('should execute without errors', () => {
|
|
146
|
+
// This test covers all branches but validates NOTHING!
|
|
147
|
+
let result1, result2, result3;
|
|
148
|
+
|
|
149
|
+
expect(() => {
|
|
150
|
+
result1 = subtleBugFunction(0, 5); // Covers x === 0 branch
|
|
151
|
+
result2 = subtleBugFunction(3, 2); // Covers x > 0 branch
|
|
152
|
+
result3 = subtleBugFunction(-1, 4); // Covers x < 0 branch
|
|
153
|
+
}).not.toThrow();
|
|
154
|
+
|
|
155
|
+
// These assertions are completely useless!
|
|
156
|
+
expect(result1).toBeDefined();
|
|
157
|
+
expect(result2).toBeDefined();
|
|
158
|
+
expect(result3).toBeDefined();
|
|
159
|
+
|
|
160
|
+
// ALL mutations will survive because we don't check actual values!
|
|
161
|
+
// return 1 -> return 0: SURVIVES (we don't check result1 === 1)
|
|
162
|
+
// x + y -> x - y: SURVIVES (we don't check result2 === 5)
|
|
163
|
+
// x - y -> x + y: SURVIVES (we don't check result3 === -5)
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ABSOLUTELY TERRIBLE TEST - will definitely have survived mutations
|
|
168
|
+
describe('reallyWeakFunction', () => {
|
|
169
|
+
it('should just run without throwing errors', () => {
|
|
170
|
+
// This test only checks that the function doesn't crash!
|
|
171
|
+
// It covers all lines but validates NOTHING about the logic
|
|
172
|
+
|
|
173
|
+
let result1, result2, result3;
|
|
174
|
+
|
|
175
|
+
// Execute the function to cover the lines
|
|
176
|
+
expect(() => {
|
|
177
|
+
result1 = reallyWeakFunction(5, 10); // Covers a * b, result > 100 (false), return result - 5
|
|
178
|
+
result2 = reallyWeakFunction(15, 8); // Covers a * b, result > 100 (true), return result + 10
|
|
179
|
+
result3 = reallyWeakFunction(2, 3); // Covers a * b, result > 100 (false), return result - 5
|
|
180
|
+
}).not.toThrow();
|
|
181
|
+
|
|
182
|
+
// These assertions are completely useless for catching mutations!
|
|
183
|
+
expect(result1).toBeDefined(); // Doesn't check if result1 === 45
|
|
184
|
+
expect(result2).toBeDefined(); // Doesn't check if result2 === 130
|
|
185
|
+
expect(result3).toBeDefined(); // Doesn't check if result3 === 1
|
|
186
|
+
|
|
187
|
+
// MUTATIONS THAT WILL SURVIVE:
|
|
188
|
+
// a * b -> a + b: SURVIVES (we don't check actual calculation)
|
|
189
|
+
// a * b -> a - b: SURVIVES (we don't check actual calculation)
|
|
190
|
+
// a * b -> a / b: SURVIVES (we don't check actual calculation)
|
|
191
|
+
// result > 100 -> result < 100: SURVIVES (we don't check branching logic)
|
|
192
|
+
// result > 100 -> result >= 100: SURVIVES (we don't check branching logic)
|
|
193
|
+
// result + 10 -> result - 10: SURVIVES (we don't check return values)
|
|
194
|
+
// result - 5 -> result + 5: SURVIVES (we don't check return values)
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ABSOLUTELY USELESS TEST - will definitely have survived mutations
|
|
199
|
+
describe('guaranteedSurvivedMutations', () => {
|
|
200
|
+
it('should exist as a function', () => {
|
|
201
|
+
// This test is so useless it only checks the function exists!
|
|
202
|
+
expect(typeof guaranteedSurvivedMutations).toBe('function');
|
|
203
|
+
|
|
204
|
+
// We call the function but don't check ANY return values
|
|
205
|
+
guaranteedSurvivedMutations(5); // Covers x === 5 branch, return 42
|
|
206
|
+
guaranteedSurvivedMutations(15); // Covers x > 10 branch, return x * 2
|
|
207
|
+
guaranteedSurvivedMutations(3); // Covers default branch, return x + 1
|
|
208
|
+
|
|
209
|
+
// NO ASSERTIONS ON RETURN VALUES!
|
|
210
|
+
// This means ALL mutations will survive:
|
|
211
|
+
// x === 5 -> x !== 5: SURVIVES (we don't check the condition)
|
|
212
|
+
// x === 5 -> x > 5: SURVIVES (we don't check the condition)
|
|
213
|
+
// return 42 -> return 0: SURVIVES (we don't check return value)
|
|
214
|
+
// return 42 -> return 1: SURVIVES (we don't check return value)
|
|
215
|
+
// x > 10 -> x < 10: SURVIVES (we don't check the condition)
|
|
216
|
+
// x * 2 -> x + 2: SURVIVES (we don't check return value)
|
|
217
|
+
// x * 2 -> x - 2: SURVIVES (we don't check return value)
|
|
218
|
+
// x + 1 -> x - 1: SURVIVES (we don't check return value)
|
|
219
|
+
// x + 1 -> x * 1: SURVIVES (we don't check return value)
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function add(a: number, b: number): number {
|
|
2
|
+
return a - b;
|
|
3
|
+
}
|
|
4
|
+
export function subtract(a: number, b: number): number {
|
|
5
|
+
if (!(!!(a < b))) {
|
|
6
|
+
return b - a;
|
|
7
|
+
}
|
|
8
|
+
return a - b;
|
|
9
|
+
}
|
|
10
|
+
export function multiply(a: number, b: number): number {
|
|
11
|
+
return a * b;
|
|
12
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze Command
|
|
3
|
+
* Full workflow: test + mutation + report
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const testCommand = require('./test');
|
|
7
|
+
const mutateCommand = require('./mutate');
|
|
8
|
+
const reportCommand = require('./report');
|
|
9
|
+
const { lineageDataExists } = require('../utils/data-loader');
|
|
10
|
+
const { section, success, error, info } = require('../utils/output-formatter');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
|
|
13
|
+
async function analyzeCommand(options) {
|
|
14
|
+
try {
|
|
15
|
+
section('🚀 Full Analysis Workflow');
|
|
16
|
+
|
|
17
|
+
// Step 1: Run tests (unless skip-tests is specified)
|
|
18
|
+
if (!options.skipTests) {
|
|
19
|
+
info('Step 1: Running tests with lineage tracking...\n');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await testCommand([], {
|
|
23
|
+
lineage: options.lineage !== false,
|
|
24
|
+
performance: options.performance !== false,
|
|
25
|
+
quality: options.quality !== false,
|
|
26
|
+
config: options.config,
|
|
27
|
+
quiet: false
|
|
28
|
+
});
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// testCommand handles its own exit, this catch is for safety
|
|
31
|
+
error('Tests failed. Analysis stopped.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
// Verify lineage data exists
|
|
36
|
+
if (!lineageDataExists()) {
|
|
37
|
+
error('Lineage data not found. Remove --skip-tests or run jest-lineage test first.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
info('Step 1: Skipped (using existing lineage data)\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Step 2: Run mutation testing (unless skip-mutation is specified)
|
|
44
|
+
if (!options.skipMutation) {
|
|
45
|
+
console.log(); // Add spacing
|
|
46
|
+
info('Step 2: Running mutation testing...\n');
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await mutateCommand({
|
|
50
|
+
data: '.jest-lineage-data.json',
|
|
51
|
+
threshold: options.threshold,
|
|
52
|
+
timeout: options.timeout || '5000',
|
|
53
|
+
verbose: false
|
|
54
|
+
});
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// Don't fail the entire workflow if mutation testing fails
|
|
57
|
+
error('Mutation testing failed, but continuing with report generation...');
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
info('Step 2: Skipped mutation testing\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Step 3: Generate report
|
|
64
|
+
console.log(); // Add spacing
|
|
65
|
+
info('Step 3: Generating HTML report...\n');
|
|
66
|
+
|
|
67
|
+
await reportCommand({
|
|
68
|
+
data: '.jest-lineage-data.json',
|
|
69
|
+
output: options.output,
|
|
70
|
+
open: options.open
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Success summary
|
|
74
|
+
console.log();
|
|
75
|
+
section('✨ Analysis Complete');
|
|
76
|
+
success('Full analysis workflow completed successfully!');
|
|
77
|
+
console.log(chalk.gray(`\nGenerated files:`));
|
|
78
|
+
console.log(chalk.gray(` • .jest-lineage-data.json (lineage data)`));
|
|
79
|
+
console.log(chalk.gray(` • ${options.output} (HTML report)`));
|
|
80
|
+
|
|
81
|
+
process.exit(0);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
error(`Analysis workflow failed: ${err.message}`);
|
|
84
|
+
if (process.env.JEST_LINEAGE_DEBUG === 'true') {
|
|
85
|
+
console.error(err.stack);
|
|
86
|
+
}
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = analyzeCommand;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutate Command
|
|
3
|
+
* Run mutation testing on existing lineage data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MutationTester = require('../../MutationTester');
|
|
7
|
+
const { loadLineageData, processLineageDataForMutation } = require('../utils/data-loader');
|
|
8
|
+
const { loadFullConfig } = require('../utils/config-loader');
|
|
9
|
+
const { spinner, printMutationSummary, error, success, info, printLineageDataSummary } = require('../utils/output-formatter');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
async function mutateCommand(options) {
|
|
13
|
+
let mutationTester = null;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Load configuration
|
|
17
|
+
const config = loadFullConfig(options);
|
|
18
|
+
|
|
19
|
+
// Load lineage data
|
|
20
|
+
info(`Loading lineage data from: ${chalk.yellow(options.data)}`);
|
|
21
|
+
const rawData = loadLineageData(options.data);
|
|
22
|
+
|
|
23
|
+
if (!options.verbose) {
|
|
24
|
+
printLineageDataSummary(rawData);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Process data for mutation testing
|
|
28
|
+
const lineageData = processLineageDataForMutation(rawData);
|
|
29
|
+
const fileCount = Object.keys(lineageData).length;
|
|
30
|
+
const lineCount = Object.values(lineageData).reduce(
|
|
31
|
+
(sum, lines) => sum + Object.keys(lines).length,
|
|
32
|
+
0
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (fileCount === 0 || lineCount === 0) {
|
|
36
|
+
error('No coverage data found in lineage file. Run tests first.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
info(`Processing ${chalk.cyan(fileCount)} files with ${chalk.cyan(lineCount)} covered lines\n`);
|
|
41
|
+
|
|
42
|
+
// Create mutation tester
|
|
43
|
+
mutationTester = new MutationTester(config);
|
|
44
|
+
mutationTester.setLineageData(lineageData);
|
|
45
|
+
|
|
46
|
+
// Run mutation testing
|
|
47
|
+
const spin = spinner('Running mutation testing...');
|
|
48
|
+
if (!options.verbose) {
|
|
49
|
+
spin.start();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const results = await mutationTester.runMutationTesting();
|
|
53
|
+
|
|
54
|
+
if (!options.verbose) {
|
|
55
|
+
spin.succeed('Mutation testing completed!');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Print results
|
|
59
|
+
printMutationSummary(results);
|
|
60
|
+
|
|
61
|
+
// Check threshold
|
|
62
|
+
const threshold = parseInt(options.threshold) || 80;
|
|
63
|
+
if (results.mutationScore < threshold) {
|
|
64
|
+
console.log(chalk.yellow(`\n⚠️ Mutation score ${results.mutationScore.toFixed(1)}% is below threshold ${threshold}%`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
} else {
|
|
67
|
+
success(`Mutation score meets threshold (${threshold}%)`);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
error(`Mutation testing failed: ${err.message}`);
|
|
72
|
+
if (options.verbose) {
|
|
73
|
+
console.error(err.stack);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Cleanup on error
|
|
77
|
+
if (mutationTester) {
|
|
78
|
+
try {
|
|
79
|
+
await mutationTester.cleanup();
|
|
80
|
+
} catch (cleanupErr) {
|
|
81
|
+
// Ignore cleanup errors
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = mutateCommand;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Command
|
|
3
|
+
* Query test coverage for specific files/lines
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { loadLineageData, processLineageDataForMutation } = require('../utils/data-loader');
|
|
7
|
+
const { section, error, formatPath } = require('../utils/output-formatter');
|
|
8
|
+
const Table = require('cli-table3');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
async function queryCommand(file, line, options) {
|
|
13
|
+
try {
|
|
14
|
+
// Load lineage data
|
|
15
|
+
const rawData = loadLineageData(options.data);
|
|
16
|
+
const lineageData = processLineageDataForMutation(rawData);
|
|
17
|
+
|
|
18
|
+
// Normalize file path
|
|
19
|
+
const normalizedFile = path.normalize(file);
|
|
20
|
+
|
|
21
|
+
// Find matching files
|
|
22
|
+
const matchingFiles = Object.keys(lineageData).filter(f =>
|
|
23
|
+
f.includes(normalizedFile) || normalizedFile.includes(path.basename(f))
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (matchingFiles.length === 0) {
|
|
27
|
+
error(`No coverage data found for file: ${chalk.yellow(file)}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If multiple matches, show them
|
|
32
|
+
if (matchingFiles.length > 1) {
|
|
33
|
+
console.log(chalk.yellow(`Found multiple matching files:`));
|
|
34
|
+
matchingFiles.forEach(f => console.log(` - ${formatPath(f)}`));
|
|
35
|
+
console.log(chalk.yellow(`\nUsing first match: ${matchingFiles[0]}\n`));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const targetFile = matchingFiles[0];
|
|
39
|
+
const fileCoverage = lineageData[targetFile];
|
|
40
|
+
|
|
41
|
+
// If line specified, show coverage for that line
|
|
42
|
+
if (line) {
|
|
43
|
+
const lineNumber = line.toString();
|
|
44
|
+
if (!fileCoverage[lineNumber]) {
|
|
45
|
+
error(`No coverage data for line ${chalk.yellow(lineNumber)} in ${chalk.yellow(file)}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
section(`📍 Coverage for ${formatPath(targetFile)}:${lineNumber}`);
|
|
50
|
+
|
|
51
|
+
const tests = fileCoverage[lineNumber];
|
|
52
|
+
const table = new Table({
|
|
53
|
+
head: [chalk.cyan('Test Name'), chalk.cyan('File'), chalk.cyan('Exec Count')],
|
|
54
|
+
colWidths: [50, 30, 12]
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
tests.forEach(test => {
|
|
58
|
+
table.push([
|
|
59
|
+
test.testName,
|
|
60
|
+
path.basename(test.testFile),
|
|
61
|
+
test.executionCount
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log(table.toString());
|
|
66
|
+
console.log(chalk.gray(`\nTotal: ${tests.length} test(s) cover this line`));
|
|
67
|
+
} else {
|
|
68
|
+
// Show coverage for entire file
|
|
69
|
+
section(`📁 Coverage for ${formatPath(targetFile)}`);
|
|
70
|
+
|
|
71
|
+
const lines = Object.keys(fileCoverage).sort((a, b) => parseInt(a) - parseInt(b));
|
|
72
|
+
const totalTests = new Set(
|
|
73
|
+
lines.flatMap(lineNum => fileCoverage[lineNum].map(t => t.testName))
|
|
74
|
+
).size;
|
|
75
|
+
|
|
76
|
+
console.log(chalk.gray(`Lines covered: ${lines.length}`));
|
|
77
|
+
console.log(chalk.gray(`Tests covering file: ${totalTests}\n`));
|
|
78
|
+
|
|
79
|
+
// Show sample of lines
|
|
80
|
+
const sampleSize = 10;
|
|
81
|
+
const sampled = lines.slice(0, sampleSize);
|
|
82
|
+
|
|
83
|
+
sampled.forEach(lineNum => {
|
|
84
|
+
const tests = fileCoverage[lineNum];
|
|
85
|
+
console.log(chalk.cyan(`Line ${lineNum}:`));
|
|
86
|
+
tests.slice(0, 3).forEach(test => {
|
|
87
|
+
console.log(` ${chalk.gray('•')} ${test.testName} ${chalk.gray(`(${path.basename(test.testFile)})`)}`);
|
|
88
|
+
});
|
|
89
|
+
if (tests.length > 3) {
|
|
90
|
+
console.log(chalk.gray(` ... and ${tests.length - 3} more test(s)`));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (lines.length > sampleSize) {
|
|
95
|
+
console.log(chalk.gray(`\n... and ${lines.length - sampleSize} more lines`));
|
|
96
|
+
console.log(chalk.yellow(`\nTip: Specify a line number to see details: jest-lineage query ${file} <line>`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
process.exit(0);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
error(`Query failed: ${err.message}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = queryCommand;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Command
|
|
3
|
+
* Generate HTML report from existing lineage data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const TestCoverageReporter = require('../../TestCoverageReporter');
|
|
7
|
+
const { loadLineageData, processLineageDataForMutation } = require('../utils/data-loader');
|
|
8
|
+
const { loadFullConfig } = require('../utils/config-loader');
|
|
9
|
+
const { spinner, success, error, info } = require('../utils/output-formatter');
|
|
10
|
+
const open = require('open');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
async function reportCommand(options) {
|
|
15
|
+
try {
|
|
16
|
+
// Load configuration
|
|
17
|
+
const config = loadFullConfig(options);
|
|
18
|
+
|
|
19
|
+
// Load lineage data
|
|
20
|
+
info(`Loading lineage data from: ${chalk.yellow(options.data)}`);
|
|
21
|
+
const rawData = loadLineageData(options.data);
|
|
22
|
+
|
|
23
|
+
const testCount = rawData.tests.length;
|
|
24
|
+
info(`Loaded ${chalk.cyan(testCount)} tests`);
|
|
25
|
+
|
|
26
|
+
// Process lineage data
|
|
27
|
+
const lineageData = processLineageDataForMutation(rawData);
|
|
28
|
+
|
|
29
|
+
// Create reporter instance with minimal config
|
|
30
|
+
const reporter = new TestCoverageReporter(
|
|
31
|
+
{ rootDir: process.cwd() },
|
|
32
|
+
{ outputFile: options.output, ...config }
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Load data into reporter
|
|
36
|
+
reporter.processLineageResults(lineageData, 'unknown');
|
|
37
|
+
|
|
38
|
+
// Generate HTML report
|
|
39
|
+
const spin = spinner('Generating HTML report...');
|
|
40
|
+
spin.start();
|
|
41
|
+
|
|
42
|
+
await reporter.generateHtmlReport();
|
|
43
|
+
|
|
44
|
+
spin.succeed('HTML report generated!');
|
|
45
|
+
|
|
46
|
+
const reportPath = path.resolve(process.cwd(), options.output);
|
|
47
|
+
success(`Report saved to: ${chalk.cyan(reportPath)}`);
|
|
48
|
+
|
|
49
|
+
// Open in browser if requested
|
|
50
|
+
if (options.open) {
|
|
51
|
+
info('Opening report in browser...');
|
|
52
|
+
await open(reportPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
process.exit(0);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
error(`Failed to generate report: ${err.message}`);
|
|
58
|
+
if (process.env.JEST_LINEAGE_DEBUG === 'true') {
|
|
59
|
+
console.error(err.stack);
|
|
60
|
+
}
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = reportCommand;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Command
|
|
3
|
+
* Run Jest tests with lineage tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { runJest } = require('../utils/jest-runner');
|
|
7
|
+
const { loadFullConfig } = require('../utils/config-loader');
|
|
8
|
+
const { lineageDataExists } = require('../utils/data-loader');
|
|
9
|
+
const { success, error, info } = require('../utils/output-formatter');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
async function testCommand(jestArgs, options) {
|
|
14
|
+
try {
|
|
15
|
+
// Load configuration
|
|
16
|
+
const config = loadFullConfig(options);
|
|
17
|
+
|
|
18
|
+
// Run Jest with lineage tracking
|
|
19
|
+
const result = await runJest({
|
|
20
|
+
args: jestArgs || [],
|
|
21
|
+
config: options.config,
|
|
22
|
+
enableLineage: options.lineage !== false,
|
|
23
|
+
enablePerformance: options.performance !== false,
|
|
24
|
+
enableQuality: options.quality !== false,
|
|
25
|
+
quiet: options.quiet
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Check if lineage data was generated
|
|
29
|
+
const dataPath = path.join(process.cwd(), '.jest-lineage-data.json');
|
|
30
|
+
if (result.success && fs.existsSync(dataPath)) {
|
|
31
|
+
const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
|
|
32
|
+
const testCount = data.tests ? data.tests.length : 0;
|
|
33
|
+
const fileCount = data.tests
|
|
34
|
+
? new Set(data.tests.flatMap(t =>
|
|
35
|
+
Object.keys(t.coverage || {}).map(k => k.split(':')[0])
|
|
36
|
+
)).size
|
|
37
|
+
: 0;
|
|
38
|
+
|
|
39
|
+
if (!options.quiet) {
|
|
40
|
+
info(`Lineage data saved to: ${dataPath}`);
|
|
41
|
+
console.log(` - ${testCount} tests tracked`);
|
|
42
|
+
console.log(` - ${fileCount} files analyzed\n`);
|
|
43
|
+
}
|
|
44
|
+
} else if (!result.success) {
|
|
45
|
+
error('Tests failed. Lineage data may be incomplete.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Exit with Jest's exit code
|
|
49
|
+
process.exit(result.exitCode);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
error(`Failed to run tests: ${err.message}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = testCommand;
|