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,290 @@
1
+ /**
2
+ * Production Babel Plugin for Jest Test Lineage Tracking
3
+ * Automatically instruments source code to track line-by-line test coverage
4
+ */
5
+ function lineageTrackerPlugin({ types: t }, options = {}) {
6
+ // Check if lineage tracking is enabled
7
+ const isEnabled = process.env.JEST_LINEAGE_ENABLED !== 'false' &&
8
+ process.env.JEST_LINEAGE_TRACKING !== 'false' &&
9
+ options.enabled !== false;
10
+
11
+ return {
12
+ name: 'lineage-tracker',
13
+ visitor: {
14
+ Program: {
15
+ enter(path, state) {
16
+ // Initialize plugin state
17
+ state.filename = state.file.opts.filename;
18
+ state.shouldInstrument = isEnabled && shouldInstrumentFile(state.filename);
19
+ state.instrumentedLines = new Set();
20
+
21
+ if (state.shouldInstrument) {
22
+ console.log(`🔧 Instrumenting: ${state.filename}`);
23
+ } else if (!isEnabled) {
24
+ console.log(`⏸️ Lineage tracking disabled for: ${state.filename}`);
25
+ }
26
+ }
27
+ },
28
+ // Instrument function declarations
29
+ FunctionDeclaration(path, state) {
30
+ if (!state.shouldInstrument) return;
31
+
32
+ const lineNumber = path.node.loc?.start.line;
33
+ if (lineNumber && !state.instrumentedLines.has(lineNumber)) {
34
+ instrumentLine(path, state, lineNumber, 'function-declaration');
35
+ state.instrumentedLines.add(lineNumber);
36
+ }
37
+ },
38
+
39
+ // Instrument function expressions and arrow functions
40
+ 'FunctionExpression|ArrowFunctionExpression'(path, state) {
41
+ if (!state.shouldInstrument) return;
42
+
43
+ const lineNumber = path.node.loc?.start.line;
44
+ if (lineNumber && !state.instrumentedLines.has(lineNumber)) {
45
+ instrumentLine(path, state, lineNumber, 'function-expression');
46
+ state.instrumentedLines.add(lineNumber);
47
+ }
48
+ },
49
+
50
+ // Instrument variable declarations
51
+ VariableDeclaration(path, state) {
52
+ if (!state.shouldInstrument) return;
53
+
54
+ const lineNumber = path.node.loc?.start.line;
55
+ if (lineNumber && !state.instrumentedLines.has(lineNumber)) {
56
+ instrumentLine(path, state, lineNumber, 'variable-declaration');
57
+ state.instrumentedLines.add(lineNumber);
58
+ }
59
+ },
60
+
61
+ // Instrument expression statements
62
+ ExpressionStatement(path, state) {
63
+ if (!state.shouldInstrument) return;
64
+
65
+ const lineNumber = path.node.loc?.start.line;
66
+ if (lineNumber && !state.instrumentedLines.has(lineNumber)) {
67
+ instrumentLine(path, state, lineNumber, 'expression-statement');
68
+ state.instrumentedLines.add(lineNumber);
69
+ }
70
+ },
71
+
72
+ // Instrument return statements
73
+ ReturnStatement(path, state) {
74
+ if (!state.shouldInstrument) return;
75
+
76
+ const lineNumber = path.node.loc?.start.line;
77
+ if (lineNumber && !state.instrumentedLines.has(lineNumber)) {
78
+ instrumentLine(path, state, lineNumber, 'return-statement');
79
+ state.instrumentedLines.add(lineNumber);
80
+ }
81
+ },
82
+
83
+ // Instrument if statements
84
+ IfStatement(path, state) {
85
+ if (!state.shouldInstrument) return;
86
+
87
+ const lineNumber = path.node.loc?.start.line;
88
+ if (lineNumber && !state.instrumentedLines.has(lineNumber)) {
89
+ instrumentLine(path, state, lineNumber, 'if-statement');
90
+ state.instrumentedLines.add(lineNumber);
91
+ }
92
+ },
93
+
94
+ // Instrument block statements (but avoid duplicating)
95
+ BlockStatement(path, state) {
96
+ if (!state.shouldInstrument) return;
97
+
98
+ // Only instrument block statements that are function bodies
99
+ if (t.isFunction(path.parent)) {
100
+ const lineNumber = path.node.loc?.start.line;
101
+ if (lineNumber && !state.instrumentedLines.has(lineNumber)) {
102
+ // Insert tracking at the beginning of the block
103
+ const trackingCall = createTrackingCall(state.filename, lineNumber, 'block-start');
104
+ path.unshiftContainer('body', trackingCall);
105
+ state.instrumentedLines.add(lineNumber);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ };
111
+ };
112
+
113
+ /**
114
+ * Determines if a file should be instrumented
115
+ */
116
+ function shouldInstrumentFile(filename) {
117
+ if (!filename) return false;
118
+
119
+ // Don't instrument test files
120
+ if (filename.includes('__tests__') ||
121
+ filename.includes('.test.') ||
122
+ filename.includes('.spec.') ||
123
+ filename.includes('testSetup.js') ||
124
+ filename.includes('TestCoverageReporter.js') ||
125
+ filename.includes('LineageTestEnvironment.js')) {
126
+ return false;
127
+ }
128
+
129
+ // Don't instrument node_modules
130
+ if (filename.includes('node_modules')) {
131
+ return false;
132
+ }
133
+
134
+ // Only instrument source files
135
+ return filename.endsWith('.ts') ||
136
+ filename.endsWith('.js') ||
137
+ filename.endsWith('.tsx') ||
138
+ filename.endsWith('.jsx');
139
+ }
140
+
141
+ /**
142
+ * Instruments a line by adding tracking call before it
143
+ */
144
+ function instrumentLine(path, state, lineNumber, nodeType) {
145
+ const trackingCall = createTrackingCall(state.filename, lineNumber, nodeType);
146
+
147
+ try {
148
+ // Insert tracking call before the current statement
149
+ path.insertBefore(trackingCall);
150
+ } catch (error) {
151
+ console.warn(`Warning: Could not instrument line ${lineNumber} in ${state.filename}:`, error.message);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Creates a tracking function call with package.json-based path detection
157
+ */
158
+ function createTrackingCall(filename, lineNumber, nodeType) {
159
+ const { types: t } = require('@babel/core');
160
+ const path = require('path');
161
+
162
+ // Use package.json as the project root reference
163
+ let relativeFilePath;
164
+ if (filename) {
165
+ const projectRoot = findProjectRoot(filename);
166
+
167
+ if (projectRoot && filename.startsWith(projectRoot)) {
168
+ // Convert absolute path to relative path from package.json location
169
+ relativeFilePath = path.relative(projectRoot, filename);
170
+ } else {
171
+ // Fallback to current working directory
172
+ const cwd = process.cwd();
173
+ if (filename.startsWith(cwd)) {
174
+ relativeFilePath = path.relative(cwd, filename);
175
+ } else {
176
+ // Last resort: extract meaningful path
177
+ relativeFilePath = extractMeaningfulPath(filename);
178
+ }
179
+ }
180
+ } else {
181
+ relativeFilePath = 'unknown';
182
+ }
183
+
184
+ // Create: global.__TRACK_LINE_EXECUTION__ && global.__TRACK_LINE_EXECUTION__(filename, lineNumber)
185
+ return t.expressionStatement(
186
+ t.logicalExpression(
187
+ '&&',
188
+ t.memberExpression(
189
+ t.identifier('global'),
190
+ t.identifier('__TRACK_LINE_EXECUTION__')
191
+ ),
192
+ t.callExpression(
193
+ t.memberExpression(
194
+ t.identifier('global'),
195
+ t.identifier('__TRACK_LINE_EXECUTION__')
196
+ ),
197
+ [
198
+ t.stringLiteral(relativeFilePath),
199
+ t.numericLiteral(lineNumber),
200
+ t.stringLiteral(nodeType)
201
+ ]
202
+ )
203
+ )
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Find the project root by looking for package.json
209
+ */
210
+ function findProjectRoot(startPath) {
211
+ const path = require('path');
212
+ const fs = require('fs');
213
+
214
+ let currentDir = path.dirname(startPath);
215
+ const root = path.parse(currentDir).root;
216
+
217
+ while (currentDir !== root) {
218
+ const packageJsonPath = path.join(currentDir, 'package.json');
219
+ if (fs.existsSync(packageJsonPath)) {
220
+ return currentDir;
221
+ }
222
+ currentDir = path.dirname(currentDir);
223
+ }
224
+
225
+ // Fallback to current working directory if no package.json found
226
+ return process.cwd();
227
+ }
228
+
229
+ /**
230
+ * Extract meaningful path from filename using smart detection (fallback)
231
+ */
232
+ function extractMeaningfulPath(filename) {
233
+ const path = require('path');
234
+ const parts = filename.split(path.sep);
235
+
236
+ // Common source directory indicators
237
+ const sourceIndicators = [
238
+ 'src', 'lib', 'source', 'app', 'server', 'client',
239
+ 'packages', 'apps', 'libs', 'modules', 'components'
240
+ ];
241
+
242
+ // Find the first occurrence of a source indicator (not last)
243
+ let sourceIndex = -1;
244
+ for (let i = 0; i < parts.length; i++) {
245
+ if (sourceIndicators.includes(parts[i])) {
246
+ sourceIndex = i;
247
+ break;
248
+ }
249
+ }
250
+
251
+ if (sourceIndex !== -1) {
252
+ // Include the source directory and everything after it
253
+ // This preserves subdirectories like 'src/services/calculationService.ts'
254
+ return parts.slice(sourceIndex).join(path.sep);
255
+ }
256
+
257
+ // If no source indicator found, try to preserve meaningful structure
258
+ const filename_only = parts[parts.length - 1];
259
+
260
+ // Look for meaningful parent directories (preserve up to 3 levels)
261
+ if (parts.length >= 3) {
262
+ const meaningfulParts = parts.slice(-3); // Take last 3 parts
263
+ // Filter out common non-meaningful directories
264
+ const filtered = meaningfulParts.filter(part =>
265
+ part &&
266
+ !part.startsWith('.') &&
267
+ part !== 'node_modules' &&
268
+ part !== 'dist' &&
269
+ part !== 'build'
270
+ );
271
+
272
+ if (filtered.length >= 2) {
273
+ return filtered.join(path.sep);
274
+ }
275
+ }
276
+
277
+ // Look for meaningful parent directories (preserve up to 2 levels)
278
+ if (parts.length >= 2) {
279
+ const parent = parts[parts.length - 2];
280
+ // If parent looks like a meaningful directory, include it
281
+ if (parent && !parent.startsWith('.') && parent !== 'node_modules') {
282
+ return path.join(parent, filename_only);
283
+ }
284
+ }
285
+
286
+ // Fallback to just the filename
287
+ return filename_only;
288
+ }
289
+
290
+ module.exports = lineageTrackerPlugin;
package/src/config.js ADDED
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Configuration for Jest Test Lineage Reporter
3
+ */
4
+
5
+ const DEFAULT_CONFIG = {
6
+ // Feature toggles
7
+ enabled: true, // Master switch to enable/disable entire system
8
+ enableLineageTracking: true, // Enable detailed line-by-line tracking
9
+ enablePerformanceTracking: true, // Enable CPU/memory monitoring
10
+ enableQualityAnalysis: true, // Enable test quality scoring
11
+
12
+ // Output settings
13
+ outputFile: 'test-lineage-report.html',
14
+ enableConsoleOutput: true,
15
+ enableDebugLogging: false,
16
+
17
+ // Performance thresholds
18
+ memoryLeakThreshold: 50 * 1024, // 50KB - allocations above this trigger memory leak alerts
19
+ gcPressureThreshold: 5, // Number of small allocations that trigger GC pressure alerts
20
+ slowExecutionThreshold: 2.0, // Multiplier for average execution time to trigger slow alerts
21
+
22
+ // Quality thresholds
23
+ qualityThreshold: 60, // Minimum quality score (0-100)
24
+ reliabilityThreshold: 60, // Minimum reliability score (0-100)
25
+ maintainabilityThreshold: 60, // Minimum maintainability score (0-100)
26
+ maxTestSmells: 2, // Maximum number of test smells before flagging
27
+
28
+ // Test quality scoring weights
29
+ qualityWeights: {
30
+ assertions: 5, // Points per assertion (up to 30 points)
31
+ errorHandling: 10, // Points per error handling pattern (up to 20 points)
32
+ edgeCases: 3, // Points per edge case test (up to 15 points)
33
+ testSmellPenalty: 5, // Points deducted per test smell
34
+ complexityPenalty: 2, // Points deducted per complexity point
35
+ lengthPenalty: 0.5 // Points deducted per line over 50
36
+ },
37
+
38
+ // Performance tracking
39
+ enableCpuCycleTracking: true,
40
+ enableMemoryTracking: true,
41
+ enableCallDepthTracking: true,
42
+ maxCallDepthTracking: 10, // Maximum call depth to track
43
+
44
+ // HTML report settings
45
+ enableInteractiveFeatures: true,
46
+ enablePerformanceDashboard: true,
47
+ enableQualityDashboard: true,
48
+ maxLinesInReport: 10000, // Maximum number of lines to include in HTML report
49
+
50
+ // Mutation testing settings
51
+ enableMutationTesting: false, // Enable mutation testing mode
52
+ mutationOperators: {
53
+ arithmetic: true, // +, -, *, /, %
54
+ comparison: true, // ==, !=, <, >, <=, >=
55
+ logical: true, // &&, ||, !
56
+ conditional: true, // if conditions, ternary operators
57
+ assignment: true, // =, +=, -=, etc.
58
+ literals: true, // numbers, booleans, strings
59
+ returns: true, // return statements
60
+ increments: true // ++, --
61
+ },
62
+ mutationThreshold: 80, // Minimum mutation score (% of mutations killed)
63
+ mutationTimeout: 5000, // Timeout per mutation test in ms
64
+ maxMutationsPerLine: 3, // Maximum mutations to generate per line
65
+ skipEquivalentMutants: true, // Skip mutations that don't change behavior
66
+
67
+ // Debug options
68
+ debugMutations: false, // Create mutation files for debugging instead of overwriting originals
69
+ debugMutationDir: './mutations-debug', // Directory to store debug mutation files
70
+
71
+ // File filtering
72
+ includePatterns: [
73
+ '**/*.js',
74
+ '**/*.ts',
75
+ '**/*.jsx',
76
+ '**/*.tsx'
77
+ ],
78
+ excludePatterns: [
79
+ '**/node_modules/**',
80
+ '**/dist/**',
81
+ '**/build/**',
82
+ '**/*.min.js',
83
+ '**/*.bundle.js'
84
+ ]
85
+ };
86
+
87
+ /**
88
+ * Validates and merges user configuration with defaults
89
+ * @param {Object} userConfig - User provided configuration
90
+ * @returns {Object} Merged and validated configuration
91
+ */
92
+ function validateAndMergeConfig(userConfig = {}) {
93
+ const config = { ...DEFAULT_CONFIG, ...userConfig };
94
+
95
+ // Validate numeric thresholds
96
+ if (typeof config.memoryLeakThreshold !== 'number' || config.memoryLeakThreshold < 0) {
97
+ console.warn('Invalid memoryLeakThreshold, using default:', DEFAULT_CONFIG.memoryLeakThreshold);
98
+ config.memoryLeakThreshold = DEFAULT_CONFIG.memoryLeakThreshold;
99
+ }
100
+
101
+ if (typeof config.gcPressureThreshold !== 'number' || config.gcPressureThreshold < 1) {
102
+ console.warn('Invalid gcPressureThreshold, using default:', DEFAULT_CONFIG.gcPressureThreshold);
103
+ config.gcPressureThreshold = DEFAULT_CONFIG.gcPressureThreshold;
104
+ }
105
+
106
+ if (typeof config.qualityThreshold !== 'number' || config.qualityThreshold < 0 || config.qualityThreshold > 100) {
107
+ console.warn('Invalid qualityThreshold, using default:', DEFAULT_CONFIG.qualityThreshold);
108
+ config.qualityThreshold = DEFAULT_CONFIG.qualityThreshold;
109
+ }
110
+
111
+ // Validate file patterns
112
+ if (!Array.isArray(config.includePatterns)) {
113
+ console.warn('Invalid includePatterns, using default');
114
+ config.includePatterns = DEFAULT_CONFIG.includePatterns;
115
+ }
116
+
117
+ if (!Array.isArray(config.excludePatterns)) {
118
+ console.warn('Invalid excludePatterns, using default');
119
+ config.excludePatterns = DEFAULT_CONFIG.excludePatterns;
120
+ }
121
+
122
+ // Validate quality weights
123
+ if (typeof config.qualityWeights !== 'object') {
124
+ console.warn('Invalid qualityWeights, using default');
125
+ config.qualityWeights = DEFAULT_CONFIG.qualityWeights;
126
+ }
127
+
128
+ return config;
129
+ }
130
+
131
+ /**
132
+ * Gets configuration from environment variables
133
+ * @returns {Object} Configuration from environment
134
+ */
135
+ function getConfigFromEnv() {
136
+ return {
137
+ // Feature toggles
138
+ enabled: process.env.JEST_LINEAGE_ENABLED !== 'false', // Default enabled, set to 'false' to disable
139
+ enableLineageTracking: process.env.JEST_LINEAGE_TRACKING !== 'false',
140
+ enablePerformanceTracking: process.env.JEST_LINEAGE_PERFORMANCE !== 'false',
141
+ enableQualityAnalysis: process.env.JEST_LINEAGE_QUALITY !== 'false',
142
+ enableMutationTesting: process.env.JEST_LINEAGE_MUTATION ? process.env.JEST_LINEAGE_MUTATION === 'true' : undefined,
143
+
144
+ // Output settings
145
+ outputFile: process.env.JEST_LINEAGE_OUTPUT_FILE,
146
+ enableDebugLogging: process.env.JEST_LINEAGE_DEBUG === 'true',
147
+
148
+ // Thresholds
149
+ memoryLeakThreshold: process.env.JEST_LINEAGE_MEMORY_THRESHOLD ?
150
+ parseInt(process.env.JEST_LINEAGE_MEMORY_THRESHOLD) : undefined,
151
+ gcPressureThreshold: process.env.JEST_LINEAGE_GC_THRESHOLD ?
152
+ parseInt(process.env.JEST_LINEAGE_GC_THRESHOLD) : undefined,
153
+ qualityThreshold: process.env.JEST_LINEAGE_QUALITY_THRESHOLD ?
154
+ parseInt(process.env.JEST_LINEAGE_QUALITY_THRESHOLD) : undefined,
155
+
156
+ // Mutation testing settings
157
+ debugMutations: process.env.JEST_LINEAGE_DEBUG_MUTATIONS ? process.env.JEST_LINEAGE_DEBUG_MUTATIONS === 'true' : undefined,
158
+ debugMutationDir: process.env.JEST_LINEAGE_DEBUG_MUTATION_DIR,
159
+
160
+ // Mutation testing thresholds
161
+ mutationThreshold: process.env.JEST_LINEAGE_MUTATION_THRESHOLD ?
162
+ parseInt(process.env.JEST_LINEAGE_MUTATION_THRESHOLD) : undefined,
163
+ mutationTimeout: process.env.JEST_LINEAGE_MUTATION_TIMEOUT ?
164
+ parseInt(process.env.JEST_LINEAGE_MUTATION_TIMEOUT) : undefined
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Loads configuration from file, environment, and defaults
170
+ * @param {Object} userConfig - User provided configuration
171
+ * @returns {Object} Final configuration
172
+ */
173
+ function loadConfig(userConfig = {}) {
174
+ const envConfig = getConfigFromEnv();
175
+
176
+ // Only merge environment config values that are actually set (not undefined)
177
+ const filteredEnvConfig = {};
178
+ Object.keys(envConfig).forEach(key => {
179
+ if (envConfig[key] !== undefined) {
180
+ filteredEnvConfig[key] = envConfig[key];
181
+ }
182
+ });
183
+
184
+ const mergedConfig = { ...userConfig, ...filteredEnvConfig };
185
+ return validateAndMergeConfig(mergedConfig);
186
+ }
187
+
188
+ module.exports = {
189
+ DEFAULT_CONFIG,
190
+ validateAndMergeConfig,
191
+ getConfigFromEnv,
192
+ loadConfig
193
+ };