stock-bot/tools/coverage-cli/src/processor.ts

288 lines
No EOL
8.6 KiB
TypeScript

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<string, PackageCoverage>();
// 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,
};
}
}