mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 06:14:59 +00:00
200 lines
6.0 KiB
JavaScript
200 lines
6.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Test Coverage Report Generator
|
|
*
|
|
* Generates a comprehensive report of function-to-test mapping
|
|
* Identifies untested functions and provides actionable recommendations
|
|
*/
|
|
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
function findFiles(dir, pattern, ignore = []) {
|
|
let results = [];
|
|
try {
|
|
const files = fs.readdirSync(dir);
|
|
|
|
for (const file of files) {
|
|
const filepath = path.join(dir, file);
|
|
const stat = fs.statSync(filepath);
|
|
const relPath = path.relative(process.cwd(), filepath);
|
|
|
|
if (ignore.some(ign => relPath.includes(ign))) continue;
|
|
|
|
if (stat.isDirectory()) {
|
|
results = results.concat(findFiles(filepath, pattern, ignore));
|
|
} else if (pattern.test(file)) {
|
|
results.push(filepath);
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
return results;
|
|
}
|
|
|
|
function extractFunctions(content) {
|
|
const functions = [];
|
|
const lines = content.split("\n");
|
|
|
|
lines.forEach((line, index) => {
|
|
const lineNum = index + 1;
|
|
|
|
// export function/const
|
|
const namedMatch = line.match(/export\s+(?:function|const|async\s+function|async\s+const)\s+(\w+)/);
|
|
if (namedMatch) {
|
|
functions.push({
|
|
name: namedMatch[1],
|
|
line: lineNum,
|
|
type: "export",
|
|
});
|
|
}
|
|
});
|
|
|
|
return functions;
|
|
}
|
|
|
|
function extractTestCases(content) {
|
|
const testNames = [];
|
|
const lines = content.split("\n");
|
|
|
|
lines.forEach((line) => {
|
|
// Match describe, it, test
|
|
const testMatch = line.match(/(?:describe|it|test)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
if (testMatch) {
|
|
testNames.push(testMatch[1]);
|
|
}
|
|
});
|
|
|
|
return testNames;
|
|
}
|
|
|
|
function generateReport() {
|
|
const ignore = ["node_modules", ".next", "build", "dist", ".git"];
|
|
|
|
// Find all source and test files
|
|
const srcFiles = findFiles("src", /\.(ts|tsx)$/, ignore)
|
|
.concat(findFiles("packages", /\.(ts|tsx)$/, ignore))
|
|
.concat(findFiles("dbal/ts", /\.(ts|tsx)$/, ignore))
|
|
.filter(f => !f.includes(".test.") && !f.includes(".spec."));
|
|
|
|
const testFiles = findFiles(".", /\.(test|spec)\.(ts|tsx)$/, ignore);
|
|
|
|
// Extract functions and tests
|
|
const functionsByFile = new Map();
|
|
const testsByFile = new Map();
|
|
|
|
srcFiles.forEach(file => {
|
|
try {
|
|
const content = fs.readFileSync(file, "utf-8");
|
|
const funcs = extractFunctions(content);
|
|
if (funcs.length > 0) {
|
|
functionsByFile.set(file, funcs);
|
|
}
|
|
} catch (e) {}
|
|
});
|
|
|
|
testFiles.forEach(file => {
|
|
try {
|
|
const content = fs.readFileSync(file, "utf-8");
|
|
const tests = extractTestCases(content);
|
|
if (tests.length > 0) {
|
|
testsByFile.set(file, tests);
|
|
}
|
|
} catch (e) {}
|
|
});
|
|
|
|
// Generate markdown report
|
|
let report = `# Function-to-Test Coverage Report\n\n`;
|
|
report += `Generated: ${new Date().toISOString()}\n\n`;
|
|
|
|
const totalFunctions = Array.from(functionsByFile.values()).reduce((sum, funcs) => sum + funcs.length, 0);
|
|
const totalTests = Array.from(testsByFile.values()).reduce((sum, tests) => sum + tests.length, 0);
|
|
|
|
report += `## Summary\n\n`;
|
|
report += `- **Total Functions**: ${totalFunctions}\n`;
|
|
report += `- **Total Test Cases**: ${totalTests}\n`;
|
|
report += `- **Source Files with Functions**: ${functionsByFile.size}\n`;
|
|
report += `- **Test Files**: ${testFiles.length}\n\n`;
|
|
|
|
report += `## Files with Function Coverage\n\n`;
|
|
|
|
const sortedFiles = Array.from(functionsByFile.entries()).sort((a, b) => {
|
|
const aPath = a[0];
|
|
const bPath = b[0];
|
|
return aPath.localeCompare(bPath);
|
|
});
|
|
|
|
sortedFiles.forEach(([file, funcs]) => {
|
|
const relFile = path.relative(process.cwd(), file);
|
|
const testFile = file.replace(/\.tsx?$/, ".test.ts").replace(/\.tsx?$/, ".test.tsx");
|
|
const hasTest = testFiles.includes(testFile) || testsByFile.has(testFile);
|
|
const status = hasTest ? "✅" : "❌";
|
|
|
|
report += `### ${status} ${relFile}\n\n`;
|
|
report += `**Functions**: ${funcs.length}\n\n`;
|
|
|
|
funcs.forEach(func => {
|
|
report += `- \`${func.name}\` (line ${func.line})\n`;
|
|
});
|
|
|
|
if (hasTest) {
|
|
const tests = testsByFile.get(testFile) || [];
|
|
report += `\n**Test Cases**: ${tests.length}\n\n`;
|
|
}
|
|
|
|
report += `\n`;
|
|
});
|
|
|
|
report += `## Test Files\n\n`;
|
|
|
|
testFiles.forEach(file => {
|
|
const relFile = path.relative(process.cwd(), file);
|
|
const tests = testsByFile.get(file) || [];
|
|
|
|
report += `### ${relFile}\n\n`;
|
|
report += `**Test Cases**: ${tests.length}\n\n`;
|
|
|
|
tests.forEach(test => {
|
|
report += `- ${test}\n`;
|
|
});
|
|
|
|
report += `\n`;
|
|
});
|
|
|
|
report += `## Recommendations\n\n`;
|
|
|
|
const untested = Array.from(functionsByFile.entries()).filter(
|
|
([file]) => !testFiles.some(t => t.endsWith(file.replace(/\.(ts|tsx)$/, ".test.ts")))
|
|
);
|
|
|
|
if (untested.length > 0) {
|
|
report += `### Files Needing Test Coverage\n\n`;
|
|
untested.forEach(([file, funcs]) => {
|
|
const relFile = path.relative(process.cwd(), file);
|
|
report += `- **${relFile}**: ${funcs.length} functions need tests\n`;
|
|
});
|
|
report += `\n`;
|
|
}
|
|
|
|
report += `### Best Practices\n\n`;
|
|
report += `1. **Parameterized Tests**: Use \`it.each()\` for testing multiple similar scenarios\n`;
|
|
report += `2. **Test Organization**: Group related tests in \`describe()\` blocks\n`;
|
|
report += `3. **Clear Descriptions**: Use descriptive test names that explain what is being tested\n`;
|
|
report += `4. **Edge Cases**: Include tests for null, undefined, empty values, and boundary conditions\n`;
|
|
report += `5. **Mocking**: Use \`vi.fn()\` and \`vi.mock()\` for external dependencies\n`;
|
|
report += `6. **Async Testing**: Use \`async/await\` and \`act()\` for async operations\n`;
|
|
report += `7. **Setup/Teardown**: Use \`beforeEach\` and \`afterEach\` for test setup and cleanup\n\n`;
|
|
|
|
return report;
|
|
}
|
|
|
|
// Write report
|
|
const report = generateReport();
|
|
fs.writeFileSync("FUNCTION_TEST_COVERAGE.md", report);
|
|
|
|
console.log("✅ Coverage report generated: FUNCTION_TEST_COVERAGE.md");
|
|
console.log("\nTo run all tests:");
|
|
console.log(" npm test -- --run\n");
|
|
console.log("To run tests in watch mode:");
|
|
console.log(" npm test\n");
|