Download tests/scripts/aggregate_coverage.py from techprotrade/annator-command-center: direct link, hf CLI and curl.
- Browser
- Download file 26.1 kB
-
https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/scripts/aggregate_coverage.py
- Command line
-
hf download hf://spaces/techprotrade/annator-command-center/tests/scripts/aggregate_coverage.py
-
curl -L -o aggregate_coverage.py https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/scripts/aggregate_coverage.py
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()) | |