@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
JavaScript
"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: `}%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: ``,
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();
}