annator-command-center / tests /scripts /aggregate_coverage.py
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 9)
81e3673 verified
Raw
History Blame Contribute Delete
26.1 kB
#!/usr/bin/env python3
"""
Unified Coverage Aggregation Script
Combines pytest JSON coverage (backend), Jest JSON coverage (frontend),
jest-expo JSON coverage (mobile), and cargo-tarpaulin JSON coverage (desktop)
into a single unified report with per-platform breakdown.
Usage:
python backend/tests/scripts/aggregate_coverage.py [OPTIONS]
Examples:
python backend/tests/scripts/aggregate_coverage.py --format text
python backend/tests/scripts/aggregate_coverage.py --format json
python backend/tests/scripts/aggregate_coverage.py --format markdown
python backend/tests/scripts/aggregate_coverage.py --output unified_coverage.json
"""
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, Any, Optional
def load_pytest_coverage(path: Path) -> Dict[str, Any]:
"""
Load pytest coverage.json from backend tests.
Args:
path: Path to coverage.json (pytest format)
Returns:
Dict with platform='python', coverage_pct, covered, total metrics
Returns coverage_pct=0.0 and error message if file not found
"""
result = {
"platform": "python",
"coverage_pct": 0.0,
"covered": 0,
"total": 0,
"branches_covered": 0,
"branches_total": 0,
"branch_coverage_pct": 0.0,
"file": str(path),
}
if not path.exists():
result["error"] = "file not found"
return result
try:
with open(path, 'r') as f:
coverage_data = json.load(f)
# Extract totals from pytest coverage.json
totals = coverage_data.get("totals", {})
result["coverage_pct"] = totals.get("percent_covered", 0.0)
result["covered"] = totals.get("covered_lines", 0)
result["total"] = totals.get("num_statements", 0)
result["branches_covered"] = totals.get("covered_branches", 0)
result["branches_total"] = totals.get("num_branches", 0)
# Calculate branch coverage percentage
if result["branches_total"] > 0:
result["branch_coverage_pct"] = (
result["branches_covered"] / result["branches_total"] * 100
)
else:
result["branch_coverage_pct"] = 0.0
except (json.JSONDecodeError, IOError) as e:
result["error"] = str(e)
return result
def load_jest_coverage(path: Path) -> Dict[str, Any]:
"""
Load Jest coverage-final.json from frontend tests.
Args:
path: Path to coverage-final.json (Jest format)
Returns:
Dict with platform='javascript', coverage_pct, covered, total metrics
Returns coverage_pct=0.0 and error message if file not found
"""
result = {
"platform": "javascript",
"coverage_pct": 0.0,
"covered": 0,
"total": 0,
"branches_covered": 0,
"branches_total": 0,
"branch_coverage_pct": 0.0,
"file": str(path),
}
if not path.exists():
result["error"] = "file not found"
return result
try:
with open(path, 'r') as f:
coverage_data = json.load(f)
# Jest coverage-final.json format:
# {
# "/path/to/file.ts": {
# "s": { "1": 10, "2": 5 }, # statement counts
# "b": { "1": [10, 5], "2": [8, 2] }, # branch counts [taken, not taken]
# "f": { "1": 10 }, # function counts
# "l": { "1": 10 } # line counts
# }
# }
total_statements = 0
covered_statements = 0
total_branches = 0
covered_branches = 0
for file_path, file_data in coverage_data.items():
# Skip node_modules and test files
if "node_modules" in file_path or "__tests__" in file_path:
continue
# Aggregate statements (s)
statements = file_data.get("s", {})
for stmt_id, count in statements.items():
total_statements += 1
if count > 0:
covered_statements += 1
# Aggregate branches (b)
branches = file_data.get("b", {})
for branch_id, counts in branches.items():
if isinstance(counts, list) and len(counts) >= 2:
total_branches += len(counts)
covered_branches += sum(1 for c in counts if c > 0)
result["covered"] = covered_statements
result["total"] = total_statements
result["branches_covered"] = covered_branches
result["branches_total"] = total_branches
# Calculate percentages
if result["total"] > 0:
result["coverage_pct"] = (result["covered"] / result["total"] * 100)
else:
result["coverage_pct"] = 0.0
if result["branches_total"] > 0:
result["branch_coverage_pct"] = (
result["branches_covered"] / result["branches_total"] * 100
)
else:
result["branch_coverage_pct"] = 0.0
except (json.JSONDecodeError, IOError) as e:
result["error"] = str(e)
return result
def load_jest_expo_coverage(path: Path) -> Dict[str, Any]:
"""
Load jest-expo coverage-final.json from mobile tests.
Args:
path: Path to coverage-final.json (jest-expo format, same as Jest)
Returns:
Dict with platform='mobile', coverage_pct, covered, total metrics
Returns coverage_pct=0.0 and error message if file not found
"""
result = {
"platform": "mobile",
"coverage_pct": 0.0,
"covered": 0,
"total": 0,
"branches_covered": 0,
"branches_total": 0,
"branch_coverage_pct": 0.0,
"file": str(path),
}
if not path.exists():
result["error"] = "file not found"
return result
try:
with open(path, 'r') as f:
coverage_data = json.load(f)
# jest-expo uses the same Jest coverage-final.json format
# {
# "/path/to/file.ts": {
# "s": { "1": 10, "2": 5 }, # statement counts
# "b": { "1": [10, 5], "2": [8, 2] }, # branch counts [taken, not taken]
# "f": { "1": 10 }, # function counts
# "l": { "1": 10 } # line counts
# }
# }
total_statements = 0
covered_statements = 0
total_branches = 0
covered_branches = 0
for file_path, file_data in coverage_data.items():
# Skip node_modules and test files
if "node_modules" in file_path or "__tests__" in file_path:
continue
# Aggregate statements (s)
statements = file_data.get("s", {})
for stmt_id, count in statements.items():
total_statements += 1
if count > 0:
covered_statements += 1
# Aggregate branches (b)
branches = file_data.get("b", {})
for branch_id, counts in branches.items():
if isinstance(counts, list) and len(counts) >= 2:
total_branches += len(counts)
covered_branches += sum(1 for c in counts if c > 0)
result["covered"] = covered_statements
result["total"] = total_statements
result["branches_covered"] = covered_branches
result["branches_total"] = total_branches
# Calculate percentages
if result["total"] > 0:
result["coverage_pct"] = (result["covered"] / result["total"] * 100)
else:
result["coverage_pct"] = 0.0
if result["branches_total"] > 0:
result["branch_coverage_pct"] = (
result["branches_covered"] / result["branches_total"] * 100
)
else:
result["branch_coverage_pct"] = 0.0
except (json.JSONDecodeError, IOError) as e:
result["error"] = str(e)
return result
def load_tarpaulin_coverage(path: Path) -> Dict[str, Any]:
"""
Load cargo-tarpaulin coverage.json from desktop tests.
Args:
path: Path to coverage.json (tarpaulin format)
Returns:
Dict with platform='rust', coverage_pct, covered, total metrics
Returns coverage_pct=0.0 and error message if file not found
"""
result = {
"platform": "rust",
"coverage_pct": 0.0,
"covered": 0,
"total": 0,
"branches_covered": 0,
"branches_total": 0,
"branch_coverage_pct": 0.0,
"file": str(path),
}
if not path.exists():
result["error"] = "file not found"
return result
try:
with open(path, 'r') as f:
coverage_data = json.load(f)
# Tarpaulin coverage.json format:
# {
# "files": {
# "path/to/file.rs": {
# "stats": {
# "covered": 50,
# "coverable": 100,
# "percent": 50.0
# }
# }
# }
# }
total_covered = 0
total_lines = 0
files = coverage_data.get("files", {})
for file_path, file_data in files.items():
stats = file_data.get("stats", {})
total_covered += stats.get("covered", 0)
total_lines += stats.get("coverable", 0)
result["covered"] = total_covered
result["total"] = total_lines
# Calculate line coverage percentage
if result["total"] > 0:
result["coverage_pct"] = (result["covered"] / result["total"] * 100)
else:
result["coverage_pct"] = 0.0
# Note: tarpaulin doesn't provide branch coverage
result["branches_covered"] = 0
result["branches_total"] = 0
result["branch_coverage_pct"] = 0.0
except (json.JSONDecodeError, IOError) as e:
result["error"] = str(e)
return result
def load_e2e_results(path: Path) -> Dict[str, Any]:
"""
Load E2E test results from unified aggregator.
Args:
path: Path to e2e_unified.json (from e2e_aggregator.py)
Returns:
Dict with platform='e2e', total_tests, passed, failed metrics
Returns error status if file not found
"""
result = {
"platform": "e2e",
"total_tests": 0,
"passed": 0,
"failed": 0,
"pass_rate": 0.0,
"duration_seconds": 0,
"platforms": {},
"file": str(path),
}
if not path.exists():
result["error"] = "file not found"
result["status"] = "not_run"
result["message"] = "E2E results not found"
return result
try:
with open(path, 'r') as f:
e2e_data = json.load(f)
# Extract aggregate E2E metrics
aggregate = e2e_data.get("aggregate", {})
platforms = e2e_data.get("platforms", [])
result["total_tests"] = aggregate.get("total_tests", 0)
result["passed"] = aggregate.get("total_passed", 0)
result["failed"] = aggregate.get("total_failed", 0)
result["pass_rate"] = aggregate.get("pass_rate", 0.0)
result["duration_seconds"] = aggregate.get("total_duration_seconds", 0)
result["status"] = "success"
# Extract per-platform E2E metrics
for platform in platforms:
platform_name = platform.get("platform", "unknown")
result["platforms"][platform_name] = {
"total": platform.get("total", 0),
"passed": platform.get("passed", 0),
"failed": platform.get("failed", 0),
"duration": platform.get("duration", 0),
}
except (json.JSONDecodeError, IOError) as e:
result["error"] = str(e)
result["status"] = "error"
result["message"] = f"Failed to load E2E results: {e}"
return result
def aggregate_coverage(
pytest_path: Path,
jest_path: Path,
jest_expo_path: Optional[Path] = None,
tarpaulin_path: Optional[Path] = None,
e2e_results_path: Optional[Path] = None
) -> Dict[str, Any]:
"""
Aggregate coverage from all platforms.
Computes overall coverage as weighted average:
(covered_backend + covered_frontend + covered_mobile + covered_rust) /
(total_backend + total_frontend + total_mobile + total_rust)
Args:
pytest_path: Path to pytest coverage.json
jest_path: Path to Jest coverage-final.json
jest_expo_path: Optional path to jest-expo coverage-final.json
tarpaulin_path: Optional path to tarpaulin coverage.json
Returns:
Dict with platforms, overall, timestamp keys
"""
python_coverage = load_pytest_coverage(pytest_path)
javascript_coverage = load_jest_coverage(jest_path)
mobile_coverage = load_jest_expo_coverage(jest_expo_path) if jest_expo_path else None
rust_coverage = load_tarpaulin_coverage(tarpaulin_path) if tarpaulin_path else None
# Build platforms dict
platforms = {
"python": python_coverage,
"javascript": javascript_coverage,
}
if mobile_coverage:
platforms["mobile"] = mobile_coverage
if rust_coverage:
platforms["rust"] = rust_coverage
# Compute overall coverage (weighted average)
total_covered = python_coverage["covered"] + javascript_coverage["covered"]
total_lines = python_coverage["total"] + javascript_coverage["total"]
total_branches_covered = (
python_coverage["branches_covered"] + javascript_coverage["branches_covered"]
)
total_branches = (
python_coverage["branches_total"] + javascript_coverage["branches_total"]
)
# Add mobile coverage if available
if mobile_coverage:
total_covered += mobile_coverage["covered"]
total_lines += mobile_coverage["total"]
total_branches_covered += mobile_coverage["branches_covered"]
total_branches += mobile_coverage["branches_total"]
# Add rust/desktop coverage if available
if rust_coverage:
total_covered += rust_coverage["covered"]
total_lines += rust_coverage["total"]
total_branches_covered += rust_coverage["branches_covered"]
total_branches += rust_coverage["branches_total"]
overall_coverage_pct = 0.0
if total_lines > 0:
overall_coverage_pct = (total_covered / total_lines * 100)
overall_branch_coverage_pct = 0.0
if total_branches > 0:
overall_branch_coverage_pct = (
total_branches_covered / total_branches * 100
)
result = {
"platforms": platforms,
"overall": {
"coverage_pct": round(overall_coverage_pct, 2),
"covered": total_covered,
"total": total_lines,
"branch_coverage_pct": round(overall_branch_coverage_pct, 2),
"branches_covered": total_branches_covered,
"branches_total": total_branches,
},
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
}
# Add E2E test metrics if available
if e2e_results_path:
e2e_results = load_e2e_results(e2e_results_path)
result["e2e_tests"] = {
"status": e2e_results.get("status", "not_run"),
"total_tests": e2e_results.get("total_tests", 0),
"passed": e2e_results.get("passed", 0),
"failed": e2e_results.get("failed", 0),
"pass_rate": e2e_results.get("pass_rate", 0.0),
"duration_seconds": e2e_results.get("duration_seconds", 0),
"platforms": e2e_results.get("platforms", {}),
}
if "error" in e2e_results:
result["e2e_tests"]["error"] = e2e_results["error"]
result["e2e_tests"]["message"] = e2e_results.get("message", "Unknown error")
return result
if total_lines > 0:
overall_coverage_pct = (total_covered / total_lines * 100)
overall_branch_coverage_pct = 0.0
if total_branches > 0:
overall_branch_coverage_pct = (
total_branches_covered / total_branches * 100
)
return {
"platforms": platforms,
"overall": {
"coverage_pct": round(overall_coverage_pct, 2),
"covered": total_covered,
"total": total_lines,
"branch_coverage_pct": round(overall_branch_coverage_pct, 2),
"branches_covered": total_branches_covered,
"branches_total": total_branches,
},
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
}
def generate_report(
aggregate_data: Dict[str, Any],
format: str = "text"
) -> str:
"""
Generate coverage report in specified format.
Args:
aggregate_data: Aggregated coverage data
format: Output format (json, text, markdown)
Returns:
Formatted report string
"""
if format == "json":
return json.dumps(aggregate_data, indent=2)
elif format == "markdown":
return generate_markdown_report(aggregate_data)
else: # text
return generate_text_report(aggregate_data)
def generate_text_report(aggregate_data: Dict[str, Any]) -> str:
"""Generate human-readable text report."""
lines = []
lines.append("=" * 80)
lines.append("UNIFIED COVERAGE REPORT")
lines.append("=" * 80)
lines.append(f"Generated: {aggregate_data['timestamp']}")
lines.append("")
# Overall metrics
overall = aggregate_data["overall"]
lines.append("OVERALL COVERAGE")
lines.append("-" * 80)
lines.append(f" Line Coverage: {overall['coverage_pct']:6.2f}% ({overall['covered']:7d} / {overall['total']:7d} lines)")
lines.append(f" Branch Coverage: {overall['branch_coverage_pct']:6.2f}% ({overall['branches_covered']:7d} / {overall['branches_total']:7d} branches)")
lines.append("")
# E2E test metrics (if available)
if "e2e_tests" in aggregate_data:
e2e = aggregate_data["e2e_tests"]
lines.append("E2E TEST RESULTS")
lines.append("-" * 80)
if e2e.get("status") == "not_run":
lines.append(f" Status: {e2e.get('message', 'Not run')}")
elif e2e.get("status") == "error":
lines.append(f" Status: ERROR - {e2e.get('message', 'Unknown error')}")
else:
lines.append(f" Total Tests: {e2e['total_tests']:5d}")
lines.append(f" Passed: {e2e['passed']:5d}")
lines.append(f" Failed: {e2e['failed']:5d}")
lines.append(f" Pass Rate: {e2e['pass_rate']:6.2f}%")
lines.append(f" Duration: {e2e['duration_seconds']:5d}s")
if e2e.get("platforms"):
lines.append("")
lines.append(" Platform Breakdown:")
for platform_name, platform_data in e2e["platforms"].items():
lines.append(f" {platform_name.upper()}: {platform_data['passed']}/{platform_data['total']} passed ({platform_data.get('duration', 0)}s)")
lines.append("")
# Platform breakdown
lines.append("PLATFORM BREAKDOWN")
lines.append("-" * 80)
for platform_name, platform_data in aggregate_data["platforms"].items():
lines.append(f"\n{platform_name.upper()}:")
lines.append(f" File: {platform_data['file']}")
if "error" in platform_data:
lines.append(f" Status: ERROR - {platform_data['error']}")
else:
lines.append(f" Line Coverage: {platform_data['coverage_pct']:6.2f}% ({platform_data['covered']:7d} / {platform_data['total']:7d} lines)")
lines.append(f" Branch Coverage: {platform_data['branch_coverage_pct']:6.2f}% ({platform_data['branches_covered']:7d} / {platform_data['branches_total']:7d} branches)")
lines.append("")
lines.append("=" * 80)
return "\n".join(lines)
def generate_markdown_report(aggregate_data: Dict[str, Any]) -> str:
"""Generate markdown table report (for PR comments)."""
lines = []
lines.append("## Unified Coverage Report")
lines.append("")
lines.append(f"**Generated:** {aggregate_data['timestamp']}")
lines.append("")
# Overall metrics
overall = aggregate_data["overall"]
lines.append("### Overall Coverage")
lines.append("")
lines.append("| Metric | Coverage | Details |")
lines.append("|--------|----------|---------|")
lines.append(f"| **Line Coverage** | **{overall['coverage_pct']:.2f}%** | {overall['covered']:,} / {overall['total']:,} lines |")
lines.append(f"| **Branch Coverage** | **{overall['branch_coverage_pct']:.2f}%** | {overall['branches_covered']:,} / {overall['branches_total']:,} branches |")
lines.append("")
# E2E test metrics (if available)
if "e2e_tests" in aggregate_data:
e2e = aggregate_data["e2e_tests"]
lines.append("### E2E Test Results")
lines.append("")
if e2e.get("status") == "not_run":
lines.append(f"**Status:** {e2e.get('message', 'Not run')}")
lines.append("")
elif e2e.get("status") == "error":
lines.append(f"**Status:** ❌ ERROR - {e2e.get('message', 'Unknown error')}")
lines.append("")
else:
lines.append("| Metric | Value |")
lines.append("|--------|-------|")
lines.append(f"| **Total Tests** | **{e2e['total_tests']}** |")
lines.append(f"| **Passed** | **{e2e['passed']}** |")
lines.append(f"| **Failed** | **{e2e['failed']}** |")
lines.append(f"| **Pass Rate** | **{e2e['pass_rate']:.2f}%** |")
lines.append(f"| **Duration** | **{e2e['duration_seconds']}s** |")
lines.append("")
if e2e.get("platforms"):
lines.append("#### Platform Breakdown")
lines.append("")
lines.append("| Platform | Tests | Passed | Failed | Duration |")
lines.append("|----------|-------|--------|--------|----------|")
for platform_name, platform_data in e2e["platforms"].items():
lines.append(f"| **{platform_name}** | {platform_data['total']} | {platform_data['passed']} | {platform_data['failed']} | {platform_data.get('duration', 0)}s |")
lines.append("")
# Platform breakdown
lines.append("### Platform Breakdown")
lines.append("")
lines.append("| Platform | Line Coverage | Branch Coverage | Status |")
lines.append("|----------|---------------|-----------------|--------|")
for platform_name, platform_data in aggregate_data["platforms"].items():
if "error" in platform_data:
status = f"❌ {platform_data['error']}"
line_cov = "N/A"
branch_cov = "N/A"
else:
status = "✅ OK"
line_cov = f"{platform_data['coverage_pct']:.2f}%"
branch_cov = f"{platform_data['branch_coverage_pct']:.2f}%"
lines.append(f"| **{platform_name}** | {line_cov} | {branch_cov} | {status} |")
lines.append("")
return "\n".join(lines)
def main():
"""Main execution entry point."""
parser = argparse.ArgumentParser(
description="Aggregate coverage from backend (pytest), frontend (Jest), mobile (jest-expo), and desktop (tarpaulin) tests"
)
parser.add_argument(
"--pytest-coverage",
type=Path,
default=Path(__file__).parent.parent / "coverage_reports" / "metrics" / "coverage.json",
help="Path to pytest coverage.json (default: backend/tests/coverage_reports/metrics/coverage.json)"
)
parser.add_argument(
"--jest-coverage",
type=Path,
default=Path(__file__).parent.parent.parent.parent / "frontend-nextjs" / "coverage" / "coverage-final.json",
help="Path to Jest coverage-final.json (default: frontend-nextjs/coverage/coverage-final.json)"
)
parser.add_argument(
"--mobile-coverage",
type=Path,
default=Path(__file__).parent.parent.parent.parent / "mobile" / "coverage" / "coverage-final.json",
help="Path to jest-expo coverage-final.json (default: mobile/coverage/coverage-final.json)"
)
parser.add_argument(
"--desktop-coverage",
type=Path,
default=Path(__file__).parent.parent.parent.parent / "frontend-nextjs" / "src-tauri" / "coverage" / "coverage.json",
help="Path to tarpaulin coverage.json (default: frontend-nextjs/src-tauri/coverage/coverage.json)"
)
parser.add_argument(
"--e2e-results",
type=Path,
default=None,
help="Path to unified E2E test results JSON (from e2e_aggregator.py)"
)
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).parent / "coverage_reports" / "unified" / "coverage.json",
help="Output path for unified coverage JSON (default: backend/tests/scripts/coverage_reports/unified/coverage.json)"
)
parser.add_argument(
"--format",
type=str,
choices=["json", "text", "markdown"],
default="text",
help="Output format (default: text)"
)
args = parser.parse_args()
# Aggregate coverage
aggregate_data = aggregate_coverage(
args.pytest_coverage,
args.jest_coverage,
args.mobile_coverage,
args.desktop_coverage,
args.e2e_results
)
# Create output directory if needed
args.output.parent.mkdir(parents=True, exist_ok=True)
# Save unified JSON
with open(args.output, 'w') as f:
json.dump(aggregate_data, f, indent=2)
# Generate and print report
report = generate_report(aggregate_data, args.format)
print(report)
# Print warnings for missing coverage files
for platform_name, platform_data in aggregate_data["platforms"].items():
if "error" in platform_data:
print(f"\n⚠️ WARNING: {platform_name} coverage file not found or invalid: {platform_data['file']}",
file=sys.stderr)
print(f"\n✅ Unified coverage saved to: {args.output}")
return 0
if __name__ == "__main__":
sys.exit(main())