UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

963 lines (958 loc) 38.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CoverageTracking = void 0; exports.createProjectConfig = createProjectConfig; exports.trackCoverage = trackCoverage; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const child_process_1 = require("child_process"); const fast_glob_1 = __importDefault(require("fast-glob")); class CoverageTracking extends events_1.EventEmitter { constructor(config) { super(); this.results = []; this.history = []; this.config = { thresholds: { statements: 90, branches: 85, functions: 90, lines: 90, overall: 90 }, reports: [ { format: 'json', output: 'coverage.json' }, { format: 'html', output: 'coverage-report.html' } ], ...config }; } async track() { this.emit('tracking:start', { projects: this.config.projects.length }); const startTime = Date.now(); try { // Load historical data if (this.config.history?.enabled) { await this.loadHistory(); } // Run coverage for each project for (const project of this.config.projects) { const result = await this.trackProject(project); this.results.push(result); } // Generate comprehensive report const report = this.generateReport(Date.now() - startTime); // Save reports in specified formats await this.saveReports(report); // Update history if (this.config.history?.enabled) { await this.updateHistory(report); } // Send notifications if (this.config.notifications) { await this.sendNotifications(report); } // Update integrations if (this.config.integrations) { await this.updateIntegrations(report); } // Generate badges if (this.config.badges?.enabled) { await this.generateBadges(report); } this.emit('tracking:complete', report); return report; } catch (error) { this.emit('tracking:error', error); throw error; } } async trackProject(project) { this.emit('project:start', project); const result = { project: project.name, timestamp: new Date(), coverage: this.createEmptyMetrics(), files: [], summary: this.createEmptySummary(), violations: [] }; try { // Run tests with coverage const startTime = Date.now(); const coverageData = await this.runCoverage(project); const duration = Date.now() - startTime; // Parse coverage data result.coverage = this.parseCoverageData(coverageData); result.files = this.parseFileCoverage(coverageData, project); result.summary = this.generateSummary(result, duration); // Calculate deltas if history exists result.deltas = this.calculateDeltas(project.name, result.coverage); // Check thresholds const thresholds = { ...this.config.thresholds, ...(project.thresholds || {}) }; result.violations = this.checkThresholds(result.coverage, thresholds); // Analyze trends result.trends = this.analyzeTrends(project.name, result.coverage); this.emit('project:complete', result); return result; } catch (error) { this.emit('project:error', { project, error }); throw error; } } async runCoverage(project) { const command = project.coverageCommand || this.getDefaultCoverageCommand(project); try { const output = (0, child_process_1.execSync)(command, { cwd: project.path, encoding: 'utf-8', stdio: 'pipe' }); // Look for coverage output files const coverageFiles = await this.findCoverageFiles(project.path); if (coverageFiles.length === 0) { throw new Error('No coverage data found'); } // Read the most recent coverage file const latestFile = coverageFiles[0]; const coverageData = await fs.readJson(latestFile); return coverageData; } catch (error) { throw new Error(`Coverage command failed: ${error.message}`); } } getDefaultCoverageCommand(project) { // Detect package manager and test framework const packageJson = path.join(project.path, 'package.json'); if (fs.existsSync(packageJson)) { const pkg = fs.readJsonSync(packageJson); if (pkg.scripts?.test) { return 'npm run test -- --coverage'; } if (pkg.devDependencies?.vitest || pkg.dependencies?.vitest) { return 'npx vitest run --coverage'; } if (pkg.devDependencies?.jest || pkg.dependencies?.jest) { return 'npx jest --coverage'; } } return 'npm test -- --coverage'; } async findCoverageFiles(projectPath) { const patterns = [ 'coverage/coverage-final.json', 'coverage/lcov.info', 'coverage.json', '.nyc_output/coverage-final.json' ]; const files = []; for (const pattern of patterns) { const matches = await (0, fast_glob_1.default)(pattern, { cwd: projectPath, absolute: true }); files.push(...matches); } // Sort by modification time (newest first) files.sort((a, b) => { const statA = fs.statSync(a); const statB = fs.statSync(b); return statB.mtime.getTime() - statA.mtime.getTime(); }); return files; } parseCoverageData(data) { // Handle different coverage formats (Istanbul, LCOV, etc.) if (data.total) { // Istanbul format return { statements: this.parseMetric(data.total.statements), branches: this.parseMetric(data.total.branches), functions: this.parseMetric(data.total.functions), lines: this.parseMetric(data.total.lines), overall: this.calculateOverall(data.total) }; } // Try to extract from other formats return this.createEmptyMetrics(); } parseMetric(metric) { if (!metric) { return { total: 0, covered: 0, percentage: 0 }; } return { total: metric.total || 0, covered: metric.covered || 0, percentage: metric.pct || 0, uncovered: this.extractUncovered(metric) }; } extractUncovered(metric) { // Extract uncovered items from coverage data const uncovered = []; if (metric.skipped) { for (const item of metric.skipped) { uncovered.push({ file: item.file || 'unknown', line: item.line, reason: 'skipped' }); } } return uncovered; } parseFileCoverage(data, project) { const files = []; if (data.files) { for (const [filePath, fileData] of Object.entries(data.files)) { const coverage = this.parseCoverageData({ total: fileData }); const relativePath = path.relative(project.path, filePath); files.push({ path: relativePath, coverage, size: this.getFileSize(filePath), complexity: this.calculateComplexity(fileData), hotspots: this.identifyHotspots(fileData), status: this.getCoverageStatus(coverage.overall) }); } } return files; } getFileSize(filePath) { try { const stats = fs.statSync(filePath); return stats.size; } catch { return 0; } } calculateComplexity(fileData) { // Simple complexity calculation based on branches and functions const branches = fileData.branches?.total || 0; const functions = fileData.functions?.total || 0; return branches + functions; } identifyHotspots(fileData) { const hotspots = []; // Identify uncovered critical sections if (fileData.statements) { const uncoveredStatements = Object.entries(fileData.s || {}) .filter(([_, hits]) => hits === 0) .length; if (uncoveredStatements > 10) { hotspots.push({ type: 'uncovered', location: 'multiple statements', severity: 'high', description: `${uncoveredStatements} uncovered statements`, suggestion: 'Add tests to cover these statements' }); } } return hotspots; } getCoverageStatus(coverage) { if (coverage >= 90) return 'excellent'; if (coverage >= 75) return 'good'; if (coverage >= 50) return 'fair'; return 'poor'; } generateSummary(result, duration) { return { totalFiles: result.files.length, coveredFiles: result.files.filter(f => f.coverage.overall > 0).length, totalLines: result.coverage.lines.total, coveredLines: result.coverage.lines.covered, testDuration: duration, testCount: 0, // Would be extracted from test results performance: { slowestTests: [], memoryUsage: 0, cpuUsage: 0 } }; } calculateDeltas(projectName, current) { const lastResult = this.getLastResult(projectName); if (!lastResult) { return undefined; } return { statements: current.statements.percentage - lastResult.coverage.statements.percentage, branches: current.branches.percentage - lastResult.coverage.branches.percentage, functions: current.functions.percentage - lastResult.coverage.functions.percentage, lines: current.lines.percentage - lastResult.coverage.lines.percentage, overall: current.overall - lastResult.coverage.overall, files: this.calculateFileDeltas(current, lastResult.coverage) }; } calculateFileDeltas(current, previous) { // Simple implementation - would be more sophisticated in practice return []; } getLastResult(projectName) { // Get last result from history or previous results return this.history.length > 0 ? this.results.find(r => r.project === projectName) : undefined; } checkThresholds(coverage, thresholds) { const violations = []; const checks = [ { metric: 'statements', actual: coverage.statements.percentage, expected: thresholds.statements }, { metric: 'branches', actual: coverage.branches.percentage, expected: thresholds.branches }, { metric: 'functions', actual: coverage.functions.percentage, expected: thresholds.functions }, { metric: 'lines', actual: coverage.lines.percentage, expected: thresholds.lines }, { metric: 'overall', actual: coverage.overall, expected: thresholds.overall } ]; for (const check of checks) { if (check.actual < check.expected) { const difference = check.expected - check.actual; violations.push({ metric: check.metric, actual: check.actual, expected: check.expected, difference, severity: difference > 10 ? 'critical' : difference > 5 ? 'error' : 'warning' }); } } return violations; } analyzeTrends(projectName, coverage) { const trends = []; // Analyze trends based on historical data const projectHistory = this.history.filter(h => h.commit && h.commit.includes(projectName)).slice(-10); if (projectHistory.length >= 3) { const recentCoverage = projectHistory.slice(-3).map(h => h.coverage); const trend = this.calculateTrendDirection(recentCoverage); trends.push({ metric: 'overall', direction: trend.direction, rate: trend.rate, period: 3, projection: trend.projection }); } return trends; } calculateTrendDirection(values) { if (values.length < 2) { return { direction: 'stable', rate: 0, projection: values[0] || 0 }; } const first = values[0]; const last = values[values.length - 1]; const change = last - first; const rate = change / values.length; return { direction: change > 1 ? 'up' : change < -1 ? 'down' : 'stable', rate: Math.abs(rate), projection: last + rate * 5 // Project 5 periods ahead }; } generateReport(duration) { const aggregated = this.aggregateCoverage(); const trends = this.analyzeTrends('all', aggregated); const insights = this.generateInsights(); const recommendations = this.generateRecommendations(); const summary = { totalProjects: this.config.projects.length, overallCoverage: aggregated.overall, trend: this.getOverallTrend(), violations: this.results.reduce((sum, r) => sum + (r.violations?.length || 0), 0), criticalIssues: this.countCriticalIssues(), lastUpdate: new Date(), duration: duration / 1000 }; return { summary, projects: this.results, aggregated, trends: { historical: this.history, predictions: this.generatePredictions(), anomalies: this.detectAnomalies() }, insights, recommendations, badges: [], timestamp: new Date() }; } aggregateCoverage() { if (this.results.length === 0) { return this.createEmptyMetrics(); } const strategy = this.config.aggregation?.strategy || 'weighted'; switch (strategy) { case 'weighted': return this.weightedAggregation(); case 'average': return this.averageAggregation(); case 'minimum': return this.minimumAggregation(); default: return this.averageAggregation(); } } weightedAggregation() { let totalWeight = 0; let weightedStatements = 0; let weightedBranches = 0; let weightedFunctions = 0; let weightedLines = 0; for (const result of this.results) { const project = this.config.projects.find(p => p.name === result.project); const weight = project?.weight || 1; totalWeight += weight; weightedStatements += result.coverage.statements.percentage * weight; weightedBranches += result.coverage.branches.percentage * weight; weightedFunctions += result.coverage.functions.percentage * weight; weightedLines += result.coverage.lines.percentage * weight; } const avgStatements = weightedStatements / totalWeight; const avgBranches = weightedBranches / totalWeight; const avgFunctions = weightedFunctions / totalWeight; const avgLines = weightedLines / totalWeight; return { statements: { total: 0, covered: 0, percentage: avgStatements }, branches: { total: 0, covered: 0, percentage: avgBranches }, functions: { total: 0, covered: 0, percentage: avgFunctions }, lines: { total: 0, covered: 0, percentage: avgLines }, overall: (avgStatements + avgBranches + avgFunctions + avgLines) / 4 }; } averageAggregation() { const count = this.results.length; const avgStatements = this.results.reduce((sum, r) => sum + r.coverage.statements.percentage, 0) / count; const avgBranches = this.results.reduce((sum, r) => sum + r.coverage.branches.percentage, 0) / count; const avgFunctions = this.results.reduce((sum, r) => sum + r.coverage.functions.percentage, 0) / count; const avgLines = this.results.reduce((sum, r) => sum + r.coverage.lines.percentage, 0) / count; return { statements: { total: 0, covered: 0, percentage: avgStatements }, branches: { total: 0, covered: 0, percentage: avgBranches }, functions: { total: 0, covered: 0, percentage: avgFunctions }, lines: { total: 0, covered: 0, percentage: avgLines }, overall: (avgStatements + avgBranches + avgFunctions + avgLines) / 4 }; } minimumAggregation() { const minStatements = Math.min(...this.results.map(r => r.coverage.statements.percentage)); const minBranches = Math.min(...this.results.map(r => r.coverage.branches.percentage)); const minFunctions = Math.min(...this.results.map(r => r.coverage.functions.percentage)); const minLines = Math.min(...this.results.map(r => r.coverage.lines.percentage)); return { statements: { total: 0, covered: 0, percentage: minStatements }, branches: { total: 0, covered: 0, percentage: minBranches }, functions: { total: 0, covered: 0, percentage: minFunctions }, lines: { total: 0, covered: 0, percentage: minLines }, overall: Math.min(minStatements, minBranches, minFunctions, minLines) }; } getOverallTrend() { if (this.history.length < 2) return 'stable'; const recent = this.history.slice(-5); const older = this.history.slice(-10, -5); const recentAvg = recent.reduce((sum, h) => sum + h.coverage, 0) / recent.length; const olderAvg = older.reduce((sum, h) => sum + h.coverage, 0) / older.length; const change = recentAvg - olderAvg; if (change > 1) return 'improving'; if (change < -1) return 'declining'; return 'stable'; } countCriticalIssues() { return this.results.reduce((count, result) => count + (result.violations?.filter(v => v.severity === 'critical').length || 0), 0); } generateInsights() { const insights = []; // Identify significant improvements for (const result of this.results) { if (result.deltas && result.deltas.overall > 5) { insights.push({ type: 'improvement', priority: 'medium', title: `${result.project} coverage improved`, description: `Coverage increased by ${result.deltas.overall.toFixed(1)}%`, actionable: false, impact: 'positive' }); } } // Identify concerning regressions for (const result of this.results) { if (result.deltas && result.deltas.overall < -5) { insights.push({ type: 'regression', priority: 'high', title: `${result.project} coverage declined`, description: `Coverage decreased by ${Math.abs(result.deltas.overall).toFixed(1)}%`, actionable: true, impact: 'negative' }); } } // Identify patterns const lowCoverageProjects = this.results.filter(r => r.coverage.overall < 70); if (lowCoverageProjects.length > 0) { insights.push({ type: 'pattern', priority: 'medium', title: 'Multiple projects with low coverage', description: `${lowCoverageProjects.length} projects have coverage below 70%`, data: lowCoverageProjects.map(p => p.project), actionable: true }); } return insights; } generateRecommendations() { const recommendations = []; // Low coverage recommendations const lowCoverageProjects = this.results.filter(r => r.coverage.overall < 80); if (lowCoverageProjects.length > 0) { recommendations.push(`Improve test coverage for ${lowCoverageProjects.length} projects below 80%`); } // Missing branch coverage const lowBranchCoverage = this.results.filter(r => r.coverage.branches.percentage < 70); if (lowBranchCoverage.length > 0) { recommendations.push('Focus on branch coverage - add tests for conditional logic and edge cases'); } // Uncovered functions const uncoveredFunctions = this.results.reduce((sum, r) => sum + (r.coverage.functions.total - r.coverage.functions.covered), 0); if (uncoveredFunctions > 10) { recommendations.push(`${uncoveredFunctions} functions remain untested - prioritize function coverage`); } // Threshold violations const criticalViolations = this.results.filter(r => r.violations?.some(v => v.severity === 'critical')); if (criticalViolations.length > 0) { recommendations.push('Address critical coverage threshold violations immediately'); } return recommendations; } generatePredictions() { // Simple linear prediction based on trends const predictions = []; if (this.history.length >= 5) { const trend = this.calculateTrendDirection(this.history.slice(-5).map(h => h.coverage)); for (let i = 1; i <= 5; i++) { const futureDate = new Date(); futureDate.setDate(futureDate.getDate() + i * 7); // Weekly predictions predictions.push({ date: futureDate, predictedCoverage: Math.max(0, Math.min(100, trend.projection + trend.rate * i)), confidence: Math.max(0.1, 0.9 - i * 0.1), // Decreasing confidence factors: ['historical trend', 'development velocity'] }); } } return predictions; } detectAnomalies() { const anomalies = []; if (this.history.length >= 10) { const coverageValues = this.history.map(h => h.coverage); const mean = coverageValues.reduce((a, b) => a + b, 0) / coverageValues.length; const stdDev = Math.sqrt(coverageValues.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / coverageValues.length); for (let i = 0; i < this.history.length; i++) { const value = this.history[i].coverage; const zScore = Math.abs(value - mean) / stdDev; if (zScore > 2) { anomalies.push({ date: this.history[i].date, type: value > mean ? 'spike' : 'drop', magnitude: zScore, possibleCause: value > mean ? 'Test addition' : 'Code addition without tests' }); } } } return anomalies; } async loadHistory() { if (this.config.history?.storage === 'file') { const historyPath = this.config.history.path || 'coverage-history.json'; if (await fs.pathExists(historyPath)) { this.history = await fs.readJson(historyPath); } } } async updateHistory(report) { if (!this.config.history?.enabled) return; // Add current results to history const entry = { date: new Date(), coverage: report.aggregated.overall, tests: this.results.reduce((sum, r) => sum + r.summary.testCount, 0), files: this.results.reduce((sum, r) => sum + r.summary.totalFiles, 0), commit: await this.getCurrentCommit(), version: await this.getCurrentVersion() }; this.history.push(entry); // Keep only recent entries const maxEntries = this.config.history.maxEntries || 100; if (this.history.length > maxEntries) { this.history = this.history.slice(-maxEntries); } // Save updated history if (this.config.history.storage === 'file') { const historyPath = this.config.history.path || 'coverage-history.json'; await fs.writeJson(historyPath, this.history, { spaces: 2 }); } } async getCurrentCommit() { try { return (0, child_process_1.execSync)('git rev-parse HEAD', { encoding: 'utf-8' }).trim(); } catch { return undefined; } } async getCurrentVersion() { try { const packageJson = await fs.readJson('package.json'); return packageJson.version; } catch { return undefined; } } async saveReports(report) { for (const reportConfig of this.config.reports) { await this.saveReport(report, reportConfig); } } async saveReport(report, config) { const outputPath = config.output; await fs.ensureDir(path.dirname(outputPath)); switch (config.format) { case 'json': await fs.writeJson(outputPath, report, { spaces: 2 }); break; case 'html': const html = this.generateHtmlReport(report); await fs.writeFile(outputPath, html); break; case 'text': const text = this.generateTextReport(report); await fs.writeFile(outputPath, text); break; default: throw new Error(`Unsupported report format: ${config.format}`); } this.emit('report:saved', { format: config.format, path: outputPath }); } generateHtmlReport(report) { return `<!DOCTYPE html> <html> <head> <title>Coverage Report</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .summary { background: #f5f5f5; padding: 15px; border-radius: 5px; } .metric { display: inline-block; margin: 10px; padding: 10px; background: white; border-radius: 3px; } .excellent { color: #4caf50; } .good { color: #8bc34a; } .fair { color: #ff9800; } .poor { color: #f44336; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; } th { background-color: #f2f2f2; } </style> </head> <body> <h1>Coverage Report</h1> <div class="summary"> <h2>Summary</h2> <div class="metric"> <strong>Overall Coverage:</strong> <span class="${this.getCoverageClass(report.aggregated.overall)}"> ${report.aggregated.overall.toFixed(1)}% </span> </div> <div class="metric"> <strong>Projects:</strong> ${report.summary.totalProjects} </div> <div class="metric"> <strong>Trend:</strong> ${report.summary.trend} </div> <div class="metric"> <strong>Violations:</strong> ${report.summary.violations} </div> </div> <h2>Project Details</h2> <table> <thead> <tr> <th>Project</th> <th>Coverage</th> <th>Statements</th> <th>Branches</th> <th>Functions</th> <th>Lines</th> <th>Status</th> </tr> </thead> <tbody> ${report.projects.map(p => ` <tr> <td>${p.project}</td> <td class="${this.getCoverageClass(p.coverage.overall)}">${p.coverage.overall.toFixed(1)}%</td> <td>${p.coverage.statements.percentage.toFixed(1)}%</td> <td>${p.coverage.branches.percentage.toFixed(1)}%</td> <td>${p.coverage.functions.percentage.toFixed(1)}%</td> <td>${p.coverage.lines.percentage.toFixed(1)}%</td> <td>${p.violations?.length || 0} violations</td> </tr> `).join('')} </tbody> </table> <h2>Recommendations</h2> <ul> ${report.recommendations.map(rec => `<li>${rec}</li>`).join('')} </ul> <footer> <p>Generated on ${report.timestamp.toISOString()}</p> </footer> </body> </html>`; } getCoverageClass(coverage) { if (coverage >= 90) return 'excellent'; if (coverage >= 75) return 'good'; if (coverage >= 50) return 'fair'; return 'poor'; } generateTextReport(report) { const lines = [ 'Coverage Report', '==============', '', `Generated: ${report.timestamp.toISOString()}`, `Overall Coverage: ${report.aggregated.overall.toFixed(1)}%`, `Trend: ${report.summary.trend}`, `Violations: ${report.summary.violations}`, '', 'Project Coverage:', '----------------' ]; for (const project of report.projects) { lines.push(`${project.project}: ${project.coverage.overall.toFixed(1)}% ` + `(S:${project.coverage.statements.percentage.toFixed(1)}% ` + `B:${project.coverage.branches.percentage.toFixed(1)}% ` + `F:${project.coverage.functions.percentage.toFixed(1)}% ` + `L:${project.coverage.lines.percentage.toFixed(1)}%)`); } if (report.recommendations.length > 0) { lines.push('', 'Recommendations:', '---------------'); report.recommendations.forEach(rec => { lines.push(`- ${rec}`); }); } return lines.join('\n'); } async sendNotifications(report) { if (!this.config.notifications) return; // Check for notification triggers const hasViolations = report.summary.violations > 0; const hasImprovements = report.projects.some(p => p.deltas && p.deltas.overall > 2); const hasRegressions = report.projects.some(p => p.deltas && p.deltas.overall < -2); for (const channel of this.config.notifications.channels) { const shouldNotify = (hasViolations && this.config.notifications.thresholdViolations) || (hasImprovements && this.config.notifications.improvements) || (hasRegressions && this.config.notifications.regressions); if (shouldNotify) { await this.sendNotification(channel, report); } } } async sendNotification(channel, report) { // Implementation would depend on the channel type this.emit('notification:sent', { channel: channel.type, report: report.summary }); } async updateIntegrations(report) { if (!this.config.integrations) return; for (const integration of this.config.integrations) { if (integration.enabled) { await this.updateIntegration(integration, report); } } } async updateIntegration(integration, report) { // Implementation would depend on the integration type this.emit('integration:updated', { type: integration.type, coverage: report.aggregated.overall }); } async generateBadges(report) { if (!this.config.badges?.enabled) return; const badges = []; // Coverage badge const coverageBadge = this.createCoverageBadge(report.aggregated.overall); badges.push(coverageBadge); // Trend badge const trendBadge = this.createTrendBadge(report.summary.trend); badges.push(trendBadge); // Save badges for (const badge of badges) { await fs.writeFile(path.join(this.config.badges.path, `${badge.type}-badge.svg`), badge.svg); } report.badges = badges; } createCoverageBadge(coverage) { const color = coverage >= 90 ? 'brightgreen' : coverage >= 75 ? 'green' : coverage >= 50 ? 'yellow' : 'red'; const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="104" height="20"> <linearGradient id="b" x2="0" y2="100%"> <stop offset="0" stop-color="#bbb" stop-opacity=".1"/> <stop offset="1" stop-opacity=".1"/> </linearGradient> <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> <rect width="63" height="20" fill="#555"/> <rect x="63" width="41" height="20" fill="${color}"/> <rect width="104" height="20" fill="url(#b)"/> <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text> <text x="31.5" y="14" fill="#fff">coverage</text> <text x="82.5" y="15" fill="#010101" fill-opacity=".3">${coverage.toFixed(0)}%</text> <text x="82.5" y="14" fill="#fff">${coverage.toFixed(0)}%</text> </g> </svg>`; return { type: 'coverage', url: `https://img.shields.io/badge/coverage-${coverage.toFixed(0)}%25-${color}.svg`, markdown: `![Coverage](https://img.shields.io/badge/coverage-${coverage.toFixed(0)}%25-${color}.svg)`, html: `<img src="https://img.shields.io/badge/coverage-${coverage.toFixed(0)}%25-${color}.svg" alt="Coverage" />`, svg }; } createTrendBadge(trend) { const color = trend === 'improving' ? 'brightgreen' : trend === 'declining' ? 'red' : 'blue'; const icon = trend === 'improving' ? '📈' : trend === 'declining' ? '📉' : '📊'; const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="84" height="20"> <rect width="84" height="20" fill="${color}"/> <text x="42" y="14" fill="#fff" text-anchor="middle" font-family="Arial" font-size="11"> ${icon} ${trend} </text> </svg>`; return { type: 'trend', url: `https://img.shields.io/badge/trend-${trend}-${color}.svg`, markdown: `![Trend](https://img.shields.io/badge/trend-${trend}-${color}.svg)`, html: `<img src="https://img.shields.io/badge/trend-${trend}-${color}.svg" alt="Trend" />`, svg }; } calculateOverall(total) { const metrics = ['statements', 'branches', 'functions', 'lines']; let sum = 0; let count = 0; for (const metric of metrics) { if (total[metric] && total[metric].pct !== undefined) { sum += total[metric].pct; count++; } } return count > 0 ? sum / count : 0; } createEmptyMetrics() { const emptyMetric = { total: 0, covered: 0, percentage: 0 }; return { statements: emptyMetric, branches: emptyMetric, functions: emptyMetric, lines: emptyMetric, overall: 0 }; } createEmptySummary() { return { totalFiles: 0, coveredFiles: 0, totalLines: 0, coveredLines: 0, testDuration: 0, testCount: 0, performance: { slowestTests: [], memoryUsage: 0, cpuUsage: 0 } }; } } exports.CoverageTracking = CoverageTracking; // Export utility functions function createProjectConfig(name, path, options) { return { name, path, type: 'all', ...options }; } async function trackCoverage(config) { const tracker = new CoverageTracking(config); return tracker.track(); }