#!/usr/bin/env python3 """ Calculate Go test coverage and generate reports. This script parses the coverage.out file generated by `go test -coverprofile`, extracts coverage statistics, and generates formatted reports. """ import sys import re import os from typing import Dict, List, Tuple def parse_coverage_file(coverage_file: str) -> Tuple[float, Dict[str, float]]: """ Parse coverage output file and extract coverage data. Args: coverage_file: Path to coverage.out file Returns: Tuple of (total_coverage, package_coverage_dict) """ if not os.path.exists(coverage_file): print(f"Error: Coverage file {coverage_file} not found", file=sys.stderr) sys.exit(1) # Run go tool cover to get coverage data import subprocess try: result = subprocess.run( ['go', 'tool', 'cover', '-func', coverage_file], capture_output=True, text=True, check=True ) except subprocess.CalledProcessError as e: print(f"Error running go tool cover: {e}", file=sys.stderr) sys.exit(1) lines = result.stdout.strip().split('\n') package_coverage = {} total_coverage = 0.0 for line in lines: # Skip empty lines if not line.strip(): continue # Check for total coverage line if line.startswith('total:'): # Extract percentage from "total: (statements) XX.X%" match = re.search(r'(\d+\.\d+)%', line) if match: total_coverage = float(match.group(1)) continue # Parse package/file coverage # Format: "package/file.go:function statements coverage%" parts = line.split() if len(parts) >= 3: file_path = parts[0] coverage_str = parts[-1] # Extract package name from file path package = file_path.split(':')[0] package_name = '/'.join(package.split('/')[:-1]) if '/' in package else package # Extract coverage percentage match = re.search(r'(\d+\.\d+)%', coverage_str) if match: coverage_pct = float(match.group(1)) # Aggregate by package if package_name not in package_coverage: package_coverage[package_name] = [] package_coverage[package_name].append(coverage_pct) # Calculate average coverage per package package_avg = { pkg: sum(coverages) / len(coverages) for pkg, coverages in package_coverage.items() } return total_coverage, package_avg def get_coverage_status(coverage: float) -> Tuple[str, str, str]: """ Get coverage status based on percentage. Args: coverage: Coverage percentage Returns: Tuple of (emoji, status_text, badge_color) """ if coverage >= 80: return '🟢', 'excellent', 'brightgreen' elif coverage >= 60: return '🟡', 'good', 'yellow' elif coverage >= 40: return '🟠', 'fair', 'orange' else: return '🔴', 'needs improvement', 'red' def generate_coverage_report(coverage_file: str, output_file: str) -> None: """ Generate a detailed coverage report in markdown format. Args: coverage_file: Path to coverage.out file output_file: Path to output markdown file """ import subprocess try: result = subprocess.run( ['go', 'tool', 'cover', '-func', coverage_file], capture_output=True, text=True, check=True ) except subprocess.CalledProcessError as e: print(f"Error generating coverage report: {e}", file=sys.stderr) sys.exit(1) with open(output_file, 'w') as f: f.write("## Coverage by Package\n\n") f.write("```\n") f.write(result.stdout) f.write("```\n") def set_github_output(name: str, value: str) -> None: """ Set GitHub Actions output variable. Args: name: Output variable name value: Output variable value """ github_output = os.environ.get('GITHUB_OUTPUT') if github_output: with open(github_output, 'a') as f: f.write(f"{name}={value}\n") else: print(f"::set-output name={name}::{value}") def main(): """Main entry point.""" if len(sys.argv) < 2: print("Usage: calculate_coverage.py [output_file]", file=sys.stderr) sys.exit(1) coverage_file = sys.argv[1] output_file = sys.argv[2] if len(sys.argv) > 2 else 'coverage_report.md' # Parse coverage data total_coverage, package_coverage = parse_coverage_file(coverage_file) # Get coverage status emoji, status, badge_color = get_coverage_status(total_coverage) # Generate detailed report generate_coverage_report(coverage_file, output_file) # Output results print(f"Total Coverage: {total_coverage}%") print(f"Status: {status}") print(f"Badge Color: {badge_color}") # Set GitHub Actions outputs set_github_output('coverage', f'{total_coverage}%') set_github_output('coverage_num', str(total_coverage)) set_github_output('status', status) set_github_output('emoji', emoji) set_github_output('badge_color', badge_color) # Print package breakdown if package_coverage: print("\nCoverage by Package:") for package, coverage in sorted(package_coverage.items()): print(f" {package}: {coverage:.1f}%") if __name__ == '__main__': main()