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
|
@@ -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
|
+
};
|