mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-11-09 05:35:21 +09:00
- Reorganize v3/ into implementation/ subfolders: - planning/, architecture/, migration/, integration/ - security/, adrs/, research/, swarm-plans/ - Add 15-agent concurrent swarm implementation plans: - SWARM-OVERVIEW.md: Hierarchical mesh topology, 4 phases - AGENT-SPECIFICATIONS.md: 15 agent configs with capabilities - TDD-LONDON-SCHOOL-PLAN.md: Mock-first TDD methodology - BENCHMARK-OPTIMIZATION.md: Performance targets (2.49x-7.47x) - DEPLOYMENT-PLAN.md: Release strategy, CI/CD, rollback - GITHUB-ISSUE-TRACKING.md: Issue workflow with agent replies - Update .claude/settings.json with proper hooks format - Add bash aliases for claude --dangerously-skip-permissions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
692 lines
23 KiB
TypeScript
Executable File
692 lines
23 KiB
TypeScript
Executable File
#!/usr/bin/env -S deno run --allow-all
|
|
/**
|
|
* Advanced coverage analysis and reporting
|
|
*/
|
|
|
|
import { parseArgs } from "https://deno.land/std@0.220.0/cli/parse_args.ts";
|
|
import { exists } from "https://deno.land/std@0.220.0/fs/exists.ts";
|
|
import { ensureDir } from "https://deno.land/std@0.220.0/fs/ensure_dir.ts";
|
|
import { walk } from "https://deno.land/std@0.220.0/fs/walk.ts";
|
|
|
|
interface CoverageData {
|
|
url: string;
|
|
ranges: Array<{
|
|
start: { line: number; col: number };
|
|
end: { line: number; col: number };
|
|
}>;
|
|
}
|
|
|
|
interface FileCoverage {
|
|
path: string;
|
|
totalLines: number;
|
|
coveredLines: number;
|
|
uncoveredLines: number[];
|
|
coverage: number;
|
|
functions: FunctionCoverage[];
|
|
}
|
|
|
|
interface FunctionCoverage {
|
|
name: string;
|
|
line: number;
|
|
covered: boolean;
|
|
}
|
|
|
|
interface CoverageReport {
|
|
timestamp: string;
|
|
summary: {
|
|
totalFiles: number;
|
|
totalLines: number;
|
|
coveredLines: number;
|
|
coverage: number;
|
|
thresholds: {
|
|
statements: number;
|
|
branches: number;
|
|
functions: number;
|
|
lines: number;
|
|
};
|
|
passed: boolean;
|
|
};
|
|
files: FileCoverage[];
|
|
uncoveredFiles: string[];
|
|
}
|
|
|
|
class CoverageAnalyzer {
|
|
private sourceDir: string;
|
|
private coverageDir: string;
|
|
private outputDir: string;
|
|
private thresholds: {
|
|
statements: number;
|
|
branches: number;
|
|
functions: number;
|
|
lines: number;
|
|
};
|
|
|
|
constructor(options: {
|
|
sourceDir: string;
|
|
coverageDir: string;
|
|
outputDir: string;
|
|
thresholds: {
|
|
statements: number;
|
|
branches: number;
|
|
functions: number;
|
|
lines: number;
|
|
};
|
|
}) {
|
|
this.sourceDir = options.sourceDir;
|
|
this.coverageDir = options.coverageDir;
|
|
this.outputDir = options.outputDir;
|
|
this.thresholds = options.thresholds;
|
|
}
|
|
|
|
async generateReport(): Promise<CoverageReport> {
|
|
console.log("📊 Analyzing coverage data...");
|
|
|
|
await ensureDir(this.outputDir);
|
|
|
|
// Load coverage data
|
|
const coverageData = await this.loadCoverageData();
|
|
|
|
// Get all source files
|
|
const sourceFiles = await this.getSourceFiles();
|
|
|
|
// Analyze each file
|
|
const fileAnalysis = await this.analyzeFiles(sourceFiles, coverageData);
|
|
|
|
// Find uncovered files
|
|
const coveredFiles = new Set(coverageData.map(d => this.normalizePath(d.url)));
|
|
const uncoveredFiles = sourceFiles.filter(file => !coveredFiles.has(file));
|
|
|
|
// Calculate summary
|
|
const summary = this.calculateSummary(fileAnalysis, uncoveredFiles.length);
|
|
|
|
const report: CoverageReport = {
|
|
timestamp: new Date().toISOString(),
|
|
summary,
|
|
files: fileAnalysis,
|
|
uncoveredFiles,
|
|
};
|
|
|
|
// Generate various report formats
|
|
await this.generateHTMLReport(report);
|
|
await this.generateJSONReport(report);
|
|
await this.generateTextReport(report);
|
|
await this.generateBadges(report);
|
|
|
|
return report;
|
|
}
|
|
|
|
private async loadCoverageData(): Promise<CoverageData[]> {
|
|
const coverageFiles: CoverageData[] = [];
|
|
|
|
if (!await exists(this.coverageDir)) {
|
|
throw new Error(`Coverage directory not found: ${this.coverageDir}`);
|
|
}
|
|
|
|
for await (const entry of walk(this.coverageDir, {
|
|
exts: [".json"],
|
|
includeDirs: false
|
|
})) {
|
|
const content = await Deno.readTextFile(entry.path);
|
|
const data = JSON.parse(content);
|
|
|
|
if (data.url && data.ranges) {
|
|
coverageFiles.push(data);
|
|
}
|
|
}
|
|
|
|
return coverageFiles;
|
|
}
|
|
|
|
private async getSourceFiles(): Promise<string[]> {
|
|
const files: string[] = [];
|
|
|
|
for await (const entry of walk(this.sourceDir, {
|
|
exts: [".ts"],
|
|
includeDirs: false,
|
|
skip: [/\.test\.ts$/, /test\.ts$/, /tests?\//]
|
|
})) {
|
|
// Normalize path
|
|
const relativePath = entry.path.replace(Deno.cwd() + "/", "");
|
|
files.push(relativePath);
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
private normalizePath(url: string): string {
|
|
// Convert file:// URLs to relative paths
|
|
if (url.startsWith("file://")) {
|
|
url = url.replace("file://", "");
|
|
}
|
|
|
|
// Remove leading slash and make relative to cwd
|
|
const cwd = Deno.cwd();
|
|
if (url.startsWith(cwd)) {
|
|
url = url.replace(cwd + "/", "");
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
private async analyzeFiles(sourceFiles: string[], coverageData: CoverageData[]): Promise<FileCoverage[]> {
|
|
const analysis: FileCoverage[] = [];
|
|
|
|
for (const file of sourceFiles) {
|
|
const coverage = coverageData.find(d => this.normalizePath(d.url) === file);
|
|
const fileAnalysis = await this.analyzeFile(file, coverage);
|
|
analysis.push(fileAnalysis);
|
|
}
|
|
|
|
return analysis;
|
|
}
|
|
|
|
private async analyzeFile(filePath: string, coverage?: CoverageData): Promise<FileCoverage> {
|
|
const content = await Deno.readTextFile(filePath);
|
|
const lines = content.split('\n');
|
|
const totalLines = lines.filter(line => line.trim() && !line.trim().startsWith('//')).length;
|
|
|
|
if (!coverage) {
|
|
return {
|
|
path: filePath,
|
|
totalLines,
|
|
coveredLines: 0,
|
|
uncoveredLines: Array.from({ length: totalLines }, (_, i) => i + 1),
|
|
coverage: 0,
|
|
functions: this.extractFunctions(content, false),
|
|
};
|
|
}
|
|
|
|
// Analyze covered lines
|
|
const coveredLines = new Set<number>();
|
|
|
|
for (const range of coverage.ranges) {
|
|
for (let line = range.start.line; line <= range.end.line; line++) {
|
|
coveredLines.add(line);
|
|
}
|
|
}
|
|
|
|
const uncoveredLines: number[] = [];
|
|
for (let i = 1; i <= lines.length; i++) {
|
|
const line = lines[i - 1].trim();
|
|
if (line && !line.startsWith('//') && !coveredLines.has(i)) {
|
|
uncoveredLines.push(i);
|
|
}
|
|
}
|
|
|
|
const coveragePercent = totalLines > 0 ? ((totalLines - uncoveredLines.length) / totalLines) * 100 : 100;
|
|
|
|
return {
|
|
path: filePath,
|
|
totalLines,
|
|
coveredLines: totalLines - uncoveredLines.length,
|
|
uncoveredLines,
|
|
coverage: coveragePercent,
|
|
functions: this.extractFunctions(content, true, coveredLines),
|
|
};
|
|
}
|
|
|
|
private extractFunctions(content: string, checkCoverage: boolean, coveredLines?: Set<number>): FunctionCoverage[] {
|
|
const functions: FunctionCoverage[] = [];
|
|
const lines = content.split('\n');
|
|
|
|
// Simple regex patterns for TypeScript functions
|
|
const patterns = [
|
|
/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
|
|
/^\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(/,
|
|
/^\s*(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\(/,
|
|
/^\s*(\w+)\s*:\s*\([^)]*\)\s*=>/,
|
|
];
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
|
|
for (const pattern of patterns) {
|
|
const match = line.match(pattern);
|
|
if (match) {
|
|
const functionName = match[1];
|
|
const lineNumber = i + 1;
|
|
const covered = checkCoverage ? (coveredLines?.has(lineNumber) ?? false) : false;
|
|
|
|
functions.push({
|
|
name: functionName,
|
|
line: lineNumber,
|
|
covered,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return functions;
|
|
}
|
|
|
|
private calculateSummary(files: FileCoverage[], uncoveredFileCount: number): CoverageReport['summary'] {
|
|
const totalFiles = files.length + uncoveredFileCount;
|
|
const totalLines = files.reduce((sum, file) => sum + file.totalLines, 0);
|
|
const coveredLines = files.reduce((sum, file) => sum + file.coveredLines, 0);
|
|
const coverage = totalLines > 0 ? (coveredLines / totalLines) * 100 : 100;
|
|
|
|
const passed = coverage >= this.thresholds.lines;
|
|
|
|
return {
|
|
totalFiles,
|
|
totalLines,
|
|
coveredLines,
|
|
coverage,
|
|
thresholds: this.thresholds,
|
|
passed,
|
|
};
|
|
}
|
|
|
|
private async generateHTMLReport(report: CoverageReport): Promise<void> {
|
|
const fileRows = report.files
|
|
.sort((a, b) => a.coverage - b.coverage)
|
|
.map(file => {
|
|
const coverageClass = file.coverage >= 80 ? 'high' : file.coverage >= 60 ? 'medium' : 'low';
|
|
const coverageBar = `<div class="coverage-bar"><div class="coverage-fill ${coverageClass}" style="width: ${file.coverage}%"></div></div>`;
|
|
|
|
return `
|
|
<tr class="${coverageClass}">
|
|
<td><a href="#file-${file.path.replace(/[^a-zA-Z0-9]/g, '-')}">${file.path}</a></td>
|
|
<td>${file.coverage.toFixed(2)}%</td>
|
|
<td>${coverageBar}</td>
|
|
<td>${file.coveredLines}/${file.totalLines}</td>
|
|
<td>${file.uncoveredLines.length}</td>
|
|
<td>${file.functions.filter(f => f.covered).length}/${file.functions.length}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
const uncoveredRows = report.uncoveredFiles.map(file => `
|
|
<tr class="uncovered">
|
|
<td>${file}</td>
|
|
<td>0%</td>
|
|
<td><div class="coverage-bar"><div class="coverage-fill low" style="width: 0%"></div></div></td>
|
|
<td>0/0</td>
|
|
<td>-</td>
|
|
<td>0/0</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
const fileDetails = report.files.map(file => {
|
|
const uncoveredLinesStr = file.uncoveredLines.length > 0
|
|
? file.uncoveredLines.slice(0, 20).join(', ') + (file.uncoveredLines.length > 20 ? '...' : '')
|
|
: 'All lines covered';
|
|
|
|
const functionRows = file.functions.map(func => `
|
|
<tr class="${func.covered ? 'covered' : 'uncovered'}">
|
|
<td>${func.name}</td>
|
|
<td>${func.line}</td>
|
|
<td>${func.covered ? '✅' : '❌'}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
return `
|
|
<div class="file-detail" id="file-${file.path.replace(/[^a-zA-Z0-9]/g, '-')}">
|
|
<h3>📄 ${file.path}</h3>
|
|
<div class="file-summary">
|
|
<div class="metric">
|
|
<span class="label">Coverage:</span>
|
|
<span class="value ${file.coverage >= 80 ? 'high' : file.coverage >= 60 ? 'medium' : 'low'}">${file.coverage.toFixed(2)}%</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="label">Lines:</span>
|
|
<span class="value">${file.coveredLines}/${file.totalLines}</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="label">Functions:</span>
|
|
<span class="value">${file.functions.filter(f => f.covered).length}/${file.functions.length}</span>
|
|
</div>
|
|
</div>
|
|
|
|
${file.uncoveredLines.length > 0 ? `
|
|
<div class="uncovered-lines">
|
|
<h4>Uncovered Lines:</h4>
|
|
<p>${uncoveredLinesStr}</p>
|
|
</div>
|
|
` : ''}
|
|
|
|
${file.functions.length > 0 ? `
|
|
<div class="functions">
|
|
<h4>Functions:</h4>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Function</th>
|
|
<th>Line</th>
|
|
<th>Covered</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${functionRows}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
` : ''}
|
|
</div>`;
|
|
}).join('');
|
|
|
|
const html = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Claude-Flow Coverage Report</title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8f9fa; }
|
|
.header { background: white; padding: 30px; border-radius: 8px; margin-bottom: 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.header h1 { margin: 0; color: #2c3e50; }
|
|
.header .timestamp { color: #7f8c8d; margin-top: 5px; }
|
|
|
|
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
.summary-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }
|
|
.summary-card h3 { margin: 0 0 10px 0; color: #2c3e50; font-size: 14px; text-transform: uppercase; }
|
|
.summary-card .value { font-size: 32px; font-weight: bold; margin: 10px 0; }
|
|
.summary-card .value.high { color: #27ae60; }
|
|
.summary-card .value.medium { color: #f39c12; }
|
|
.summary-card .value.low { color: #e74c3c; }
|
|
.summary-card .threshold { font-size: 12px; color: #7f8c8d; }
|
|
|
|
.coverage-status { padding: 15px; border-radius: 8px; margin-bottom: 30px; text-align: center; font-weight: bold; }
|
|
.coverage-status.passed { background: #d4edda; color: #155724; }
|
|
.coverage-status.failed { background: #f8d7da; color: #721c24; }
|
|
|
|
.files-table { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 30px; }
|
|
.files-table h2 { margin: 0; padding: 20px; background: #2c3e50; color: white; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1; }
|
|
th { background: #f8f9fa; font-weight: 600; }
|
|
|
|
.coverage-bar { width: 100px; height: 20px; background: #ecf0f1; border-radius: 10px; overflow: hidden; }
|
|
.coverage-fill { height: 100%; transition: width 0.3s ease; }
|
|
.coverage-fill.high { background: linear-gradient(90deg, #27ae60, #2ecc71); }
|
|
.coverage-fill.medium { background: linear-gradient(90deg, #f39c12, #e67e22); }
|
|
.coverage-fill.low { background: linear-gradient(90deg, #e74c3c, #c0392b); }
|
|
|
|
.high { background-color: rgba(39, 174, 96, 0.1); }
|
|
.medium { background-color: rgba(243, 156, 18, 0.1); }
|
|
.low { background-color: rgba(231, 76, 60, 0.1); }
|
|
.uncovered { background-color: rgba(231, 76, 60, 0.2); }
|
|
|
|
.file-detail { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.file-detail h3 { margin: 0 0 15px 0; color: #2c3e50; }
|
|
.file-summary { display: flex; gap: 20px; margin-bottom: 20px; }
|
|
.metric { display: flex; flex-direction: column; }
|
|
.metric .label { font-size: 12px; color: #7f8c8d; text-transform: uppercase; }
|
|
.metric .value { font-size: 18px; font-weight: bold; margin-top: 5px; }
|
|
|
|
.uncovered-lines { margin: 15px 0; }
|
|
.uncovered-lines h4 { margin: 0 0 10px 0; color: #e74c3c; }
|
|
.functions { margin: 15px 0; }
|
|
.functions h4 { margin: 0 0 10px 0; color: #2c3e50; }
|
|
.functions table { margin-top: 10px; }
|
|
.covered { background-color: rgba(39, 174, 96, 0.1); }
|
|
|
|
.footer { text-align: center; margin-top: 40px; color: #7f8c8d; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>📊 Claude-Flow Coverage Report</h1>
|
|
<div class="timestamp">Generated: ${report.timestamp}</div>
|
|
</div>
|
|
|
|
<div class="coverage-status ${report.summary.passed ? 'passed' : 'failed'}">
|
|
${report.summary.passed ? '✅ Coverage thresholds passed' : '❌ Coverage thresholds not met'}
|
|
</div>
|
|
|
|
<div class="summary">
|
|
<div class="summary-card">
|
|
<h3>Overall Coverage</h3>
|
|
<div class="value ${report.summary.coverage >= 80 ? 'high' : report.summary.coverage >= 60 ? 'medium' : 'low'}">${report.summary.coverage.toFixed(2)}%</div>
|
|
<div class="threshold">Threshold: ${this.thresholds.lines}%</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<h3>Total Files</h3>
|
|
<div class="value">${report.summary.totalFiles}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<h3>Covered Lines</h3>
|
|
<div class="value">${report.summary.coveredLines}</div>
|
|
<div class="threshold">of ${report.summary.totalLines}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<h3>Uncovered Files</h3>
|
|
<div class="value ${report.uncoveredFiles.length === 0 ? 'high' : 'low'}">${report.uncoveredFiles.length}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="files-table">
|
|
<h2>📁 File Coverage</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>File</th>
|
|
<th>Coverage</th>
|
|
<th>Progress</th>
|
|
<th>Lines</th>
|
|
<th>Uncovered</th>
|
|
<th>Functions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${fileRows}
|
|
${uncoveredRows}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="file-details">
|
|
<h2>📄 File Details</h2>
|
|
${fileDetails}
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p>Claude-Flow Coverage Report - Advanced AI Agent Orchestration System</p>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
|
|
await Deno.writeTextFile(`${this.outputDir}/coverage-detailed.html`, html);
|
|
console.log(" ✅ Detailed HTML coverage report generated");
|
|
}
|
|
|
|
private async generateJSONReport(report: CoverageReport): Promise<void> {
|
|
await Deno.writeTextFile(
|
|
`${this.outputDir}/coverage-report.json`,
|
|
JSON.stringify(report, null, 2)
|
|
);
|
|
console.log(" ✅ JSON coverage report generated");
|
|
}
|
|
|
|
private async generateTextReport(report: CoverageReport): Promise<void> {
|
|
const lines: string[] = [];
|
|
|
|
lines.push("📊 CLAUDE-FLOW COVERAGE REPORT");
|
|
lines.push("=".repeat(50));
|
|
lines.push(`Generated: ${report.timestamp}`);
|
|
lines.push("");
|
|
|
|
lines.push("📈 SUMMARY");
|
|
lines.push("-".repeat(20));
|
|
lines.push(`Overall Coverage: ${report.summary.coverage.toFixed(2)}%`);
|
|
lines.push(`Total Files: ${report.summary.totalFiles}`);
|
|
lines.push(`Covered Lines: ${report.summary.coveredLines}/${report.summary.totalLines}`);
|
|
lines.push(`Uncovered Files: ${report.uncoveredFiles.length}`);
|
|
lines.push("");
|
|
|
|
lines.push("🎯 THRESHOLDS");
|
|
lines.push("-".repeat(20));
|
|
lines.push(`Lines: ${report.summary.coverage.toFixed(2)}% (threshold: ${report.summary.thresholds.lines}%)`);
|
|
lines.push(`Status: ${report.summary.passed ? '✅ PASSED' : '❌ FAILED'}`);
|
|
lines.push("");
|
|
|
|
if (report.files.length > 0) {
|
|
lines.push("📁 FILES BY COVERAGE");
|
|
lines.push("-".repeat(20));
|
|
|
|
const sortedFiles = [...report.files].sort((a, b) => a.coverage - b.coverage);
|
|
|
|
for (const file of sortedFiles) {
|
|
const status = file.coverage >= 80 ? '✅' : file.coverage >= 60 ? '⚠️' : '❌';
|
|
lines.push(`${status} ${file.path}: ${file.coverage.toFixed(2)}% (${file.coveredLines}/${file.totalLines} lines)`);
|
|
|
|
if (file.uncoveredLines.length > 0 && file.uncoveredLines.length <= 10) {
|
|
lines.push(` Uncovered lines: ${file.uncoveredLines.join(', ')}`);
|
|
} else if (file.uncoveredLines.length > 10) {
|
|
lines.push(` Uncovered lines: ${file.uncoveredLines.slice(0, 10).join(', ')}... (+${file.uncoveredLines.length - 10} more)`);
|
|
}
|
|
}
|
|
lines.push("");
|
|
}
|
|
|
|
if (report.uncoveredFiles.length > 0) {
|
|
lines.push("🚫 UNCOVERED FILES");
|
|
lines.push("-".repeat(20));
|
|
for (const file of report.uncoveredFiles) {
|
|
lines.push(`❌ ${file}`);
|
|
}
|
|
lines.push("");
|
|
}
|
|
|
|
lines.push("💡 RECOMMENDATIONS");
|
|
lines.push("-".repeat(20));
|
|
|
|
const lowCoverageFiles = report.files.filter(f => f.coverage < 60);
|
|
if (lowCoverageFiles.length > 0) {
|
|
lines.push("• Focus on improving coverage for these files:");
|
|
for (const file of lowCoverageFiles.slice(0, 5)) {
|
|
lines.push(` - ${file.path} (${file.coverage.toFixed(2)}%)`);
|
|
}
|
|
}
|
|
|
|
if (report.uncoveredFiles.length > 0) {
|
|
lines.push("• Add tests for uncovered files:");
|
|
for (const file of report.uncoveredFiles.slice(0, 5)) {
|
|
lines.push(` - ${file}`);
|
|
}
|
|
}
|
|
|
|
if (report.summary.coverage >= 90) {
|
|
lines.push("• Excellent coverage! Consider adding edge case tests.");
|
|
} else if (report.summary.coverage >= 80) {
|
|
lines.push("• Good coverage! Focus on critical paths and error handling.");
|
|
} else {
|
|
lines.push("• Coverage needs improvement. Add tests for core functionality first.");
|
|
}
|
|
|
|
await Deno.writeTextFile(`${this.outputDir}/coverage-report.txt`, lines.join('\n'));
|
|
console.log(" ✅ Text coverage report generated");
|
|
}
|
|
|
|
private async generateBadges(report: CoverageReport): Promise<void> {
|
|
const coverage = report.summary.coverage;
|
|
const color = coverage >= 90 ? 'brightgreen' :
|
|
coverage >= 80 ? 'green' :
|
|
coverage >= 70 ? 'yellow' :
|
|
coverage >= 60 ? 'orange' : 'red';
|
|
|
|
const badgeUrl = `https://img.shields.io/badge/coverage-${coverage.toFixed(1)}%25-${color}`;
|
|
|
|
const badgeMarkdown = ``;
|
|
const badgeHTML = `<img src="${badgeUrl}" alt="Coverage ${coverage.toFixed(1)}%">`;
|
|
|
|
const badges = {
|
|
url: badgeUrl,
|
|
markdown: badgeMarkdown,
|
|
html: badgeHTML,
|
|
coverage: coverage.toFixed(1),
|
|
color,
|
|
status: report.summary.passed ? 'passed' : 'failed',
|
|
};
|
|
|
|
await Deno.writeTextFile(
|
|
`${this.outputDir}/coverage-badges.json`,
|
|
JSON.stringify(badges, null, 2)
|
|
);
|
|
|
|
console.log(" ✅ Coverage badges generated");
|
|
}
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const args = parseArgs(Deno.args, {
|
|
string: ["source-dir", "coverage-dir", "output-dir"],
|
|
boolean: ["help"],
|
|
default: {
|
|
"source-dir": "./src",
|
|
"coverage-dir": "./tests/results/coverage",
|
|
"output-dir": "./tests/results",
|
|
},
|
|
alias: {
|
|
h: "help",
|
|
s: "source-dir",
|
|
c: "coverage-dir",
|
|
o: "output-dir",
|
|
},
|
|
});
|
|
|
|
if (args.help) {
|
|
console.log(`
|
|
📊 Claude-Flow Coverage Analysis Tool
|
|
|
|
USAGE:
|
|
coverage-report.ts [OPTIONS]
|
|
|
|
OPTIONS:
|
|
--source-dir, -s Source code directory (default: ./src)
|
|
--coverage-dir, -c Coverage data directory (default: ./tests/results/coverage)
|
|
--output-dir, -o Output directory for reports (default: ./tests/results)
|
|
--help, -h Show this help
|
|
|
|
EXAMPLES:
|
|
# Generate coverage report with defaults
|
|
./scripts/coverage-report.ts
|
|
|
|
# Custom directories
|
|
./scripts/coverage-report.ts --source-dir ./lib --coverage-dir ./coverage --output-dir ./reports
|
|
`);
|
|
return;
|
|
}
|
|
|
|
const thresholds = {
|
|
statements: 80,
|
|
branches: 75,
|
|
functions: 80,
|
|
lines: 80,
|
|
};
|
|
|
|
const analyzer = new CoverageAnalyzer({
|
|
sourceDir: args["source-dir"],
|
|
coverageDir: args["coverage-dir"],
|
|
outputDir: args["output-dir"],
|
|
thresholds,
|
|
});
|
|
|
|
try {
|
|
const report = await analyzer.generateReport();
|
|
|
|
console.log("\n📊 COVERAGE SUMMARY");
|
|
console.log("=".repeat(40));
|
|
console.log(`Overall Coverage: ${report.summary.coverage.toFixed(2)}%`);
|
|
console.log(`Total Files: ${report.summary.totalFiles}`);
|
|
console.log(`Covered Lines: ${report.summary.coveredLines}/${report.summary.totalLines}`);
|
|
console.log(`Uncovered Files: ${report.uncoveredFiles.length}`);
|
|
|
|
const status = report.summary.passed ? "✅ PASSED" : "❌ FAILED";
|
|
console.log(`Status: ${status}`);
|
|
|
|
console.log(`\n📄 Reports generated in: ${args["output-dir"]}`);
|
|
console.log(` - Detailed HTML: coverage-detailed.html`);
|
|
console.log(` - JSON Data: coverage-report.json`);
|
|
console.log(` - Text Summary: coverage-report.txt`);
|
|
console.log(` - Badges: coverage-badges.json`);
|
|
|
|
Deno.exit(report.summary.passed ? 0 : 1);
|
|
|
|
} catch (error) {
|
|
console.error("❌ Coverage analysis failed:", error.message);
|
|
Deno.exit(1);
|
|
}
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
main();
|
|
} |