jest-test-lineage-reporter 2.0.2 → 2.1.1
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 +281 -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/init.js +214 -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 +99 -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,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Formatter
|
|
3
|
+
* Format console output with colors and formatting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Print success message
|
|
11
|
+
* @param {string} message - Success message
|
|
12
|
+
*/
|
|
13
|
+
function success(message) {
|
|
14
|
+
console.log(chalk.green(`✅ ${message}`));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Print error message
|
|
19
|
+
* @param {string} message - Error message
|
|
20
|
+
*/
|
|
21
|
+
function error(message) {
|
|
22
|
+
console.error(chalk.red(`❌ ${message}`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Print warning message
|
|
27
|
+
* @param {string} message - Warning message
|
|
28
|
+
*/
|
|
29
|
+
function warning(message) {
|
|
30
|
+
console.log(chalk.yellow(`⚠️ ${message}`));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Print info message
|
|
35
|
+
* @param {string} message - Info message
|
|
36
|
+
*/
|
|
37
|
+
function info(message) {
|
|
38
|
+
console.log(chalk.cyan(`ℹ️ ${message}`));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Print section header
|
|
43
|
+
* @param {string} title - Section title
|
|
44
|
+
*/
|
|
45
|
+
function section(title) {
|
|
46
|
+
console.log(chalk.bold.cyan(`\n${title}`));
|
|
47
|
+
console.log(chalk.gray('═'.repeat(title.length + 2)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a spinner for long operations
|
|
52
|
+
* @param {string} text - Spinner text
|
|
53
|
+
* @returns {object} Ora spinner instance
|
|
54
|
+
*/
|
|
55
|
+
function spinner(text) {
|
|
56
|
+
return ora({
|
|
57
|
+
text,
|
|
58
|
+
color: 'cyan',
|
|
59
|
+
spinner: 'dots'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Print mutation results summary
|
|
65
|
+
* @param {object} results - Mutation test results
|
|
66
|
+
*/
|
|
67
|
+
function printMutationSummary(results) {
|
|
68
|
+
section('🧬 Mutation Testing Results');
|
|
69
|
+
|
|
70
|
+
console.log(`📊 ${chalk.bold('Total Mutations:')} ${results.totalMutations}`);
|
|
71
|
+
console.log(`${chalk.green('✅ Killed:')} ${results.killedMutations}`);
|
|
72
|
+
console.log(`${chalk.red('🔴 Survived:')} ${results.survivedMutations}`);
|
|
73
|
+
console.log(`${chalk.yellow('⏰ Timeout:')} ${results.timeoutMutations || 0}`);
|
|
74
|
+
console.log(`${chalk.gray('❌ Error:')} ${results.errorMutations || 0}`);
|
|
75
|
+
console.log(`${chalk.bold.cyan('🎯 Mutation Score:')} ${chalk.bold(results.mutationScore.toFixed(1))}%`);
|
|
76
|
+
|
|
77
|
+
if (results.mutationScore >= 80) {
|
|
78
|
+
console.log(chalk.green('\n✅ Excellent mutation score!'));
|
|
79
|
+
} else if (results.mutationScore >= 60) {
|
|
80
|
+
console.log(chalk.yellow('\n⚠️ Good mutation score, but room for improvement'));
|
|
81
|
+
} else {
|
|
82
|
+
console.log(chalk.red('\n❌ Low mutation score - consider improving test quality'));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Print lineage data summary
|
|
88
|
+
* @param {object} data - Lineage data
|
|
89
|
+
*/
|
|
90
|
+
function printLineageDataSummary(data) {
|
|
91
|
+
const testCount = data.tests.length;
|
|
92
|
+
const failedTests = data.tests.filter(t => t.failed).length;
|
|
93
|
+
const passedTests = testCount - failedTests;
|
|
94
|
+
|
|
95
|
+
console.log(chalk.cyan(`\n📊 Lineage data loaded:`));
|
|
96
|
+
console.log(` ${chalk.green('✓')} ${passedTests} tests passed`);
|
|
97
|
+
if (failedTests > 0) {
|
|
98
|
+
console.log(` ${chalk.red('✗')} ${failedTests} tests failed`);
|
|
99
|
+
}
|
|
100
|
+
console.log(` ${chalk.gray('📅')} Generated: ${new Date(data.timestamp).toLocaleString()}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Format file path for display
|
|
105
|
+
* @param {string} filePath - File path
|
|
106
|
+
* @returns {string} Formatted path
|
|
107
|
+
*/
|
|
108
|
+
function formatPath(filePath) {
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
if (filePath.startsWith(cwd)) {
|
|
111
|
+
return chalk.gray(filePath.replace(cwd, '.'));
|
|
112
|
+
}
|
|
113
|
+
return chalk.gray(filePath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
success,
|
|
118
|
+
error,
|
|
119
|
+
warning,
|
|
120
|
+
info,
|
|
121
|
+
section,
|
|
122
|
+
spinner,
|
|
123
|
+
printMutationSummary,
|
|
124
|
+
printLineageDataSummary,
|
|
125
|
+
formatPath
|
|
126
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Example file to demonstrate call depth tracking
|
|
2
|
+
|
|
3
|
+
export function directFunction(x: number): number {
|
|
4
|
+
return x * 2; // This will be depth 1 when called directly from tests
|
|
5
|
+
}
|
|
6
|
+
export function oneLevel(x: number): number {
|
|
7
|
+
return directFunction(x) + 1; // directFunction will be depth 2 here
|
|
8
|
+
}
|
|
9
|
+
export function twoLevels(x: number): number {
|
|
10
|
+
return oneLevel(x) + 1; // directFunction will be depth 3 here
|
|
11
|
+
}
|
|
12
|
+
export function threeLevels(x: number): number {
|
|
13
|
+
return twoLevels(x) + 1; // directFunction will be depth 4 here
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Complex function that calls multiple other functions
|
|
17
|
+
export function complexFunction(x: number): number {
|
|
18
|
+
const a = directFunction(x); // depth 2
|
|
19
|
+
const b = oneLevel(x); // directFunction will be depth 3 here
|
|
20
|
+
const c = twoLevels(x); // directFunction will be depth 4 here
|
|
21
|
+
return a + b + c;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Recursive function to test deep call stacks
|
|
25
|
+
export function recursiveFunction(n: number, depth: number = 0): number {
|
|
26
|
+
if (depth >= 3) {
|
|
27
|
+
return directFunction(n); // This will be very deep
|
|
28
|
+
}
|
|
29
|
+
return recursiveFunction(n, depth + 1) + 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Global array to hold leaked memory - this will cause actual memory leaks
|
|
33
|
+
const memoryLeakStorage: any[] = [];
|
|
34
|
+
export function memoryLeakFunction(size: number): number {
|
|
35
|
+
// Create large objects and store them globally (this leaks memory!)
|
|
36
|
+
const largeObject = {
|
|
37
|
+
id: Date.now(),
|
|
38
|
+
data: new Array(size).fill(0).map((_, i) => ({
|
|
39
|
+
index: i,
|
|
40
|
+
value: Math.random(),
|
|
41
|
+
timestamp: new Date(),
|
|
42
|
+
largeString: 'x'.repeat(1000),
|
|
43
|
+
// 1KB string per item
|
|
44
|
+
metadata: {
|
|
45
|
+
created: Date.now(),
|
|
46
|
+
processed: false,
|
|
47
|
+
tags: ['memory', 'leak', 'test', 'large'],
|
|
48
|
+
history: new Array(100).fill(0).map(() => Math.random())
|
|
49
|
+
}
|
|
50
|
+
}))
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Store in global array - this prevents garbage collection (memory leak!)
|
|
54
|
+
memoryLeakStorage.push(largeObject);
|
|
55
|
+
|
|
56
|
+
// Also call our tracked function
|
|
57
|
+
return directFunction(size);
|
|
58
|
+
}
|
|
59
|
+
export function clearMemoryLeaks(): number {
|
|
60
|
+
const count = memoryLeakStorage.length;
|
|
61
|
+
memoryLeakStorage.length = 0; // Clear the array
|
|
62
|
+
return count;
|
|
63
|
+
}
|
|
64
|
+
export function getMemoryLeakCount(): number {
|
|
65
|
+
return memoryLeakStorage.length;
|
|
66
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Examples demonstrating GC pressure and how to fix it
|
|
2
|
+
|
|
3
|
+
// BAD: Creates many small objects (causes GC pressure)
|
|
4
|
+
export function badGCPressureFunction(count: number): number {
|
|
5
|
+
let total = 0;
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < count; i++) {
|
|
8
|
+
// Creating new objects in a loop - BAD for GC
|
|
9
|
+
const tempObj = {
|
|
10
|
+
id: i,
|
|
11
|
+
value: Math.random(),
|
|
12
|
+
timestamp: Date.now(),
|
|
13
|
+
processed: false
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// More object creation
|
|
17
|
+
const result = {
|
|
18
|
+
input: tempObj,
|
|
19
|
+
output: tempObj.value * 2,
|
|
20
|
+
metadata: {
|
|
21
|
+
created: new Date(),
|
|
22
|
+
index: i
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
total += result.output;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// GOOD: Reuses objects (reduces GC pressure)
|
|
33
|
+
export function goodGCPressureFunction(count: number): number {
|
|
34
|
+
let total = 0;
|
|
35
|
+
|
|
36
|
+
// Reuse objects instead of creating new ones
|
|
37
|
+
const reusableObj = {
|
|
38
|
+
id: 0,
|
|
39
|
+
value: 0,
|
|
40
|
+
timestamp: 0,
|
|
41
|
+
processed: false
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const reusableResult = {
|
|
45
|
+
input: reusableObj,
|
|
46
|
+
output: 0,
|
|
47
|
+
metadata: {
|
|
48
|
+
created: new Date(),
|
|
49
|
+
index: 0
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < count; i++) {
|
|
54
|
+
// Reuse existing objects - GOOD for GC
|
|
55
|
+
reusableObj.id = i;
|
|
56
|
+
reusableObj.value = Math.random();
|
|
57
|
+
reusableObj.timestamp = Date.now();
|
|
58
|
+
reusableObj.processed = false;
|
|
59
|
+
|
|
60
|
+
reusableResult.output = reusableObj.value * 2;
|
|
61
|
+
reusableResult.metadata.index = i;
|
|
62
|
+
|
|
63
|
+
total += reusableResult.output;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return total;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// BAD: String concatenation in loop (creates many temporary strings)
|
|
70
|
+
export function badStringConcatenation(items: string[]): string {
|
|
71
|
+
let result = '';
|
|
72
|
+
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
// Each += creates a new string object - BAD for GC
|
|
75
|
+
result += item + ', ';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// GOOD: Use array join (single allocation)
|
|
82
|
+
export function goodStringConcatenation(items: string[]): string {
|
|
83
|
+
// Single allocation - GOOD for GC
|
|
84
|
+
return items.join(', ');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// BAD: Array operations that create many intermediate arrays
|
|
88
|
+
export function badArrayOperations(numbers: number[]): number[] {
|
|
89
|
+
return numbers
|
|
90
|
+
.map(x => x * 2) // Creates new array
|
|
91
|
+
.filter(x => x > 10) // Creates another new array
|
|
92
|
+
.map(x => x + 1) // Creates another new array
|
|
93
|
+
.filter(x => x % 2 === 0); // Creates final array
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// GOOD: Single pass with reduce (one allocation)
|
|
97
|
+
export function goodArrayOperations(numbers: number[]): number[] {
|
|
98
|
+
return numbers.reduce((acc: number[], x: number) => {
|
|
99
|
+
const doubled = x * 2;
|
|
100
|
+
if (doubled > 10) {
|
|
101
|
+
const incremented = doubled + 1;
|
|
102
|
+
if (incremented % 2 === 0) {
|
|
103
|
+
acc.push(incremented);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return acc;
|
|
107
|
+
}, []);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Object pool pattern for heavy reuse
|
|
111
|
+
class ObjectPool<T> {
|
|
112
|
+
private pool: T[] = [];
|
|
113
|
+
private createFn: () => T;
|
|
114
|
+
private resetFn: (obj: T) => void;
|
|
115
|
+
|
|
116
|
+
constructor(createFn: () => T, resetFn: (obj: T) => void, initialSize: number = 10) {
|
|
117
|
+
this.createFn = createFn;
|
|
118
|
+
this.resetFn = resetFn;
|
|
119
|
+
|
|
120
|
+
// Pre-allocate objects
|
|
121
|
+
for (let i = 0; i < initialSize; i++) {
|
|
122
|
+
this.pool.push(this.createFn());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
acquire(): T {
|
|
127
|
+
return this.pool.pop() || this.createFn();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
release(obj: T): void {
|
|
131
|
+
this.resetFn(obj);
|
|
132
|
+
this.pool.push(obj);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Example using object pool
|
|
137
|
+
export function objectPoolExample(count: number): number {
|
|
138
|
+
const pool = new ObjectPool(
|
|
139
|
+
() => ({ id: 0, value: 0, result: 0 }),
|
|
140
|
+
(obj) => { obj.id = 0; obj.value = 0; obj.result = 0; },
|
|
141
|
+
50
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
let total = 0;
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < count; i++) {
|
|
147
|
+
const obj = pool.acquire();
|
|
148
|
+
obj.id = i;
|
|
149
|
+
obj.value = Math.random();
|
|
150
|
+
obj.result = obj.value * 2;
|
|
151
|
+
|
|
152
|
+
total += obj.result;
|
|
153
|
+
|
|
154
|
+
pool.release(obj); // Return to pool for reuse
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return total;
|
|
158
|
+
}
|