UNPKG

claude-flow

Version:

Enterprise-grade AI agent orchestration with WASM-powered ReasoningBank memory and AgentDB vector database (always uses latest agentic-flow)

692 lines (586 loc) • 23.7 kB
#!/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 = `![Coverage](${badgeUrl})`; 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(); }