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.
@@ -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');