#!/usr/bin/env node
/**
* Generate comprehensive test coverage report
* Works around Jest TypeScript coverage limitations
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
console.log('🔍 Generating comprehensive test coverage report...\n');
// Step 1: Run tests and collect coverage from built files
console.log('📊 Running test suite with coverage collection...');
try {
// Run tests with coverage on dist files (which have source maps)
const testCommand = 'NODE_OPTIONS="--experimental-vm-modules" npx jest --config=jest.typescript.config.json --coverage --silent --maxWorkers=1';
const testOutput = execSync(testCommand, {
encoding: 'utf8',
cwd: process.cwd(),
timeout: 120000 // 2 minute timeout
});
console.log('✅ Test execution completed');
} catch (error) {
console.warn('⚠️ Some tests may have failed, but continuing with coverage analysis');
// Continue even if some tests fail
}
// Step 2: Analyze coverage data
let coverageData = {};
let coverageSummary = null;
try {
if (fs.existsSync('coverage/coverage-final.json')) {
const rawData = fs.readFileSync('coverage/coverage-final.json', 'utf8');
coverageData = JSON.parse(rawData);
console.log('✅ Coverage data found');
} else {
console.warn('⚠️ No coverage-final.json found');
}
} catch (error) {
console.warn('⚠️ Could not parse coverage data:', error.message);
}
// Step 3: Generate manual coverage statistics
console.log('\n📈 Analyzing test coverage...');
// Get test file counts
const testFiles = [
'tests/utils/',
'tests/config/',
'tests/server/',
'tests/managers/',
'tests/security/',
'tests/property/',
'tests/unit/',
'tests/cache/',
'tests/tools/'
].flatMap(dir => {
try {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter(file => file.endsWith('.test.js'))
.map(file => path.join(dir, file));
} catch (error) {
return [];
}
});
// Get source file counts
const sourceFiles = [
'src/utils/',
'src/config/',
'src/client/',
'src/tools/',
'src/server/',
'src/cache/',
'src/performance/',
'src/security/'
].flatMap(dir => {
try {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir, { recursive: true })
.filter(file => file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.includes('.test.'))
.map(file => path.join(dir, file));
} catch (error) {
return [];
}
});
// Calculate test-to-source ratios
const testCoverage = {
testFiles: testFiles.length,
sourceFiles: sourceFiles.length,
testRatio: testFiles.length > 0 ? (testFiles.length / sourceFiles.length * 100).toFixed(1) : '0.0'
};
console.log('\n📊 Test Coverage Analysis:');
console.log('============================');
console.log(`Total Source Files: ${testCoverage.sourceFiles}`);
console.log(`Total Test Files: ${testCoverage.testFiles}`);
console.log(`Test-to-Source Ratio: ${testCoverage.testRatio}%`);
// Step 4: Analyze specific module coverage
const moduleAnalysis = {
'utils/': {
source: sourceFiles.filter(f => f.startsWith('src/utils/')).length,
tests: testFiles.filter(f => f.startsWith('tests/utils/')).length
},
'config/': {
source: sourceFiles.filter(f => f.startsWith('src/config/')).length,
tests: testFiles.filter(f => f.startsWith('tests/config/')).length
},
'client/': {
source: sourceFiles.filter(f => f.startsWith('src/client/')).length,
tests: testFiles.filter(f => f.startsWith('tests/client/') || f.startsWith('tests/managers/')).length
},
'tools/': {
source: sourceFiles.filter(f => f.startsWith('src/tools/')).length,
tests: testFiles.filter(f => f.startsWith('tests/tools/')).length
},
'server/': {
source: sourceFiles.filter(f => f.startsWith('src/server/')).length,
tests: testFiles.filter(f => f.startsWith('tests/server/')).length
}
};
console.log('\n🎯 Module-Specific Coverage:');
console.log('==============================');
Object.entries(moduleAnalysis).forEach(([module, stats]) => {
const ratio = stats.source > 0 ? (stats.tests / stats.source * 100).toFixed(1) : '0.0';
const status = parseFloat(ratio) >= 50 ? '✅' : parseFloat(ratio) >= 25 ? '⚠️' : '❌';
console.log(`${status} ${module}: ${stats.tests}/${stats.source} files (${ratio}%)`);
});
// Step 5: Extract actual coverage from HTML report if available
let actualCoverage = null;
try {
if (fs.existsSync('coverage/index.html')) {
const htmlContent = fs.readFileSync('coverage/index.html', 'utf8');
// Try to extract coverage percentages from HTML
const extractPercentage = (metric) => {
const regex = new RegExp(`${metric}[^>]*>\\s*([0-9.]+)%`, 'i');
const match = htmlContent.match(regex);
return match ? parseFloat(match[1]) : null;
};
actualCoverage = {
statements: extractPercentage('statements') || 0,
branches: extractPercentage('branches') || 0,
functions: extractPercentage('functions') || 0,
lines: extractPercentage('lines') || 0
};
}
} catch (error) {
console.warn('⚠️ Could not extract coverage from HTML report');
}
// Step 6: Generate final report
console.log('\n🎯 Final Coverage Report:');
console.log('==========================');
if (actualCoverage && (actualCoverage.lines > 0 || actualCoverage.statements > 0)) {
console.log(`Lines: ${actualCoverage.lines.toFixed(2)}%`);
console.log(`Statements: ${actualCoverage.statements.toFixed(2)}%`);
console.log(`Branches: ${actualCoverage.branches.toFixed(2)}%`);
console.log(`Functions: ${actualCoverage.functions.toFixed(2)}%`);
} else {
// Fallback: estimate based on test coverage
const estimatedCoverage = Math.min(parseFloat(testCoverage.testRatio) * 0.6, 85); // Conservative estimate
console.log(`Estimated Coverage: ~${estimatedCoverage.toFixed(1)}% (based on test-to-source ratio)`);
console.log('Note: Actual line-level coverage requires proper Jest/TypeScript integration');
}
// Step 7: Recommendations
console.log('\n💡 Coverage Improvement Recommendations:');
console.log('=========================================');
const lowCoverageModules = Object.entries(moduleAnalysis)
.filter(([_, stats]) => stats.source > 0 && (stats.tests / stats.source) < 0.5)
.map(([module, stats]) => ({
module,
deficit: stats.source - stats.tests,
ratio: stats.tests / stats.source
}))
.sort((a, b) => b.deficit - a.deficit);
if (lowCoverageModules.length > 0) {
console.log('🎯 Priority areas for test improvement:');
lowCoverageModules.forEach((item, index) => {
console.log(`${index + 1}. ${item.module} - Add ${item.deficit} more test files (${(item.ratio * 100).toFixed(1)}% coverage)`);
});
} else {
console.log('✅ All modules have good test coverage ratios!');
}
// Step 8: Save report
const report = {
timestamp: new Date().toISOString(),
testCoverage,
moduleAnalysis,
actualCoverage,
recommendations: lowCoverageModules
};
fs.writeFileSync('coverage/coverage-analysis.json', JSON.stringify(report, null, 2));
console.log('\n✅ Coverage analysis saved to coverage/coverage-analysis.json');
// Exit with appropriate code
const overallSuccess = testCoverage.testRatio >= 30 && lowCoverageModules.length <= 2;
process.exit(overallSuccess ? 0 : 1);