Spaces:
Sleeping
Sleeping
Download tests/scripts/cross_platform_coverage_gate.py from techprotrade/annator-command-center: direct link, hf CLI and curl.
- Browser
- Download file 25.4 kB
-
https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/scripts/cross_platform_coverage_gate.py
- Command line
-
hf download hf://spaces/techprotrade/annator-command-center/tests/scripts/cross_platform_coverage_gate.py
-
curl -L -o cross_platform_coverage_gate.py https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/scripts/cross_platform_coverage_gate.py
25.4 kB
| #!/usr/bin/env python3 | |
| """ | |
| Cross-Platform Coverage Gate Enforcement Script | |
| Purpose: Enforce platform-specific coverage minimums (backend >=70%, frontend >=80%, | |
| mobile >=50%, desktop >=40%) while computing weighted overall score for CI/CD quality gates. | |
| Usage: | |
| python cross_platform_coverage_gate.py [options] | |
| Options: | |
| --backend-coverage PATH Path to pytest coverage.json (default: relative path) | |
| --frontend-coverage PATH Path to Jest coverage-final.json (default: relative path) | |
| --mobile-coverage PATH Path to jest-expo coverage-final.json (default: relative path) | |
| --desktop-coverage PATH Path to tarpaulin coverage.json (default: relative path) | |
| --weights CSV Override default weights (comma-separated: backend=0.35,frontend=0.40,...) | |
| --thresholds CSV Override default thresholds (comma-separated: backend=70,frontend=80,...) | |
| --output-json PATH Path for JSON output (default: cross_platform_summary.json) | |
| --format FORMAT Output format: text|json|markdown (default: text) | |
| --strict Exit 1 if any platform below threshold (default: warning only) | |
| Example: | |
| python cross_platform_coverage_gate.py --format text | |
| python cross_platform_coverage_gate.py --strict --format json | |
| python cross_platform_coverage_gate.py --thresholds backend=75,frontend=85 --format markdown | |
| """ | |
| import argparse | |
| import json | |
| import logging | |
| import os | |
| import sys | |
| from datetime import datetime | |
| from pathlib import Path | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(levelname)s: %(message)s' | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Platform-specific thresholds (from REQUIREMENTS.md) | |
| PLATFORM_THRESHOLDS = { | |
| "backend": 70.0, | |
| "frontend": 80.0, | |
| "mobile": 50.0, | |
| "desktop": 40.0 | |
| } | |
| # Platform weights (from RESEARCH.md recommendation) | |
| # Total must sum to 1.0 | |
| PLATFORM_WEIGHTS = { | |
| "backend": 0.35, | |
| "frontend": 0.40, | |
| "mobile": 0.15, | |
| "desktop": 0.10 | |
| } | |
| # Default file paths relative to script location | |
| DEFAULT_BACKEND_COVERAGE = Path(__file__).parent.parent.parent / "tests/coverage_reports/metrics/coverage.json" | |
| DEFAULT_FRONTEND_COVERAGE = Path(__file__).parent.parent.parent.parent / "frontend-nextjs/coverage/coverage-final.json" | |
| DEFAULT_MOBILE_COVERAGE = Path(__file__).parent.parent.parent.parent / "mobile/coverage/coverage-final.json" | |
| DEFAULT_DESKTOP_COVERAGE = Path(__file__).parent.parent.parent.parent / "frontend-nextjs/src-tauri/coverage/coverage.json" | |
| DEFAULT_OUTPUT_JSON = Path(__file__).parent.parent / "coverage_reports/metrics/cross_platform_summary.json" | |
| def load_backend_coverage(path): | |
| """ | |
| Load backend coverage from pytest coverage.json format. | |
| Expected schema: | |
| { | |
| "totals": { | |
| "percent_covered": 75.0, | |
| "covered_lines": 1500, | |
| "num_statements": 2000 | |
| } | |
| } | |
| Args: | |
| path: Path to pytest coverage.json | |
| Returns: | |
| Dict with coverage_pct, covered, total, file_path, error (if applicable) | |
| """ | |
| result = { | |
| "coverage_pct": 0.0, | |
| "covered": 0, | |
| "total": 0, | |
| "file_path": str(path), | |
| "error": None | |
| } | |
| if not path.exists(): | |
| logger.warning(f"Backend coverage file not found: {path}") | |
| result["error"] = "file not found" | |
| return result | |
| try: | |
| with open(path, 'r') as f: | |
| data = json.load(f) | |
| totals = 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) | |
| logger.info(f"Backend coverage: {result['coverage_pct']:.2f}% ({result['covered']}/{result['total']} lines)") | |
| except (json.JSONDecodeError, IOError) as e: | |
| logger.error(f"Error loading backend coverage: {e}") | |
| result["error"] = str(e) | |
| return result | |
| def load_frontend_coverage(path): | |
| """ | |
| Load frontend coverage from Jest coverage-final.json format. | |
| Expected schema: | |
| { | |
| "/path/to/file.ts": { | |
| "s": { "1": 10, "2": 5 }, # statement counts | |
| "b": { "1": [10, 5] }, # branch counts [taken, not taken] | |
| "f": { "1": 10 }, # function counts | |
| "l": { "1": 10 } # line counts | |
| } | |
| } | |
| Args: | |
| path: Path to Jest coverage-final.json | |
| Returns: | |
| Dict with coverage_pct, covered, total, file_path, error (if applicable) | |
| """ | |
| result = { | |
| "coverage_pct": 0.0, | |
| "covered": 0, | |
| "total": 0, | |
| "file_path": str(path), | |
| "error": None | |
| } | |
| if not path.exists(): | |
| logger.warning(f"Frontend coverage file not found: {path}") | |
| result["error"] = "file not found" | |
| return result | |
| try: | |
| with open(path, 'r') as f: | |
| data = json.load(f) | |
| total_statements = 0 | |
| covered_statements = 0 | |
| for file_path, file_data in data.items(): | |
| # Skip node_modules and test files | |
| if "node_modules" in file_path or "__tests__" in file_path: | |
| continue | |
| statements = file_data.get("s", {}) | |
| for stmt_id, count in statements.items(): | |
| total_statements += 1 | |
| if count > 0: | |
| covered_statements += 1 | |
| result["covered"] = covered_statements | |
| result["total"] = total_statements | |
| result["coverage_pct"] = (covered_statements / total_statements * 100) if total_statements > 0 else 0.0 | |
| logger.info(f"Frontend coverage: {result['coverage_pct']:.2f}% ({result['covered']}/{result['total']} statements)") | |
| except (json.JSONDecodeError, IOError) as e: | |
| logger.error(f"Error loading frontend coverage: {e}") | |
| result["error"] = str(e) | |
| return result | |
| def load_mobile_coverage(path): | |
| """ | |
| Load mobile coverage from jest-expo coverage-final.json format. | |
| Note: jest-expo uses the same Jest coverage-final.json format as frontend. | |
| Args: | |
| path: Path to jest-expo coverage-final.json | |
| Returns: | |
| Dict with coverage_pct, covered, total, file_path, error (if applicable) | |
| """ | |
| result = { | |
| "coverage_pct": 0.0, | |
| "covered": 0, | |
| "total": 0, | |
| "file_path": str(path), | |
| "error": None | |
| } | |
| if not path.exists(): | |
| logger.warning(f"Mobile coverage file not found: {path}") | |
| result["error"] = "file not found" | |
| return result | |
| try: | |
| with open(path, 'r') as f: | |
| data = json.load(f) | |
| total_statements = 0 | |
| covered_statements = 0 | |
| for file_path, file_data in data.items(): | |
| # Skip node_modules and test files | |
| if "node_modules" in file_path or "__tests__" in file_path: | |
| continue | |
| statements = file_data.get("s", {}) | |
| for stmt_id, count in statements.items(): | |
| total_statements += 1 | |
| if count > 0: | |
| covered_statements += 1 | |
| result["covered"] = covered_statements | |
| result["total"] = total_statements | |
| result["coverage_pct"] = (covered_statements / total_statements * 100) if total_statements > 0 else 0.0 | |
| logger.info(f"Mobile coverage: {result['coverage_pct']:.2f}% ({result['covered']}/{result['total']} statements)") | |
| except (json.JSONDecodeError, IOError) as e: | |
| logger.error(f"Error loading mobile coverage: {e}") | |
| result["error"] = str(e) | |
| return result | |
| def load_desktop_coverage(path): | |
| """ | |
| Load desktop coverage from tarpaulin coverage.json format. | |
| Expected schema: | |
| { | |
| "files": { | |
| "path/to/file.rs": { | |
| "stats": { | |
| "covered": 50, | |
| "coverable": 100, | |
| "percent": 50.0 | |
| } | |
| } | |
| } | |
| } | |
| Args: | |
| path: Path to tarpaulin coverage.json | |
| Returns: | |
| Dict with coverage_pct, covered, total, file_path, error (if applicable) | |
| """ | |
| result = { | |
| "coverage_pct": 0.0, | |
| "covered": 0, | |
| "total": 0, | |
| "file_path": str(path), | |
| "error": None | |
| } | |
| if not path.exists(): | |
| logger.warning(f"Desktop coverage file not found: {path}") | |
| result["error"] = "file not found" | |
| return result | |
| try: | |
| with open(path, 'r') as f: | |
| data = json.load(f) | |
| total_covered = 0 | |
| total_lines = 0 | |
| files = 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 | |
| result["coverage_pct"] = (total_covered / total_lines * 100) if total_lines > 0 else 0.0 | |
| logger.info(f"Desktop coverage: {result['coverage_pct']:.2f}% ({result['covered']}/{result['total']} lines)") | |
| except (json.JSONDecodeError, IOError) as e: | |
| logger.error(f"Error loading desktop coverage: {e}") | |
| result["error"] = str(e) | |
| return result | |
| def check_platform_thresholds( | |
| coverage_data, | |
| thresholds | |
| ): | |
| """ | |
| Check each platform against its minimum threshold. | |
| Args: | |
| coverage_data: Dict mapping platform names to coverage dicts | |
| {"backend": {"coverage_pct": 75.0, ...}, ...} | |
| thresholds: Dict mapping platform names to minimum thresholds | |
| {"backend": 70.0, "frontend": 80.0, ...} | |
| Returns: | |
| (all_passed, list_of_failure_messages) | |
| """ | |
| failures = [] | |
| for platform, threshold in thresholds.items(): | |
| if platform not in coverage_data: | |
| logger.warning(f"Platform '{platform}' not in coverage data, skipping threshold check") | |
| continue | |
| platform_data = coverage_data[platform] | |
| coverage = platform_data.get("coverage_pct", 0.0) | |
| if coverage < threshold: | |
| gap = threshold - coverage | |
| failure_msg = ( | |
| f"{platform.capitalize()}: {coverage:.2f}% < {threshold:.2f}% " | |
| f"(gap: {gap:.2f}%)" | |
| ) | |
| failures.append(failure_msg) | |
| logger.error(failure_msg) | |
| all_passed = len(failures) == 0 | |
| return all_passed, failures | |
| def compute_weighted_coverage( | |
| coverage_data, | |
| weights | |
| ): | |
| """ | |
| Compute weighted overall coverage score. | |
| Args: | |
| coverage_data: Dict mapping platform names to coverage dicts | |
| weights: Dict mapping platform names to weight values | |
| Returns: | |
| Dict with overall_pct, platform_breakdown, validation_status | |
| """ | |
| # Validate weights sum to 1.0 (with tolerance for floating point errors) | |
| total_weight = sum(weights.values()) | |
| if not (0.99 <= total_weight <= 1.01): | |
| logger.warning( | |
| f"Weights sum to {total_weight:.2f}, expected 1.0. " | |
| f"Results may be inaccurate. Weights: {weights}" | |
| ) | |
| weighted_sum = 0.0 | |
| platform_breakdown = [] | |
| for platform, weight in weights.items(): | |
| if platform not in coverage_data: | |
| logger.warning(f"Platform '{platform}' not in coverage data, skipping") | |
| continue | |
| platform_data = coverage_data[platform] | |
| coverage = platform_data.get("coverage_pct", 0.0) | |
| contribution = coverage * weight | |
| weighted_sum += contribution | |
| platform_breakdown.append({ | |
| "platform": platform, | |
| "coverage_pct": coverage, | |
| "weight": weight, | |
| "contribution": contribution | |
| }) | |
| return { | |
| "overall_pct": weighted_sum, | |
| "platform_breakdown": platform_breakdown, | |
| "validation": { | |
| "total_weight": total_weight, | |
| "valid": 0.99 <= total_weight <= 1.01 | |
| } | |
| } | |
| def generate_text_report(data): | |
| """Generate human-readable text report.""" | |
| lines = [] | |
| lines.append("=" * 70) | |
| lines.append("Cross-Platform Coverage Report") | |
| lines.append("=" * 70) | |
| lines.append("") | |
| # Platform breakdown | |
| lines.append("Platform Coverage:") | |
| for platform_info in data["weighted"]["platform_breakdown"]: | |
| platform = platform_info["platform"].capitalize() | |
| coverage = platform_info["coverage_pct"] | |
| weight = platform_info["weight"] | |
| contribution = platform_info["contribution"] | |
| lines.append(f" {platform}: {coverage:.2f}% (weight: {weight*100:.0f}%, contribution: {contribution:.2f}%)") | |
| lines.append("") | |
| # Overall weighted score | |
| overall = data["weighted"]["overall_pct"] | |
| lines.append(f"Overall Weighted Coverage: {overall:.2f}%") | |
| lines.append("") | |
| # Threshold checks | |
| lines.append("Platform Threshold Checks:") | |
| thresholds = data["thresholds"] | |
| for platform, threshold in thresholds.items(): | |
| if platform in data["platforms"]: | |
| platform_data = data["platforms"][platform] | |
| coverage = platform_data["coverage_pct"] | |
| passed = coverage >= threshold | |
| status = "✓ PASS" if passed else "✗ FAIL" | |
| lines.append(f" {platform.capitalize():10s}: {coverage:5.2f}% >= {threshold:5.2f}% ... {status}") | |
| lines.append("") | |
| # Failures | |
| if data["threshold_failures"]: | |
| lines.append("Failed Thresholds:") | |
| for failure in data["threshold_failures"]: | |
| lines.append(f" - {failure}") | |
| lines.append("") | |
| else: | |
| lines.append("All platforms passed minimum thresholds! ✓") | |
| lines.append("") | |
| # Timestamp | |
| lines.append(f"Generated: {data['timestamp']}") | |
| lines.append("=" * 70) | |
| return "\n".join(lines) | |
| def generate_json_report(data): | |
| """Generate machine-readable JSON report.""" | |
| return json.dumps(data, indent=2) | |
| def generate_markdown_report(data): | |
| """Generate markdown report (PR comment format).""" | |
| lines = [] | |
| lines.append("## Cross-Platform Coverage Report") | |
| lines.append("") | |
| # Overall badge | |
| overall = data["weighted"]["overall_pct"] | |
| lines.append(f"### Overall: {overall:.2f}%") | |
| lines.append("") | |
| # Platform table | |
| lines.append("| Platform | Coverage | Weight | Threshold | Status |") | |
| lines.append("|----------|----------|--------|-----------|--------|") | |
| thresholds = data["thresholds"] | |
| for platform_info in data["weighted"]["platform_breakdown"]: | |
| platform = platform_info["platform"].capitalize() | |
| coverage = platform_info["coverage_pct"] | |
| weight = platform_info["weight"] | |
| threshold = thresholds.get(platform_info["platform"], 0.0) | |
| passed = coverage >= threshold | |
| status = "✓" if passed else "✗" | |
| lines.append(f"| {platform} | {coverage:.2f}% | {weight*100:.0f}% | ≥{threshold:.0f}% | {status} |") | |
| lines.append("") | |
| # Failures | |
| if data["threshold_failures"]: | |
| lines.append("### Failed Thresholds") | |
| for failure in data["threshold_failures"]: | |
| lines.append(f"- {failure}") | |
| lines.append("") | |
| lines.append(f"*Generated: {data['timestamp']}*") | |
| return "\n".join(lines) | |
| def generate_pr_comment( | |
| aggregate_data, | |
| thresholds, | |
| event_type | |
| ): | |
| """ | |
| Generate PR comment in markdown format. | |
| Args: | |
| aggregate_data: Aggregated coverage data from compute_weighted_coverage | |
| thresholds: Platform-specific thresholds | |
| event_type: 'pull_request' or 'push' | |
| Returns: | |
| Markdown string for PR comment | |
| """ | |
| lines = [] | |
| # Header | |
| lines.append("## Cross-Platform Coverage Report") | |
| lines.append("") | |
| lines.append(f"**Generated:** {aggregate_data['timestamp']}") | |
| lines.append(f"**Event:** {event_type}") | |
| lines.append("") | |
| # Overall summary | |
| overall = aggregate_data.get("weighted", {}) | |
| overall_pct = overall.get("overall_pct", 0.0) | |
| lines.append("### Overall Coverage") | |
| lines.append("") | |
| lines.append("| Metric | Value |") | |
| lines.append("|--------|-------|") | |
| lines.append(f"| **Weighted Overall** | **{overall_pct:.2f}%** |") | |
| # Determine target based on event type | |
| target = 75.0 if event_type == "pull_request" else 80.0 | |
| gap = target - overall_pct | |
| gap_text = f"+{abs(gap):.2f}%" if gap < 0 else f"{gap:.2f}%" | |
| lines.append(f"| **Target** | {target:.0f}% |") | |
| lines.append(f"| **Gap** | {gap_text} |") | |
| lines.append("") | |
| # Overall status | |
| if overall_pct >= target: | |
| lines.append("### ✅ Overall Target Met") | |
| elif overall_pct >= 60: | |
| lines.append("### ⚠️ Overall Below Target") | |
| else: | |
| lines.append("### ❌ Critical Coverage Gap") | |
| lines.append("") | |
| # Platform breakdown | |
| lines.append("### Platform Breakdown") | |
| lines.append("") | |
| lines.append("| Platform | Coverage | Threshold | Status |") | |
| lines.append("|----------|----------|-----------|--------|") | |
| for platform_info in overall.get("platform_breakdown", []): | |
| platform_name = platform_info["platform"].capitalize() | |
| coverage_pct = platform_info.get("coverage_pct", 0.0) | |
| threshold = thresholds.get(platform_info["platform"], 70.0) | |
| status = "✅" if coverage_pct >= threshold else "❌" | |
| gap = threshold - coverage_pct | |
| gap_indicator = f" ({gap:+.2f}%)" if gap > 0 else "" | |
| lines.append( | |
| f"| **{platform_name}** | " | |
| f"{coverage_pct:.2f}%{gap_indicator} | " | |
| f"{threshold:.0f}% | " | |
| f"{status} |" | |
| ) | |
| lines.append("") | |
| # Failing platforms section | |
| failures = [] | |
| for platform_name, coverage_data in aggregate_data.get("platforms", {}).items(): | |
| coverage_pct = coverage_data.get("coverage_pct", 0.0) | |
| threshold = thresholds.get(platform_name, 70.0) | |
| if coverage_pct < threshold: | |
| gap = threshold - coverage_pct | |
| failures.append(f"- **{platform_name.capitalize()}**: {coverage_pct:.2f}% < {threshold:.0f}% (gap: {gap:.2f}%)") | |
| if failures: | |
| lines.append("### Platforms Below Threshold") | |
| lines.append("") | |
| for failure in failures: | |
| lines.append(failure) | |
| lines.append("") | |
| lines.append("**Enforcement:**") | |
| if event_type == "pull_request": | |
| lines.append("- PR: Warning only (allows development flexibility)") | |
| else: | |
| lines.append("- Main: Build will fail (strict enforcement)") | |
| lines.append("") | |
| # Remediation steps | |
| if failures: | |
| lines.append("### Remediation Steps") | |
| lines.append("") | |
| for failure in failures: | |
| platform = failure.split(":")[0].strip("**").lower() | |
| if platform == "backend": | |
| lines.append("#### Backend") | |
| lines.append("- Run: `pytest tests/ --cov=core --cov=api --cov=tools --cov-report=term-missing`") | |
| lines.append("- Add tests for uncovered lines in high-impact services") | |
| lines.append("- Focus on governance, episodic memory, LLM services") | |
| elif platform == "frontend": | |
| lines.append("#### Frontend") | |
| lines.append("- Run: `npm test -- --coverage --coverageReporters=json`") | |
| lines.append("- Add component tests for uncovered UI elements") | |
| lines.append("- Focus on canvas, chat, agent execution screens") | |
| elif platform == "mobile": | |
| lines.append("#### Mobile") | |
| lines.append("- Run: `npm test -- --coverage`") | |
| lines.append("- Add tests for device features (camera, location, notifications)") | |
| lines.append("- Focus on platform-specific components (iOS vs Android)") | |
| elif platform == "desktop": | |
| lines.append("#### Desktop") | |
| lines.append("- Run: `cargo tarpaulin --out Json`") | |
| lines.append("- Add tests for IPC handlers, system tray, file operations") | |
| lines.append("- Focus on platform-specific code (Windows/macOS/Linux)") | |
| lines.append("") | |
| # Artifacts section | |
| lines.append("### Artifacts") | |
| lines.append("") | |
| lines.append(f"- [View Full Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})") | |
| lines.append("- Coverage reports available for 30 days in workflow artifacts") | |
| return "\n".join(lines) | |
| def parse_weights_arg(weights_str): | |
| """Parse weights argument string (e.g., 'backend=0.35,frontend=0.40').""" | |
| weights = {} | |
| for pair in weights_str.split(','): | |
| key, value = pair.split('=') | |
| weights[key.strip()] = float(value.strip()) | |
| return weights | |
| def parse_thresholds_arg(thresholds_str): | |
| """Parse thresholds argument string (e.g., 'backend=70,frontend=80').""" | |
| thresholds = {} | |
| for pair in thresholds_str.split(','): | |
| key, value = pair.split('=') | |
| thresholds[key.strip()] = float(value.strip()) | |
| return thresholds | |
| def main(): | |
| """Main execution function.""" | |
| parser = argparse.ArgumentParser( | |
| description="Cross-platform coverage enforcement with platform-specific thresholds" | |
| ) | |
| parser.add_argument( | |
| "--backend-coverage", | |
| type=Path, | |
| default=DEFAULT_BACKEND_COVERAGE, | |
| help="Path to pytest coverage.json" | |
| ) | |
| parser.add_argument( | |
| "--frontend-coverage", | |
| type=Path, | |
| default=DEFAULT_FRONTEND_COVERAGE, | |
| help="Path to Jest coverage-final.json" | |
| ) | |
| parser.add_argument( | |
| "--mobile-coverage", | |
| type=Path, | |
| default=DEFAULT_MOBILE_COVERAGE, | |
| help="Path to jest-expo coverage-final.json" | |
| ) | |
| parser.add_argument( | |
| "--desktop-coverage", | |
| type=Path, | |
| default=DEFAULT_DESKTOP_COVERAGE, | |
| help="Path to tarpaulin coverage.json" | |
| ) | |
| parser.add_argument( | |
| "--weights", | |
| type=str, | |
| help="Override default weights (comma-separated: backend=0.35,frontend=0.40,...)" | |
| ) | |
| parser.add_argument( | |
| "--thresholds", | |
| type=str, | |
| help="Override default thresholds (comma-separated: backend=70,frontend=80,...)" | |
| ) | |
| parser.add_argument( | |
| "--output-json", | |
| type=Path, | |
| default=DEFAULT_OUTPUT_JSON, | |
| help="Path for JSON output" | |
| ) | |
| parser.add_argument( | |
| "--format", | |
| type=str, | |
| choices=["text", "json", "markdown", "pr-comment"], | |
| default="text", | |
| help="Output format" | |
| ) | |
| parser.add_argument( | |
| "--event-type", | |
| type=str, | |
| choices=["pull_request", "push"], | |
| default=None, | |
| help="GitHub event type (default: auto-detect from GITHUB_EVENT_NAME env var)" | |
| ) | |
| parser.add_argument( | |
| "--strict", | |
| action="store_true", | |
| help="Exit 1 if any platform below threshold" | |
| ) | |
| args = parser.parse_args() | |
| # Detect event type from environment if not specified | |
| if args.event_type is None: | |
| event_type = os.getenv("GITHUB_EVENT_NAME", "push") | |
| else: | |
| event_type = args.event_type | |
| # Use custom weights/thresholds if provided | |
| weights = parse_weights_arg(args.weights) if args.weights else PLATFORM_WEIGHTS.copy() | |
| thresholds = parse_thresholds_arg(args.thresholds) if args.thresholds else PLATFORM_THRESHOLDS.copy() | |
| logger.info(f"Using platform weights: {weights}") | |
| logger.info(f"Using platform thresholds: {thresholds}") | |
| logger.info(f"Event type: {event_type}") | |
| # Load all platform coverages | |
| coverage_data = { | |
| "backend": load_backend_coverage(args.backend_coverage), | |
| "frontend": load_frontend_coverage(args.frontend_coverage), | |
| "mobile": load_mobile_coverage(args.mobile_coverage), | |
| "desktop": load_desktop_coverage(args.desktop_coverage) | |
| } | |
| # Check platform thresholds | |
| all_passed, failures = check_platform_thresholds(coverage_data, thresholds) | |
| # Compute weighted overall | |
| weighted_result = compute_weighted_coverage(coverage_data, weights) | |
| # Build result data | |
| result = { | |
| "timestamp": datetime.utcnow().isoformat() + "Z", | |
| "platforms": coverage_data, | |
| "thresholds": thresholds, | |
| "threshold_failures": failures, | |
| "all_thresholds_passed": all_passed, | |
| "weighted": weighted_result | |
| } | |
| # Generate report | |
| if args.format == "text": | |
| report = generate_text_report(result) | |
| print(report) | |
| elif args.format == "json": | |
| report = generate_json_report(result) | |
| print(report) | |
| elif args.format == "markdown": | |
| report = generate_markdown_report(result) | |
| print(report) | |
| elif args.format == "pr-comment": | |
| report = generate_pr_comment(result, thresholds, event_type) | |
| print(report) | |
| # Save JSON output | |
| args.output_json.parent.mkdir(parents=True, exist_ok=True) | |
| with open(args.output_json, 'w') as f: | |
| json.dump(result, f, indent=2) | |
| logger.info(f"JSON report saved to: {args.output_json}") | |
| # Exit with error code if strict mode and any threshold failed | |
| if args.strict and not all_passed: | |
| logger.error("Strict mode: One or more platforms below threshold") | |
| sys.exit(1) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |