import type { CoverageConfig, CoverageReport, PackageCoverage, CoverageMetric, FileCoverage } from './types'; export class CoverageProcessor { constructor(private config: CoverageConfig) {} process(rawCoverage: any, testResults: any[]): CoverageReport { // Use the package information from raw coverage if available const packages = rawCoverage.packages ? this.processPackagesCoverage(rawCoverage.packages) : this.groupByPackage(rawCoverage.files, testResults); const overall = this.calculateOverallCoverage(packages); return { timestamp: new Date().toISOString(), packages, overall, config: this.config, }; } private processPackagesCoverage(packagesData: any): PackageCoverage[] { const packages: PackageCoverage[] = []; for (const [packageName, packageData] of Object.entries(packagesData)) { if (!packageData || typeof packageData !== 'object') continue; const pkg: PackageCoverage = { name: packageName, path: '', // Will be set from files if available lines: this.createMetricFromRaw(packageData.lines), functions: this.createMetricFromRaw(packageData.functions), branches: this.createMetricFromRaw(packageData.branches), statements: this.createMetricFromRaw(packageData.lines), // Often same as lines files: [], }; // Process files if available if (packageData.files && Array.isArray(packageData.files)) { for (const file of packageData.files) { const fileCoverage = this.processFile(file); pkg.files.push(fileCoverage); // Set package path from first file if not set if (!pkg.path && file.path) { pkg.path = this.getPackagePath(file.path); } } } // Only include packages that have files with coverage data if (pkg.files.length > 0) { packages.push(pkg); } } return packages; } private createMetricFromRaw(rawMetric: any): CoverageMetric { if (!rawMetric || typeof rawMetric !== 'object') { return this.createEmptyMetric(); } const total = rawMetric.found || 0; const covered = rawMetric.hit || 0; return { total, covered, skipped: 0, percentage: total > 0 ? (covered / total) * 100 : 100, }; } private groupByPackage(files: any[], testResults: any[]): PackageCoverage[] { const packageMap = new Map(); // Group files by package for (const file of files) { const packageName = this.getPackageFromPath(file.path); if (!packageMap.has(packageName)) { packageMap.set(packageName, { name: packageName, path: this.getPackagePath(file.path), lines: this.createEmptyMetric(), functions: this.createEmptyMetric(), branches: this.createEmptyMetric(), statements: this.createEmptyMetric(), files: [], }); } const pkg = packageMap.get(packageName)!; const fileCoverage = this.processFile(file); pkg.files.push(fileCoverage); this.addMetrics(pkg.lines, fileCoverage.lines); this.addMetrics(pkg.functions, fileCoverage.functions); this.addMetrics(pkg.branches, fileCoverage.branches); this.addMetrics(pkg.statements, fileCoverage.statements); } // Calculate percentages for each package const packages = Array.from(packageMap.values()); for (const pkg of packages) { this.calculatePercentage(pkg.lines); this.calculatePercentage(pkg.functions); this.calculatePercentage(pkg.branches); this.calculatePercentage(pkg.statements); } return packages; } private processFile(file: any): FileCoverage { const lines = this.createMetric(file.lines.found, file.lines.hit); const functions = this.createMetric(file.functions.found, file.functions.hit); const branches = this.createMetric(file.branches.found, file.branches.hit); // Statements often equal lines in simple coverage tools const statements = this.createMetric(file.lines.found, file.lines.hit); return { path: file.path, lines, functions, branches, statements, }; } private createEmptyMetric(): CoverageMetric { return { total: 0, covered: 0, skipped: 0, percentage: 0, }; } private createMetric(total: number, covered: number): CoverageMetric { return { total, covered, skipped: 0, percentage: total > 0 ? (covered / total) * 100 : 100, }; } private addMetrics(target: CoverageMetric, source: CoverageMetric): void { target.total += source.total; target.covered += source.covered; target.skipped += source.skipped; } private calculatePercentage(metric: CoverageMetric): void { metric.percentage = metric.total > 0 ? (metric.covered / metric.total) * 100 : 100; } private calculateOverallCoverage(packages: PackageCoverage[]): CoverageReport['overall'] { const overall = { lines: this.createEmptyMetric(), functions: this.createEmptyMetric(), branches: this.createEmptyMetric(), statements: this.createEmptyMetric(), }; for (const pkg of packages) { this.addMetrics(overall.lines, pkg.lines); this.addMetrics(overall.functions, pkg.functions); this.addMetrics(overall.branches, pkg.branches); this.addMetrics(overall.statements, pkg.statements); } this.calculatePercentage(overall.lines); this.calculatePercentage(overall.functions); this.calculatePercentage(overall.branches); this.calculatePercentage(overall.statements); return overall; } private getPackageFromPath(filePath: string): string { const normalizedPath = filePath.replace(/\\/g, '/'); // Try to extract package name from path const patterns = [ /packages\/([^/]+)\//, /apps\/stock\/([^/]+)\//, /apps\/([^/]+)\//, /libs\/core\/([^/]+)\//, /libs\/data\/([^/]+)\//, /libs\/services\/([^/]+)\//, /libs\/([^/]+)\//, /tools\/([^/]+)\//, /@stock-bot\/([^/]+)\//, ]; for (const pattern of patterns) { const match = normalizedPath.match(pattern); if (match) { return `@stock-bot/${match[1]}`; } } // Default to root return 'root'; } private getPackagePath(filePath: string): string { const normalizedPath = filePath.replace(/\\/g, '/'); // Extract package root path const patterns = [ /(.*\/packages\/[^/]+)\//, /(.*\/apps\/stock\/[^/]+)\//, /(.*\/apps\/[^/]+)\//, /(.*\/libs\/core\/[^/]+)\//, /(.*\/libs\/data\/[^/]+)\//, /(.*\/libs\/services\/[^/]+)\//, /(.*\/libs\/[^/]+)\//, /(.*\/tools\/[^/]+)\//, ]; for (const pattern of patterns) { const match = normalizedPath.match(pattern); if (match) { return match[1]; } } // Default to workspace root return this.config.workspaceRoot || process.cwd(); } checkThresholds(report: CoverageReport): { passed: boolean; failures: Array<{ metric: string; expected: number; actual: number }>; } { const failures: Array<{ metric: string; expected: number; actual: number }> = []; const { thresholds } = this.config; const { overall } = report; if (thresholds.lines !== undefined && overall.lines.percentage < thresholds.lines) { failures.push({ metric: 'lines', expected: thresholds.lines, actual: overall.lines.percentage, }); } if (thresholds.functions !== undefined && overall.functions.percentage < thresholds.functions) { failures.push({ metric: 'functions', expected: thresholds.functions, actual: overall.functions.percentage, }); } if (thresholds.branches !== undefined && overall.branches.percentage < thresholds.branches) { failures.push({ metric: 'branches', expected: thresholds.branches, actual: overall.branches.percentage, }); } if (thresholds.statements !== undefined && overall.statements.percentage < thresholds.statements) { failures.push({ metric: 'statements', expected: thresholds.statements, actual: overall.statements.percentage, }); } return { passed: failures.length === 0, failures, }; } }