jest-test-lineage-reporter 2.0.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/LICENSE +21 -0
- package/README.md +822 -0
- package/babel.config.js +22 -0
- package/package.json +73 -0
- package/src/TestCoverageReporter.js +3307 -0
- package/src/babel-plugin-lineage-tracker.js +290 -0
- package/src/config.js +193 -0
- package/src/testSetup.js +943 -0
package/src/testSetup.js
ADDED
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest setup file to enable precise per-test tracking
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Test Quality Analysis Functions
|
|
6
|
+
function analyzeTestQuality(testFunction, testName) {
|
|
7
|
+
const functionString = testFunction.toString();
|
|
8
|
+
const qualityMetrics = {
|
|
9
|
+
assertions: 0,
|
|
10
|
+
asyncOperations: 0,
|
|
11
|
+
mockUsage: 0,
|
|
12
|
+
errorHandling: 0,
|
|
13
|
+
edgeCases: 0,
|
|
14
|
+
complexity: 0,
|
|
15
|
+
maintainability: 0,
|
|
16
|
+
reliability: 0,
|
|
17
|
+
testSmells: [],
|
|
18
|
+
codePatterns: [],
|
|
19
|
+
dependencies: new Set(),
|
|
20
|
+
isolationScore: 0,
|
|
21
|
+
testLength: functionString.split('\n').length,
|
|
22
|
+
setupTeardown: 0
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Count assertions - more comprehensive patterns
|
|
26
|
+
const assertionPatterns = [
|
|
27
|
+
/expect\(/g, // expect(value)
|
|
28
|
+
/\.toBe\(/g, // .toBe(value)
|
|
29
|
+
/\.toEqual\(/g, // .toEqual(value)
|
|
30
|
+
/\.toMatch\(/g, // .toMatch(pattern)
|
|
31
|
+
/\.toContain\(/g, // .toContain(item)
|
|
32
|
+
/\.toThrow\(/g, // .toThrow()
|
|
33
|
+
/\.toHaveLength\(/g, // .toHaveLength(number)
|
|
34
|
+
/\.toBeGreaterThan\(/g, // .toBeGreaterThan(number)
|
|
35
|
+
/\.toBeGreaterThanOrEqual\(/g, // .toBeGreaterThanOrEqual(number)
|
|
36
|
+
/\.toBeLessThan\(/g, // .toBeLessThan(number)
|
|
37
|
+
/\.toBeLessThanOrEqual\(/g, // .toBeLessThanOrEqual(number)
|
|
38
|
+
/\.toBeCloseTo\(/g, // .toBeCloseTo(number)
|
|
39
|
+
/\.toHaveProperty\(/g, // .toHaveProperty(key)
|
|
40
|
+
/\.toBeNull\(/g, // .toBeNull()
|
|
41
|
+
/\.toBeUndefined\(/g, // .toBeUndefined()
|
|
42
|
+
/\.toBeDefined\(/g, // .toBeDefined()
|
|
43
|
+
/\.toBeTruthy\(/g, // .toBeTruthy()
|
|
44
|
+
/\.toBeFalsy\(/g, // .toBeFalsy()
|
|
45
|
+
/\.not\.toThrow\(/g, // .not.toThrow()
|
|
46
|
+
/\.not\.toBe\(/g, // .not.toBe()
|
|
47
|
+
/assert\(/g, // assert(condition)
|
|
48
|
+
/should\./g, // should.be.true
|
|
49
|
+
/\.to\./g, // chai assertions
|
|
50
|
+
/\.be\./g // chai assertions
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
let totalAssertions = 0;
|
|
54
|
+
assertionPatterns.forEach(pattern => {
|
|
55
|
+
const matches = functionString.match(pattern);
|
|
56
|
+
if (matches) {
|
|
57
|
+
totalAssertions += matches.length;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
qualityMetrics.assertions = totalAssertions;
|
|
61
|
+
|
|
62
|
+
// Count async operations
|
|
63
|
+
const asyncPatterns = [
|
|
64
|
+
/await\s+/g, /\.then\(/g, /\.catch\(/g, /Promise\./g, /async\s+/g,
|
|
65
|
+
/setTimeout\(/g, /setInterval\(/g, /requestAnimationFrame\(/g
|
|
66
|
+
];
|
|
67
|
+
asyncPatterns.forEach(pattern => {
|
|
68
|
+
const matches = functionString.match(pattern);
|
|
69
|
+
if (matches) qualityMetrics.asyncOperations += matches.length;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Count mock usage
|
|
73
|
+
const mockPatterns = [
|
|
74
|
+
/jest\.mock\(/g, /jest\.spyOn\(/g, /\.mockImplementation\(/g, /\.mockReturnValue\(/g,
|
|
75
|
+
/\.mockResolvedValue\(/g, /\.mockRejectedValue\(/g, /sinon\./g, /stub\(/g, /spy\(/g
|
|
76
|
+
];
|
|
77
|
+
mockPatterns.forEach(pattern => {
|
|
78
|
+
const matches = functionString.match(pattern);
|
|
79
|
+
if (matches) qualityMetrics.mockUsage += matches.length;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Count error handling
|
|
83
|
+
const errorPatterns = [
|
|
84
|
+
/try\s*\{/g, /catch\s*\(/g, /throw\s+/g, /toThrow\(/g, /toThrowError\(/g,
|
|
85
|
+
/\.rejects\./g, /\.resolves\./g
|
|
86
|
+
];
|
|
87
|
+
errorPatterns.forEach(pattern => {
|
|
88
|
+
const matches = functionString.match(pattern);
|
|
89
|
+
if (matches) qualityMetrics.errorHandling += matches.length;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Detect edge cases
|
|
93
|
+
const edgeCasePatterns = [
|
|
94
|
+
/null/g, /undefined/g, /empty/g, /zero/g, /negative/g, /boundary/g,
|
|
95
|
+
/edge/g, /limit/g, /max/g, /min/g, /invalid/g, /error/g
|
|
96
|
+
];
|
|
97
|
+
edgeCasePatterns.forEach(pattern => {
|
|
98
|
+
const matches = functionString.match(pattern);
|
|
99
|
+
if (matches) qualityMetrics.edgeCases += matches.length;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Calculate complexity (cyclomatic complexity approximation)
|
|
103
|
+
const complexityPatterns = [
|
|
104
|
+
/if\s*\(/g, /else/g, /for\s*\(/g, /while\s*\(/g, /switch\s*\(/g,
|
|
105
|
+
/case\s+/g, /catch\s*\(/g, /&&/g, /\|\|/g, /\?/g
|
|
106
|
+
];
|
|
107
|
+
complexityPatterns.forEach(pattern => {
|
|
108
|
+
const matches = functionString.match(pattern);
|
|
109
|
+
if (matches) qualityMetrics.complexity += matches.length;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Detect test smells
|
|
113
|
+
if (qualityMetrics.testLength > 50) {
|
|
114
|
+
qualityMetrics.testSmells.push('Long Test');
|
|
115
|
+
}
|
|
116
|
+
if (qualityMetrics.assertions === 0) {
|
|
117
|
+
qualityMetrics.testSmells.push('No Assertions');
|
|
118
|
+
}
|
|
119
|
+
if (qualityMetrics.assertions > 10) {
|
|
120
|
+
qualityMetrics.testSmells.push('Too Many Assertions');
|
|
121
|
+
}
|
|
122
|
+
if (functionString.includes('sleep') || functionString.includes('wait')) {
|
|
123
|
+
qualityMetrics.testSmells.push('Sleep/Wait Usage');
|
|
124
|
+
}
|
|
125
|
+
if (qualityMetrics.mockUsage > 5) {
|
|
126
|
+
qualityMetrics.testSmells.push('Excessive Mocking');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Calculate maintainability score (0-100)
|
|
130
|
+
let maintainabilityScore = 100;
|
|
131
|
+
maintainabilityScore -= Math.min(qualityMetrics.testLength * 0.5, 25);
|
|
132
|
+
maintainabilityScore -= Math.min(qualityMetrics.complexity * 2, 20);
|
|
133
|
+
maintainabilityScore -= qualityMetrics.testSmells.length * 10;
|
|
134
|
+
maintainabilityScore += Math.min(qualityMetrics.assertions * 2, 20);
|
|
135
|
+
qualityMetrics.maintainability = Math.max(0, maintainabilityScore);
|
|
136
|
+
|
|
137
|
+
// Calculate reliability score (0-100)
|
|
138
|
+
let reliabilityScore = 50;
|
|
139
|
+
reliabilityScore += Math.min(qualityMetrics.assertions * 5, 30);
|
|
140
|
+
reliabilityScore += Math.min(qualityMetrics.errorHandling * 10, 20);
|
|
141
|
+
reliabilityScore += Math.min(qualityMetrics.edgeCases * 3, 15);
|
|
142
|
+
reliabilityScore -= qualityMetrics.testSmells.length * 5;
|
|
143
|
+
qualityMetrics.reliability = Math.max(0, Math.min(100, reliabilityScore));
|
|
144
|
+
|
|
145
|
+
// Calculate isolation score (0-100)
|
|
146
|
+
let isolationScore = 100;
|
|
147
|
+
isolationScore -= Math.min(qualityMetrics.mockUsage * 5, 30);
|
|
148
|
+
isolationScore -= qualityMetrics.dependencies.size * 10;
|
|
149
|
+
qualityMetrics.isolationScore = Math.max(0, isolationScore);
|
|
150
|
+
|
|
151
|
+
return qualityMetrics;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if lineage tracking is enabled
|
|
155
|
+
const isEnabled = process.env.JEST_LINEAGE_ENABLED !== 'false';
|
|
156
|
+
const isTrackingEnabled = process.env.JEST_LINEAGE_TRACKING !== 'false';
|
|
157
|
+
const isPerformanceEnabled = process.env.JEST_LINEAGE_PERFORMANCE !== 'false';
|
|
158
|
+
const isQualityEnabled = process.env.JEST_LINEAGE_QUALITY !== 'false';
|
|
159
|
+
|
|
160
|
+
// Global tracker for test coverage
|
|
161
|
+
global.__TEST_LINEAGE_TRACKER__ = {
|
|
162
|
+
currentTest: null,
|
|
163
|
+
testCoverage: new Map(),
|
|
164
|
+
isTracking: isEnabled && isTrackingEnabled,
|
|
165
|
+
isPerformanceTracking: isEnabled && isPerformanceEnabled,
|
|
166
|
+
isQualityTracking: isEnabled && isQualityEnabled,
|
|
167
|
+
currentTestFile: null, // Track current test file
|
|
168
|
+
config: {
|
|
169
|
+
enabled: isEnabled,
|
|
170
|
+
lineageTracking: isTrackingEnabled,
|
|
171
|
+
performanceTracking: isPerformanceEnabled,
|
|
172
|
+
qualityTracking: isQualityEnabled
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Store original test functions
|
|
177
|
+
const originalIt = global.it;
|
|
178
|
+
const originalTest = global.test;
|
|
179
|
+
|
|
180
|
+
// Enhanced test wrapper that tracks coverage per individual test
|
|
181
|
+
function createTestWrapper(originalFn, testType) {
|
|
182
|
+
return function wrappedTest(testName, testFn, timeout) {
|
|
183
|
+
// If no test function provided, it's a pending test
|
|
184
|
+
if (!testFn) {
|
|
185
|
+
return originalFn(testName, testFn, timeout);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Wrap the test function with tracking
|
|
189
|
+
const wrappedTestFn = async function(...args) {
|
|
190
|
+
// Get the current test file path from Jest's context
|
|
191
|
+
let testFilePath = 'unknown';
|
|
192
|
+
try {
|
|
193
|
+
// Method 1: Try expect.getState() - this is the most reliable method
|
|
194
|
+
const expectState = expect.getState();
|
|
195
|
+
if (expectState && expectState.testPath) {
|
|
196
|
+
testFilePath = expectState.testPath;
|
|
197
|
+
}
|
|
198
|
+
// Method 2: Try global Jest context
|
|
199
|
+
else if (global.jasmine && global.jasmine.testPath) {
|
|
200
|
+
testFilePath = global.jasmine.testPath;
|
|
201
|
+
}
|
|
202
|
+
// Method 3: Use stack trace to find test file as fallback
|
|
203
|
+
else {
|
|
204
|
+
const stack = new Error().stack;
|
|
205
|
+
|
|
206
|
+
// Look for test file patterns in the stack trace
|
|
207
|
+
const testFilePatterns = [
|
|
208
|
+
/at.*\/([^\/]+\.test\.[jt]s):/,
|
|
209
|
+
/at.*\/([^\/]+\.spec\.[jt]s):/,
|
|
210
|
+
/at.*\/(src\/__tests__\/[^:]+\.test\.[jt]s):/,
|
|
211
|
+
/at.*\/(src\/__tests__\/[^:]+\.spec\.[jt]s):/,
|
|
212
|
+
/at.*\/(__tests__\/[^:]+\.test\.[jt]s):/,
|
|
213
|
+
/at.*\/(__tests__\/[^:]+\.spec\.[jt]s):/
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
for (const pattern of testFilePatterns) {
|
|
217
|
+
const match = stack.match(pattern);
|
|
218
|
+
if (match) {
|
|
219
|
+
testFilePath = match[1];
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
testFilePath = 'unknown';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Start tracking for this specific test
|
|
229
|
+
global.__TEST_LINEAGE_TRACKER__.currentTest = {
|
|
230
|
+
name: testName,
|
|
231
|
+
type: testType,
|
|
232
|
+
testFile: testFilePath,
|
|
233
|
+
startTime: Date.now(),
|
|
234
|
+
coverage: new Map(),
|
|
235
|
+
qualityMetrics: {
|
|
236
|
+
assertions: 0,
|
|
237
|
+
asyncOperations: 0,
|
|
238
|
+
mockUsage: 0,
|
|
239
|
+
errorHandling: 0,
|
|
240
|
+
edgeCases: 0,
|
|
241
|
+
complexity: 0,
|
|
242
|
+
maintainability: 0,
|
|
243
|
+
reliability: 0,
|
|
244
|
+
testSmells: [],
|
|
245
|
+
codePatterns: [],
|
|
246
|
+
dependencies: new Set(),
|
|
247
|
+
isolationScore: 0,
|
|
248
|
+
testLength: 0,
|
|
249
|
+
setupTeardown: 0
|
|
250
|
+
},
|
|
251
|
+
startMetrics: capturePerformanceMetrics()
|
|
252
|
+
};
|
|
253
|
+
global.__TEST_LINEAGE_TRACKER__.isTracking = true;
|
|
254
|
+
|
|
255
|
+
// Analyze test quality
|
|
256
|
+
if (typeof testFn === 'function') {
|
|
257
|
+
global.__TEST_LINEAGE_TRACKER__.currentTest.qualityMetrics = analyzeTestQuality(testFn, testName);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// Execute the actual test
|
|
262
|
+
const result = await testFn.apply(this, args);
|
|
263
|
+
|
|
264
|
+
// Store the coverage data for this test
|
|
265
|
+
const testId = `${testName}::${Date.now()}`;
|
|
266
|
+
const testData = {
|
|
267
|
+
name: testName,
|
|
268
|
+
type: testType,
|
|
269
|
+
testFile: global.__TEST_LINEAGE_TRACKER__.currentTest.testFile,
|
|
270
|
+
duration: Date.now() - global.__TEST_LINEAGE_TRACKER__.currentTest.startTime,
|
|
271
|
+
coverage: new Map(global.__TEST_LINEAGE_TRACKER__.currentTest.coverage),
|
|
272
|
+
qualityMetrics: global.__TEST_LINEAGE_TRACKER__.currentTest.qualityMetrics
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
global.__TEST_LINEAGE_TRACKER__.testCoverage.set(testId, testData);
|
|
276
|
+
|
|
277
|
+
// Skip storing persistent data and writing files during mutation testing
|
|
278
|
+
if (process.env.JEST_LINEAGE_MUTATION !== 'true') {
|
|
279
|
+
// Also store in a more persistent way for the reporter
|
|
280
|
+
if (!global.__LINEAGE_PERSISTENT_DATA__) {
|
|
281
|
+
global.__LINEAGE_PERSISTENT_DATA__ = [];
|
|
282
|
+
}
|
|
283
|
+
global.__LINEAGE_PERSISTENT_DATA__.push(testData);
|
|
284
|
+
|
|
285
|
+
// Write to file for reporter to read
|
|
286
|
+
writeTrackingDataToFile();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return result;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// Still store coverage data even if test fails
|
|
292
|
+
const testId = `${testName}::${Date.now()}::FAILED`;
|
|
293
|
+
global.__TEST_LINEAGE_TRACKER__.testCoverage.set(testId, {
|
|
294
|
+
name: testName,
|
|
295
|
+
type: testType,
|
|
296
|
+
testFile: global.__TEST_LINEAGE_TRACKER__.currentTest.testFile,
|
|
297
|
+
duration: Date.now() - global.__TEST_LINEAGE_TRACKER__.currentTest.startTime,
|
|
298
|
+
coverage: new Map(global.__TEST_LINEAGE_TRACKER__.currentTest.coverage),
|
|
299
|
+
qualityMetrics: global.__TEST_LINEAGE_TRACKER__.currentTest.qualityMetrics,
|
|
300
|
+
failed: true
|
|
301
|
+
});
|
|
302
|
+
throw error;
|
|
303
|
+
} finally {
|
|
304
|
+
// Clean up tracking state
|
|
305
|
+
global.__TEST_LINEAGE_TRACKER__.currentTest = null;
|
|
306
|
+
global.__TEST_LINEAGE_TRACKER__.isTracking = false;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Call original function with wrapped test
|
|
311
|
+
return originalFn(testName, wrappedTestFn, timeout);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Cache for discovered source directories to avoid repeated filesystem operations
|
|
316
|
+
const sourceDirectoryCache = new Map();
|
|
317
|
+
|
|
318
|
+
// Find the project root by looking for package.json
|
|
319
|
+
function findProjectRoot(startPath) {
|
|
320
|
+
const path = require('path');
|
|
321
|
+
const fs = require('fs');
|
|
322
|
+
|
|
323
|
+
let currentDir = startPath;
|
|
324
|
+
const root = path.parse(currentDir).root;
|
|
325
|
+
|
|
326
|
+
while (currentDir !== root) {
|
|
327
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
328
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
329
|
+
return currentDir;
|
|
330
|
+
}
|
|
331
|
+
currentDir = path.dirname(currentDir);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fallback to current working directory if no package.json found
|
|
335
|
+
return process.cwd();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Smart auto-detection of source file paths using package.json as root
|
|
339
|
+
function resolveSourceFilePath(relativeFilePath) {
|
|
340
|
+
const path = require('path');
|
|
341
|
+
const fs = require('fs');
|
|
342
|
+
|
|
343
|
+
// Check cache first
|
|
344
|
+
const cacheKey = relativeFilePath;
|
|
345
|
+
if (sourceDirectoryCache.has(cacheKey)) {
|
|
346
|
+
return sourceDirectoryCache.get(cacheKey);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Find project root using package.json
|
|
350
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
351
|
+
|
|
352
|
+
// Strategy 1: Try relative to project root (most reliable)
|
|
353
|
+
const projectRelativePath = path.resolve(projectRoot, relativeFilePath);
|
|
354
|
+
if (fs.existsSync(projectRelativePath)) {
|
|
355
|
+
sourceDirectoryCache.set(cacheKey, projectRelativePath);
|
|
356
|
+
return projectRelativePath;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Strategy 2: Try relative to current working directory
|
|
360
|
+
const cwdRelativePath = path.resolve(process.cwd(), relativeFilePath);
|
|
361
|
+
if (fs.existsSync(cwdRelativePath)) {
|
|
362
|
+
sourceDirectoryCache.set(cacheKey, cwdRelativePath);
|
|
363
|
+
return cwdRelativePath;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Strategy 3: Auto-discover by scanning from project root
|
|
367
|
+
const filename = path.basename(relativeFilePath);
|
|
368
|
+
const discoveredPaths = discoverSourceDirectories(projectRoot, filename);
|
|
369
|
+
|
|
370
|
+
for (const discoveredPath of discoveredPaths) {
|
|
371
|
+
if (fs.existsSync(discoveredPath)) {
|
|
372
|
+
sourceDirectoryCache.set(cacheKey, discoveredPath);
|
|
373
|
+
return discoveredPath;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Strategy 4: Fallback to common patterns from project root
|
|
378
|
+
const commonPatterns = [
|
|
379
|
+
path.resolve(projectRoot, 'src', filename),
|
|
380
|
+
path.resolve(projectRoot, 'lib', filename),
|
|
381
|
+
path.resolve(projectRoot, 'source', filename),
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
for (const fallbackPath of commonPatterns) {
|
|
385
|
+
if (fs.existsSync(fallbackPath)) {
|
|
386
|
+
sourceDirectoryCache.set(cacheKey, fallbackPath);
|
|
387
|
+
return fallbackPath;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// If still not found, return the project relative path for error reporting
|
|
392
|
+
sourceDirectoryCache.set(cacheKey, projectRelativePath);
|
|
393
|
+
return projectRelativePath;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Auto-discover source directories by scanning the project structure
|
|
397
|
+
function discoverSourceDirectories(projectRoot, targetFilename) {
|
|
398
|
+
const path = require('path');
|
|
399
|
+
const fs = require('fs');
|
|
400
|
+
|
|
401
|
+
const discoveredPaths = [];
|
|
402
|
+
const maxDepth = 3; // Limit search depth for performance
|
|
403
|
+
|
|
404
|
+
function scanDirectory(dir, depth = 0) {
|
|
405
|
+
if (depth > maxDepth) return;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
409
|
+
|
|
410
|
+
for (const entry of entries) {
|
|
411
|
+
if (entry.isDirectory()) {
|
|
412
|
+
const dirName = entry.name;
|
|
413
|
+
|
|
414
|
+
// Skip common non-source directories
|
|
415
|
+
if (shouldSkipDirectory(dirName)) continue;
|
|
416
|
+
|
|
417
|
+
const fullDirPath = path.join(dir, dirName);
|
|
418
|
+
|
|
419
|
+
// Check if target file exists in this directory
|
|
420
|
+
const potentialFilePath = path.join(fullDirPath, targetFilename);
|
|
421
|
+
if (fs.existsSync(potentialFilePath)) {
|
|
422
|
+
discoveredPaths.push(potentialFilePath);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Recursively scan subdirectories
|
|
426
|
+
scanDirectory(fullDirPath, depth + 1);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} catch (error) {
|
|
430
|
+
// Skip directories we can't read
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
scanDirectory(projectRoot);
|
|
435
|
+
|
|
436
|
+
// Sort by preference (shorter paths first, then by common source directory names)
|
|
437
|
+
return discoveredPaths.sort((a, b) => {
|
|
438
|
+
const aDepth = a.split(path.sep).length;
|
|
439
|
+
const bDepth = b.split(path.sep).length;
|
|
440
|
+
|
|
441
|
+
if (aDepth !== bDepth) {
|
|
442
|
+
return aDepth - bDepth; // Prefer shorter paths
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Prefer common source directory names
|
|
446
|
+
const sourcePreference = ['src', 'lib', 'source', 'app'];
|
|
447
|
+
const aScore = getSourceDirectoryScore(a, sourcePreference);
|
|
448
|
+
const bScore = getSourceDirectoryScore(b, sourcePreference);
|
|
449
|
+
|
|
450
|
+
return bScore - aScore; // Higher score first
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Check if a directory should be skipped during auto-discovery
|
|
455
|
+
function shouldSkipDirectory(dirName) {
|
|
456
|
+
const skipPatterns = [
|
|
457
|
+
'node_modules',
|
|
458
|
+
'.git',
|
|
459
|
+
'.next',
|
|
460
|
+
'.nuxt',
|
|
461
|
+
'dist',
|
|
462
|
+
'build',
|
|
463
|
+
'coverage',
|
|
464
|
+
'.nyc_output',
|
|
465
|
+
'tmp',
|
|
466
|
+
'temp',
|
|
467
|
+
'.cache',
|
|
468
|
+
'.vscode',
|
|
469
|
+
'.idea',
|
|
470
|
+
'__pycache__',
|
|
471
|
+
'.pytest_cache',
|
|
472
|
+
'vendor',
|
|
473
|
+
'target',
|
|
474
|
+
'bin',
|
|
475
|
+
'obj'
|
|
476
|
+
];
|
|
477
|
+
|
|
478
|
+
return skipPatterns.includes(dirName) || dirName.startsWith('.');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Score source directories by preference
|
|
482
|
+
function getSourceDirectoryScore(filePath, sourcePreference) {
|
|
483
|
+
const pathParts = filePath.split(path.sep);
|
|
484
|
+
let score = 0;
|
|
485
|
+
|
|
486
|
+
for (const part of pathParts) {
|
|
487
|
+
const index = sourcePreference.indexOf(part);
|
|
488
|
+
if (index !== -1) {
|
|
489
|
+
score += sourcePreference.length - index; // Higher score for preferred names
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return score;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// High-resolution performance measurement
|
|
497
|
+
function capturePerformanceMetrics() {
|
|
498
|
+
const hrTime = process.hrtime.bigint();
|
|
499
|
+
const cpuUsage = process.cpuUsage();
|
|
500
|
+
const memUsage = process.memoryUsage();
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
wallTime: Number(hrTime) / 1000000, // Convert to microseconds
|
|
504
|
+
cpuTime: (cpuUsage.user + cpuUsage.system) / 1000, // Convert to microseconds
|
|
505
|
+
memoryUsage: memUsage.heapUsed,
|
|
506
|
+
timestamp: Date.now()
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Estimate CPU cycles based on timing and system characteristics
|
|
511
|
+
function estimateCpuCycles(cpuTimeMicros, wallTimeMicros) {
|
|
512
|
+
// Get CPU frequency estimate (this is approximate)
|
|
513
|
+
const estimatedCpuFrequencyGHz = getCpuFrequencyEstimate();
|
|
514
|
+
|
|
515
|
+
// Calculate cycles: CPU time (seconds) * frequency (cycles/second)
|
|
516
|
+
const cpuTimeSeconds = cpuTimeMicros / 1000000;
|
|
517
|
+
const estimatedCycles = Math.round(cpuTimeSeconds * estimatedCpuFrequencyGHz * 1000000000);
|
|
518
|
+
|
|
519
|
+
return estimatedCycles;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Estimate CPU frequency (this is a rough approximation)
|
|
523
|
+
function getCpuFrequencyEstimate() {
|
|
524
|
+
// Try to get CPU info from Node.js (if available)
|
|
525
|
+
try {
|
|
526
|
+
const os = require('os');
|
|
527
|
+
const cpus = os.cpus();
|
|
528
|
+
if (cpus && cpus.length > 0 && cpus[0].speed) {
|
|
529
|
+
return cpus[0].speed / 1000; // Convert MHz to GHz
|
|
530
|
+
}
|
|
531
|
+
} catch (error) {
|
|
532
|
+
// Fallback if CPU info not available
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Fallback to common CPU frequency estimate (2.5 GHz)
|
|
536
|
+
return 2.5;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Advanced performance profiling for hotspot detection
|
|
540
|
+
function profileLineExecution(filePath, lineNumber, executionCallback) {
|
|
541
|
+
const startMetrics = capturePerformanceMetrics();
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const result = executionCallback();
|
|
545
|
+
|
|
546
|
+
const endMetrics = capturePerformanceMetrics();
|
|
547
|
+
const performanceProfile = {
|
|
548
|
+
wallTime: endMetrics.wallTime - startMetrics.wallTime,
|
|
549
|
+
cpuTime: endMetrics.cpuTime - startMetrics.cpuTime,
|
|
550
|
+
memoryDelta: endMetrics.memoryUsage - startMetrics.memoryUsage,
|
|
551
|
+
cpuCycles: estimateCpuCycles(
|
|
552
|
+
endMetrics.cpuTime - startMetrics.cpuTime,
|
|
553
|
+
endMetrics.wallTime - startMetrics.wallTime
|
|
554
|
+
)
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
return { result, performanceProfile };
|
|
558
|
+
} catch (error) {
|
|
559
|
+
const endMetrics = capturePerformanceMetrics();
|
|
560
|
+
const performanceProfile = {
|
|
561
|
+
wallTime: endMetrics.wallTime - startMetrics.wallTime,
|
|
562
|
+
cpuTime: endMetrics.cpuTime - startMetrics.cpuTime,
|
|
563
|
+
memoryDelta: endMetrics.memoryUsage - startMetrics.memoryUsage,
|
|
564
|
+
cpuCycles: estimateCpuCycles(
|
|
565
|
+
endMetrics.cpuTime - startMetrics.cpuTime,
|
|
566
|
+
endMetrics.wallTime - startMetrics.wallTime
|
|
567
|
+
),
|
|
568
|
+
error: error.message
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
throw { originalError: error, performanceProfile };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Calculate call depth by analyzing the JavaScript call stack
|
|
576
|
+
function calculateCallDepth() {
|
|
577
|
+
try {
|
|
578
|
+
// Create a stack trace
|
|
579
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
580
|
+
Error.prepareStackTrace = (_, stack) => stack;
|
|
581
|
+
const stack = new Error().stack;
|
|
582
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
583
|
+
|
|
584
|
+
if (!Array.isArray(stack)) {
|
|
585
|
+
return 1; // Fallback if stack trace is not available
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Filter out internal tracking functions and Jest infrastructure
|
|
589
|
+
const relevantFrames = stack.filter(frame => {
|
|
590
|
+
const fileName = frame.getFileName();
|
|
591
|
+
const functionName = frame.getFunctionName() || '';
|
|
592
|
+
|
|
593
|
+
// Skip internal tracking functions
|
|
594
|
+
if (functionName.includes('__TRACK_LINE_EXECUTION__') ||
|
|
595
|
+
functionName.includes('calculateCallDepth') ||
|
|
596
|
+
functionName.includes('resolveSourceFilePath')) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Skip Jest internal functions
|
|
601
|
+
if (fileName && (
|
|
602
|
+
fileName.includes('jest-runner') ||
|
|
603
|
+
fileName.includes('jest-runtime') ||
|
|
604
|
+
fileName.includes('jest-environment') ||
|
|
605
|
+
fileName.includes('testSetup.js') ||
|
|
606
|
+
fileName.includes('node_modules/jest') ||
|
|
607
|
+
fileName.includes('babel-plugin-lineage-tracker')
|
|
608
|
+
)) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Skip Node.js internal functions
|
|
613
|
+
if (!fileName || fileName.startsWith('node:') || fileName.includes('internal/')) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return true;
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Calculate depth based on relevant frames
|
|
621
|
+
// Depth 1 = called directly from test
|
|
622
|
+
// Depth 2 = called from a function that was called from test
|
|
623
|
+
// etc.
|
|
624
|
+
const depth = Math.max(1, relevantFrames.length - 1);
|
|
625
|
+
|
|
626
|
+
// Cap the depth at a reasonable maximum
|
|
627
|
+
return Math.min(depth, 10);
|
|
628
|
+
|
|
629
|
+
} catch (error) {
|
|
630
|
+
// Fallback to depth 1 if stack trace analysis fails
|
|
631
|
+
return 1;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Method to write tracking data to file
|
|
636
|
+
function writeTrackingDataToFile() {
|
|
637
|
+
// Skip writing during mutation testing to avoid creating reports
|
|
638
|
+
if (process.env.JEST_LINEAGE_MUTATION === 'true') {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const fs = require('fs');
|
|
643
|
+
const path = require('path');
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const filePath = path.join(process.cwd(), '.jest-lineage-data.json');
|
|
647
|
+
|
|
648
|
+
// Check if we should merge with existing data (default: false - recreate from scratch)
|
|
649
|
+
const shouldMerge = process.env.JEST_LINEAGE_MERGE === 'true';
|
|
650
|
+
|
|
651
|
+
let existingData = { timestamp: Date.now(), tests: [] };
|
|
652
|
+
if (shouldMerge && fs.existsSync(filePath)) {
|
|
653
|
+
try {
|
|
654
|
+
existingData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
655
|
+
} catch (e) {
|
|
656
|
+
// If file is corrupted, start fresh
|
|
657
|
+
existingData = { timestamp: Date.now(), tests: [] };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const tests = global.__LINEAGE_PERSISTENT_DATA__ || [];
|
|
662
|
+
|
|
663
|
+
// Don't write if we have no tests or if all tests have empty coverage
|
|
664
|
+
if (tests.length === 0) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Check if any test has actual coverage data
|
|
669
|
+
const hasAnyCoverage = tests.some(test => test.coverage && test.coverage.size > 0);
|
|
670
|
+
if (!hasAnyCoverage) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Convert Map objects to plain objects for JSON serialization
|
|
675
|
+
const serializedTests = tests.map(testData => ({
|
|
676
|
+
name: testData.name,
|
|
677
|
+
type: testData.type,
|
|
678
|
+
testFile: testData.testFile,
|
|
679
|
+
duration: testData.duration,
|
|
680
|
+
coverage: testData.coverage instanceof Map ? Object.fromEntries(testData.coverage) : testData.coverage,
|
|
681
|
+
qualityMetrics: testData.qualityMetrics || {
|
|
682
|
+
assertions: 0,
|
|
683
|
+
asyncOperations: 0,
|
|
684
|
+
mockUsage: 0,
|
|
685
|
+
errorHandling: 0,
|
|
686
|
+
edgeCases: 0,
|
|
687
|
+
complexity: 0,
|
|
688
|
+
maintainability: 50,
|
|
689
|
+
reliability: 50,
|
|
690
|
+
testSmells: [],
|
|
691
|
+
codePatterns: [],
|
|
692
|
+
isolationScore: 100,
|
|
693
|
+
testLength: 0
|
|
694
|
+
}
|
|
695
|
+
}));
|
|
696
|
+
|
|
697
|
+
let dataToWrite;
|
|
698
|
+
if (shouldMerge) {
|
|
699
|
+
// Merge with existing data (replace tests with same name to get latest coverage data)
|
|
700
|
+
const existingTestsByName = new Map(existingData.tests.map(t => [t.name, t]));
|
|
701
|
+
|
|
702
|
+
// Add/replace tests with new data
|
|
703
|
+
serializedTests.forEach(newTest => {
|
|
704
|
+
existingTestsByName.set(newTest.name, newTest);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
dataToWrite = {
|
|
708
|
+
timestamp: Date.now(),
|
|
709
|
+
tests: Array.from(existingTestsByName.values())
|
|
710
|
+
};
|
|
711
|
+
} else {
|
|
712
|
+
// Recreate from scratch (default behavior)
|
|
713
|
+
dataToWrite = {
|
|
714
|
+
timestamp: Date.now(),
|
|
715
|
+
tests: serializedTests
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
fs.writeFileSync(filePath, JSON.stringify(dataToWrite, null, 2));
|
|
720
|
+
|
|
721
|
+
// console.log(`📝 Wrote tracking data: ${dataToWrite.tests.length} total tests to ${filePath}`);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
console.warn('Warning: Could not write tracking data to file:', error.message);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Replace global test functions with our wrapped versions
|
|
728
|
+
global.it = createTestWrapper(originalIt, 'it');
|
|
729
|
+
global.test = createTestWrapper(originalTest, 'test');
|
|
730
|
+
|
|
731
|
+
// Copy over any additional properties from original functions
|
|
732
|
+
if (originalIt) {
|
|
733
|
+
Object.keys(originalIt).forEach(key => {
|
|
734
|
+
if (typeof originalIt[key] === 'function') {
|
|
735
|
+
global.it[key] = originalIt[key];
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (originalTest) {
|
|
741
|
+
Object.keys(originalTest).forEach(key => {
|
|
742
|
+
if (typeof originalTest[key] === 'function') {
|
|
743
|
+
global.test[key] = originalTest[key];
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Function for line execution tracking with call depth and performance analysis
|
|
749
|
+
global.__TRACK_LINE_EXECUTION__ = function(filePath, lineNumber, nodeType) {
|
|
750
|
+
// Skip tracking during mutation testing to avoid conflicts
|
|
751
|
+
if (process.env.JEST_LINEAGE_MUTATION === 'true') {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (global.__TEST_LINEAGE_TRACKER__.isTracking &&
|
|
756
|
+
global.__TEST_LINEAGE_TRACKER__.currentTest) {
|
|
757
|
+
|
|
758
|
+
// High-resolution performance measurement
|
|
759
|
+
const performanceStart = capturePerformanceMetrics();
|
|
760
|
+
|
|
761
|
+
// Convert relative path to full path using dynamic resolution
|
|
762
|
+
const fullPath = resolveSourceFilePath(filePath);
|
|
763
|
+
const key = `${fullPath}:${lineNumber}`;
|
|
764
|
+
|
|
765
|
+
// Calculate call depth by analyzing the call stack
|
|
766
|
+
const callDepth = calculateCallDepth();
|
|
767
|
+
|
|
768
|
+
// Store execution with depth information
|
|
769
|
+
const currentCount = global.__TEST_LINEAGE_TRACKER__.currentTest.coverage.get(key) || 0;
|
|
770
|
+
global.__TEST_LINEAGE_TRACKER__.currentTest.coverage.set(key, currentCount + 1);
|
|
771
|
+
|
|
772
|
+
// Store depth information for this execution
|
|
773
|
+
const depthKey = `${key}:depth`;
|
|
774
|
+
const depthData = global.__TEST_LINEAGE_TRACKER__.currentTest.coverage.get(depthKey) || {};
|
|
775
|
+
depthData[callDepth] = (depthData[callDepth] || 0) + 1;
|
|
776
|
+
global.__TEST_LINEAGE_TRACKER__.currentTest.coverage.set(depthKey, depthData);
|
|
777
|
+
|
|
778
|
+
// Store performance metrics for this execution
|
|
779
|
+
const performanceEnd = capturePerformanceMetrics();
|
|
780
|
+
const performanceKey = `${key}:performance`;
|
|
781
|
+
const performanceData = global.__TEST_LINEAGE_TRACKER__.currentTest.coverage.get(performanceKey) || {
|
|
782
|
+
totalExecutions: 0,
|
|
783
|
+
totalCpuTime: 0,
|
|
784
|
+
totalWallTime: 0,
|
|
785
|
+
totalMemoryDelta: 0,
|
|
786
|
+
minExecutionTime: Infinity,
|
|
787
|
+
maxExecutionTime: 0,
|
|
788
|
+
executionTimes: [],
|
|
789
|
+
cpuCycles: [],
|
|
790
|
+
memorySnapshots: [],
|
|
791
|
+
performanceVariance: 0,
|
|
792
|
+
performanceStdDev: 0,
|
|
793
|
+
performanceP95: 0,
|
|
794
|
+
performanceP99: 0,
|
|
795
|
+
slowExecutions: 0,
|
|
796
|
+
fastExecutions: 0,
|
|
797
|
+
memoryLeaks: 0,
|
|
798
|
+
gcPressure: 0
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// Calculate execution metrics
|
|
802
|
+
const wallTime = performanceEnd.wallTime - performanceStart.wallTime;
|
|
803
|
+
const cpuTime = performanceEnd.cpuTime - performanceStart.cpuTime;
|
|
804
|
+
const memoryDelta = performanceEnd.memoryUsage - performanceStart.memoryUsage;
|
|
805
|
+
const cpuCycles = estimateCpuCycles(cpuTime, wallTime);
|
|
806
|
+
|
|
807
|
+
// Update performance data
|
|
808
|
+
performanceData.totalExecutions += 1;
|
|
809
|
+
performanceData.totalCpuTime += cpuTime;
|
|
810
|
+
performanceData.totalWallTime += wallTime;
|
|
811
|
+
performanceData.totalMemoryDelta += memoryDelta;
|
|
812
|
+
performanceData.minExecutionTime = Math.min(performanceData.minExecutionTime, wallTime);
|
|
813
|
+
performanceData.maxExecutionTime = Math.max(performanceData.maxExecutionTime, wallTime);
|
|
814
|
+
|
|
815
|
+
// Store recent execution times (keep last 100 for analysis)
|
|
816
|
+
performanceData.executionTimes.push(wallTime);
|
|
817
|
+
performanceData.cpuCycles.push(cpuCycles);
|
|
818
|
+
performanceData.memorySnapshots.push({
|
|
819
|
+
timestamp: performanceEnd.timestamp,
|
|
820
|
+
heapUsed: performanceEnd.memoryUsage,
|
|
821
|
+
delta: memoryDelta
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// Performance classification
|
|
825
|
+
const avgTime = performanceData.totalWallTime / Math.max(1, performanceData.totalExecutions);
|
|
826
|
+
if (wallTime > avgTime * 2) {
|
|
827
|
+
performanceData.slowExecutions++;
|
|
828
|
+
} else if (wallTime < avgTime * 0.5) {
|
|
829
|
+
performanceData.fastExecutions++;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Memory leak detection - very sensitive threshold for testing
|
|
833
|
+
if (Math.abs(memoryDelta) > 50 * 1024) { // > 50KB allocation (very sensitive)
|
|
834
|
+
performanceData.memoryLeaks++;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// GC pressure detection (frequent small allocations)
|
|
838
|
+
if (memoryDelta > 0 && memoryDelta < 10 * 1024) { // < 10KB but > 0
|
|
839
|
+
performanceData.gcPressure++;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (performanceData.executionTimes.length > 100) {
|
|
843
|
+
performanceData.executionTimes.shift();
|
|
844
|
+
performanceData.cpuCycles.shift();
|
|
845
|
+
performanceData.memorySnapshots.shift();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Calculate statistical metrics
|
|
849
|
+
if (performanceData.executionTimes.length > 5) {
|
|
850
|
+
const times = performanceData.executionTimes.slice();
|
|
851
|
+
times.sort((a, b) => a - b);
|
|
852
|
+
|
|
853
|
+
// Calculate percentiles
|
|
854
|
+
const p95Index = Math.floor(times.length * 0.95);
|
|
855
|
+
const p99Index = Math.floor(times.length * 0.99);
|
|
856
|
+
performanceData.performanceP95 = times[p95Index] || 0;
|
|
857
|
+
performanceData.performanceP99 = times[p99Index] || 0;
|
|
858
|
+
|
|
859
|
+
// Calculate variance and standard deviation
|
|
860
|
+
const mean = times.reduce((sum, t) => sum + t, 0) / times.length;
|
|
861
|
+
const variance = times.reduce((sum, t) => sum + Math.pow(t - mean, 2), 0) / times.length;
|
|
862
|
+
performanceData.performanceVariance = variance;
|
|
863
|
+
performanceData.performanceStdDev = Math.sqrt(variance);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
global.__TEST_LINEAGE_TRACKER__.currentTest.coverage.set(performanceKey, performanceData);
|
|
867
|
+
|
|
868
|
+
// Store additional metadata about the node type
|
|
869
|
+
const metaKey = `${key}:meta`;
|
|
870
|
+
if (!global.__TEST_LINEAGE_TRACKER__.currentTest.coverage.has(metaKey)) {
|
|
871
|
+
global.__TEST_LINEAGE_TRACKER__.currentTest.coverage.set(metaKey, {
|
|
872
|
+
nodeType: nodeType,
|
|
873
|
+
firstExecution: Date.now(),
|
|
874
|
+
minDepth: callDepth,
|
|
875
|
+
maxDepth: callDepth
|
|
876
|
+
});
|
|
877
|
+
} else {
|
|
878
|
+
const meta = global.__TEST_LINEAGE_TRACKER__.currentTest.coverage.get(metaKey);
|
|
879
|
+
meta.minDepth = Math.min(meta.minDepth, callDepth);
|
|
880
|
+
meta.maxDepth = Math.max(meta.maxDepth, callDepth);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
// Export results for the reporter
|
|
886
|
+
global.__GET_LINEAGE_RESULTS__ = function() {
|
|
887
|
+
// Return empty results during mutation testing to prevent report generation
|
|
888
|
+
if (process.env.JEST_LINEAGE_MUTATION === 'true') {
|
|
889
|
+
return {};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const results = {};
|
|
893
|
+
|
|
894
|
+
// Use persistent data if available, fallback to test coverage map
|
|
895
|
+
const dataSource = global.__LINEAGE_PERSISTENT_DATA__ || Array.from(global.__TEST_LINEAGE_TRACKER__.testCoverage.values());
|
|
896
|
+
|
|
897
|
+
console.log(`🔍 Getting lineage results from ${dataSource.length} tests`);
|
|
898
|
+
|
|
899
|
+
dataSource.forEach((testData) => {
|
|
900
|
+
testData.coverage.forEach((count, key) => {
|
|
901
|
+
// Skip metadata entries
|
|
902
|
+
if (key.includes(':meta')) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const [filePath, lineNumber] = key.split(':');
|
|
907
|
+
|
|
908
|
+
// Skip test files and node_modules
|
|
909
|
+
if (filePath.includes('__tests__') ||
|
|
910
|
+
filePath.includes('.test.') ||
|
|
911
|
+
filePath.includes('.spec.') ||
|
|
912
|
+
filePath.includes('node_modules')) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (!results[filePath]) {
|
|
917
|
+
results[filePath] = {};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (!results[filePath][lineNumber]) {
|
|
921
|
+
results[filePath][lineNumber] = [];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
results[filePath][lineNumber].push({
|
|
925
|
+
testName: testData.name,
|
|
926
|
+
testFile: 'current-test-file', // Will be updated by reporter
|
|
927
|
+
executionCount: count,
|
|
928
|
+
duration: testData.duration,
|
|
929
|
+
type: 'precise',
|
|
930
|
+
failed: testData.failed || false
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
console.log(`✅ Added precise data: ${filePath}:${lineNumber} -> ${testData.name} (${count} executions)`);
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
console.log(`🔍 Final results: ${Object.keys(results).length} files with precise data`);
|
|
938
|
+
return results;
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
console.log('🎯 Test lineage tracking setup completed');
|