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,3307 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { loadConfig } = require('./config');
|
|
4
|
+
const MutationTester = require('./MutationTester');
|
|
5
|
+
|
|
6
|
+
class TestCoverageReporter {
|
|
7
|
+
constructor(globalConfig, options) {
|
|
8
|
+
this.globalConfig = globalConfig;
|
|
9
|
+
this.options = options || {};
|
|
10
|
+
this.coverageData = {};
|
|
11
|
+
this.testCoverageMap = new Map(); // Map to store individual test coverage
|
|
12
|
+
this.currentTestFile = null;
|
|
13
|
+
this.baselineCoverage = null;
|
|
14
|
+
|
|
15
|
+
// Validate configuration
|
|
16
|
+
this.validateConfig();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
validateConfig() {
|
|
20
|
+
if (!this.globalConfig) {
|
|
21
|
+
throw new Error('TestCoverageReporter: globalConfig is required');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Set default options
|
|
25
|
+
this.options = {
|
|
26
|
+
outputFile: this.options.outputFile || 'test-lineage-report.html',
|
|
27
|
+
memoryLeakThreshold: this.options.memoryLeakThreshold || 50 * 1024, // 50KB
|
|
28
|
+
gcPressureThreshold: this.options.gcPressureThreshold || 5,
|
|
29
|
+
qualityThreshold: this.options.qualityThreshold || 60,
|
|
30
|
+
enableDebugLogging: this.options.enableDebugLogging || false,
|
|
31
|
+
...this.options
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// This method is called after a single test file (suite) has completed.
|
|
36
|
+
async onTestResult(test, testResult, _aggregatedResult) {
|
|
37
|
+
const testFilePath = test.path;
|
|
38
|
+
|
|
39
|
+
// Store test file path for later use
|
|
40
|
+
this.currentTestFile = testFilePath;
|
|
41
|
+
|
|
42
|
+
// Always process fallback coverage for now, but mark for potential precise data
|
|
43
|
+
const coverage = testResult.coverage;
|
|
44
|
+
if (!coverage) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Process each individual test result (fallback mode)
|
|
49
|
+
testResult.testResults.forEach((testCase, index) => {
|
|
50
|
+
if (testCase.status === 'passed') {
|
|
51
|
+
this.processIndividualTestCoverage(testCase, coverage, testFilePath, index);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
processLineageResults(lineageResults, testFilePath) {
|
|
57
|
+
console.log('🎯 Processing precise lineage tracking results...');
|
|
58
|
+
|
|
59
|
+
// lineageResults format: { filePath: { lineNumber: [testInfo, ...] } }
|
|
60
|
+
for (const filePath in lineageResults) {
|
|
61
|
+
// Skip test files - we only want to track coverage of source files
|
|
62
|
+
if (filePath.includes('__tests__') || filePath.includes('.test.') || filePath.includes('.spec.')) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize the data structure for this file if it doesn't exist
|
|
67
|
+
if (!this.coverageData[filePath]) {
|
|
68
|
+
this.coverageData[filePath] = {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const lineNumber in lineageResults[filePath]) {
|
|
72
|
+
const testInfos = lineageResults[filePath][lineNumber];
|
|
73
|
+
|
|
74
|
+
// Convert to our expected format
|
|
75
|
+
const processedTests = testInfos.map(testInfo => ({
|
|
76
|
+
name: testInfo.testName,
|
|
77
|
+
file: path.basename(testInfo.testFile || testFilePath),
|
|
78
|
+
fullPath: testInfo.testFile || testFilePath,
|
|
79
|
+
executionCount: testInfo.executionCount || 1,
|
|
80
|
+
timestamp: testInfo.timestamp || Date.now(),
|
|
81
|
+
type: 'precise' // Mark as precise tracking
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
this.coverageData[filePath][lineNumber] = processedTests;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
processIndividualTestCoverage(testCase, coverage, testFilePath, _testIndex) {
|
|
90
|
+
const testName = testCase.fullName;
|
|
91
|
+
|
|
92
|
+
// Since Jest doesn't provide per-test coverage, we'll use heuristics
|
|
93
|
+
// to estimate which tests likely covered which lines
|
|
94
|
+
|
|
95
|
+
for (const filePath in coverage) {
|
|
96
|
+
// Skip test files
|
|
97
|
+
if (filePath.includes('__tests__') || filePath.includes('.test.') || filePath.includes('.spec.')) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fileCoverage = coverage[filePath];
|
|
102
|
+
const statementMap = fileCoverage.statementMap;
|
|
103
|
+
const statements = fileCoverage.s;
|
|
104
|
+
|
|
105
|
+
// Initialize the data structure for this file if it doesn't exist
|
|
106
|
+
if (!this.coverageData[filePath]) {
|
|
107
|
+
this.coverageData[filePath] = {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Analyze which lines were covered
|
|
111
|
+
for (const statementId in statements) {
|
|
112
|
+
if (statements[statementId] > 0) {
|
|
113
|
+
const lineNumber = String(statementMap[statementId].start.line);
|
|
114
|
+
|
|
115
|
+
if (!this.coverageData[filePath][lineNumber]) {
|
|
116
|
+
this.coverageData[filePath][lineNumber] = [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Use heuristics to determine if this test likely covered this line
|
|
120
|
+
if (this.isTestLikelyCoveringLine(testCase, filePath, lineNumber, fileCoverage)) {
|
|
121
|
+
// Add test with more detailed information
|
|
122
|
+
const testInfo = {
|
|
123
|
+
name: testName,
|
|
124
|
+
file: path.basename(testFilePath),
|
|
125
|
+
fullPath: testFilePath,
|
|
126
|
+
duration: testCase.duration || 0,
|
|
127
|
+
confidence: this.calculateConfidence(testCase, filePath, lineNumber)
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
this.coverageData[filePath][lineNumber].push(testInfo);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
isTestLikelyCoveringLine(testCase, _filePath, _lineNumber, _fileCoverage) {
|
|
138
|
+
// Heuristic 1: If test name mentions the function/file being tested
|
|
139
|
+
const _testName = testCase.fullName.toLowerCase();
|
|
140
|
+
|
|
141
|
+
// Simplified heuristic - for now, include all tests
|
|
142
|
+
// TODO: Implement more sophisticated heuristics
|
|
143
|
+
|
|
144
|
+
// Heuristic 3: If it's a simple file with few tests, assume all tests cover most lines
|
|
145
|
+
// This is a fallback for when we can't determine specific coverage
|
|
146
|
+
return true; // For now, include all tests (we'll refine this)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
calculateConfidence(_testCase, _filePath, _lineNumber) {
|
|
150
|
+
// Simplified confidence calculation
|
|
151
|
+
// TODO: Implement more sophisticated confidence scoring
|
|
152
|
+
return 75; // Default confidence
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
extractCodeKeywords(sourceCode) {
|
|
156
|
+
// Extract function names, variable names, etc. from source code line
|
|
157
|
+
const keywords = [];
|
|
158
|
+
|
|
159
|
+
// Match function names: function name() or name: function() or const name =
|
|
160
|
+
const functionMatches = sourceCode.match(/(?:function\s+(\w+)|(\w+)\s*[:=]\s*(?:function|\()|(?:const|let|var)\s+(\w+))/g);
|
|
161
|
+
if (functionMatches) {
|
|
162
|
+
functionMatches.forEach(match => {
|
|
163
|
+
const nameMatch = match.match(/(\w+)/);
|
|
164
|
+
if (nameMatch) keywords.push(nameMatch[1]);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Match method calls: object.method()
|
|
169
|
+
const methodMatches = sourceCode.match(/(\w+)\s*\(/g);
|
|
170
|
+
if (methodMatches) {
|
|
171
|
+
methodMatches.forEach(match => {
|
|
172
|
+
const nameMatch = match.match(/(\w+)/);
|
|
173
|
+
if (nameMatch) keywords.push(nameMatch[1]);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return keywords;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getSourceCodeLine(filePath, lineNumber) {
|
|
181
|
+
try {
|
|
182
|
+
const sourceCode = fs.readFileSync(filePath, 'utf8');
|
|
183
|
+
const lines = sourceCode.split('\n');
|
|
184
|
+
return lines[lineNumber - 1] || '';
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return '';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// This method is called after all tests in the entire run have completed.
|
|
191
|
+
async onRunComplete(_contexts, _results) {
|
|
192
|
+
// Try to get precise tracking data before generating reports
|
|
193
|
+
this.tryGetPreciseTrackingData();
|
|
194
|
+
|
|
195
|
+
this.generateReport();
|
|
196
|
+
await this.generateHtmlReport();
|
|
197
|
+
|
|
198
|
+
// Run mutation testing if enabled
|
|
199
|
+
await this.runMutationTestingIfEnabled();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Run mutation testing if enabled in configuration
|
|
204
|
+
*/
|
|
205
|
+
async runMutationTestingIfEnabled() {
|
|
206
|
+
const config = loadConfig(this.options);
|
|
207
|
+
|
|
208
|
+
if (config.enableMutationTesting) {
|
|
209
|
+
console.log('\n🧬 Mutation testing enabled, starting mutation analysis...');
|
|
210
|
+
|
|
211
|
+
let mutationTester = null;
|
|
212
|
+
try {
|
|
213
|
+
mutationTester = new MutationTester(config);
|
|
214
|
+
|
|
215
|
+
// Pass the current coverage data directly instead of loading from file
|
|
216
|
+
const lineageData = this.convertCoverageDataToLineageFormat();
|
|
217
|
+
if (!lineageData || Object.keys(lineageData).length === 0) {
|
|
218
|
+
console.log('⚠️ No lineage data available for mutation testing');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Set the lineage data directly
|
|
223
|
+
mutationTester.setLineageData(lineageData);
|
|
224
|
+
|
|
225
|
+
// Run mutation testing
|
|
226
|
+
const results = await mutationTester.runMutationTesting();
|
|
227
|
+
|
|
228
|
+
// Store results for HTML report integration
|
|
229
|
+
this.mutationResults = results;
|
|
230
|
+
|
|
231
|
+
// Regenerate HTML report with mutation data
|
|
232
|
+
await this.generateHtmlReport();
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('❌ Mutation testing failed:', error.message);
|
|
236
|
+
if (this.options.enableDebugLogging) {
|
|
237
|
+
console.error(error.stack);
|
|
238
|
+
}
|
|
239
|
+
} finally {
|
|
240
|
+
// Always cleanup, even if there was an error
|
|
241
|
+
if (mutationTester) {
|
|
242
|
+
try {
|
|
243
|
+
await mutationTester.cleanup();
|
|
244
|
+
} catch (cleanupError) {
|
|
245
|
+
console.error('❌ Error during mutation testing cleanup:', cleanupError.message);
|
|
246
|
+
// Try emergency cleanup as last resort
|
|
247
|
+
mutationTester.emergencyCleanup();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
tryGetPreciseTrackingData() {
|
|
255
|
+
// Try to get data from global persistent data first (most reliable)
|
|
256
|
+
if (global.__LINEAGE_PERSISTENT_DATA__ && global.__LINEAGE_PERSISTENT_DATA__.length > 0) {
|
|
257
|
+
console.log('🎯 Found precise lineage tracking data from global persistent data! Replacing estimated data...');
|
|
258
|
+
|
|
259
|
+
// Clear existing coverage data and replace with precise data
|
|
260
|
+
this.coverageData = {};
|
|
261
|
+
this.processFileTrackingData(global.__LINEAGE_PERSISTENT_DATA__);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fallback to global function
|
|
266
|
+
if (global.__GET_LINEAGE_RESULTS__) {
|
|
267
|
+
const lineageResults = global.__GET_LINEAGE_RESULTS__();
|
|
268
|
+
if (Object.keys(lineageResults).length > 0) {
|
|
269
|
+
console.log('🎯 Found precise lineage tracking data from global function! Replacing estimated data...');
|
|
270
|
+
|
|
271
|
+
// Clear existing coverage data and replace with precise data
|
|
272
|
+
this.coverageData = {};
|
|
273
|
+
this.processLineageResults(lineageResults, 'precise-tracking');
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Last resort: try to read tracking data from file
|
|
279
|
+
const fileData = this.readTrackingDataFromFile();
|
|
280
|
+
if (fileData) {
|
|
281
|
+
console.log('🎯 Found precise lineage tracking data from file! Replacing estimated data...');
|
|
282
|
+
|
|
283
|
+
// Clear existing coverage data and replace with precise data
|
|
284
|
+
this.coverageData = {};
|
|
285
|
+
this.processFileTrackingData(fileData);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log('⚠️ No precise tracking data found, using estimated coverage');
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Convert the current coverage data to the format expected by MutationTester
|
|
295
|
+
*/
|
|
296
|
+
convertCoverageDataToLineageFormat() {
|
|
297
|
+
const lineageData = {};
|
|
298
|
+
|
|
299
|
+
// Iterate through all coverage data and convert to mutation testing format
|
|
300
|
+
// The actual structure is: this.coverageData[filePath][lineNumber] = [testInfo, ...]
|
|
301
|
+
for (const [filePath, fileData] of Object.entries(this.coverageData)) {
|
|
302
|
+
if (!fileData || typeof fileData !== 'object') continue;
|
|
303
|
+
|
|
304
|
+
for (const [lineNumber, tests] of Object.entries(fileData)) {
|
|
305
|
+
if (!Array.isArray(tests) || tests.length === 0) continue;
|
|
306
|
+
|
|
307
|
+
if (!lineageData[filePath]) {
|
|
308
|
+
lineageData[filePath] = {};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
lineageData[filePath][lineNumber] = tests.map(test => ({
|
|
312
|
+
testName: test.name || test.testName || 'Unknown test',
|
|
313
|
+
testType: test.testType || test.type || 'it',
|
|
314
|
+
testFile: test.testFile || test.file || 'unknown',
|
|
315
|
+
executionCount: test.executionCount || 1,
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log(`🔄 Converted coverage data to lineage format: ${Object.keys(lineageData).length} files`);
|
|
321
|
+
Object.keys(lineageData).forEach(filePath => {
|
|
322
|
+
const lineCount = Object.keys(lineageData[filePath]).length;
|
|
323
|
+
console.log(` ${filePath}: ${lineCount} lines`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return lineageData;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
readTrackingDataFromFile() {
|
|
330
|
+
try {
|
|
331
|
+
const filePath = path.join(process.cwd(), '.jest-lineage-data.json');
|
|
332
|
+
if (fs.existsSync(filePath)) {
|
|
333
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
334
|
+
console.log(`📖 Read tracking data: ${data.tests.length} tests from file`);
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
return data.tests;
|
|
339
|
+
} else {
|
|
340
|
+
console.log(`⚠️ Tracking data file not found: ${filePath}`);
|
|
341
|
+
}
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.warn('Warning: Could not read tracking data from file:', error.message);
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
processFileTrackingData(testDataArray) {
|
|
349
|
+
if (!Array.isArray(testDataArray)) {
|
|
350
|
+
console.warn('⚠️ processFileTrackingData: testDataArray is not an array');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
console.log(`🔍 Processing ${testDataArray.length} test data entries`);
|
|
355
|
+
|
|
356
|
+
let processedFiles = 0;
|
|
357
|
+
let processedLines = 0;
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
testDataArray.forEach((testData, index) => {
|
|
361
|
+
try {
|
|
362
|
+
if (!testData || typeof testData !== 'object') {
|
|
363
|
+
console.warn(`⚠️ Skipping invalid test data at index ${index}:`, testData);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!testData.coverage || typeof testData.coverage !== 'object') {
|
|
368
|
+
console.warn(`⚠️ Skipping test data with invalid coverage at index ${index}:`, testData.name);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// testData.coverage is now a plain object, not a Map
|
|
373
|
+
Object.entries(testData.coverage).forEach(([key, count]) => {
|
|
374
|
+
try {
|
|
375
|
+
// Skip metadata, depth, and performance entries for now (process them separately)
|
|
376
|
+
if (key.includes(':meta') || key.includes(':depth') || key.includes(':performance')) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const parts = key.split(':');
|
|
381
|
+
if (parts.length < 2) {
|
|
382
|
+
console.warn(`⚠️ Invalid key format: ${key}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const lineNumber = parts.pop(); // Last part is line number
|
|
387
|
+
const filePath = parts.join(':'); // Rejoin in case path contains colons
|
|
388
|
+
|
|
389
|
+
// Validate line number
|
|
390
|
+
if (isNaN(parseInt(lineNumber))) {
|
|
391
|
+
console.warn(`⚠️ Invalid line number: ${lineNumber} for file: ${filePath}`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Skip test files and node_modules
|
|
396
|
+
if (filePath.includes('__tests__') ||
|
|
397
|
+
filePath.includes('.test.') ||
|
|
398
|
+
filePath.includes('.spec.') ||
|
|
399
|
+
filePath.includes('node_modules')) {
|
|
400
|
+
console.log(`🔍 DEBUG: Skipping test/node_modules file: ${filePath}`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
//console.log(`🔍 DEBUG: Processing coverage for ${filePath}:${lineNumber} (count: ${count})`);
|
|
405
|
+
processedLines++;
|
|
406
|
+
|
|
407
|
+
// Get depth data for this line
|
|
408
|
+
const depthKey = `${filePath}:${lineNumber}:depth`;
|
|
409
|
+
const depthData = testData.coverage[depthKey] || { 1: count };
|
|
410
|
+
|
|
411
|
+
// Get metadata for this line
|
|
412
|
+
const metaKey = `${filePath}:${lineNumber}:meta`;
|
|
413
|
+
const metaData = testData.coverage[metaKey] || {};
|
|
414
|
+
|
|
415
|
+
// Get performance data for this line
|
|
416
|
+
const performanceKey = `${filePath}:${lineNumber}:performance`;
|
|
417
|
+
const performanceData = testData.coverage[performanceKey] || {
|
|
418
|
+
totalExecutions: count,
|
|
419
|
+
totalCpuTime: 0,
|
|
420
|
+
totalWallTime: 0,
|
|
421
|
+
totalMemoryDelta: 0,
|
|
422
|
+
minExecutionTime: 0,
|
|
423
|
+
maxExecutionTime: 0,
|
|
424
|
+
executionTimes: [],
|
|
425
|
+
cpuCycles: []
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// Initialize the data structure for this file if it doesn't exist
|
|
429
|
+
if (!this.coverageData[filePath]) {
|
|
430
|
+
this.coverageData[filePath] = {};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!this.coverageData[filePath][lineNumber]) {
|
|
434
|
+
this.coverageData[filePath][lineNumber] = [];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Add test with precise tracking information including depth, performance, and quality
|
|
438
|
+
const testInfo = {
|
|
439
|
+
name: testData.name || 'Unknown test',
|
|
440
|
+
file: testData.testFile || 'unknown-test-file',
|
|
441
|
+
fullPath: testData.testFile || 'unknown-test-file',
|
|
442
|
+
executionCount: typeof count === 'number' ? count : 1,
|
|
443
|
+
duration: testData.duration || 0,
|
|
444
|
+
type: 'precise', // Mark as precise tracking
|
|
445
|
+
depthData: depthData, // Call depth information
|
|
446
|
+
minDepth: metaData.minDepth || 1,
|
|
447
|
+
maxDepth: metaData.maxDepth || 1,
|
|
448
|
+
nodeType: metaData.nodeType || 'unknown',
|
|
449
|
+
performance: {
|
|
450
|
+
totalCpuTime: performanceData.totalCpuTime || 0,
|
|
451
|
+
totalWallTime: performanceData.totalWallTime || 0,
|
|
452
|
+
avgCpuTime: performanceData.totalExecutions > 0 ? performanceData.totalCpuTime / performanceData.totalExecutions : 0,
|
|
453
|
+
avgWallTime: performanceData.totalExecutions > 0 ? performanceData.totalWallTime / performanceData.totalExecutions : 0,
|
|
454
|
+
totalMemoryDelta: performanceData.totalMemoryDelta || 0,
|
|
455
|
+
minExecutionTime: performanceData.minExecutionTime || 0,
|
|
456
|
+
maxExecutionTime: performanceData.maxExecutionTime || 0,
|
|
457
|
+
totalCpuCycles: performanceData.cpuCycles ? performanceData.cpuCycles.reduce((sum, cycles) => sum + cycles, 0) : 0,
|
|
458
|
+
avgCpuCycles: performanceData.cpuCycles && performanceData.cpuCycles.length > 0 ?
|
|
459
|
+
performanceData.cpuCycles.reduce((sum, cycles) => sum + cycles, 0) / performanceData.cpuCycles.length : 0,
|
|
460
|
+
performanceVariance: performanceData.performanceVariance || 0,
|
|
461
|
+
performanceStdDev: performanceData.performanceStdDev || 0,
|
|
462
|
+
performanceP95: performanceData.performanceP95 || 0,
|
|
463
|
+
performanceP99: performanceData.performanceP99 || 0,
|
|
464
|
+
slowExecutions: performanceData.slowExecutions || 0,
|
|
465
|
+
fastExecutions: performanceData.fastExecutions || 0,
|
|
466
|
+
memoryLeaks: performanceData.memoryLeaks || 0,
|
|
467
|
+
gcPressure: performanceData.gcPressure || 0
|
|
468
|
+
},
|
|
469
|
+
quality: testData.qualityMetrics || {
|
|
470
|
+
assertions: 0,
|
|
471
|
+
asyncOperations: 0,
|
|
472
|
+
mockUsage: 0,
|
|
473
|
+
errorHandling: 0,
|
|
474
|
+
edgeCases: 0,
|
|
475
|
+
complexity: 0,
|
|
476
|
+
maintainability: 50,
|
|
477
|
+
reliability: 50,
|
|
478
|
+
testSmells: [],
|
|
479
|
+
codePatterns: [],
|
|
480
|
+
isolationScore: 100,
|
|
481
|
+
testLength: 0
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
this.coverageData[filePath][lineNumber].push(testInfo);
|
|
486
|
+
|
|
487
|
+
// console.log(`🔍 DEBUG: Added coverage for "${filePath}":${lineNumber} -> ${testData.name} (${count} executions)`);
|
|
488
|
+
|
|
489
|
+
} catch (entryError) {
|
|
490
|
+
console.warn(`⚠️ Error processing coverage entry ${key}:`, entryError.message);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
} catch (testError) {
|
|
494
|
+
console.warn(`⚠️ Error processing test data at index ${index}:`, testError.message);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
console.log(`✅ Processed tracking data for ${Object.keys(this.coverageData).length} files (${processedLines} lines processed)`);
|
|
499
|
+
|
|
500
|
+
// Debug: Show what files were processed
|
|
501
|
+
Object.keys(this.coverageData).forEach(filePath => {
|
|
502
|
+
const lineCount = Object.keys(this.coverageData[filePath]).length;
|
|
503
|
+
console.log(` 📁 ${filePath}: ${lineCount} lines`);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
generateReport() {
|
|
508
|
+
console.log('\n--- Jest Test Lineage Reporter: Line-by-Line Test Coverage ---');
|
|
509
|
+
|
|
510
|
+
// Generate test quality summary first
|
|
511
|
+
this.generateTestQualitySummary();
|
|
512
|
+
|
|
513
|
+
for (const filePath in this.coverageData) {
|
|
514
|
+
const lineCoverage = this.coverageData[filePath];
|
|
515
|
+
|
|
516
|
+
const lineNumbers = Object.keys(lineCoverage).sort((a, b) => parseInt(a) - parseInt(b));
|
|
517
|
+
|
|
518
|
+
if (lineNumbers.length === 0) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
for (const line of lineNumbers) {
|
|
523
|
+
const testInfos = lineCoverage[line];
|
|
524
|
+
const uniqueTests = this.deduplicateTests(testInfos);
|
|
525
|
+
uniqueTests.forEach(testInfo => {
|
|
526
|
+
const testName = typeof testInfo === 'string' ? testInfo : testInfo.name;
|
|
527
|
+
const testFile = typeof testInfo === 'object' ? testInfo.file : 'Unknown';
|
|
528
|
+
const executionCount = typeof testInfo === 'object' ? testInfo.executionCount : 1;
|
|
529
|
+
const trackingType = typeof testInfo === 'object' && testInfo.type === 'precise' ? '✅ PRECISE' : '⚠️ ESTIMATED';
|
|
530
|
+
|
|
531
|
+
// Add depth information for precise tracking
|
|
532
|
+
let depthInfo = '';
|
|
533
|
+
if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.depthData) {
|
|
534
|
+
const depths = Object.keys(testInfo.depthData).map(d => parseInt(d)).sort((a, b) => a - b);
|
|
535
|
+
if (depths.length === 1) {
|
|
536
|
+
depthInfo = `, depth ${depths[0]}`;
|
|
537
|
+
} else {
|
|
538
|
+
depthInfo = `, depths ${depths.join(',')}`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Add performance information for precise tracking
|
|
543
|
+
let performanceInfo = '';
|
|
544
|
+
if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.performance) {
|
|
545
|
+
const perf = testInfo.performance;
|
|
546
|
+
if (perf.avgCpuCycles > 0) {
|
|
547
|
+
const cycles = perf.avgCpuCycles > 1000000 ?
|
|
548
|
+
`${(perf.avgCpuCycles / 1000000).toFixed(1)}M` :
|
|
549
|
+
`${Math.round(perf.avgCpuCycles)}`;
|
|
550
|
+
const cpuTime = perf.avgCpuTime > 1000 ?
|
|
551
|
+
`${(perf.avgCpuTime / 1000).toFixed(2)}ms` :
|
|
552
|
+
`${perf.avgCpuTime.toFixed(1)}μs`;
|
|
553
|
+
|
|
554
|
+
// Add memory information
|
|
555
|
+
let memoryInfo = '';
|
|
556
|
+
if (perf.totalMemoryDelta !== 0) {
|
|
557
|
+
const memoryMB = Math.abs(perf.totalMemoryDelta) / (1024 * 1024);
|
|
558
|
+
const memorySign = perf.totalMemoryDelta > 0 ? '+' : '-';
|
|
559
|
+
if (memoryMB > 1) {
|
|
560
|
+
memoryInfo = `, ${memorySign}${memoryMB.toFixed(1)}MB`;
|
|
561
|
+
} else {
|
|
562
|
+
const memoryKB = Math.abs(perf.totalMemoryDelta) / 1024;
|
|
563
|
+
memoryInfo = `, ${memorySign}${memoryKB.toFixed(1)}KB`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Add performance alerts
|
|
568
|
+
let alerts = '';
|
|
569
|
+
const memoryMB = Math.abs(perf.totalMemoryDelta || 0) / (1024 * 1024);
|
|
570
|
+
if (memoryMB > 1) alerts += ` 🚨LEAK`;
|
|
571
|
+
if (perf.gcPressure > 5) alerts += ` 🗑️GC`;
|
|
572
|
+
if (perf.slowExecutions > perf.fastExecutions) alerts += ` 🐌SLOW`;
|
|
573
|
+
|
|
574
|
+
performanceInfo = `, ${cycles} cycles, ${cpuTime}${memoryInfo}${alerts}`;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Add quality information for precise tracking
|
|
579
|
+
let qualityInfo = '';
|
|
580
|
+
if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.quality) {
|
|
581
|
+
const quality = testInfo.quality;
|
|
582
|
+
const qualityBadges = [];
|
|
583
|
+
|
|
584
|
+
// Quality score with explanation
|
|
585
|
+
let qualityScore = '';
|
|
586
|
+
if (quality.maintainability >= 80) {
|
|
587
|
+
qualityScore = '🏆 High Quality';
|
|
588
|
+
} else if (quality.maintainability >= 60) {
|
|
589
|
+
qualityScore = '✅ Good Quality';
|
|
590
|
+
} else if (quality.maintainability >= 40) {
|
|
591
|
+
qualityScore = '⚠️ Fair Quality';
|
|
592
|
+
} else {
|
|
593
|
+
qualityScore = '❌ Poor Quality';
|
|
594
|
+
}
|
|
595
|
+
qualityBadges.push(`${qualityScore} (${Math.round(quality.maintainability)}%)`);
|
|
596
|
+
|
|
597
|
+
// Reliability score
|
|
598
|
+
if (quality.reliability >= 80) qualityBadges.push(`🛡️ Reliable (${Math.round(quality.reliability)}%)`);
|
|
599
|
+
else if (quality.reliability >= 60) qualityBadges.push(`🔒 Stable (${Math.round(quality.reliability)}%)`);
|
|
600
|
+
else qualityBadges.push(`⚠️ Fragile (${Math.round(quality.reliability)}%)`);
|
|
601
|
+
|
|
602
|
+
// Test smells with details
|
|
603
|
+
if (quality.testSmells && quality.testSmells.length > 0) {
|
|
604
|
+
qualityBadges.push(`🚨 ${quality.testSmells.length} Smells: ${quality.testSmells.join(', ')}`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Positive indicators
|
|
608
|
+
if (quality.assertions > 5) qualityBadges.push(`🎯 ${quality.assertions} Assertions`);
|
|
609
|
+
if (quality.errorHandling > 0) qualityBadges.push(`🔒 ${quality.errorHandling} Error Tests`);
|
|
610
|
+
if (quality.edgeCases > 3) qualityBadges.push(`🎪 ${quality.edgeCases} Edge Cases`);
|
|
611
|
+
|
|
612
|
+
if (qualityBadges.length > 0) {
|
|
613
|
+
qualityInfo = ` [${qualityBadges.join(', ')}]`;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// console.log(` - "${testName}" (${testFile}, ${executionCount} executions${depthInfo}${performanceInfo}) ${trackingType}${qualityInfo}`);
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
console.log('\n--- Report End ---');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async generateHtmlReport() {
|
|
626
|
+
console.log('\n📄 Generating HTML coverage report...');
|
|
627
|
+
|
|
628
|
+
let html = this.generateHtmlTemplate();
|
|
629
|
+
|
|
630
|
+
// Validate coverage data before processing
|
|
631
|
+
const validFiles = this.validateCoverageData();
|
|
632
|
+
|
|
633
|
+
for (const filePath of validFiles) {
|
|
634
|
+
try {
|
|
635
|
+
html += await this.generateCodeTreeSection(filePath);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
console.error(`❌ Error generating section for ${filePath}:`, error.message);
|
|
638
|
+
html += this.generateErrorSection(filePath, error.message);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
html += this.generateHtmlFooter();
|
|
643
|
+
|
|
644
|
+
// Write HTML file
|
|
645
|
+
const outputPath = path.join(process.cwd(), 'test-lineage-report.html');
|
|
646
|
+
fs.writeFileSync(outputPath, html, 'utf8');
|
|
647
|
+
|
|
648
|
+
console.log(`✅ HTML report generated: ${outputPath}`);
|
|
649
|
+
console.log('🌐 Open the file in your browser to view the visual coverage report');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
validateCoverageData() {
|
|
653
|
+
const validFiles = [];
|
|
654
|
+
|
|
655
|
+
for (const filePath in this.coverageData) {
|
|
656
|
+
const fileData = this.coverageData[filePath];
|
|
657
|
+
|
|
658
|
+
if (!fileData) {
|
|
659
|
+
console.warn(`⚠️ Skipping ${filePath}: No coverage data`);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (typeof fileData !== 'object') {
|
|
664
|
+
console.warn(`⚠️ Skipping ${filePath}: Invalid data type (${typeof fileData})`);
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (Object.keys(fileData).length === 0) {
|
|
669
|
+
console.warn(`⚠️ Skipping ${filePath}: Empty coverage data`);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Validate that the data structure is correct
|
|
674
|
+
let hasValidData = false;
|
|
675
|
+
for (const lineNumber in fileData) {
|
|
676
|
+
const lineData = fileData[lineNumber];
|
|
677
|
+
if (Array.isArray(lineData) && lineData.length > 0) {
|
|
678
|
+
hasValidData = true;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (!hasValidData) {
|
|
684
|
+
console.warn(`⚠️ Skipping ${filePath}: No valid line data found`);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
validFiles.push(filePath);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// console.log(`✅ Validated ${validFiles.length} files for HTML report`);
|
|
692
|
+
return validFiles;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
findMatchingCoveragePath(targetPath) {
|
|
696
|
+
const targetBasename = path.basename(targetPath);
|
|
697
|
+
|
|
698
|
+
// Strategy 1: Find exact basename match
|
|
699
|
+
for (const coveragePath of Object.keys(this.coverageData)) {
|
|
700
|
+
if (path.basename(coveragePath) === targetBasename) {
|
|
701
|
+
return coveragePath;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Strategy 2: Convert both paths to relative from package.json and compare
|
|
706
|
+
const projectRoot = this.findProjectRoot(process.cwd());
|
|
707
|
+
const targetRelative = this.makeRelativeToProject(targetPath, projectRoot);
|
|
708
|
+
|
|
709
|
+
for (const coveragePath of Object.keys(this.coverageData)) {
|
|
710
|
+
const coverageRelative = this.makeRelativeToProject(coveragePath, projectRoot);
|
|
711
|
+
if (targetRelative === coverageRelative) {
|
|
712
|
+
return coveragePath;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Strategy 3: Find path that contains the target filename
|
|
717
|
+
for (const coveragePath of Object.keys(this.coverageData)) {
|
|
718
|
+
if (coveragePath.includes(targetBasename)) {
|
|
719
|
+
return coveragePath;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Strategy 4: Find path where target contains the coverage filename
|
|
724
|
+
for (const coveragePath of Object.keys(this.coverageData)) {
|
|
725
|
+
const coverageBasename = path.basename(coveragePath);
|
|
726
|
+
if (targetPath.includes(coverageBasename)) {
|
|
727
|
+
return coveragePath;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Strategy 5: Fuzzy matching - remove common path differences
|
|
732
|
+
const normalizedTarget = this.normalizePath(targetPath);
|
|
733
|
+
for (const coveragePath of Object.keys(this.coverageData)) {
|
|
734
|
+
const normalizedCoverage = this.normalizePath(coveragePath);
|
|
735
|
+
if (normalizedTarget === normalizedCoverage) {
|
|
736
|
+
return coveragePath;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
findProjectRoot(startPath) {
|
|
744
|
+
let currentDir = startPath;
|
|
745
|
+
const root = path.parse(currentDir).root;
|
|
746
|
+
|
|
747
|
+
while (currentDir !== root) {
|
|
748
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
749
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
750
|
+
return currentDir;
|
|
751
|
+
}
|
|
752
|
+
currentDir = path.dirname(currentDir);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Fallback to current working directory if no package.json found
|
|
756
|
+
return process.cwd();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
makeRelativeToProject(filePath, projectRoot) {
|
|
760
|
+
// If path is absolute and starts with project root, make it relative
|
|
761
|
+
if (path.isAbsolute(filePath) && filePath.startsWith(projectRoot)) {
|
|
762
|
+
return path.relative(projectRoot, filePath);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// If path is already relative, return as-is
|
|
766
|
+
if (!path.isAbsolute(filePath)) {
|
|
767
|
+
return filePath;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Try to find common base with project root
|
|
771
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
772
|
+
if (!relativePath.startsWith('..')) {
|
|
773
|
+
return relativePath;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Fallback to original path
|
|
777
|
+
return filePath;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
normalizePath(filePath) {
|
|
781
|
+
// Remove common path variations that might cause mismatches
|
|
782
|
+
return filePath
|
|
783
|
+
.replace(/\/services\//g, '/') // Remove /services/ directory
|
|
784
|
+
.replace(/\/src\//g, '/') // Remove /src/ directory
|
|
785
|
+
.replace(/\/lib\//g, '/') // Remove /lib/ directory
|
|
786
|
+
.replace(/\/app\//g, '/') // Remove /app/ directory
|
|
787
|
+
.replace(/\/server\//g, '/') // Remove /server/ directory
|
|
788
|
+
.replace(/\/client\//g, '/') // Remove /client/ directory
|
|
789
|
+
.replace(/\/+/g, '/') // Replace multiple slashes with single
|
|
790
|
+
.toLowerCase(); // Case insensitive matching
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
generateErrorSection(filePath, errorMessage) {
|
|
794
|
+
return `<div class="file-section">
|
|
795
|
+
<div class="file-header">❌ Error processing ${path.basename(filePath)}</div>
|
|
796
|
+
<div class="error">
|
|
797
|
+
<p><strong>File:</strong> ${filePath}</p>
|
|
798
|
+
<p><strong>Error:</strong> ${errorMessage}</p>
|
|
799
|
+
<p><strong>Suggestion:</strong> Check if the file exists and has valid coverage data.</p>
|
|
800
|
+
</div>
|
|
801
|
+
</div>`;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
generateHtmlTemplate() {
|
|
805
|
+
return `<!DOCTYPE html>
|
|
806
|
+
<html lang="en">
|
|
807
|
+
<head>
|
|
808
|
+
<meta charset="UTF-8">
|
|
809
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
810
|
+
<title>Jest Test Lineage Report</title>
|
|
811
|
+
<style>
|
|
812
|
+
body {
|
|
813
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
814
|
+
margin: 0;
|
|
815
|
+
padding: 20px;
|
|
816
|
+
background-color: #f5f5f5;
|
|
817
|
+
line-height: 1.6;
|
|
818
|
+
}
|
|
819
|
+
.header {
|
|
820
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
821
|
+
color: white;
|
|
822
|
+
padding: 30px;
|
|
823
|
+
border-radius: 10px;
|
|
824
|
+
margin-bottom: 20px;
|
|
825
|
+
text-align: center;
|
|
826
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
827
|
+
}
|
|
828
|
+
.navigation {
|
|
829
|
+
background: white;
|
|
830
|
+
padding: 20px;
|
|
831
|
+
margin-bottom: 20px;
|
|
832
|
+
border-radius: 10px;
|
|
833
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
834
|
+
display: flex;
|
|
835
|
+
justify-content: space-between;
|
|
836
|
+
align-items: center;
|
|
837
|
+
flex-wrap: wrap;
|
|
838
|
+
gap: 15px;
|
|
839
|
+
}
|
|
840
|
+
.nav-buttons {
|
|
841
|
+
display: flex;
|
|
842
|
+
gap: 10px;
|
|
843
|
+
flex-wrap: wrap;
|
|
844
|
+
}
|
|
845
|
+
.nav-btn, .action-btn {
|
|
846
|
+
padding: 10px 20px;
|
|
847
|
+
border: none;
|
|
848
|
+
border-radius: 6px;
|
|
849
|
+
cursor: pointer;
|
|
850
|
+
font-size: 14px;
|
|
851
|
+
font-weight: 500;
|
|
852
|
+
transition: all 0.2s ease;
|
|
853
|
+
}
|
|
854
|
+
.nav-btn {
|
|
855
|
+
background: #e9ecef;
|
|
856
|
+
color: #495057;
|
|
857
|
+
}
|
|
858
|
+
.nav-btn.active {
|
|
859
|
+
background: #007bff;
|
|
860
|
+
color: white;
|
|
861
|
+
}
|
|
862
|
+
.nav-btn:hover {
|
|
863
|
+
background: #007bff;
|
|
864
|
+
color: white;
|
|
865
|
+
}
|
|
866
|
+
.action-btn {
|
|
867
|
+
background: #28a745;
|
|
868
|
+
color: white;
|
|
869
|
+
}
|
|
870
|
+
.action-btn:hover {
|
|
871
|
+
background: #218838;
|
|
872
|
+
}
|
|
873
|
+
.sort-controls {
|
|
874
|
+
display: flex;
|
|
875
|
+
align-items: center;
|
|
876
|
+
gap: 10px;
|
|
877
|
+
}
|
|
878
|
+
.sort-controls label {
|
|
879
|
+
font-weight: 500;
|
|
880
|
+
color: #495057;
|
|
881
|
+
}
|
|
882
|
+
.sort-controls select {
|
|
883
|
+
padding: 8px 12px;
|
|
884
|
+
border: 1px solid #ced4da;
|
|
885
|
+
border-radius: 4px;
|
|
886
|
+
background: white;
|
|
887
|
+
font-size: 14px;
|
|
888
|
+
}
|
|
889
|
+
.header h1 {
|
|
890
|
+
margin: 0;
|
|
891
|
+
font-size: 2.5em;
|
|
892
|
+
font-weight: 300;
|
|
893
|
+
}
|
|
894
|
+
.header p {
|
|
895
|
+
margin: 10px 0 0 0;
|
|
896
|
+
opacity: 0.9;
|
|
897
|
+
font-size: 1.1em;
|
|
898
|
+
}
|
|
899
|
+
.file-section {
|
|
900
|
+
background: white;
|
|
901
|
+
margin-bottom: 30px;
|
|
902
|
+
border-radius: 10px;
|
|
903
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
904
|
+
overflow: hidden;
|
|
905
|
+
}
|
|
906
|
+
.file-header {
|
|
907
|
+
background: #2c3e50;
|
|
908
|
+
color: white;
|
|
909
|
+
padding: 20px;
|
|
910
|
+
font-size: 1.2em;
|
|
911
|
+
font-weight: 500;
|
|
912
|
+
cursor: pointer;
|
|
913
|
+
transition: background 0.2s ease;
|
|
914
|
+
display: flex;
|
|
915
|
+
justify-content: space-between;
|
|
916
|
+
align-items: center;
|
|
917
|
+
}
|
|
918
|
+
.file-header:hover {
|
|
919
|
+
background: #34495e;
|
|
920
|
+
}
|
|
921
|
+
.file-header .expand-icon {
|
|
922
|
+
font-size: 18px;
|
|
923
|
+
transition: transform 0.3s ease;
|
|
924
|
+
}
|
|
925
|
+
.file-header.expanded .expand-icon {
|
|
926
|
+
transform: rotate(90deg);
|
|
927
|
+
}
|
|
928
|
+
.file-content {
|
|
929
|
+
display: none;
|
|
930
|
+
}
|
|
931
|
+
.file-content.expanded {
|
|
932
|
+
display: block;
|
|
933
|
+
}
|
|
934
|
+
.file-path {
|
|
935
|
+
font-family: 'Courier New', monospace;
|
|
936
|
+
opacity: 0.8;
|
|
937
|
+
font-size: 0.9em;
|
|
938
|
+
}
|
|
939
|
+
.code-container {
|
|
940
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
941
|
+
font-size: 14px;
|
|
942
|
+
line-height: 1.5;
|
|
943
|
+
background: #f8f9fa;
|
|
944
|
+
}
|
|
945
|
+
.code-line {
|
|
946
|
+
display: flex;
|
|
947
|
+
border-bottom: 1px solid #e9ecef;
|
|
948
|
+
transition: background-color 0.2s ease;
|
|
949
|
+
}
|
|
950
|
+
.code-line:hover {
|
|
951
|
+
background-color: #e3f2fd;
|
|
952
|
+
}
|
|
953
|
+
.line-number {
|
|
954
|
+
background: #6c757d;
|
|
955
|
+
color: white;
|
|
956
|
+
padding: 8px 12px;
|
|
957
|
+
min-width: 60px;
|
|
958
|
+
text-align: right;
|
|
959
|
+
font-weight: bold;
|
|
960
|
+
user-select: none;
|
|
961
|
+
}
|
|
962
|
+
.line-covered {
|
|
963
|
+
background: #28a745;
|
|
964
|
+
}
|
|
965
|
+
.line-uncovered {
|
|
966
|
+
background: #dc3545;
|
|
967
|
+
}
|
|
968
|
+
.line-content {
|
|
969
|
+
flex: 1;
|
|
970
|
+
padding: 8px 16px;
|
|
971
|
+
white-space: pre;
|
|
972
|
+
overflow-x: auto;
|
|
973
|
+
}
|
|
974
|
+
.coverage-indicator {
|
|
975
|
+
background: #17a2b8;
|
|
976
|
+
color: white;
|
|
977
|
+
padding: 8px 12px;
|
|
978
|
+
min-width: 80px;
|
|
979
|
+
text-align: center;
|
|
980
|
+
font-size: 12px;
|
|
981
|
+
cursor: pointer;
|
|
982
|
+
user-select: none;
|
|
983
|
+
transition: background-color 0.2s ease;
|
|
984
|
+
}
|
|
985
|
+
.coverage-indicator:hover {
|
|
986
|
+
background: #138496;
|
|
987
|
+
}
|
|
988
|
+
.coverage-details {
|
|
989
|
+
display: none;
|
|
990
|
+
background: #fff3cd;
|
|
991
|
+
border-left: 4px solid #ffc107;
|
|
992
|
+
margin: 0;
|
|
993
|
+
padding: 15px;
|
|
994
|
+
border-bottom: 1px solid #e9ecef;
|
|
995
|
+
}
|
|
996
|
+
.coverage-details.expanded {
|
|
997
|
+
display: block;
|
|
998
|
+
}
|
|
999
|
+
.test-badge {
|
|
1000
|
+
display: inline-block;
|
|
1001
|
+
background: #007bff;
|
|
1002
|
+
color: white;
|
|
1003
|
+
padding: 4px 8px;
|
|
1004
|
+
margin: 2px;
|
|
1005
|
+
border-radius: 12px;
|
|
1006
|
+
font-size: 11px;
|
|
1007
|
+
transition: all 0.2s ease;
|
|
1008
|
+
}
|
|
1009
|
+
.test-badge:hover {
|
|
1010
|
+
background: #0056b3;
|
|
1011
|
+
transform: translateY(-1px);
|
|
1012
|
+
}
|
|
1013
|
+
.depth-badge {
|
|
1014
|
+
display: inline-block;
|
|
1015
|
+
background: #6c757d;
|
|
1016
|
+
color: white;
|
|
1017
|
+
padding: 2px 6px;
|
|
1018
|
+
margin-left: 4px;
|
|
1019
|
+
border-radius: 8px;
|
|
1020
|
+
font-size: 9px;
|
|
1021
|
+
font-weight: bold;
|
|
1022
|
+
vertical-align: middle;
|
|
1023
|
+
}
|
|
1024
|
+
.depth-badge.depth-1 {
|
|
1025
|
+
background: #28a745; /* Green for direct calls */
|
|
1026
|
+
}
|
|
1027
|
+
.depth-badge.depth-2 {
|
|
1028
|
+
background: #ffc107; /* Yellow for one level deep */
|
|
1029
|
+
color: #212529;
|
|
1030
|
+
}
|
|
1031
|
+
.depth-badge.depth-3 {
|
|
1032
|
+
background: #fd7e14; /* Orange for two levels deep */
|
|
1033
|
+
}
|
|
1034
|
+
.depth-badge.depth-4,
|
|
1035
|
+
.depth-badge.depth-5,
|
|
1036
|
+
.depth-badge.depth-6,
|
|
1037
|
+
.depth-badge.depth-7,
|
|
1038
|
+
.depth-badge.depth-8,
|
|
1039
|
+
.depth-badge.depth-9,
|
|
1040
|
+
.depth-badge.depth-10 {
|
|
1041
|
+
background: #dc3545; /* Red for very deep calls */
|
|
1042
|
+
}
|
|
1043
|
+
.lines-analysis {
|
|
1044
|
+
background: white;
|
|
1045
|
+
padding: 30px;
|
|
1046
|
+
border-radius: 10px;
|
|
1047
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
1048
|
+
margin-bottom: 30px;
|
|
1049
|
+
}
|
|
1050
|
+
.lines-table {
|
|
1051
|
+
width: 100%;
|
|
1052
|
+
border-collapse: collapse;
|
|
1053
|
+
margin-top: 20px;
|
|
1054
|
+
}
|
|
1055
|
+
.lines-table th,
|
|
1056
|
+
.lines-table td {
|
|
1057
|
+
padding: 12px;
|
|
1058
|
+
text-align: left;
|
|
1059
|
+
border-bottom: 1px solid #e9ecef;
|
|
1060
|
+
}
|
|
1061
|
+
.lines-table th {
|
|
1062
|
+
background: #f8f9fa;
|
|
1063
|
+
font-weight: 600;
|
|
1064
|
+
color: #495057;
|
|
1065
|
+
position: sticky;
|
|
1066
|
+
top: 0;
|
|
1067
|
+
}
|
|
1068
|
+
.lines-table tr:hover {
|
|
1069
|
+
background: #f8f9fa;
|
|
1070
|
+
}
|
|
1071
|
+
.file-name {
|
|
1072
|
+
font-family: 'Courier New', monospace;
|
|
1073
|
+
font-weight: 500;
|
|
1074
|
+
color: #007bff;
|
|
1075
|
+
}
|
|
1076
|
+
.line-number {
|
|
1077
|
+
font-family: 'Courier New', monospace;
|
|
1078
|
+
font-weight: bold;
|
|
1079
|
+
color: #6c757d;
|
|
1080
|
+
text-align: center;
|
|
1081
|
+
}
|
|
1082
|
+
.code-preview {
|
|
1083
|
+
font-family: 'Courier New', monospace;
|
|
1084
|
+
font-size: 12px;
|
|
1085
|
+
color: #495057;
|
|
1086
|
+
max-width: 300px;
|
|
1087
|
+
overflow: hidden;
|
|
1088
|
+
text-overflow: ellipsis;
|
|
1089
|
+
white-space: nowrap;
|
|
1090
|
+
}
|
|
1091
|
+
.test-count,
|
|
1092
|
+
.execution-count,
|
|
1093
|
+
.cpu-cycles,
|
|
1094
|
+
.cpu-time,
|
|
1095
|
+
.wall-time {
|
|
1096
|
+
text-align: center;
|
|
1097
|
+
font-weight: 600;
|
|
1098
|
+
}
|
|
1099
|
+
.cpu-cycles {
|
|
1100
|
+
color: #dc3545;
|
|
1101
|
+
font-family: 'Courier New', monospace;
|
|
1102
|
+
}
|
|
1103
|
+
.cpu-time {
|
|
1104
|
+
color: #fd7e14;
|
|
1105
|
+
font-family: 'Courier New', monospace;
|
|
1106
|
+
}
|
|
1107
|
+
.wall-time {
|
|
1108
|
+
color: #6f42c1;
|
|
1109
|
+
font-family: 'Courier New', monospace;
|
|
1110
|
+
}
|
|
1111
|
+
.max-depth,
|
|
1112
|
+
.depth-range,
|
|
1113
|
+
.quality-score,
|
|
1114
|
+
.reliability-score,
|
|
1115
|
+
.test-smells {
|
|
1116
|
+
text-align: center;
|
|
1117
|
+
}
|
|
1118
|
+
.quality-excellent {
|
|
1119
|
+
color: #28a745;
|
|
1120
|
+
font-weight: bold;
|
|
1121
|
+
}
|
|
1122
|
+
.quality-good {
|
|
1123
|
+
color: #6f42c1;
|
|
1124
|
+
font-weight: bold;
|
|
1125
|
+
}
|
|
1126
|
+
.quality-fair {
|
|
1127
|
+
color: #ffc107;
|
|
1128
|
+
font-weight: bold;
|
|
1129
|
+
}
|
|
1130
|
+
.quality-poor {
|
|
1131
|
+
color: #dc3545;
|
|
1132
|
+
font-weight: bold;
|
|
1133
|
+
}
|
|
1134
|
+
.smells-none {
|
|
1135
|
+
color: #28a745;
|
|
1136
|
+
font-weight: bold;
|
|
1137
|
+
}
|
|
1138
|
+
.smells-few {
|
|
1139
|
+
color: #ffc107;
|
|
1140
|
+
font-weight: bold;
|
|
1141
|
+
cursor: help;
|
|
1142
|
+
}
|
|
1143
|
+
.smells-many {
|
|
1144
|
+
color: #dc3545;
|
|
1145
|
+
font-weight: bold;
|
|
1146
|
+
cursor: help;
|
|
1147
|
+
}
|
|
1148
|
+
.smells-few[title]:hover,
|
|
1149
|
+
.smells-many[title]:hover {
|
|
1150
|
+
text-decoration: underline;
|
|
1151
|
+
}
|
|
1152
|
+
.performance-analysis,
|
|
1153
|
+
.quality-analysis,
|
|
1154
|
+
.mutations-analysis {
|
|
1155
|
+
background: white;
|
|
1156
|
+
padding: 30px;
|
|
1157
|
+
border-radius: 10px;
|
|
1158
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
1159
|
+
margin-bottom: 30px;
|
|
1160
|
+
}
|
|
1161
|
+
.performance-summary,
|
|
1162
|
+
.quality-summary {
|
|
1163
|
+
margin-bottom: 30px;
|
|
1164
|
+
}
|
|
1165
|
+
.perf-stats,
|
|
1166
|
+
.quality-stats {
|
|
1167
|
+
display: flex;
|
|
1168
|
+
gap: 20px;
|
|
1169
|
+
margin: 20px 0;
|
|
1170
|
+
flex-wrap: wrap;
|
|
1171
|
+
}
|
|
1172
|
+
.perf-stat,
|
|
1173
|
+
.quality-stat {
|
|
1174
|
+
background: #f8f9fa;
|
|
1175
|
+
padding: 20px;
|
|
1176
|
+
border-radius: 8px;
|
|
1177
|
+
text-align: center;
|
|
1178
|
+
min-width: 120px;
|
|
1179
|
+
}
|
|
1180
|
+
.perf-number,
|
|
1181
|
+
.quality-number {
|
|
1182
|
+
font-size: 24px;
|
|
1183
|
+
font-weight: bold;
|
|
1184
|
+
color: #007bff;
|
|
1185
|
+
}
|
|
1186
|
+
.perf-label,
|
|
1187
|
+
.quality-label {
|
|
1188
|
+
font-size: 12px;
|
|
1189
|
+
color: #6c757d;
|
|
1190
|
+
margin-top: 5px;
|
|
1191
|
+
}
|
|
1192
|
+
.performance-tables,
|
|
1193
|
+
.quality-tables {
|
|
1194
|
+
display: flex;
|
|
1195
|
+
gap: 30px;
|
|
1196
|
+
flex-wrap: wrap;
|
|
1197
|
+
}
|
|
1198
|
+
.perf-table-container,
|
|
1199
|
+
.quality-table-container {
|
|
1200
|
+
flex: 1;
|
|
1201
|
+
min-width: 400px;
|
|
1202
|
+
}
|
|
1203
|
+
.perf-table,
|
|
1204
|
+
.quality-table {
|
|
1205
|
+
width: 100%;
|
|
1206
|
+
border-collapse: collapse;
|
|
1207
|
+
margin-top: 10px;
|
|
1208
|
+
}
|
|
1209
|
+
.perf-table th,
|
|
1210
|
+
.perf-table td,
|
|
1211
|
+
.quality-table th,
|
|
1212
|
+
.quality-table td {
|
|
1213
|
+
padding: 8px 12px;
|
|
1214
|
+
text-align: left;
|
|
1215
|
+
border-bottom: 1px solid #e9ecef;
|
|
1216
|
+
}
|
|
1217
|
+
.perf-table th,
|
|
1218
|
+
.quality-table th {
|
|
1219
|
+
background: #f8f9fa;
|
|
1220
|
+
font-weight: 600;
|
|
1221
|
+
}
|
|
1222
|
+
.memory-usage {
|
|
1223
|
+
color: #6f42c1;
|
|
1224
|
+
font-weight: bold;
|
|
1225
|
+
}
|
|
1226
|
+
.memory-leaks {
|
|
1227
|
+
color: #dc3545;
|
|
1228
|
+
font-weight: bold;
|
|
1229
|
+
}
|
|
1230
|
+
.quality-distribution {
|
|
1231
|
+
margin: 20px 0;
|
|
1232
|
+
}
|
|
1233
|
+
.quality-bars {
|
|
1234
|
+
margin: 15px 0;
|
|
1235
|
+
}
|
|
1236
|
+
.quality-bar {
|
|
1237
|
+
margin: 10px 0;
|
|
1238
|
+
}
|
|
1239
|
+
.bar {
|
|
1240
|
+
width: 100%;
|
|
1241
|
+
height: 20px;
|
|
1242
|
+
background: #e9ecef;
|
|
1243
|
+
border-radius: 10px;
|
|
1244
|
+
overflow: hidden;
|
|
1245
|
+
margin: 5px 0;
|
|
1246
|
+
}
|
|
1247
|
+
.fill {
|
|
1248
|
+
height: 100%;
|
|
1249
|
+
transition: width 0.3s ease;
|
|
1250
|
+
}
|
|
1251
|
+
.fill.excellent {
|
|
1252
|
+
background: #28a745;
|
|
1253
|
+
}
|
|
1254
|
+
.fill.good {
|
|
1255
|
+
background: #6f42c1;
|
|
1256
|
+
}
|
|
1257
|
+
.fill.fair {
|
|
1258
|
+
background: #ffc107;
|
|
1259
|
+
}
|
|
1260
|
+
.fill.poor {
|
|
1261
|
+
background: #dc3545;
|
|
1262
|
+
}
|
|
1263
|
+
.quality-issues {
|
|
1264
|
+
color: #dc3545;
|
|
1265
|
+
font-size: 12px;
|
|
1266
|
+
}
|
|
1267
|
+
.recommendations {
|
|
1268
|
+
color: #007bff;
|
|
1269
|
+
font-size: 12px;
|
|
1270
|
+
font-style: italic;
|
|
1271
|
+
}
|
|
1272
|
+
.performance-help {
|
|
1273
|
+
background: #f8f9fa;
|
|
1274
|
+
border: 1px solid #e9ecef;
|
|
1275
|
+
border-radius: 8px;
|
|
1276
|
+
padding: 15px;
|
|
1277
|
+
margin: 15px 0;
|
|
1278
|
+
font-size: 14px;
|
|
1279
|
+
}
|
|
1280
|
+
.help-section {
|
|
1281
|
+
margin: 10px 0;
|
|
1282
|
+
padding: 8px;
|
|
1283
|
+
background: white;
|
|
1284
|
+
border-radius: 4px;
|
|
1285
|
+
border-left: 4px solid #007bff;
|
|
1286
|
+
}
|
|
1287
|
+
.help-section strong {
|
|
1288
|
+
color: #495057;
|
|
1289
|
+
}
|
|
1290
|
+
.help-section em {
|
|
1291
|
+
color: #28a745;
|
|
1292
|
+
font-weight: 600;
|
|
1293
|
+
}
|
|
1294
|
+
.test-file {
|
|
1295
|
+
font-weight: bold;
|
|
1296
|
+
color: #495057;
|
|
1297
|
+
margin-top: 10px;
|
|
1298
|
+
margin-bottom: 5px;
|
|
1299
|
+
}
|
|
1300
|
+
.test-file:first-child {
|
|
1301
|
+
margin-top: 0;
|
|
1302
|
+
}
|
|
1303
|
+
.line-coverage {
|
|
1304
|
+
padding: 20px;
|
|
1305
|
+
}
|
|
1306
|
+
.line-item {
|
|
1307
|
+
margin-bottom: 20px;
|
|
1308
|
+
padding: 15px;
|
|
1309
|
+
border-left: 4px solid #3498db;
|
|
1310
|
+
background: #f8f9fa;
|
|
1311
|
+
border-radius: 0 5px 5px 0;
|
|
1312
|
+
}
|
|
1313
|
+
.line-number {
|
|
1314
|
+
font-weight: bold;
|
|
1315
|
+
color: #2c3e50;
|
|
1316
|
+
font-size: 1.1em;
|
|
1317
|
+
margin-bottom: 10px;
|
|
1318
|
+
}
|
|
1319
|
+
.test-count {
|
|
1320
|
+
color: #7f8c8d;
|
|
1321
|
+
font-size: 0.9em;
|
|
1322
|
+
}
|
|
1323
|
+
.test-list {
|
|
1324
|
+
margin-top: 10px;
|
|
1325
|
+
}
|
|
1326
|
+
.test-item {
|
|
1327
|
+
background: white;
|
|
1328
|
+
margin: 5px 0;
|
|
1329
|
+
padding: 8px 12px;
|
|
1330
|
+
border-radius: 20px;
|
|
1331
|
+
display: inline-block;
|
|
1332
|
+
margin-right: 8px;
|
|
1333
|
+
margin-bottom: 8px;
|
|
1334
|
+
border: 1px solid #e1e8ed;
|
|
1335
|
+
font-size: 0.85em;
|
|
1336
|
+
transition: all 0.2s ease;
|
|
1337
|
+
}
|
|
1338
|
+
.test-item:hover {
|
|
1339
|
+
background: #3498db;
|
|
1340
|
+
color: white;
|
|
1341
|
+
transform: translateY(-1px);
|
|
1342
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
1343
|
+
}
|
|
1344
|
+
.stats {
|
|
1345
|
+
background: white;
|
|
1346
|
+
padding: 20px;
|
|
1347
|
+
border-radius: 10px;
|
|
1348
|
+
margin-bottom: 30px;
|
|
1349
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
1350
|
+
}
|
|
1351
|
+
.stats h3 {
|
|
1352
|
+
margin-top: 0;
|
|
1353
|
+
color: #2c3e50;
|
|
1354
|
+
}
|
|
1355
|
+
.stat-item {
|
|
1356
|
+
display: inline-block;
|
|
1357
|
+
margin-right: 30px;
|
|
1358
|
+
margin-bottom: 10px;
|
|
1359
|
+
}
|
|
1360
|
+
.stat-number {
|
|
1361
|
+
font-size: 2em;
|
|
1362
|
+
font-weight: bold;
|
|
1363
|
+
color: #3498db;
|
|
1364
|
+
}
|
|
1365
|
+
.stat-label {
|
|
1366
|
+
display: block;
|
|
1367
|
+
color: #7f8c8d;
|
|
1368
|
+
font-size: 0.9em;
|
|
1369
|
+
}
|
|
1370
|
+
.footer {
|
|
1371
|
+
text-align: center;
|
|
1372
|
+
color: #7f8c8d;
|
|
1373
|
+
margin-top: 40px;
|
|
1374
|
+
padding: 20px;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/* Mutation Testing Styles */
|
|
1378
|
+
.mutation-results {
|
|
1379
|
+
background: #f8f9fa;
|
|
1380
|
+
border: 1px solid #dee2e6;
|
|
1381
|
+
border-radius: 8px;
|
|
1382
|
+
margin: 15px 0;
|
|
1383
|
+
padding: 15px;
|
|
1384
|
+
}
|
|
1385
|
+
.mutation-results h4 {
|
|
1386
|
+
margin: 0 0 15px 0;
|
|
1387
|
+
color: #495057;
|
|
1388
|
+
font-size: 16px;
|
|
1389
|
+
}
|
|
1390
|
+
.mutation-summary {
|
|
1391
|
+
display: flex;
|
|
1392
|
+
align-items: center;
|
|
1393
|
+
gap: 15px;
|
|
1394
|
+
margin-bottom: 15px;
|
|
1395
|
+
flex-wrap: wrap;
|
|
1396
|
+
}
|
|
1397
|
+
.mutation-score {
|
|
1398
|
+
font-weight: bold;
|
|
1399
|
+
padding: 8px 12px;
|
|
1400
|
+
border-radius: 6px;
|
|
1401
|
+
font-size: 14px;
|
|
1402
|
+
}
|
|
1403
|
+
.mutation-score-good {
|
|
1404
|
+
background: #d4edda;
|
|
1405
|
+
color: #155724;
|
|
1406
|
+
border: 1px solid #c3e6cb;
|
|
1407
|
+
}
|
|
1408
|
+
.mutation-score-fair {
|
|
1409
|
+
background: #fff3cd;
|
|
1410
|
+
color: #856404;
|
|
1411
|
+
border: 1px solid #ffeaa7;
|
|
1412
|
+
}
|
|
1413
|
+
.mutation-score-poor {
|
|
1414
|
+
background: #f8d7da;
|
|
1415
|
+
color: #721c24;
|
|
1416
|
+
border: 1px solid #f5c6cb;
|
|
1417
|
+
}
|
|
1418
|
+
.mutation-stats {
|
|
1419
|
+
display: flex;
|
|
1420
|
+
gap: 10px;
|
|
1421
|
+
flex-wrap: wrap;
|
|
1422
|
+
}
|
|
1423
|
+
.mutation-stat {
|
|
1424
|
+
padding: 4px 8px;
|
|
1425
|
+
border-radius: 4px;
|
|
1426
|
+
font-size: 12px;
|
|
1427
|
+
font-weight: 500;
|
|
1428
|
+
}
|
|
1429
|
+
.mutation-stat.killed {
|
|
1430
|
+
background: #d4edda;
|
|
1431
|
+
color: #155724;
|
|
1432
|
+
}
|
|
1433
|
+
.mutation-stat.survived {
|
|
1434
|
+
background: #f8d7da;
|
|
1435
|
+
color: #721c24;
|
|
1436
|
+
}
|
|
1437
|
+
.mutation-stat.error {
|
|
1438
|
+
background: #f8d7da;
|
|
1439
|
+
color: #721c24;
|
|
1440
|
+
}
|
|
1441
|
+
.mutation-stat.timeout {
|
|
1442
|
+
background: #fff3cd;
|
|
1443
|
+
color: #856404;
|
|
1444
|
+
}
|
|
1445
|
+
.mutation-details {
|
|
1446
|
+
margin-top: 15px;
|
|
1447
|
+
}
|
|
1448
|
+
.mutation-group {
|
|
1449
|
+
margin-bottom: 15px;
|
|
1450
|
+
}
|
|
1451
|
+
.mutation-group h5 {
|
|
1452
|
+
margin: 0 0 10px 0;
|
|
1453
|
+
font-size: 14px;
|
|
1454
|
+
color: #495057;
|
|
1455
|
+
}
|
|
1456
|
+
.mutation-group summary {
|
|
1457
|
+
cursor: pointer;
|
|
1458
|
+
font-weight: 500;
|
|
1459
|
+
color: #495057;
|
|
1460
|
+
margin-bottom: 10px;
|
|
1461
|
+
}
|
|
1462
|
+
.mutation-item {
|
|
1463
|
+
display: flex;
|
|
1464
|
+
align-items: center;
|
|
1465
|
+
gap: 10px;
|
|
1466
|
+
padding: 8px 12px;
|
|
1467
|
+
margin: 5px 0;
|
|
1468
|
+
border-radius: 4px;
|
|
1469
|
+
font-size: 13px;
|
|
1470
|
+
flex-wrap: wrap;
|
|
1471
|
+
}
|
|
1472
|
+
.mutation-item.survived {
|
|
1473
|
+
background: #f8d7da;
|
|
1474
|
+
border-left: 4px solid #dc3545;
|
|
1475
|
+
}
|
|
1476
|
+
.mutation-item.killed {
|
|
1477
|
+
background: #d4edda;
|
|
1478
|
+
border-left: 4px solid #28a745;
|
|
1479
|
+
}
|
|
1480
|
+
.mutation-item.error {
|
|
1481
|
+
background: #f8d7da;
|
|
1482
|
+
border-left: 4px solid #dc3545;
|
|
1483
|
+
}
|
|
1484
|
+
.mutation-type {
|
|
1485
|
+
background: #6c757d;
|
|
1486
|
+
color: white;
|
|
1487
|
+
padding: 2px 6px;
|
|
1488
|
+
border-radius: 3px;
|
|
1489
|
+
font-size: 11px;
|
|
1490
|
+
font-weight: 500;
|
|
1491
|
+
text-transform: uppercase;
|
|
1492
|
+
min-width: 80px;
|
|
1493
|
+
text-align: center;
|
|
1494
|
+
}
|
|
1495
|
+
.mutation-description {
|
|
1496
|
+
flex: 1;
|
|
1497
|
+
color: #495057;
|
|
1498
|
+
}
|
|
1499
|
+
.mutation-tests {
|
|
1500
|
+
color: #6c757d;
|
|
1501
|
+
font-size: 12px;
|
|
1502
|
+
}
|
|
1503
|
+
.mutation-error {
|
|
1504
|
+
color: #721c24;
|
|
1505
|
+
font-style: italic;
|
|
1506
|
+
flex: 1;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/* Mutation Dashboard Styles */
|
|
1510
|
+
.mutations-overview {
|
|
1511
|
+
margin-bottom: 30px;
|
|
1512
|
+
}
|
|
1513
|
+
.mutations-summary-cards {
|
|
1514
|
+
display: grid;
|
|
1515
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
1516
|
+
gap: 20px;
|
|
1517
|
+
margin-bottom: 30px;
|
|
1518
|
+
}
|
|
1519
|
+
.summary-card {
|
|
1520
|
+
background: white;
|
|
1521
|
+
padding: 20px;
|
|
1522
|
+
border-radius: 8px;
|
|
1523
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
1524
|
+
text-align: center;
|
|
1525
|
+
border: 2px solid #e9ecef;
|
|
1526
|
+
}
|
|
1527
|
+
.summary-card.excellent {
|
|
1528
|
+
border-color: #28a745;
|
|
1529
|
+
background: #f8fff9;
|
|
1530
|
+
}
|
|
1531
|
+
.summary-card.good {
|
|
1532
|
+
border-color: #6f42c1;
|
|
1533
|
+
background: #f8f7ff;
|
|
1534
|
+
}
|
|
1535
|
+
.summary-card.fair {
|
|
1536
|
+
border-color: #ffc107;
|
|
1537
|
+
background: #fffef8;
|
|
1538
|
+
}
|
|
1539
|
+
.summary-card.poor {
|
|
1540
|
+
border-color: #dc3545;
|
|
1541
|
+
background: #fff8f8;
|
|
1542
|
+
}
|
|
1543
|
+
.summary-card.survived {
|
|
1544
|
+
border-color: #dc3545;
|
|
1545
|
+
background: #fff8f8;
|
|
1546
|
+
}
|
|
1547
|
+
.summary-card.killed {
|
|
1548
|
+
border-color: #28a745;
|
|
1549
|
+
background: #f8fff9;
|
|
1550
|
+
}
|
|
1551
|
+
.summary-card h3 {
|
|
1552
|
+
margin: 0 0 10px 0;
|
|
1553
|
+
font-size: 14px;
|
|
1554
|
+
color: #6c757d;
|
|
1555
|
+
font-weight: 600;
|
|
1556
|
+
}
|
|
1557
|
+
.big-number {
|
|
1558
|
+
font-size: 2.5em;
|
|
1559
|
+
font-weight: bold;
|
|
1560
|
+
color: #495057;
|
|
1561
|
+
margin: 10px 0;
|
|
1562
|
+
}
|
|
1563
|
+
.subtitle {
|
|
1564
|
+
font-size: 12px;
|
|
1565
|
+
color: #6c757d;
|
|
1566
|
+
}
|
|
1567
|
+
.mutations-chart {
|
|
1568
|
+
margin: 30px 0;
|
|
1569
|
+
}
|
|
1570
|
+
.mutation-bars {
|
|
1571
|
+
margin: 15px 0;
|
|
1572
|
+
}
|
|
1573
|
+
.mutation-bar {
|
|
1574
|
+
margin: 10px 0;
|
|
1575
|
+
}
|
|
1576
|
+
.mutation-bar span {
|
|
1577
|
+
display: block;
|
|
1578
|
+
margin-bottom: 5px;
|
|
1579
|
+
font-size: 14px;
|
|
1580
|
+
font-weight: 500;
|
|
1581
|
+
}
|
|
1582
|
+
.mutation-killed {
|
|
1583
|
+
color: #28a745;
|
|
1584
|
+
}
|
|
1585
|
+
.mutation-survived {
|
|
1586
|
+
color: #dc3545;
|
|
1587
|
+
}
|
|
1588
|
+
.mutation-timeout {
|
|
1589
|
+
color: #ffc107;
|
|
1590
|
+
}
|
|
1591
|
+
.mutation-error {
|
|
1592
|
+
color: #dc3545;
|
|
1593
|
+
}
|
|
1594
|
+
.fill.killed {
|
|
1595
|
+
background: #28a745;
|
|
1596
|
+
}
|
|
1597
|
+
.fill.survived {
|
|
1598
|
+
background: #dc3545;
|
|
1599
|
+
}
|
|
1600
|
+
.fill.timeout {
|
|
1601
|
+
background: #ffc107;
|
|
1602
|
+
}
|
|
1603
|
+
.fill.error {
|
|
1604
|
+
background: #dc3545;
|
|
1605
|
+
}
|
|
1606
|
+
.mutations-files {
|
|
1607
|
+
margin-top: 30px;
|
|
1608
|
+
}
|
|
1609
|
+
.mutations-table {
|
|
1610
|
+
width: 100%;
|
|
1611
|
+
border-collapse: collapse;
|
|
1612
|
+
margin-top: 15px;
|
|
1613
|
+
}
|
|
1614
|
+
.mutations-table th,
|
|
1615
|
+
.mutations-table td {
|
|
1616
|
+
padding: 12px;
|
|
1617
|
+
text-align: left;
|
|
1618
|
+
border-bottom: 1px solid #e9ecef;
|
|
1619
|
+
}
|
|
1620
|
+
.mutations-table th {
|
|
1621
|
+
background: #f8f9fa;
|
|
1622
|
+
font-weight: 600;
|
|
1623
|
+
position: sticky;
|
|
1624
|
+
top: 0;
|
|
1625
|
+
}
|
|
1626
|
+
.mutations-table tr:hover {
|
|
1627
|
+
background: #f8f9fa;
|
|
1628
|
+
}
|
|
1629
|
+
.mutations-table .killed {
|
|
1630
|
+
color: #28a745;
|
|
1631
|
+
font-weight: bold;
|
|
1632
|
+
}
|
|
1633
|
+
.mutations-table .survived {
|
|
1634
|
+
color: #dc3545;
|
|
1635
|
+
font-weight: bold;
|
|
1636
|
+
}
|
|
1637
|
+
.no-data {
|
|
1638
|
+
text-align: center;
|
|
1639
|
+
padding: 40px;
|
|
1640
|
+
color: #6c757d;
|
|
1641
|
+
}
|
|
1642
|
+
.no-data h3 {
|
|
1643
|
+
margin-bottom: 10px;
|
|
1644
|
+
color: #495057;
|
|
1645
|
+
}
|
|
1646
|
+
.file-row {
|
|
1647
|
+
cursor: pointer;
|
|
1648
|
+
}
|
|
1649
|
+
.file-row:hover {
|
|
1650
|
+
background: #f8f9fa;
|
|
1651
|
+
}
|
|
1652
|
+
.file-details {
|
|
1653
|
+
background: #f8f9fa;
|
|
1654
|
+
}
|
|
1655
|
+
.file-mutation-details {
|
|
1656
|
+
padding: 15px;
|
|
1657
|
+
}
|
|
1658
|
+
.mutations-by-line {
|
|
1659
|
+
margin: 10px 0;
|
|
1660
|
+
}
|
|
1661
|
+
.line-mutations {
|
|
1662
|
+
margin-bottom: 20px;
|
|
1663
|
+
border: 1px solid #e9ecef;
|
|
1664
|
+
border-radius: 6px;
|
|
1665
|
+
padding: 15px;
|
|
1666
|
+
background: white;
|
|
1667
|
+
}
|
|
1668
|
+
.line-mutations h5 {
|
|
1669
|
+
margin: 0 0 10px 0;
|
|
1670
|
+
color: #495057;
|
|
1671
|
+
font-size: 14px;
|
|
1672
|
+
font-weight: 600;
|
|
1673
|
+
}
|
|
1674
|
+
.mutations-list {
|
|
1675
|
+
display: flex;
|
|
1676
|
+
flex-direction: column;
|
|
1677
|
+
gap: 10px;
|
|
1678
|
+
}
|
|
1679
|
+
.mutation-detail {
|
|
1680
|
+
padding: 10px;
|
|
1681
|
+
border-radius: 4px;
|
|
1682
|
+
border-left: 4px solid #6c757d;
|
|
1683
|
+
}
|
|
1684
|
+
.mutation-detail.killed {
|
|
1685
|
+
background: #d4edda;
|
|
1686
|
+
border-left-color: #28a745;
|
|
1687
|
+
}
|
|
1688
|
+
.mutation-detail.survived {
|
|
1689
|
+
background: #f8d7da;
|
|
1690
|
+
border-left-color: #dc3545;
|
|
1691
|
+
}
|
|
1692
|
+
.mutation-detail.error {
|
|
1693
|
+
background: #fff3cd;
|
|
1694
|
+
border-left-color: #ffc107;
|
|
1695
|
+
}
|
|
1696
|
+
.mutation-header {
|
|
1697
|
+
display: flex;
|
|
1698
|
+
justify-content: space-between;
|
|
1699
|
+
align-items: center;
|
|
1700
|
+
margin-bottom: 8px;
|
|
1701
|
+
}
|
|
1702
|
+
.mutation-status {
|
|
1703
|
+
font-weight: bold;
|
|
1704
|
+
font-size: 12px;
|
|
1705
|
+
}
|
|
1706
|
+
.mutation-type {
|
|
1707
|
+
background: #6c757d;
|
|
1708
|
+
color: white;
|
|
1709
|
+
padding: 2px 6px;
|
|
1710
|
+
border-radius: 3px;
|
|
1711
|
+
font-size: 11px;
|
|
1712
|
+
font-weight: 500;
|
|
1713
|
+
text-transform: uppercase;
|
|
1714
|
+
}
|
|
1715
|
+
.mutation-change {
|
|
1716
|
+
margin: 8px 0;
|
|
1717
|
+
font-size: 13px;
|
|
1718
|
+
}
|
|
1719
|
+
.mutation-change code {
|
|
1720
|
+
background: #f8f9fa;
|
|
1721
|
+
padding: 2px 4px;
|
|
1722
|
+
border-radius: 3px;
|
|
1723
|
+
font-family: 'Courier New', monospace;
|
|
1724
|
+
}
|
|
1725
|
+
.mutation-tests {
|
|
1726
|
+
font-size: 12px;
|
|
1727
|
+
color: #6c757d;
|
|
1728
|
+
font-style: italic;
|
|
1729
|
+
}
|
|
1730
|
+
</style>
|
|
1731
|
+
<script>
|
|
1732
|
+
function toggleCoverage(lineNumber, filePath) {
|
|
1733
|
+
const detailsId = 'details-' + filePath.replace(/[^a-zA-Z0-9]/g, '_') + '-' + lineNumber;
|
|
1734
|
+
const details = document.getElementById(detailsId);
|
|
1735
|
+
if (details) {
|
|
1736
|
+
details.classList.toggle('expanded');
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
</script>
|
|
1740
|
+
</head>
|
|
1741
|
+
<body>
|
|
1742
|
+
<div class="header">
|
|
1743
|
+
<h1>🧪 Jest Test Lineage Report</h1>
|
|
1744
|
+
<p>Click on coverage indicators to see which tests cover each line</p>
|
|
1745
|
+
<div class="file-path">Generated on ${new Date().toLocaleString()}</div>
|
|
1746
|
+
</div>
|
|
1747
|
+
|
|
1748
|
+
<div class="navigation">
|
|
1749
|
+
<div class="nav-buttons">
|
|
1750
|
+
<button id="view-files" class="nav-btn active" onclick="showView('files')">📁 Files View</button>
|
|
1751
|
+
<button id="view-lines" class="nav-btn" onclick="showView('lines')">📊 Lines Analysis</button>
|
|
1752
|
+
<button id="view-performance" class="nav-btn" onclick="showView('performance')">🔥 Performance Analytics</button>
|
|
1753
|
+
<button id="view-quality" class="nav-btn" onclick="showView('quality')">🧪 Test Quality</button>
|
|
1754
|
+
<button id="view-mutations" class="nav-btn" onclick="showView('mutations')">🧬 Mutation Testing</button>
|
|
1755
|
+
<button id="expand-all" class="action-btn" onclick="expandAll()">📖 Expand All</button>
|
|
1756
|
+
<button id="collapse-all" class="action-btn" onclick="collapseAll()">📕 Collapse All</button>
|
|
1757
|
+
</div>
|
|
1758
|
+
<div class="sort-controls" id="sort-controls" style="display: none;">
|
|
1759
|
+
<label>Sort by:</label>
|
|
1760
|
+
<select id="sort-by" onchange="sortLines()">
|
|
1761
|
+
<option value="executions">Most Executions</option>
|
|
1762
|
+
<option value="tests">Most Tests</option>
|
|
1763
|
+
<option value="depth">Deepest Calls</option>
|
|
1764
|
+
<option value="cpuCycles">Most CPU Cycles</option>
|
|
1765
|
+
<option value="cpuTime">Most CPU Time</option>
|
|
1766
|
+
<option value="wallTime">Most Wall Time</option>
|
|
1767
|
+
<option value="quality">Best Quality</option>
|
|
1768
|
+
<option value="reliability">Most Reliable</option>
|
|
1769
|
+
<option value="maintainability">Most Maintainable</option>
|
|
1770
|
+
<option value="testSmells">Most Test Smells</option>
|
|
1771
|
+
<option value="file">File Name</option>
|
|
1772
|
+
</select>
|
|
1773
|
+
</div>
|
|
1774
|
+
</div>
|
|
1775
|
+
|
|
1776
|
+
<div id="files-view">
|
|
1777
|
+
`;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
async generateCodeTreeSection(filePath) {
|
|
1781
|
+
try {
|
|
1782
|
+
// Check if file exists first
|
|
1783
|
+
if (!fs.existsSync(filePath)) {
|
|
1784
|
+
console.warn(`⚠️ File not found: ${filePath}`);
|
|
1785
|
+
|
|
1786
|
+
// Try to find the file with alternative paths
|
|
1787
|
+
const alternativePaths = this.findAlternativeFilePaths(filePath);
|
|
1788
|
+
if (alternativePaths.length > 0) {
|
|
1789
|
+
console.log(`🔍 Trying alternative paths for ${path.basename(filePath)}:`, alternativePaths);
|
|
1790
|
+
for (const altPath of alternativePaths) {
|
|
1791
|
+
if (fs.existsSync(altPath)) {
|
|
1792
|
+
console.log(`✅ Found file at: ${altPath}`);
|
|
1793
|
+
return this.generateCodeTreeSection(altPath); // Recursive call with found path
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
return `<div class="file-section">
|
|
1799
|
+
<div class="file-header">❌ Error reading ${path.basename(filePath)}</div>
|
|
1800
|
+
<div class="error">
|
|
1801
|
+
<p><strong>File not found:</strong> ${filePath}</p>
|
|
1802
|
+
<p><strong>Tried alternatives:</strong> ${alternativePaths.slice(0, 3).join(', ')}</p>
|
|
1803
|
+
<p><strong>Suggestion:</strong> Check your project structure and source directory configuration.</p>
|
|
1804
|
+
</div>
|
|
1805
|
+
</div>`;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// Read the source file content
|
|
1809
|
+
const sourceCode = fs.readFileSync(filePath, 'utf8');
|
|
1810
|
+
const lines = sourceCode.split('\n');
|
|
1811
|
+
|
|
1812
|
+
// Check if we have coverage data for this file
|
|
1813
|
+
let lineCoverage = this.coverageData[filePath];
|
|
1814
|
+
let actualFilePath = filePath;
|
|
1815
|
+
|
|
1816
|
+
if (!lineCoverage || typeof lineCoverage !== 'object') {
|
|
1817
|
+
// Try to find coverage data with smart path matching
|
|
1818
|
+
const matchedPath = this.findMatchingCoveragePath(filePath);
|
|
1819
|
+
if (matchedPath) {
|
|
1820
|
+
console.log(`🔧 Path mismatch resolved: "${filePath}" -> "${matchedPath}"`);
|
|
1821
|
+
lineCoverage = this.coverageData[matchedPath];
|
|
1822
|
+
actualFilePath = matchedPath;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
if (!lineCoverage || typeof lineCoverage !== 'object') {
|
|
1827
|
+
console.warn(`⚠️ No coverage data found for: ${filePath}`);
|
|
1828
|
+
|
|
1829
|
+
// Debug: Log all available coverage data
|
|
1830
|
+
// Debug logging removed for production
|
|
1831
|
+
|
|
1832
|
+
console.log(`🔍 DEBUG: Looking for: "${filePath}"`);
|
|
1833
|
+
console.log(`🔍 DEBUG: Exact match exists: ${this.coverageData.hasOwnProperty(filePath)}`);
|
|
1834
|
+
|
|
1835
|
+
// Check for similar paths
|
|
1836
|
+
const similarPaths = Object.keys(this.coverageData).filter(key =>
|
|
1837
|
+
key.includes(path.basename(filePath)) || filePath.includes(path.basename(key))
|
|
1838
|
+
);
|
|
1839
|
+
if (similarPaths.length > 0) {
|
|
1840
|
+
console.log(`🔍 DEBUG: Similar paths found:`, similarPaths);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// Log the actual data structure for the first few files
|
|
1844
|
+
console.log(`🔍 DEBUG: Sample coverage data structure:`);
|
|
1845
|
+
Object.entries(this.coverageData).slice(0, 2).forEach(([key, value]) => {
|
|
1846
|
+
console.log(` File: ${key}`);
|
|
1847
|
+
console.log(` Type: ${typeof value}, Keys: ${value ? Object.keys(value).length : 'N/A'}`);
|
|
1848
|
+
if (value && typeof value === 'object') {
|
|
1849
|
+
const sampleLines = Object.keys(value).slice(0, 3);
|
|
1850
|
+
console.log(` Sample lines: ${sampleLines.join(', ')}`);
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
return `<div class="file-section">
|
|
1855
|
+
<div class="file-header">❌ Error reading ${path.basename(filePath)}</div>
|
|
1856
|
+
<div class="error">
|
|
1857
|
+
<p><strong>File:</strong> ${filePath}</p>
|
|
1858
|
+
<p><strong>Error:</strong> No coverage data available</p>
|
|
1859
|
+
<p><strong>Available files:</strong> ${Object.keys(this.coverageData).length} files tracked</p>
|
|
1860
|
+
<p><strong>Similar paths:</strong> ${similarPaths.length > 0 ? similarPaths.join(', ') : 'None found'}</p>
|
|
1861
|
+
<p><strong>Suggestion:</strong> Make sure the file is being instrumented and tests are running.</p>
|
|
1862
|
+
</div>
|
|
1863
|
+
</div>`;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const coveredLineNumbers = Object.keys(lineCoverage).map(n => parseInt(n));
|
|
1867
|
+
|
|
1868
|
+
// Calculate stats for this file
|
|
1869
|
+
const totalLines = lines.length;
|
|
1870
|
+
const coveredLines = coveredLineNumbers.length;
|
|
1871
|
+
const totalTests = new Set();
|
|
1872
|
+
Object.values(lineCoverage).forEach(tests => {
|
|
1873
|
+
if (Array.isArray(tests)) {
|
|
1874
|
+
tests.forEach(test => totalTests.add(test));
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
const fileId = actualFilePath.replace(/[^a-zA-Z0-9]/g, '_');
|
|
1879
|
+
|
|
1880
|
+
let html = `
|
|
1881
|
+
<div class="file-section">
|
|
1882
|
+
<div class="file-header" onclick="toggleFile('${fileId}')">
|
|
1883
|
+
<div>
|
|
1884
|
+
📄 ${path.basename(filePath)}
|
|
1885
|
+
<div class="file-path">${filePath}</div>
|
|
1886
|
+
</div>
|
|
1887
|
+
<span class="expand-icon">▶</span>
|
|
1888
|
+
</div>
|
|
1889
|
+
<div class="file-content" id="content-${fileId}">
|
|
1890
|
+
<div class="stats">
|
|
1891
|
+
<h3>File Statistics</h3>
|
|
1892
|
+
<div class="stat-item">
|
|
1893
|
+
<div class="stat-number">${coveredLines}</div>
|
|
1894
|
+
<span class="stat-label">Lines Covered</span>
|
|
1895
|
+
</div>
|
|
1896
|
+
<div class="stat-item">
|
|
1897
|
+
<div class="stat-number">${totalLines}</div>
|
|
1898
|
+
<span class="stat-label">Total Lines</span>
|
|
1899
|
+
</div>
|
|
1900
|
+
<div class="stat-item">
|
|
1901
|
+
<div class="stat-number">${totalTests.size}</div>
|
|
1902
|
+
<span class="stat-label">Unique Tests</span>
|
|
1903
|
+
</div>
|
|
1904
|
+
</div>
|
|
1905
|
+
<div class="code-container">
|
|
1906
|
+
`;
|
|
1907
|
+
|
|
1908
|
+
// Generate each line of code
|
|
1909
|
+
lines.forEach((lineContent, index) => {
|
|
1910
|
+
const lineNumber = index + 1;
|
|
1911
|
+
const isCovered = coveredLineNumbers.includes(lineNumber);
|
|
1912
|
+
const lineNumberClass = isCovered ? 'line-covered' : 'line-uncovered';
|
|
1913
|
+
|
|
1914
|
+
html += `
|
|
1915
|
+
<div class="code-line">
|
|
1916
|
+
<div class="line-number ${lineNumberClass}">${lineNumber}</div>
|
|
1917
|
+
<div class="line-content">${this.escapeHtml(lineContent || ' ')}</div>`;
|
|
1918
|
+
|
|
1919
|
+
if (isCovered) {
|
|
1920
|
+
const testInfos = lineCoverage[lineNumber];
|
|
1921
|
+
if (testInfos && Array.isArray(testInfos)) {
|
|
1922
|
+
const uniqueTests = this.deduplicateTests(testInfos);
|
|
1923
|
+
html += `
|
|
1924
|
+
<div class="coverage-indicator" onclick="toggleCoverage(${lineNumber}, '${fileId}')">
|
|
1925
|
+
${uniqueTests.length} test${uniqueTests.length !== 1 ? 's' : ''}
|
|
1926
|
+
</div>`;
|
|
1927
|
+
} else {
|
|
1928
|
+
html += `
|
|
1929
|
+
<div class="coverage-indicator" style="background: #ffc107;">
|
|
1930
|
+
Invalid data
|
|
1931
|
+
</div>`;
|
|
1932
|
+
}
|
|
1933
|
+
} else {
|
|
1934
|
+
html += `
|
|
1935
|
+
<div class="coverage-indicator" style="background: #6c757d;">
|
|
1936
|
+
No coverage
|
|
1937
|
+
</div>`;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
html += `
|
|
1941
|
+
</div>`;
|
|
1942
|
+
|
|
1943
|
+
// Add coverage details (initially hidden)
|
|
1944
|
+
if (isCovered) {
|
|
1945
|
+
const testInfos = lineCoverage[lineNumber];
|
|
1946
|
+
if (testInfos && Array.isArray(testInfos) && testInfos.length > 0) {
|
|
1947
|
+
const uniqueTests = this.deduplicateTests(testInfos);
|
|
1948
|
+
const testsByFile = this.groupTestInfosByFile(uniqueTests);
|
|
1949
|
+
|
|
1950
|
+
// Get mutation results for this line
|
|
1951
|
+
const mutationResults = this.getMutationResultsForLine(actualFilePath, lineNumber);
|
|
1952
|
+
|
|
1953
|
+
html += `
|
|
1954
|
+
<div id="details-${fileId}-${lineNumber}" class="coverage-details">
|
|
1955
|
+
<strong>Line ${lineNumber} is covered by ${uniqueTests.length} test${uniqueTests.length !== 1 ? 's' : ''}:</strong>`;
|
|
1956
|
+
|
|
1957
|
+
// Add mutation testing results if available
|
|
1958
|
+
if (mutationResults && mutationResults.length > 0) {
|
|
1959
|
+
html += this.generateMutationResultsHtml(mutationResults, lineNumber);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
for (const [testFile, tests] of Object.entries(testsByFile)) {
|
|
1963
|
+
if (tests && Array.isArray(tests)) {
|
|
1964
|
+
html += `
|
|
1965
|
+
<div class="test-file">📁 ${testFile}</div>`;
|
|
1966
|
+
tests.forEach(testInfo => {
|
|
1967
|
+
if (testInfo) {
|
|
1968
|
+
const testName = typeof testInfo === 'string' ? testInfo : (testInfo.name || 'Unknown test');
|
|
1969
|
+
const executionCount = typeof testInfo === 'object' ? (testInfo.executionCount || 1) : 1;
|
|
1970
|
+
const trackingType = typeof testInfo === 'object' && testInfo.type === 'precise' ? 'PRECISE' : 'ESTIMATED';
|
|
1971
|
+
|
|
1972
|
+
// Generate depth information
|
|
1973
|
+
let depthInfo = '';
|
|
1974
|
+
let depthBadge = '';
|
|
1975
|
+
if (typeof testInfo === 'object' && testInfo.type === 'precise' && testInfo.depthData) {
|
|
1976
|
+
const depths = Object.keys(testInfo.depthData).map(d => parseInt(d)).sort((a, b) => a - b);
|
|
1977
|
+
const minDepth = Math.min(...depths);
|
|
1978
|
+
|
|
1979
|
+
if (depths.length === 1) {
|
|
1980
|
+
depthInfo = `, depth ${depths[0]}`;
|
|
1981
|
+
depthBadge = `<span class="depth-badge depth-${minDepth}">D${minDepth}</span>`;
|
|
1982
|
+
} else {
|
|
1983
|
+
depthInfo = `, depths ${depths.join(',')}`;
|
|
1984
|
+
depthBadge = `<span class="depth-badge depth-${minDepth}">D${depths.join(',')}</span>`;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const badgeColor = trackingType === 'PRECISE' ? '#28a745' : '#ffc107';
|
|
1989
|
+
const icon = trackingType === 'PRECISE' ? '✅' : '⚠️';
|
|
1990
|
+
|
|
1991
|
+
html += `<span class="test-badge"
|
|
1992
|
+
style="background-color: ${badgeColor};"
|
|
1993
|
+
title="${testName} (${trackingType}: ${executionCount} executions${depthInfo})">${icon} ${testName} ${depthBadge}</span>`;
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
} else {
|
|
1999
|
+
html += `
|
|
2000
|
+
<div id="details-${fileId}-${lineNumber}" class="coverage-details">
|
|
2001
|
+
<strong>Line ${lineNumber}: No valid test data available</strong>
|
|
2002
|
+
</div>`;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
html += `
|
|
2006
|
+
</div>`;
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
html += `
|
|
2011
|
+
</div>
|
|
2012
|
+
</div>
|
|
2013
|
+
</div>`;
|
|
2014
|
+
|
|
2015
|
+
return html;
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
console.error(`Error reading file ${filePath}:`, error.message);
|
|
2018
|
+
return `<div class="file-section">
|
|
2019
|
+
<div class="file-header">❌ Error reading ${path.basename(filePath)}</div>
|
|
2020
|
+
<div class="error">
|
|
2021
|
+
<p><strong>File:</strong> ${filePath}</p>
|
|
2022
|
+
<p><strong>Error:</strong> ${error.message}</p>
|
|
2023
|
+
<p><strong>Suggestion:</strong> Make sure the file exists and is readable.</p>
|
|
2024
|
+
</div>
|
|
2025
|
+
</div>`;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
escapeHtml(text) {
|
|
2030
|
+
return text
|
|
2031
|
+
.replace(/&/g, '&')
|
|
2032
|
+
.replace(/</g, '<')
|
|
2033
|
+
.replace(/>/g, '>')
|
|
2034
|
+
.replace(/"/g, '"')
|
|
2035
|
+
.replace(/'/g, ''');
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
deduplicateTests(testInfos) {
|
|
2039
|
+
const seen = new Set();
|
|
2040
|
+
return testInfos.filter(testInfo => {
|
|
2041
|
+
const key = typeof testInfo === 'string' ? testInfo : testInfo.name;
|
|
2042
|
+
if (seen.has(key)) {
|
|
2043
|
+
return false;
|
|
2044
|
+
}
|
|
2045
|
+
seen.add(key);
|
|
2046
|
+
return true;
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
groupTestInfosByFile(tests) {
|
|
2051
|
+
const grouped = {};
|
|
2052
|
+
tests.forEach(testInfo => {
|
|
2053
|
+
const testFile = typeof testInfo === 'string' ? 'Unknown' : testInfo.file;
|
|
2054
|
+
|
|
2055
|
+
if (!grouped[testFile]) grouped[testFile] = [];
|
|
2056
|
+
grouped[testFile].push(testInfo);
|
|
2057
|
+
});
|
|
2058
|
+
return grouped;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
getConfidenceColor(confidence) {
|
|
2062
|
+
if (confidence >= 80) return '#28a745'; // Green - high confidence
|
|
2063
|
+
if (confidence >= 60) return '#ffc107'; // Yellow - medium confidence
|
|
2064
|
+
if (confidence >= 40) return '#fd7e14'; // Orange - low confidence
|
|
2065
|
+
return '#dc3545'; // Red - very low confidence
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
groupTestsByFile(tests) {
|
|
2069
|
+
const grouped = {};
|
|
2070
|
+
tests.forEach(testName => {
|
|
2071
|
+
// Extract test file from test name based on describe block patterns
|
|
2072
|
+
let testFile = 'Unknown';
|
|
2073
|
+
|
|
2074
|
+
if (testName.includes('Calculator')) testFile = 'calculator.test.ts';
|
|
2075
|
+
else if (testName.includes('Add Function Only')) testFile = 'add-only.test.ts';
|
|
2076
|
+
else if (testName.includes('Multiply Function Only')) testFile = 'multiply-only.test.ts';
|
|
2077
|
+
else if (testName.includes('Subtract Function Only')) testFile = 'subtract-only.test.ts';
|
|
2078
|
+
|
|
2079
|
+
if (!grouped[testFile]) grouped[testFile] = [];
|
|
2080
|
+
grouped[testFile].push(testName);
|
|
2081
|
+
});
|
|
2082
|
+
return grouped;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
findAlternativeFilePaths(originalPath) {
|
|
2086
|
+
const filename = path.basename(originalPath);
|
|
2087
|
+
const cwd = process.cwd();
|
|
2088
|
+
|
|
2089
|
+
// Use the same smart discovery logic as testSetup.js
|
|
2090
|
+
return this.discoverFileInProject(cwd, filename);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
discoverFileInProject(projectRoot, targetFilename) {
|
|
2094
|
+
const discoveredPaths = [];
|
|
2095
|
+
const maxDepth = 4; // Slightly deeper search for reporter
|
|
2096
|
+
|
|
2097
|
+
const scanDirectory = (dir, depth = 0) => {
|
|
2098
|
+
if (depth > maxDepth) return;
|
|
2099
|
+
|
|
2100
|
+
try {
|
|
2101
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2102
|
+
|
|
2103
|
+
for (const entry of entries) {
|
|
2104
|
+
if (entry.isFile() && entry.name === targetFilename) {
|
|
2105
|
+
discoveredPaths.push(path.join(dir, entry.name));
|
|
2106
|
+
} else if (entry.isDirectory()) {
|
|
2107
|
+
const dirName = entry.name;
|
|
2108
|
+
|
|
2109
|
+
// Skip common non-source directories
|
|
2110
|
+
if (this.shouldSkipDirectory(dirName)) continue;
|
|
2111
|
+
|
|
2112
|
+
const fullDirPath = path.join(dir, dirName);
|
|
2113
|
+
scanDirectory(fullDirPath, depth + 1);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
// Skip directories we can't read
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
|
|
2121
|
+
scanDirectory(projectRoot);
|
|
2122
|
+
|
|
2123
|
+
// Sort by preference (source directories first, then by depth)
|
|
2124
|
+
return discoveredPaths.sort((a, b) => {
|
|
2125
|
+
const aScore = this.getPathScore(a);
|
|
2126
|
+
const bScore = this.getPathScore(b);
|
|
2127
|
+
return bScore - aScore; // Higher score first
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
shouldSkipDirectory(dirName) {
|
|
2132
|
+
const skipPatterns = [
|
|
2133
|
+
'node_modules', '.git', '.next', '.nuxt', 'dist', 'build',
|
|
2134
|
+
'coverage', '.nyc_output', 'tmp', 'temp', '.cache',
|
|
2135
|
+
'.vscode', '.idea', '__pycache__', '.pytest_cache',
|
|
2136
|
+
'vendor', 'target', 'bin', 'obj', '.DS_Store'
|
|
2137
|
+
];
|
|
2138
|
+
|
|
2139
|
+
return skipPatterns.includes(dirName) || dirName.startsWith('.');
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
getPathScore(filePath) {
|
|
2143
|
+
const pathParts = filePath.split(path.sep);
|
|
2144
|
+
let score = 0;
|
|
2145
|
+
|
|
2146
|
+
// Prefer shorter paths
|
|
2147
|
+
score += Math.max(0, 10 - pathParts.length);
|
|
2148
|
+
|
|
2149
|
+
// Prefer common source directory names
|
|
2150
|
+
const sourcePreference = ['src', 'lib', 'source', 'app', 'server', 'client'];
|
|
2151
|
+
for (const part of pathParts) {
|
|
2152
|
+
const index = sourcePreference.indexOf(part);
|
|
2153
|
+
if (index !== -1) {
|
|
2154
|
+
score += sourcePreference.length - index;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// Bonus for TypeScript files
|
|
2159
|
+
if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
|
|
2160
|
+
score += 2;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
return score;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
generateTestQualitySummary() {
|
|
2167
|
+
console.log('\n🧪 TEST QUALITY & PERFORMANCE ANALYSIS');
|
|
2168
|
+
console.log('=' .repeat(60));
|
|
2169
|
+
|
|
2170
|
+
const qualityStats = {
|
|
2171
|
+
totalTests: 0,
|
|
2172
|
+
highQuality: 0,
|
|
2173
|
+
goodQuality: 0,
|
|
2174
|
+
fairQuality: 0,
|
|
2175
|
+
poorQuality: 0,
|
|
2176
|
+
totalSmells: 0,
|
|
2177
|
+
memoryLeaks: 0,
|
|
2178
|
+
gcPressure: 0,
|
|
2179
|
+
slowTests: 0,
|
|
2180
|
+
reliableTests: 0,
|
|
2181
|
+
totalAssertions: 0,
|
|
2182
|
+
totalComplexity: 0
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
const testSmellTypes = {};
|
|
2186
|
+
const performanceIssues = [];
|
|
2187
|
+
|
|
2188
|
+
// Analyze all tests
|
|
2189
|
+
for (const filePath in this.coverageData) {
|
|
2190
|
+
const lineCoverage = this.coverageData[filePath];
|
|
2191
|
+
for (const lineNumber in lineCoverage) {
|
|
2192
|
+
const tests = lineCoverage[lineNumber];
|
|
2193
|
+
if (!Array.isArray(tests)) continue;
|
|
2194
|
+
|
|
2195
|
+
tests.forEach(test => {
|
|
2196
|
+
if (test.type === 'precise' && test.quality) {
|
|
2197
|
+
qualityStats.totalTests++;
|
|
2198
|
+
|
|
2199
|
+
// Quality classification
|
|
2200
|
+
if (test.quality.maintainability >= 80) qualityStats.highQuality++;
|
|
2201
|
+
else if (test.quality.maintainability >= 60) qualityStats.goodQuality++;
|
|
2202
|
+
else if (test.quality.maintainability >= 40) qualityStats.fairQuality++;
|
|
2203
|
+
else qualityStats.poorQuality++;
|
|
2204
|
+
|
|
2205
|
+
// Reliability
|
|
2206
|
+
if (test.quality.reliability >= 80) qualityStats.reliableTests++;
|
|
2207
|
+
|
|
2208
|
+
// Test smells
|
|
2209
|
+
if (test.quality.testSmells) {
|
|
2210
|
+
qualityStats.totalSmells += test.quality.testSmells.length;
|
|
2211
|
+
test.quality.testSmells.forEach(smell => {
|
|
2212
|
+
testSmellTypes[smell] = (testSmellTypes[smell] || 0) + 1;
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// Performance issues - check for large memory allocations
|
|
2217
|
+
if (test.performance) {
|
|
2218
|
+
// Check for memory leaks (large allocations)
|
|
2219
|
+
const memoryMB = Math.abs(test.performance.totalMemoryDelta || 0) / (1024 * 1024);
|
|
2220
|
+
if (memoryMB > 1) { // > 1MB
|
|
2221
|
+
qualityStats.memoryLeaks++;
|
|
2222
|
+
performanceIssues.push(`${test.name}: Large memory allocation (${memoryMB.toFixed(1)}MB)`);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// Check for GC pressure
|
|
2226
|
+
if (test.performance.gcPressure > 5) {
|
|
2227
|
+
qualityStats.gcPressure++;
|
|
2228
|
+
performanceIssues.push(`${test.name}: High GC pressure (${test.performance.gcPressure})`);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// Check for slow executions
|
|
2232
|
+
if (test.performance.slowExecutions > test.performance.fastExecutions) {
|
|
2233
|
+
qualityStats.slowTests++;
|
|
2234
|
+
performanceIssues.push(`${test.name}: Inconsistent performance`);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
qualityStats.totalAssertions += test.quality.assertions || 0;
|
|
2239
|
+
qualityStats.totalComplexity += test.quality.complexity || 0;
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// Display quality summary
|
|
2246
|
+
console.log(`\n📊 QUALITY DISTRIBUTION (${qualityStats.totalTests} tests analyzed):`);
|
|
2247
|
+
console.log(` 🏆 High Quality (80-100%): ${qualityStats.highQuality} tests (${((qualityStats.highQuality/qualityStats.totalTests)*100).toFixed(1)}%)`);
|
|
2248
|
+
console.log(` ✅ Good Quality (60-79%): ${qualityStats.goodQuality} tests (${((qualityStats.goodQuality/qualityStats.totalTests)*100).toFixed(1)}%)`);
|
|
2249
|
+
console.log(` ⚠️ Fair Quality (40-59%): ${qualityStats.fairQuality} tests (${((qualityStats.fairQuality/qualityStats.totalTests)*100).toFixed(1)}%)`);
|
|
2250
|
+
console.log(` ❌ Poor Quality (0-39%): ${qualityStats.poorQuality} tests (${((qualityStats.poorQuality/qualityStats.totalTests)*100).toFixed(1)}%)`);
|
|
2251
|
+
|
|
2252
|
+
console.log(`\n🛡️ RELIABILITY METRICS:`);
|
|
2253
|
+
console.log(` 🔒 Reliable Tests: ${qualityStats.reliableTests}/${qualityStats.totalTests} (${((qualityStats.reliableTests/qualityStats.totalTests)*100).toFixed(1)}%)`);
|
|
2254
|
+
console.log(` 🎯 Total Assertions: ${qualityStats.totalAssertions} (avg: ${(qualityStats.totalAssertions/qualityStats.totalTests).toFixed(1)} per test)`);
|
|
2255
|
+
console.log(` 🔧 Total Complexity: ${qualityStats.totalComplexity} (avg: ${(qualityStats.totalComplexity/qualityStats.totalTests).toFixed(1)} per test)`);
|
|
2256
|
+
|
|
2257
|
+
// Test smells breakdown
|
|
2258
|
+
if (qualityStats.totalSmells > 0) {
|
|
2259
|
+
console.log(`\n🚨 TEST SMELLS DETECTED (${qualityStats.totalSmells} total):`);
|
|
2260
|
+
Object.entries(testSmellTypes).forEach(([smell, count]) => {
|
|
2261
|
+
console.log(` • ${smell}: ${count} occurrences`);
|
|
2262
|
+
});
|
|
2263
|
+
|
|
2264
|
+
console.log(`\n💡 HOW TO IMPROVE TEST SCORES:`);
|
|
2265
|
+
if (testSmellTypes['Long Test']) {
|
|
2266
|
+
console.log(` 📏 Long Tests: Break down tests >50 lines into smaller, focused tests`);
|
|
2267
|
+
}
|
|
2268
|
+
if (testSmellTypes['No Assertions']) {
|
|
2269
|
+
console.log(` 🎯 No Assertions: Add expect() statements to verify behavior`);
|
|
2270
|
+
}
|
|
2271
|
+
if (testSmellTypes['Too Many Assertions']) {
|
|
2272
|
+
console.log(` 🔢 Too Many Assertions: Split tests with >10 assertions into separate test cases`);
|
|
2273
|
+
}
|
|
2274
|
+
if (testSmellTypes['Excessive Mocking']) {
|
|
2275
|
+
console.log(` 🎭 Excessive Mocking: Reduce mocks >5 per test, consider integration testing`);
|
|
2276
|
+
}
|
|
2277
|
+
if (testSmellTypes['Sleep/Wait Usage']) {
|
|
2278
|
+
console.log(` ⏰ Sleep/Wait Usage: Replace with proper async/await or mock timers`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// Performance issues
|
|
2283
|
+
if (performanceIssues.length > 0) {
|
|
2284
|
+
console.log(`\n🔥 PERFORMANCE ISSUES DETECTED:`);
|
|
2285
|
+
console.log(` 🚨 Memory Leaks: ${qualityStats.memoryLeaks} tests`);
|
|
2286
|
+
console.log(` 🗑️ GC Pressure: ${qualityStats.gcPressure} tests`);
|
|
2287
|
+
console.log(` 🐌 Slow Tests: ${qualityStats.slowTests} tests`);
|
|
2288
|
+
|
|
2289
|
+
console.log(`\n⚡ PERFORMANCE RECOMMENDATIONS:`);
|
|
2290
|
+
if (qualityStats.memoryLeaks > 0) {
|
|
2291
|
+
console.log(` 💾 Memory Leaks: Review large object allocations, ensure proper cleanup`);
|
|
2292
|
+
console.log(` • Look for objects >1MB that aren't being released`);
|
|
2293
|
+
console.log(` • Check for global variables holding references`);
|
|
2294
|
+
console.log(` • Ensure event listeners are properly removed`);
|
|
2295
|
+
}
|
|
2296
|
+
if (qualityStats.gcPressure > 0) {
|
|
2297
|
+
console.log(` 🗑️ GC Pressure: Reduce frequent small allocations, reuse objects`);
|
|
2298
|
+
console.log(` • Object pooling: Reuse objects instead of creating new ones`);
|
|
2299
|
+
console.log(` • Batch operations: Process multiple items at once`);
|
|
2300
|
+
console.log(` • Avoid creating objects in loops`);
|
|
2301
|
+
console.log(` • Use primitive values when possible`);
|
|
2302
|
+
}
|
|
2303
|
+
if (qualityStats.slowTests > 0) {
|
|
2304
|
+
console.log(` 🐌 Slow Tests: Profile inconsistent tests, optimize heavy operations`);
|
|
2305
|
+
console.log(` • Use async/await instead of setTimeout`);
|
|
2306
|
+
console.log(` • Mock heavy operations in tests`);
|
|
2307
|
+
console.log(` • Reduce test data size`);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
console.log('\n' + '=' .repeat(60));
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
generateLinesData() {
|
|
2315
|
+
const linesData = [];
|
|
2316
|
+
|
|
2317
|
+
for (const filePath in this.coverageData) {
|
|
2318
|
+
const lineCoverage = this.coverageData[filePath];
|
|
2319
|
+
|
|
2320
|
+
for (const lineNumber in lineCoverage) {
|
|
2321
|
+
const tests = lineCoverage[lineNumber];
|
|
2322
|
+
if (!Array.isArray(tests) || tests.length === 0) continue;
|
|
2323
|
+
|
|
2324
|
+
let totalExecutions = 0;
|
|
2325
|
+
let maxDepth = 1;
|
|
2326
|
+
let minDepth = 1;
|
|
2327
|
+
const depths = new Set();
|
|
2328
|
+
|
|
2329
|
+
let totalCpuCycles = 0;
|
|
2330
|
+
let totalCpuTime = 0;
|
|
2331
|
+
let totalWallTime = 0;
|
|
2332
|
+
let maxCpuCycles = 0;
|
|
2333
|
+
let avgQuality = 0;
|
|
2334
|
+
let avgReliability = 0;
|
|
2335
|
+
let avgMaintainability = 0;
|
|
2336
|
+
let totalTestSmells = 0;
|
|
2337
|
+
let totalAssertions = 0;
|
|
2338
|
+
let totalComplexity = 0;
|
|
2339
|
+
let totalMemoryDelta = 0;
|
|
2340
|
+
let memoryLeaks = 0;
|
|
2341
|
+
let gcPressure = 0;
|
|
2342
|
+
let slowExecutions = 0;
|
|
2343
|
+
let fastExecutions = 0;
|
|
2344
|
+
let allTestSmells = new Set(); // Collect unique test smells
|
|
2345
|
+
|
|
2346
|
+
tests.forEach(test => {
|
|
2347
|
+
totalExecutions += test.executionCount || 1;
|
|
2348
|
+
if (test.maxDepth) maxDepth = Math.max(maxDepth, test.maxDepth);
|
|
2349
|
+
if (test.minDepth) minDepth = Math.min(minDepth, test.minDepth);
|
|
2350
|
+
if (test.depthData) {
|
|
2351
|
+
Object.keys(test.depthData).forEach(d => depths.add(parseInt(d)));
|
|
2352
|
+
}
|
|
2353
|
+
if (test.performance) {
|
|
2354
|
+
totalCpuCycles += test.performance.totalCpuCycles || 0;
|
|
2355
|
+
totalCpuTime += test.performance.totalCpuTime || 0;
|
|
2356
|
+
totalWallTime += test.performance.totalWallTime || 0;
|
|
2357
|
+
maxCpuCycles = Math.max(maxCpuCycles, test.performance.avgCpuCycles || 0);
|
|
2358
|
+
|
|
2359
|
+
// Add memory tracking
|
|
2360
|
+
totalMemoryDelta += Math.abs(test.performance.totalMemoryDelta || 0);
|
|
2361
|
+
memoryLeaks += test.performance.memoryLeaks || 0;
|
|
2362
|
+
gcPressure += test.performance.gcPressure || 0;
|
|
2363
|
+
slowExecutions += test.performance.slowExecutions || 0;
|
|
2364
|
+
fastExecutions += test.performance.fastExecutions || 0;
|
|
2365
|
+
}
|
|
2366
|
+
if (test.quality) {
|
|
2367
|
+
avgQuality += test.quality.maintainability || 0;
|
|
2368
|
+
avgReliability += test.quality.reliability || 0;
|
|
2369
|
+
avgMaintainability += test.quality.maintainability || 0;
|
|
2370
|
+
totalTestSmells += test.quality.testSmells ? test.quality.testSmells.length : 0;
|
|
2371
|
+
totalAssertions += test.quality.assertions || 0;
|
|
2372
|
+
totalComplexity += test.quality.complexity || 0;
|
|
2373
|
+
|
|
2374
|
+
// Collect individual test smells
|
|
2375
|
+
if (test.quality.testSmells && Array.isArray(test.quality.testSmells)) {
|
|
2376
|
+
test.quality.testSmells.forEach(smell => allTestSmells.add(smell));
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
// Calculate averages
|
|
2382
|
+
avgQuality = tests.length > 0 ? avgQuality / tests.length : 0;
|
|
2383
|
+
avgReliability = tests.length > 0 ? avgReliability / tests.length : 0;
|
|
2384
|
+
avgMaintainability = tests.length > 0 ? avgMaintainability / tests.length : 0;
|
|
2385
|
+
|
|
2386
|
+
// Try to get code preview (simplified)
|
|
2387
|
+
let codePreview = `Line ${lineNumber}`;
|
|
2388
|
+
try {
|
|
2389
|
+
if (fs.existsSync(filePath)) {
|
|
2390
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
2391
|
+
const lines = content.split('\n');
|
|
2392
|
+
const lineIndex = parseInt(lineNumber) - 1;
|
|
2393
|
+
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
2394
|
+
codePreview = lines[lineIndex].trim().substring(0, 50) + (lines[lineIndex].trim().length > 50 ? '...' : '');
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
// Fallback to line number only
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
const depthArray = Array.from(depths).sort((a, b) => a - b);
|
|
2402
|
+
const depthRange = depthArray.length > 1 ? `${Math.min(...depthArray)}-${Math.max(...depthArray)}` : `${depthArray[0] || 1}`;
|
|
2403
|
+
|
|
2404
|
+
linesData.push({
|
|
2405
|
+
fileName: path.basename(filePath),
|
|
2406
|
+
filePath: filePath,
|
|
2407
|
+
lineNumber: parseInt(lineNumber),
|
|
2408
|
+
codePreview: codePreview,
|
|
2409
|
+
testCount: tests.length,
|
|
2410
|
+
totalExecutions: totalExecutions,
|
|
2411
|
+
maxDepth: maxDepth,
|
|
2412
|
+
minDepth: minDepth,
|
|
2413
|
+
depthRange: depthRange,
|
|
2414
|
+
performance: {
|
|
2415
|
+
totalCpuCycles: totalCpuCycles,
|
|
2416
|
+
avgCpuCycles: totalExecutions > 0 ? totalCpuCycles / totalExecutions : 0,
|
|
2417
|
+
totalCpuTime: totalCpuTime,
|
|
2418
|
+
avgCpuTime: totalExecutions > 0 ? totalCpuTime / totalExecutions : 0,
|
|
2419
|
+
totalWallTime: totalWallTime,
|
|
2420
|
+
avgWallTime: totalExecutions > 0 ? totalWallTime / totalExecutions : 0,
|
|
2421
|
+
maxCpuCycles: maxCpuCycles,
|
|
2422
|
+
totalMemoryDelta: totalMemoryDelta,
|
|
2423
|
+
memoryLeaks: memoryLeaks,
|
|
2424
|
+
gcPressure: gcPressure,
|
|
2425
|
+
slowExecutions: slowExecutions,
|
|
2426
|
+
fastExecutions: fastExecutions
|
|
2427
|
+
},
|
|
2428
|
+
quality: {
|
|
2429
|
+
avgQuality: avgQuality,
|
|
2430
|
+
avgReliability: avgReliability,
|
|
2431
|
+
avgMaintainability: avgMaintainability,
|
|
2432
|
+
totalTestSmells: totalTestSmells,
|
|
2433
|
+
totalAssertions: totalAssertions,
|
|
2434
|
+
totalComplexity: totalComplexity,
|
|
2435
|
+
qualityScore: (avgQuality + avgReliability + avgMaintainability) / 3,
|
|
2436
|
+
testSmells: Array.from(allTestSmells) // Include the actual smell names
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
return linesData;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
generateHtmlFooter() {
|
|
2446
|
+
// Calculate overall stats
|
|
2447
|
+
const allFiles = Object.keys(this.coverageData);
|
|
2448
|
+
const totalLines = allFiles.reduce((sum, file) => sum + Object.keys(this.coverageData[file]).length, 0);
|
|
2449
|
+
const uniqueTestNames = new Set();
|
|
2450
|
+
|
|
2451
|
+
allFiles.forEach(file => {
|
|
2452
|
+
Object.values(this.coverageData[file]).forEach(tests => {
|
|
2453
|
+
tests.forEach(test => {
|
|
2454
|
+
// Use test name to identify unique tests, not the entire test object
|
|
2455
|
+
uniqueTestNames.add(test.name);
|
|
2456
|
+
});
|
|
2457
|
+
});
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2460
|
+
// console.log(`🔍 DEBUG: Total unique test names found: ${uniqueTestNames.size}`);
|
|
2461
|
+
// console.log(`🔍 DEBUG: Test names: ${Array.from(uniqueTestNames).join(', ')}`);
|
|
2462
|
+
|
|
2463
|
+
return `
|
|
2464
|
+
</div>
|
|
2465
|
+
|
|
2466
|
+
<div id="lines-view" style="display: none;">
|
|
2467
|
+
<div class="lines-analysis">
|
|
2468
|
+
<h2>📊 Lines Analysis</h2>
|
|
2469
|
+
<div id="lines-table"></div>
|
|
2470
|
+
</div>
|
|
2471
|
+
</div>
|
|
2472
|
+
|
|
2473
|
+
<div id="performance-view" style="display: none;">
|
|
2474
|
+
<div class="performance-analysis">
|
|
2475
|
+
<h2>🔥 Performance Analytics</h2>
|
|
2476
|
+
<div id="performance-dashboard"></div>
|
|
2477
|
+
</div>
|
|
2478
|
+
</div>
|
|
2479
|
+
|
|
2480
|
+
<div id="quality-view" style="display: none;">
|
|
2481
|
+
<div class="quality-analysis">
|
|
2482
|
+
<h2>🧪 Test Quality Analysis</h2>
|
|
2483
|
+
<div id="quality-dashboard"></div>
|
|
2484
|
+
</div>
|
|
2485
|
+
</div>
|
|
2486
|
+
|
|
2487
|
+
<div id="mutations-view" style="display: none;">
|
|
2488
|
+
<div class="mutations-analysis">
|
|
2489
|
+
<h2>🧬 Mutation Testing Results</h2>
|
|
2490
|
+
<div id="mutations-dashboard"></div>
|
|
2491
|
+
</div>
|
|
2492
|
+
</div>
|
|
2493
|
+
|
|
2494
|
+
<div class="stats">
|
|
2495
|
+
<h3>📊 Overall Statistics</h3>
|
|
2496
|
+
<div class="stat-item">
|
|
2497
|
+
<div class="stat-number">${allFiles.length}</div>
|
|
2498
|
+
<span class="stat-label">Files Analyzed</span>
|
|
2499
|
+
</div>
|
|
2500
|
+
<div class="stat-item">
|
|
2501
|
+
<div class="stat-number">${totalLines}</div>
|
|
2502
|
+
<span class="stat-label">Total Lines Covered</span>
|
|
2503
|
+
</div>
|
|
2504
|
+
<div class="stat-item">
|
|
2505
|
+
<div class="stat-number">${uniqueTestNames.size}</div>
|
|
2506
|
+
<span class="stat-label">Total Unique Tests</span>
|
|
2507
|
+
</div>
|
|
2508
|
+
</div>
|
|
2509
|
+
|
|
2510
|
+
<div class="footer">
|
|
2511
|
+
<p>Generated by Jest Test Lineage Reporter</p>
|
|
2512
|
+
<p>This report shows which tests cover which lines of code to help identify test redundancy and coverage gaps.</p>
|
|
2513
|
+
</div>
|
|
2514
|
+
|
|
2515
|
+
<script>
|
|
2516
|
+
// Global data for lines analysis
|
|
2517
|
+
window.linesData = ${JSON.stringify(this.generateLinesData())};
|
|
2518
|
+
|
|
2519
|
+
function showView(viewName) {
|
|
2520
|
+
document.querySelectorAll('.nav-btn').forEach(btn => btn.classList.remove('active'));
|
|
2521
|
+
document.getElementById('view-' + viewName).classList.add('active');
|
|
2522
|
+
|
|
2523
|
+
document.getElementById('files-view').style.display = viewName === 'files' ? 'block' : 'none';
|
|
2524
|
+
document.getElementById('lines-view').style.display = viewName === 'lines' ? 'block' : 'none';
|
|
2525
|
+
document.getElementById('performance-view').style.display = viewName === 'performance' ? 'block' : 'none';
|
|
2526
|
+
document.getElementById('quality-view').style.display = viewName === 'quality' ? 'block' : 'none';
|
|
2527
|
+
document.getElementById('mutations-view').style.display = viewName === 'mutations' ? 'block' : 'none';
|
|
2528
|
+
document.getElementById('sort-controls').style.display = viewName === 'lines' ? 'flex' : 'none';
|
|
2529
|
+
|
|
2530
|
+
if (viewName === 'lines') {
|
|
2531
|
+
generateLinesTable();
|
|
2532
|
+
} else if (viewName === 'performance') {
|
|
2533
|
+
generatePerformanceDashboard();
|
|
2534
|
+
} else if (viewName === 'quality') {
|
|
2535
|
+
generateQualityDashboard();
|
|
2536
|
+
} else if (viewName === 'mutations') {
|
|
2537
|
+
generateMutationsDashboard();
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
function toggleFile(fileId) {
|
|
2542
|
+
const content = document.getElementById('content-' + fileId);
|
|
2543
|
+
const header = content.previousElementSibling;
|
|
2544
|
+
const icon = header.querySelector('.expand-icon');
|
|
2545
|
+
|
|
2546
|
+
if (content.classList.contains('expanded')) {
|
|
2547
|
+
content.classList.remove('expanded');
|
|
2548
|
+
header.classList.remove('expanded');
|
|
2549
|
+
icon.textContent = '▶';
|
|
2550
|
+
} else {
|
|
2551
|
+
content.classList.add('expanded');
|
|
2552
|
+
header.classList.add('expanded');
|
|
2553
|
+
icon.textContent = '▼';
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function expandAll() {
|
|
2558
|
+
document.querySelectorAll('.file-content').forEach(content => {
|
|
2559
|
+
content.classList.add('expanded');
|
|
2560
|
+
const header = content.previousElementSibling;
|
|
2561
|
+
const icon = header.querySelector('.expand-icon');
|
|
2562
|
+
header.classList.add('expanded');
|
|
2563
|
+
icon.textContent = '▼';
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function collapseAll() {
|
|
2568
|
+
document.querySelectorAll('.file-content').forEach(content => {
|
|
2569
|
+
content.classList.remove('expanded');
|
|
2570
|
+
const header = content.previousElementSibling;
|
|
2571
|
+
const icon = header.querySelector('.expand-icon');
|
|
2572
|
+
header.classList.remove('expanded');
|
|
2573
|
+
icon.textContent = '▶';
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
function generateLinesTable() {
|
|
2578
|
+
const sortBy = document.getElementById('sort-by').value;
|
|
2579
|
+
const sortedLines = [...window.linesData].sort((a, b) => {
|
|
2580
|
+
switch (sortBy) {
|
|
2581
|
+
case 'executions':
|
|
2582
|
+
return b.totalExecutions - a.totalExecutions;
|
|
2583
|
+
case 'tests':
|
|
2584
|
+
return b.testCount - a.testCount;
|
|
2585
|
+
case 'depth':
|
|
2586
|
+
return b.maxDepth - a.maxDepth;
|
|
2587
|
+
case 'cpuCycles':
|
|
2588
|
+
return (b.performance?.totalCpuCycles || 0) - (a.performance?.totalCpuCycles || 0);
|
|
2589
|
+
case 'cpuTime':
|
|
2590
|
+
return (b.performance?.totalCpuTime || 0) - (a.performance?.totalCpuTime || 0);
|
|
2591
|
+
case 'wallTime':
|
|
2592
|
+
return (b.performance?.totalWallTime || 0) - (a.performance?.totalWallTime || 0);
|
|
2593
|
+
case 'quality':
|
|
2594
|
+
return (b.quality?.qualityScore || 0) - (a.quality?.qualityScore || 0);
|
|
2595
|
+
case 'reliability':
|
|
2596
|
+
return (b.quality?.avgReliability || 0) - (a.quality?.avgReliability || 0);
|
|
2597
|
+
case 'maintainability':
|
|
2598
|
+
return (b.quality?.avgMaintainability || 0) - (a.quality?.avgMaintainability || 0);
|
|
2599
|
+
case 'testSmells':
|
|
2600
|
+
return (b.quality?.totalTestSmells || 0) - (a.quality?.totalTestSmells || 0);
|
|
2601
|
+
case 'file':
|
|
2602
|
+
return a.fileName.localeCompare(b.fileName);
|
|
2603
|
+
default:
|
|
2604
|
+
return 0;
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
let html = \`<table class="lines-table">
|
|
2609
|
+
<thead>
|
|
2610
|
+
<tr>
|
|
2611
|
+
<th>File</th>
|
|
2612
|
+
<th>Line</th>
|
|
2613
|
+
<th>Code Preview</th>
|
|
2614
|
+
<th>Tests</th>
|
|
2615
|
+
<th>Executions</th>
|
|
2616
|
+
<th>CPU Cycles</th>
|
|
2617
|
+
<th>CPU Time (μs)</th>
|
|
2618
|
+
<th>Wall Time (μs)</th>
|
|
2619
|
+
<th>Quality Score</th>
|
|
2620
|
+
<th>Reliability</th>
|
|
2621
|
+
<th>Test Smells</th>
|
|
2622
|
+
<th>Max Depth</th>
|
|
2623
|
+
<th>Depth Range</th>
|
|
2624
|
+
</tr>
|
|
2625
|
+
</thead>
|
|
2626
|
+
<tbody>\`;
|
|
2627
|
+
|
|
2628
|
+
sortedLines.forEach(line => {
|
|
2629
|
+
const depthClass = line.maxDepth <= 1 ? 'depth-1' :
|
|
2630
|
+
line.maxDepth <= 2 ? 'depth-2' :
|
|
2631
|
+
line.maxDepth <= 3 ? 'depth-3' : 'depth-4';
|
|
2632
|
+
|
|
2633
|
+
// Format performance numbers
|
|
2634
|
+
const formatCycles = (cycles) => {
|
|
2635
|
+
if (cycles > 1000000) return \`\${(cycles / 1000000).toFixed(1)}M\`;
|
|
2636
|
+
if (cycles > 1000) return \`\${(cycles / 1000).toFixed(1)}K\`;
|
|
2637
|
+
return Math.round(cycles).toString();
|
|
2638
|
+
};
|
|
2639
|
+
|
|
2640
|
+
const formatTime = (microseconds) => {
|
|
2641
|
+
if (microseconds > 1000) return \`\${(microseconds / 1000).toFixed(2)}ms\`;
|
|
2642
|
+
return \`\${microseconds.toFixed(1)}μs\`;
|
|
2643
|
+
};
|
|
2644
|
+
|
|
2645
|
+
// Format quality scores
|
|
2646
|
+
const formatQuality = (score) => {
|
|
2647
|
+
const rounded = Math.round(score);
|
|
2648
|
+
if (rounded >= 80) return \`<span class="quality-excellent">\${rounded}%</span>\`;
|
|
2649
|
+
if (rounded >= 60) return \`<span class="quality-good">\${rounded}%</span>\`;
|
|
2650
|
+
if (rounded >= 40) return \`<span class="quality-fair">\${rounded}%</span>\`;
|
|
2651
|
+
return \`<span class="quality-poor">\${rounded}%</span>\`;
|
|
2652
|
+
};
|
|
2653
|
+
|
|
2654
|
+
const formatTestSmells = (count, smells) => {
|
|
2655
|
+
if (count === 0) return \`<span class="smells-none">0</span>\`;
|
|
2656
|
+
|
|
2657
|
+
const smellsText = smells && smells.length > 0 ? smells.join(', ') : '';
|
|
2658
|
+
const title = smellsText ? \`title="\${smellsText}"\` : '';
|
|
2659
|
+
|
|
2660
|
+
if (count <= 2) return \`<span class="smells-few" \${title}>\${count}</span>\`;
|
|
2661
|
+
return \`<span class="smells-many" \${title}>\${count}</span>\`;
|
|
2662
|
+
};
|
|
2663
|
+
|
|
2664
|
+
html += \`<tr>
|
|
2665
|
+
<td class="file-name">\${line.fileName}</td>
|
|
2666
|
+
<td class="line-number">\${line.lineNumber}</td>
|
|
2667
|
+
<td class="code-preview">\${line.codePreview}</td>
|
|
2668
|
+
<td class="test-count">\${line.testCount}</td>
|
|
2669
|
+
<td class="execution-count">\${line.totalExecutions}</td>
|
|
2670
|
+
<td class="cpu-cycles">\${formatCycles(line.performance?.totalCpuCycles || 0)}</td>
|
|
2671
|
+
<td class="cpu-time">\${formatTime(line.performance?.totalCpuTime || 0)}</td>
|
|
2672
|
+
<td class="wall-time">\${formatTime(line.performance?.totalWallTime || 0)}</td>
|
|
2673
|
+
<td class="quality-score">\${formatQuality(line.quality?.qualityScore || 0)}</td>
|
|
2674
|
+
<td class="reliability-score">\${formatQuality(line.quality?.avgReliability || 0)}</td>
|
|
2675
|
+
<td class="test-smells">\${formatTestSmells(line.quality?.totalTestSmells || 0, line.quality?.testSmells)}</td>
|
|
2676
|
+
<td class="max-depth"><span class="depth-badge \${depthClass}">D\${line.maxDepth}</span></td>
|
|
2677
|
+
<td class="depth-range">\${line.depthRange}</td>
|
|
2678
|
+
</tr>\`;
|
|
2679
|
+
});
|
|
2680
|
+
|
|
2681
|
+
html += \`</tbody></table>\`;
|
|
2682
|
+
document.getElementById('lines-table').innerHTML = html;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
function sortLines() {
|
|
2686
|
+
generateLinesTable();
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
function generatePerformanceDashboard() {
|
|
2690
|
+
const performanceData = window.linesData || [];
|
|
2691
|
+
|
|
2692
|
+
// Calculate performance statistics
|
|
2693
|
+
let totalCpuCycles = 0;
|
|
2694
|
+
let totalMemoryUsage = 0;
|
|
2695
|
+
let memoryLeaks = 0;
|
|
2696
|
+
let gcPressureIssues = 0;
|
|
2697
|
+
let slowTests = 0;
|
|
2698
|
+
|
|
2699
|
+
performanceData.forEach(line => {
|
|
2700
|
+
if (line.performance) {
|
|
2701
|
+
totalCpuCycles += line.performance.totalCpuCycles || 0;
|
|
2702
|
+
totalMemoryUsage += Math.abs(line.performance.totalMemoryDelta || 0);
|
|
2703
|
+
memoryLeaks += line.performance.memoryLeaks || 0;
|
|
2704
|
+
gcPressureIssues += line.performance.gcPressure || 0;
|
|
2705
|
+
if ((line.performance.slowExecutions || 0) > (line.performance.fastExecutions || 0)) {
|
|
2706
|
+
slowTests++;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2710
|
+
|
|
2711
|
+
// Sort by performance metrics
|
|
2712
|
+
const topCpuLines = [...performanceData]
|
|
2713
|
+
.filter(line => line.performance && line.performance.totalCpuCycles > 0)
|
|
2714
|
+
.sort((a, b) => (b.performance.totalCpuCycles || 0) - (a.performance.totalCpuCycles || 0))
|
|
2715
|
+
.slice(0, 10);
|
|
2716
|
+
|
|
2717
|
+
const topMemoryLines = [...performanceData]
|
|
2718
|
+
.filter(line => line.performance && Math.abs(line.performance.totalMemoryDelta || 0) > 0)
|
|
2719
|
+
.sort((a, b) => Math.abs(b.performance.totalMemoryDelta || 0) - Math.abs(a.performance.totalMemoryDelta || 0))
|
|
2720
|
+
.slice(0, 10);
|
|
2721
|
+
|
|
2722
|
+
// Debug logging
|
|
2723
|
+
console.log('Performance Dashboard Data:', {
|
|
2724
|
+
totalLines: performanceData.length,
|
|
2725
|
+
totalCpuCycles,
|
|
2726
|
+
totalMemoryUsage,
|
|
2727
|
+
memoryLeaks,
|
|
2728
|
+
gcPressureIssues,
|
|
2729
|
+
slowTests,
|
|
2730
|
+
sampleLine: performanceData[0]
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
let html = \`
|
|
2734
|
+
<div class="performance-summary">
|
|
2735
|
+
<h3>🔥 Performance Overview</h3>
|
|
2736
|
+
<div class="performance-help">
|
|
2737
|
+
<h4>📚 Understanding Performance Metrics:</h4>
|
|
2738
|
+
<div class="help-section">
|
|
2739
|
+
<strong>🚨 Memory Leaks:</strong> Large allocations (>50KB) that may not be properly cleaned up.
|
|
2740
|
+
<br><em>Action:</em> Review object lifecycle, ensure proper cleanup, avoid global references.
|
|
2741
|
+
</div>
|
|
2742
|
+
<div class="help-section">
|
|
2743
|
+
<strong>🗑️ GC Pressure:</strong> Frequent small allocations that stress the garbage collector.
|
|
2744
|
+
<br><em>Action:</em> Use object pooling, batch operations, avoid creating objects in loops.
|
|
2745
|
+
</div>
|
|
2746
|
+
<div class="help-section">
|
|
2747
|
+
<strong>🐌 Slow Tests:</strong> Tests with inconsistent performance or high variance.
|
|
2748
|
+
<br><em>Action:</em> Use async/await, mock heavy operations, reduce test data size.
|
|
2749
|
+
</div>
|
|
2750
|
+
</div>
|
|
2751
|
+
<div class="perf-stats">
|
|
2752
|
+
<div class="perf-stat">
|
|
2753
|
+
<div class="perf-number">\${(totalCpuCycles / 1000000).toFixed(1)}M</div>
|
|
2754
|
+
<div class="perf-label">Total CPU Cycles</div>
|
|
2755
|
+
</div>
|
|
2756
|
+
<div class="perf-stat">
|
|
2757
|
+
<div class="perf-number">\${(totalMemoryUsage / (1024 * 1024)).toFixed(1)}MB</div>
|
|
2758
|
+
<div class="perf-label">Memory Usage</div>
|
|
2759
|
+
</div>
|
|
2760
|
+
<div class="perf-stat">
|
|
2761
|
+
<div class="perf-number">\${memoryLeaks}</div>
|
|
2762
|
+
<div class="perf-label">Memory Leaks</div>
|
|
2763
|
+
</div>
|
|
2764
|
+
<div class="perf-stat">
|
|
2765
|
+
<div class="perf-number">\${gcPressureIssues}</div>
|
|
2766
|
+
<div class="perf-label">GC Pressure</div>
|
|
2767
|
+
</div>
|
|
2768
|
+
<div class="perf-stat">
|
|
2769
|
+
<div class="perf-number">\${slowTests}</div>
|
|
2770
|
+
<div class="perf-label">Slow Tests</div>
|
|
2771
|
+
</div>
|
|
2772
|
+
</div>
|
|
2773
|
+
</div>
|
|
2774
|
+
|
|
2775
|
+
<div class="performance-tables">
|
|
2776
|
+
<div class="perf-table-container">
|
|
2777
|
+
<h4>🔥 Top CPU Intensive Lines</h4>
|
|
2778
|
+
<table class="perf-table">
|
|
2779
|
+
<thead>
|
|
2780
|
+
<tr><th>File</th><th>Line</th><th>CPU Cycles</th><th>Executions</th></tr>
|
|
2781
|
+
</thead>
|
|
2782
|
+
<tbody>
|
|
2783
|
+
\`;
|
|
2784
|
+
|
|
2785
|
+
topCpuLines.forEach(line => {
|
|
2786
|
+
const cycles = line.performance.totalCpuCycles > 1000000 ?
|
|
2787
|
+
\`\${(line.performance.totalCpuCycles / 1000000).toFixed(1)}M\` :
|
|
2788
|
+
\`\${Math.round(line.performance.totalCpuCycles)}\`;
|
|
2789
|
+
html += \`<tr>
|
|
2790
|
+
<td>\${line.fileName}</td>
|
|
2791
|
+
<td>\${line.lineNumber}</td>
|
|
2792
|
+
<td class="cpu-cycles">\${cycles}</td>
|
|
2793
|
+
<td>\${line.totalExecutions}</td>
|
|
2794
|
+
</tr>\`;
|
|
2795
|
+
});
|
|
2796
|
+
|
|
2797
|
+
html += \`
|
|
2798
|
+
</tbody>
|
|
2799
|
+
</table>
|
|
2800
|
+
</div>
|
|
2801
|
+
|
|
2802
|
+
<div class="perf-table-container">
|
|
2803
|
+
<h4>💾 Top Memory Usage Lines</h4>
|
|
2804
|
+
<table class="perf-table">
|
|
2805
|
+
<thead>
|
|
2806
|
+
<tr><th>File</th><th>Line</th><th>Memory Usage</th><th>Leaks</th></tr>
|
|
2807
|
+
</thead>
|
|
2808
|
+
<tbody>
|
|
2809
|
+
\`;
|
|
2810
|
+
|
|
2811
|
+
topMemoryLines.forEach(line => {
|
|
2812
|
+
const memory = Math.abs(line.performance.totalMemoryDelta || 0);
|
|
2813
|
+
const memoryStr = memory > 1024 * 1024 ?
|
|
2814
|
+
\`\${(memory / (1024 * 1024)).toFixed(1)}MB\` :
|
|
2815
|
+
\`\${(memory / 1024).toFixed(1)}KB\`;
|
|
2816
|
+
html += \`<tr>
|
|
2817
|
+
<td>\${line.fileName}</td>
|
|
2818
|
+
<td>\${line.lineNumber}</td>
|
|
2819
|
+
<td class="memory-usage">\${memoryStr}</td>
|
|
2820
|
+
<td class="memory-leaks">\${line.performance.memoryLeaks || 0}</td>
|
|
2821
|
+
</tr>\`;
|
|
2822
|
+
});
|
|
2823
|
+
|
|
2824
|
+
html += \`
|
|
2825
|
+
</tbody>
|
|
2826
|
+
</table>
|
|
2827
|
+
</div>
|
|
2828
|
+
</div>
|
|
2829
|
+
\`;
|
|
2830
|
+
|
|
2831
|
+
document.getElementById('performance-dashboard').innerHTML = html;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
function generateQualityDashboard() {
|
|
2835
|
+
const qualityData = window.linesData;
|
|
2836
|
+
|
|
2837
|
+
// Calculate quality statistics
|
|
2838
|
+
let totalTests = 0;
|
|
2839
|
+
let highQuality = 0;
|
|
2840
|
+
let goodQuality = 0;
|
|
2841
|
+
let fairQuality = 0;
|
|
2842
|
+
let poorQuality = 0;
|
|
2843
|
+
let totalSmells = 0;
|
|
2844
|
+
let totalAssertions = 0;
|
|
2845
|
+
|
|
2846
|
+
const smellTypes = {};
|
|
2847
|
+
|
|
2848
|
+
qualityData.forEach(line => {
|
|
2849
|
+
if (line.quality) {
|
|
2850
|
+
totalTests += line.testCount;
|
|
2851
|
+
totalAssertions += line.quality.totalAssertions;
|
|
2852
|
+
totalSmells += line.quality.totalTestSmells;
|
|
2853
|
+
|
|
2854
|
+
const avgQuality = line.quality.qualityScore;
|
|
2855
|
+
if (avgQuality >= 80) highQuality++;
|
|
2856
|
+
else if (avgQuality >= 60) goodQuality++;
|
|
2857
|
+
else if (avgQuality >= 40) fairQuality++;
|
|
2858
|
+
else poorQuality++;
|
|
2859
|
+
}
|
|
2860
|
+
});
|
|
2861
|
+
|
|
2862
|
+
// Sort by quality metrics
|
|
2863
|
+
const topQualityLines = [...qualityData].sort((a, b) => b.quality.qualityScore - a.quality.qualityScore).slice(0, 10);
|
|
2864
|
+
const worstQualityLines = [...qualityData].sort((a, b) => a.quality.qualityScore - b.quality.qualityScore).slice(0, 10);
|
|
2865
|
+
|
|
2866
|
+
let html = \`
|
|
2867
|
+
<div class="quality-summary">
|
|
2868
|
+
<h3>🧪 Test Quality Overview</h3>
|
|
2869
|
+
<div class="quality-stats">
|
|
2870
|
+
<div class="quality-stat">
|
|
2871
|
+
<div class="quality-number">\${totalTests}</div>
|
|
2872
|
+
<div class="quality-label">Total Tests</div>
|
|
2873
|
+
</div>
|
|
2874
|
+
<div class="quality-stat">
|
|
2875
|
+
<div class="quality-number">\${totalAssertions}</div>
|
|
2876
|
+
<div class="quality-label">Total Assertions</div>
|
|
2877
|
+
</div>
|
|
2878
|
+
<div class="quality-stat">
|
|
2879
|
+
<div class="quality-number">\${totalSmells}</div>
|
|
2880
|
+
<div class="quality-label">Test Smells</div>
|
|
2881
|
+
</div>
|
|
2882
|
+
<div class="quality-stat">
|
|
2883
|
+
<div class="quality-number">\${(totalAssertions/totalTests).toFixed(1)}</div>
|
|
2884
|
+
<div class="quality-label">Avg Assertions/Test</div>
|
|
2885
|
+
</div>
|
|
2886
|
+
</div>
|
|
2887
|
+
|
|
2888
|
+
<div class="quality-distribution">
|
|
2889
|
+
<h4>Quality Distribution</h4>
|
|
2890
|
+
<div class="quality-bars">
|
|
2891
|
+
<div class="quality-bar">
|
|
2892
|
+
<span class="quality-excellent">🏆 High Quality: \${highQuality} lines</span>
|
|
2893
|
+
<div class="bar"><div class="fill excellent" style="width: \${(highQuality/qualityData.length)*100}%"></div></div>
|
|
2894
|
+
</div>
|
|
2895
|
+
<div class="quality-bar">
|
|
2896
|
+
<span class="quality-good">✅ Good Quality: \${goodQuality} lines</span>
|
|
2897
|
+
<div class="bar"><div class="fill good" style="width: \${(goodQuality/qualityData.length)*100}%"></div></div>
|
|
2898
|
+
</div>
|
|
2899
|
+
<div class="quality-bar">
|
|
2900
|
+
<span class="quality-fair">⚠️ Fair Quality: \${fairQuality} lines</span>
|
|
2901
|
+
<div class="bar"><div class="fill fair" style="width: \${(fairQuality/qualityData.length)*100}%"></div></div>
|
|
2902
|
+
</div>
|
|
2903
|
+
<div class="quality-bar">
|
|
2904
|
+
<span class="quality-poor">❌ Poor Quality: \${poorQuality} lines</span>
|
|
2905
|
+
<div class="bar"><div class="fill poor" style="width: \${(poorQuality/qualityData.length)*100}%"></div></div>
|
|
2906
|
+
</div>
|
|
2907
|
+
</div>
|
|
2908
|
+
</div>
|
|
2909
|
+
</div>
|
|
2910
|
+
|
|
2911
|
+
<div class="quality-tables">
|
|
2912
|
+
<div class="quality-table-container">
|
|
2913
|
+
<h4>🏆 Highest Quality Tests</h4>
|
|
2914
|
+
<table class="quality-table">
|
|
2915
|
+
<thead>
|
|
2916
|
+
<tr><th>File</th><th>Line</th><th>Quality Score</th><th>Reliability</th><th>Test Smells</th></tr>
|
|
2917
|
+
</thead>
|
|
2918
|
+
<tbody>
|
|
2919
|
+
\`;
|
|
2920
|
+
|
|
2921
|
+
topQualityLines.forEach(line => {
|
|
2922
|
+
const qualityClass = line.quality.qualityScore >= 80 ? 'quality-excellent' :
|
|
2923
|
+
line.quality.qualityScore >= 60 ? 'quality-good' :
|
|
2924
|
+
line.quality.qualityScore >= 40 ? 'quality-fair' : 'quality-poor';
|
|
2925
|
+
html += \`<tr>
|
|
2926
|
+
<td>\${line.fileName}</td>
|
|
2927
|
+
<td>\${line.lineNumber}</td>
|
|
2928
|
+
<td class="\${qualityClass}">\${Math.round(line.quality.qualityScore)}%</td>
|
|
2929
|
+
<td class="\${qualityClass}">\${Math.round(line.quality.avgReliability)}%</td>
|
|
2930
|
+
<td class="smells-\${line.quality.totalTestSmells === 0 ? 'none' : line.quality.totalTestSmells <= 2 ? 'few' : 'many'}">\${line.quality.totalTestSmells}</td>
|
|
2931
|
+
</tr>\`;
|
|
2932
|
+
});
|
|
2933
|
+
|
|
2934
|
+
html += \`
|
|
2935
|
+
</tbody>
|
|
2936
|
+
</table>
|
|
2937
|
+
</div>
|
|
2938
|
+
|
|
2939
|
+
<div class="quality-table-container">
|
|
2940
|
+
<h4>🚨 Tests Needing Improvement</h4>
|
|
2941
|
+
<table class="quality-table">
|
|
2942
|
+
<thead>
|
|
2943
|
+
<tr><th>File</th><th>Line</th><th>Quality Score</th><th>Issues</th><th>Recommendations</th></tr>
|
|
2944
|
+
</thead>
|
|
2945
|
+
<tbody>
|
|
2946
|
+
\`;
|
|
2947
|
+
|
|
2948
|
+
worstQualityLines.forEach(line => {
|
|
2949
|
+
const qualityClass = line.quality.qualityScore >= 80 ? 'quality-excellent' :
|
|
2950
|
+
line.quality.qualityScore >= 60 ? 'quality-good' :
|
|
2951
|
+
line.quality.qualityScore >= 40 ? 'quality-fair' : 'quality-poor';
|
|
2952
|
+
|
|
2953
|
+
let recommendations = [];
|
|
2954
|
+
if (line.quality.totalTestSmells > 0) recommendations.push('Fix test smells');
|
|
2955
|
+
if (line.quality.totalAssertions < 3) recommendations.push('Add more assertions');
|
|
2956
|
+
if (line.quality.avgReliability < 60) recommendations.push('Improve error handling');
|
|
2957
|
+
|
|
2958
|
+
html += \`<tr>
|
|
2959
|
+
<td>\${line.fileName}</td>
|
|
2960
|
+
<td>\${line.lineNumber}</td>
|
|
2961
|
+
<td class="\${qualityClass}">\${Math.round(line.quality.qualityScore)}%</td>
|
|
2962
|
+
<td class="quality-issues">
|
|
2963
|
+
\${line.quality.totalTestSmells} smells\${line.quality.testSmells && line.quality.testSmells.length > 0 ? ' (' + line.quality.testSmells.join(', ') + ')' : ''},
|
|
2964
|
+
\${line.quality.totalAssertions} assertions
|
|
2965
|
+
</td>
|
|
2966
|
+
<td class="recommendations">\${recommendations.join(', ') || 'Good as is'}</td>
|
|
2967
|
+
</tr>\`;
|
|
2968
|
+
});
|
|
2969
|
+
|
|
2970
|
+
html += \`
|
|
2971
|
+
</tbody>
|
|
2972
|
+
</table>
|
|
2973
|
+
</div>
|
|
2974
|
+
</div>
|
|
2975
|
+
\`;
|
|
2976
|
+
|
|
2977
|
+
document.getElementById('quality-dashboard').innerHTML = html;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
function generateMutationsDashboard() {
|
|
2981
|
+
// Get mutation data from the global variable set by the mutation testing
|
|
2982
|
+
let mutationData = window.mutationTestingResults || ${JSON.stringify(this.mutationResults || {})};
|
|
2983
|
+
|
|
2984
|
+
if (!mutationData || mutationData.totalMutations === undefined || mutationData.totalMutations === 0) {
|
|
2985
|
+
document.getElementById('mutations-dashboard').innerHTML = \`
|
|
2986
|
+
<div class="no-data">
|
|
2987
|
+
<h3>🧬 No Mutation Testing Data Available</h3>
|
|
2988
|
+
<p>Run mutation testing to see results here.</p>
|
|
2989
|
+
</div>
|
|
2990
|
+
\`;
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
// Use the actual structure returned by MutationTester
|
|
2995
|
+
const summary = {
|
|
2996
|
+
total: mutationData.totalMutations || 0,
|
|
2997
|
+
killed: mutationData.killedMutations || 0,
|
|
2998
|
+
survived: mutationData.survivedMutations || 0,
|
|
2999
|
+
timeout: mutationData.timeoutMutations || 0,
|
|
3000
|
+
error: mutationData.errorMutations || 0
|
|
3001
|
+
};
|
|
3002
|
+
const mutationScore = mutationData.mutationScore || 0;
|
|
3003
|
+
const scoreClass = mutationScore >= 80 ? 'excellent' : mutationScore >= 60 ? 'good' : mutationScore >= 40 ? 'fair' : 'poor';
|
|
3004
|
+
|
|
3005
|
+
let html = \`
|
|
3006
|
+
<div class="mutations-overview">
|
|
3007
|
+
<div class="mutations-summary-cards">
|
|
3008
|
+
<div class="summary-card \${scoreClass}">
|
|
3009
|
+
<h3>🎯 Mutation Score</h3>
|
|
3010
|
+
<div class="big-number">\${mutationScore}%</div>
|
|
3011
|
+
<div class="subtitle">\${summary.killed}/\${summary.total} mutations killed</div>
|
|
3012
|
+
</div>
|
|
3013
|
+
<div class="summary-card">
|
|
3014
|
+
<h3>🔬 Total Mutations</h3>
|
|
3015
|
+
<div class="big-number">\${summary.total}</div>
|
|
3016
|
+
<div class="subtitle">Generated across all files</div>
|
|
3017
|
+
</div>
|
|
3018
|
+
<div class="summary-card survived">
|
|
3019
|
+
<h3>🔴 Survived</h3>
|
|
3020
|
+
<div class="big-number">\${summary.survived}</div>
|
|
3021
|
+
<div class="subtitle">Mutations not caught by tests</div>
|
|
3022
|
+
</div>
|
|
3023
|
+
<div class="summary-card killed">
|
|
3024
|
+
<h3>✅ Killed</h3>
|
|
3025
|
+
<div class="big-number">\${summary.killed}</div>
|
|
3026
|
+
<div class="subtitle">Mutations caught by tests</div>
|
|
3027
|
+
</div>
|
|
3028
|
+
</div>
|
|
3029
|
+
|
|
3030
|
+
<div class="mutations-chart">
|
|
3031
|
+
<h4>Mutation Results Distribution</h4>
|
|
3032
|
+
<div class="mutation-bars">
|
|
3033
|
+
<div class="mutation-bar">
|
|
3034
|
+
<span class="mutation-killed">✅ Killed: \${summary.killed} mutations</span>
|
|
3035
|
+
<div class="bar"><div class="fill killed" style="width: \${summary.total > 0 ? (summary.killed/summary.total)*100 : 0}%"></div></div>
|
|
3036
|
+
</div>
|
|
3037
|
+
<div class="mutation-bar">
|
|
3038
|
+
<span class="mutation-survived">🔴 Survived: \${summary.survived} mutations</span>
|
|
3039
|
+
<div class="bar"><div class="fill survived" style="width: \${summary.total > 0 ? (summary.survived/summary.total)*100 : 0}%"></div></div>
|
|
3040
|
+
</div>
|
|
3041
|
+
<div class="mutation-bar">
|
|
3042
|
+
<span class="mutation-timeout">⏰ Timeout: \${summary.timeout || 0} mutations</span>
|
|
3043
|
+
<div class="bar"><div class="fill timeout" style="width: \${summary.total > 0 ? ((summary.timeout || 0)/summary.total)*100 : 0}%"></div></div>
|
|
3044
|
+
</div>
|
|
3045
|
+
<div class="mutation-bar">
|
|
3046
|
+
<span class="mutation-error">❌ Error: \${summary.error || 0} mutations</span>
|
|
3047
|
+
<div class="bar"><div class="fill error" style="width: \${summary.total > 0 ? ((summary.error || 0)/summary.total)*100 : 0}%"></div></div>
|
|
3048
|
+
</div>
|
|
3049
|
+
</div>
|
|
3050
|
+
</div>
|
|
3051
|
+
</div>
|
|
3052
|
+
\`;
|
|
3053
|
+
|
|
3054
|
+
// Add file-by-file breakdown if available
|
|
3055
|
+
if (mutationData.fileResults) {
|
|
3056
|
+
html += \`
|
|
3057
|
+
<div class="mutations-files">
|
|
3058
|
+
<h4>📁 File-by-File Results</h4>
|
|
3059
|
+
<table class="mutations-table">
|
|
3060
|
+
<thead>
|
|
3061
|
+
<tr><th>File</th><th>Total</th><th>Killed</th><th>Survived</th><th>Score</th><th>Status</th></tr>
|
|
3062
|
+
</thead>
|
|
3063
|
+
<tbody>
|
|
3064
|
+
\`;
|
|
3065
|
+
|
|
3066
|
+
Object.entries(mutationData.fileResults).forEach(([filePath, fileData]) => {
|
|
3067
|
+
const fileName = filePath.split('/').pop();
|
|
3068
|
+
const fileScore = fileData.totalMutations > 0 ? Math.round((fileData.killedMutations / fileData.totalMutations) * 100) : 0;
|
|
3069
|
+
const fileScoreClass = fileScore >= 80 ? 'excellent' : fileScore >= 60 ? 'good' : fileScore >= 40 ? 'fair' : 'poor';
|
|
3070
|
+
const statusIcon = fileScore >= 80 ? '🏆' : fileScore >= 60 ? '✅' : fileScore >= 40 ? '⚠️' : '❌';
|
|
3071
|
+
|
|
3072
|
+
html += \`<tr class="file-row" onclick="toggleFileDetails('\${filePath.replace(/[^a-zA-Z0-9]/g, '_')}')">
|
|
3073
|
+
<td class="file-name">\${fileName}</td>
|
|
3074
|
+
<td>\${fileData.totalMutations}</td>
|
|
3075
|
+
<td class="killed">\${fileData.killedMutations}</td>
|
|
3076
|
+
<td class="survived">\${fileData.survivedMutations}</td>
|
|
3077
|
+
<td class="\${fileScoreClass}">\${fileScore}%</td>
|
|
3078
|
+
<td>\${statusIcon}</td>
|
|
3079
|
+
</tr>
|
|
3080
|
+
<tr id="details-\${filePath.replace(/[^a-zA-Z0-9]/g, '_')}" class="file-details" style="display: none;">
|
|
3081
|
+
<td colspan="6">
|
|
3082
|
+
<div class="file-mutation-details">
|
|
3083
|
+
\${generateFileDetailedMutations(fileData, filePath)}
|
|
3084
|
+
</div>
|
|
3085
|
+
</td>
|
|
3086
|
+
</tr>\`;
|
|
3087
|
+
});
|
|
3088
|
+
|
|
3089
|
+
html += \`
|
|
3090
|
+
</tbody>
|
|
3091
|
+
</table>
|
|
3092
|
+
</div>
|
|
3093
|
+
\`;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
document.getElementById('mutations-dashboard').innerHTML = html;
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
function generateFileDetailedMutations(fileData, filePath) {
|
|
3100
|
+
if (!fileData.mutations || fileData.mutations.length === 0) {
|
|
3101
|
+
return '<p>No mutations found for this file.</p>';
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
// Group mutations by line
|
|
3105
|
+
const mutationsByLine = {};
|
|
3106
|
+
fileData.mutations.forEach(mutation => {
|
|
3107
|
+
const line = mutation.line || 'unknown';
|
|
3108
|
+
if (!mutationsByLine[line]) {
|
|
3109
|
+
mutationsByLine[line] = [];
|
|
3110
|
+
}
|
|
3111
|
+
mutationsByLine[line].push(mutation);
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
let html = '<div class="mutations-by-line">';
|
|
3115
|
+
|
|
3116
|
+
Object.entries(mutationsByLine).forEach(([line, mutations]) => {
|
|
3117
|
+
html += \`<div class="line-mutations">
|
|
3118
|
+
<h5>Line \${line} (\${mutations.length} mutations)</h5>
|
|
3119
|
+
<div class="mutations-list">\`;
|
|
3120
|
+
|
|
3121
|
+
mutations.forEach(mutation => {
|
|
3122
|
+
const statusClass = mutation.status === 'killed' ? 'killed' :
|
|
3123
|
+
mutation.status === 'survived' ? 'survived' : 'error';
|
|
3124
|
+
const statusIcon = mutation.status === 'killed' ? '✅' :
|
|
3125
|
+
mutation.status === 'survived' ? '❌' : '⚠️';
|
|
3126
|
+
|
|
3127
|
+
const testsInfo = mutation.killedBy && mutation.killedBy.length > 0 ?
|
|
3128
|
+
\`Killed by: \${mutation.killedBy.join(', ')}\` :
|
|
3129
|
+
mutation.status === 'survived' ? 'No tests killed this mutation' :
|
|
3130
|
+
mutation.error ? \`Error: \${mutation.error}\` : 'Unknown status';
|
|
3131
|
+
|
|
3132
|
+
html += \`<div class="mutation-detail \${statusClass}">
|
|
3133
|
+
<div class="mutation-header">
|
|
3134
|
+
<span class="mutation-status">\${statusIcon} \${mutation.status.toUpperCase()}</span>
|
|
3135
|
+
<span class="mutation-type">\${mutation.mutatorName || mutation.type || 'Unknown'}</span>
|
|
3136
|
+
</div>
|
|
3137
|
+
<div class="mutation-change">
|
|
3138
|
+
<strong>Original:</strong> <code>\${mutation.original || 'N/A'}</code><br>
|
|
3139
|
+
<strong>Mutated:</strong> <code>\${mutation.replacement || 'N/A'}</code>
|
|
3140
|
+
</div>
|
|
3141
|
+
<div class="mutation-tests">\${testsInfo}</div>
|
|
3142
|
+
</div>\`;
|
|
3143
|
+
});
|
|
3144
|
+
|
|
3145
|
+
html += '</div></div>';
|
|
3146
|
+
});
|
|
3147
|
+
|
|
3148
|
+
html += '</div>';
|
|
3149
|
+
return html;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
function toggleFileDetails(fileId) {
|
|
3153
|
+
const details = document.getElementById('details-' + fileId);
|
|
3154
|
+
if (details) {
|
|
3155
|
+
details.style.display = details.style.display === 'none' ? 'table-row' : 'none';
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
</script>
|
|
3159
|
+
|
|
3160
|
+
<!-- Inject mutation testing results -->
|
|
3161
|
+
<script>
|
|
3162
|
+
window.mutationTestingResults = ${JSON.stringify(this.mutationResults || {})};
|
|
3163
|
+
</script>
|
|
3164
|
+
</body>
|
|
3165
|
+
</html>`;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
/**
|
|
3169
|
+
* Get mutation testing results for a specific line
|
|
3170
|
+
*/
|
|
3171
|
+
getMutationResultsForLine(filePath, lineNumber) {
|
|
3172
|
+
if (!this.mutationResults || !this.mutationResults.fileResults) {
|
|
3173
|
+
return [];
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
const fileResults = this.mutationResults.fileResults[filePath];
|
|
3177
|
+
if (!fileResults || !fileResults.lineResults) {
|
|
3178
|
+
return [];
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
const lineResults = fileResults.lineResults[lineNumber];
|
|
3182
|
+
if (!lineResults || !lineResults.mutations) {
|
|
3183
|
+
return [];
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
return lineResults.mutations;
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
/**
|
|
3190
|
+
* Generate HTML for mutation testing results
|
|
3191
|
+
*/
|
|
3192
|
+
generateMutationResultsHtml(mutationResults, lineNumber) {
|
|
3193
|
+
const totalMutations = mutationResults.length;
|
|
3194
|
+
const killedMutations = mutationResults.filter(m => m.status === 'killed').length;
|
|
3195
|
+
const survivedMutations = mutationResults.filter(m => m.status === 'survived').length;
|
|
3196
|
+
const errorMutations = mutationResults.filter(m => m.status === 'error').length;
|
|
3197
|
+
const timeoutMutations = mutationResults.filter(m => m.status === 'timeout').length;
|
|
3198
|
+
|
|
3199
|
+
const mutationScore = totalMutations > 0 ? Math.round((killedMutations / totalMutations) * 100) : 0;
|
|
3200
|
+
const scoreClass = mutationScore >= 80 ? 'mutation-score-good' :
|
|
3201
|
+
mutationScore >= 60 ? 'mutation-score-fair' : 'mutation-score-poor';
|
|
3202
|
+
|
|
3203
|
+
let html = `
|
|
3204
|
+
<div class="mutation-results">
|
|
3205
|
+
<h4>🧬 Mutation Testing Results</h4>
|
|
3206
|
+
<div class="mutation-summary">
|
|
3207
|
+
<div class="mutation-score ${scoreClass}">
|
|
3208
|
+
Score: ${mutationScore}%
|
|
3209
|
+
</div>
|
|
3210
|
+
<div class="mutation-stats">
|
|
3211
|
+
<span class="mutation-stat killed">✅ ${killedMutations} killed</span>
|
|
3212
|
+
<span class="mutation-stat survived">🔴 ${survivedMutations} survived</span>`;
|
|
3213
|
+
|
|
3214
|
+
if (errorMutations > 0) {
|
|
3215
|
+
html += `<span class="mutation-stat error">❌ ${errorMutations} error</span>`;
|
|
3216
|
+
}
|
|
3217
|
+
if (timeoutMutations > 0) {
|
|
3218
|
+
html += `<span class="mutation-stat timeout">⏰ ${timeoutMutations} timeout</span>`;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
html += `
|
|
3222
|
+
</div>
|
|
3223
|
+
</div>
|
|
3224
|
+
<div class="mutation-details">`;
|
|
3225
|
+
|
|
3226
|
+
// Group mutations by status
|
|
3227
|
+
const mutationsByStatus = {
|
|
3228
|
+
survived: mutationResults.filter(m => m.status === 'survived'),
|
|
3229
|
+
killed: mutationResults.filter(m => m.status === 'killed'),
|
|
3230
|
+
error: mutationResults.filter(m => m.status === 'error'),
|
|
3231
|
+
timeout: mutationResults.filter(m => m.status === 'timeout')
|
|
3232
|
+
};
|
|
3233
|
+
|
|
3234
|
+
// Show survived mutations first (most important)
|
|
3235
|
+
if (mutationsByStatus.survived.length > 0) {
|
|
3236
|
+
html += `
|
|
3237
|
+
<div class="mutation-group survived">
|
|
3238
|
+
<h5>🔴 Survived Mutations (${mutationsByStatus.survived.length})</h5>`;
|
|
3239
|
+
mutationsByStatus.survived.forEach(mutation => {
|
|
3240
|
+
html += `
|
|
3241
|
+
<div class="mutation-item survived">
|
|
3242
|
+
<span class="mutation-type">${mutation.mutationType}</span>
|
|
3243
|
+
<span class="mutation-description">${this.getMutationDescription(mutation)}</span>
|
|
3244
|
+
<span class="mutation-tests">Tests run: ${mutation.testsRun || 0}</span>
|
|
3245
|
+
</div>`;
|
|
3246
|
+
});
|
|
3247
|
+
html += `</div>`;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// Show killed mutations (collapsed by default)
|
|
3251
|
+
if (mutationsByStatus.killed.length > 0) {
|
|
3252
|
+
html += `
|
|
3253
|
+
<details class="mutation-group killed">
|
|
3254
|
+
<summary>✅ Killed Mutations (${mutationsByStatus.killed.length})</summary>`;
|
|
3255
|
+
mutationsByStatus.killed.forEach(mutation => {
|
|
3256
|
+
html += `
|
|
3257
|
+
<div class="mutation-item killed">
|
|
3258
|
+
<span class="mutation-type">${mutation.mutationType}</span>
|
|
3259
|
+
<span class="mutation-description">${this.getMutationDescription(mutation)}</span>
|
|
3260
|
+
<span class="mutation-tests">Tests run: ${mutation.testsRun || 0}</span>
|
|
3261
|
+
</div>`;
|
|
3262
|
+
});
|
|
3263
|
+
html += `</details>`;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
// Show errors if any
|
|
3267
|
+
if (mutationsByStatus.error.length > 0) {
|
|
3268
|
+
html += `
|
|
3269
|
+
<details class="mutation-group error">
|
|
3270
|
+
<summary>❌ Error Mutations (${mutationsByStatus.error.length})</summary>`;
|
|
3271
|
+
mutationsByStatus.error.forEach(mutation => {
|
|
3272
|
+
html += `
|
|
3273
|
+
<div class="mutation-item error">
|
|
3274
|
+
<span class="mutation-type">${mutation.mutationType}</span>
|
|
3275
|
+
<span class="mutation-error">${mutation.error || 'Unknown error'}</span>
|
|
3276
|
+
</div>`;
|
|
3277
|
+
});
|
|
3278
|
+
html += `</details>`;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
html += `
|
|
3282
|
+
</div>
|
|
3283
|
+
</div>`;
|
|
3284
|
+
|
|
3285
|
+
return html;
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
/**
|
|
3289
|
+
* Get a human-readable description of a mutation
|
|
3290
|
+
*/
|
|
3291
|
+
getMutationDescription(mutation) {
|
|
3292
|
+
const descriptions = {
|
|
3293
|
+
'arithmetic': 'Changed arithmetic operator (+, -, *, /, %)',
|
|
3294
|
+
'comparison': 'Changed comparison operator (==, !=, <, >, <=, >=)',
|
|
3295
|
+
'logical': 'Changed logical operator (&&, ||, !)',
|
|
3296
|
+
'conditional': 'Negated conditional statement',
|
|
3297
|
+
'literals': 'Changed literal value (number, boolean, string)',
|
|
3298
|
+
'returns': 'Changed return value to null',
|
|
3299
|
+
'increments': 'Changed increment/decrement operator (++, --)',
|
|
3300
|
+
'assignment': 'Changed assignment operator (+=, -=, *=, /=)'
|
|
3301
|
+
};
|
|
3302
|
+
|
|
3303
|
+
return descriptions[mutation.mutationType] || `${mutation.mutationType} mutation`;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
module.exports = TestCoverageReporter;
|