Skip to main content
Glama
code-coverage.ts7.58 kB
/** * Code Coverage Tracker * Tracks which lines were executed during debugging. */ import { logger } from '../utils/logger.js'; export interface FileCoverage { file: string; executedLines: Set<number>; totalLines?: number; coveragePercent?: number; firstHitAt: Date; lastHitAt: Date; hitCounts: Map<number, number>; } export interface CoverageReport { startedAt: Date; endedAt?: Date; files: Map<string, FileCoverage>; totalFiles: number; totalLinesExecuted: number; uniqueLinesExecuted: number; } export class CodeCoverageTracker { private currentReport: CoverageReport | null = null; private isTracking: boolean = false; /** * Start tracking code coverage */ startTracking(): CoverageReport { this.currentReport = { startedAt: new Date(), files: new Map(), totalFiles: 0, totalLinesExecuted: 0, uniqueLinesExecuted: 0, }; this.isTracking = true; logger.info('Code coverage tracking started'); return this.currentReport; } /** * Stop tracking code coverage */ stopTracking(): CoverageReport | null { if (!this.currentReport) return null; this.currentReport.endedAt = new Date(); this.isTracking = false; // Calculate final statistics this.updateStatistics(); logger.info('Code coverage tracking stopped'); return this.currentReport; } /** * Record a line execution */ recordLineExecution(file: string, line: number): void { if (!this.currentReport || !this.isTracking) return; let fileCoverage = this.currentReport.files.get(file); if (!fileCoverage) { fileCoverage = { file, executedLines: new Set(), firstHitAt: new Date(), lastHitAt: new Date(), hitCounts: new Map(), }; this.currentReport.files.set(file, fileCoverage); this.currentReport.totalFiles++; } const wasNew = !fileCoverage.executedLines.has(line); fileCoverage.executedLines.add(line); fileCoverage.lastHitAt = new Date(); // Update hit count const currentCount = fileCoverage.hitCounts.get(line) || 0; fileCoverage.hitCounts.set(line, currentCount + 1); this.currentReport.totalLinesExecuted++; if (wasNew) { this.currentReport.uniqueLinesExecuted++; } } /** * Update statistics for all files */ private updateStatistics(): void { if (!this.currentReport) return; for (const coverage of this.currentReport.files.values()) { if (coverage.totalLines && coverage.totalLines > 0) { coverage.coveragePercent = (coverage.executedLines.size / coverage.totalLines) * 100; } } } /** * Set total lines for a file (for percentage calculation) */ setFileTotalLines(file: string, totalLines: number): void { if (!this.currentReport) return; const coverage = this.currentReport.files.get(file); if (coverage) { coverage.totalLines = totalLines; coverage.coveragePercent = (coverage.executedLines.size / totalLines) * 100; } } /** * Get coverage for a specific file */ getFileCoverage(file: string): FileCoverage | undefined { return this.currentReport?.files.get(file); } /** * Get all file coverages */ getAllFileCoverages(): FileCoverage[] { if (!this.currentReport) return []; return Array.from(this.currentReport.files.values()); } /** * Get uncovered lines for a file */ getUncoveredLines(file: string, totalLines: number): number[] { const coverage = this.currentReport?.files.get(file); if (!coverage) { return Array.from({ length: totalLines }, (_, i) => i + 1); } const uncovered: number[] = []; for (let i = 1; i <= totalLines; i++) { if (!coverage.executedLines.has(i)) { uncovered.push(i); } } return uncovered; } /** * Get hot spots (most executed lines) */ getHotSpots(limit: number = 10): Array<{ file: string; line: number; hitCount: number }> { if (!this.currentReport) return []; const hotSpots: Array<{ file: string; line: number; hitCount: number }> = []; for (const [file, coverage] of this.currentReport.files) { for (const [line, hitCount] of coverage.hitCounts) { hotSpots.push({ file, line, hitCount }); } } return hotSpots .sort((a, b) => b.hitCount - a.hitCount) .slice(0, limit); } /** * Generate coverage summary */ getSummary(): { isTracking: boolean; duration?: number; totalFiles: number; totalLinesExecuted: number; uniqueLinesExecuted: number; filesSummary: Array<{ file: string; executedLines: number; totalLines?: number; coveragePercent?: number; }>; } { if (!this.currentReport) { return { isTracking: false, totalFiles: 0, totalLinesExecuted: 0, uniqueLinesExecuted: 0, filesSummary: [], }; } const duration = this.currentReport.endedAt ? this.currentReport.endedAt.getTime() - this.currentReport.startedAt.getTime() : Date.now() - this.currentReport.startedAt.getTime(); const filesSummary = Array.from(this.currentReport.files.values()).map((f) => ({ file: f.file, executedLines: f.executedLines.size, totalLines: f.totalLines, coveragePercent: f.coveragePercent, })); return { isTracking: this.isTracking, duration, totalFiles: this.currentReport.totalFiles, totalLinesExecuted: this.currentReport.totalLinesExecuted, uniqueLinesExecuted: this.currentReport.uniqueLinesExecuted, filesSummary, }; } /** * Generate text report */ generateReport(): string { const summary = this.getSummary(); const lines: string[] = []; lines.push('=== Code Coverage Report ==='); lines.push(`Status: ${summary.isTracking ? 'Active' : 'Stopped'}`); if (summary.duration) { lines.push(`Duration: ${(summary.duration / 1000).toFixed(2)}s`); } lines.push(`Total files: ${summary.totalFiles}`); lines.push(`Unique lines executed: ${summary.uniqueLinesExecuted}`); lines.push(`Total line executions: ${summary.totalLinesExecuted}`); lines.push(''); lines.push('--- Per-File Coverage ---'); for (const file of summary.filesSummary) { let coverageStr = `${file.executedLines} lines`; if (file.totalLines) { coverageStr += ` / ${file.totalLines} (${file.coveragePercent?.toFixed(1)}%)`; } lines.push(` ${file.file}: ${coverageStr}`); } return lines.join('\n'); } /** * Reset coverage data */ reset(): void { this.currentReport = null; this.isTracking = false; } /** * Export coverage data as JSON */ exportData(): object | null { if (!this.currentReport) return null; return { startedAt: this.currentReport.startedAt.toISOString(), endedAt: this.currentReport.endedAt?.toISOString(), totalFiles: this.currentReport.totalFiles, totalLinesExecuted: this.currentReport.totalLinesExecuted, uniqueLinesExecuted: this.currentReport.uniqueLinesExecuted, files: Array.from(this.currentReport.files.entries()).map(([file, coverage]) => ({ file, executedLines: Array.from(coverage.executedLines), totalLines: coverage.totalLines, coveragePercent: coverage.coveragePercent, hitCounts: Object.fromEntries(coverage.hitCounts), })), }; } get tracking(): boolean { return this.isTracking; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kpanuragh/xdebug-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server