#!/usr/bin/env python3 # CodeDetect覆盖率报告工具 import os import sys import json import argparse import subprocess from pathlib import Path from typing import Dict, List, Any, Optional from dataclasses import dataclass, asdict import xml.etree.ElementTree as ET from datetime import datetime @dataclass class CoverageData: """覆盖率数据""" file_path: str line_coverage: float branch_coverage: float total_lines: int covered_lines: int total_branches: int covered_branches: int functions: List[Dict[str, Any]] @dataclass class CoverageReport: """覆盖率报告""" project_name: str timestamp: str overall_coverage: Dict[str, float] file_coverage: List[CoverageData] summary: Dict[str, Any] class CoverageReporter: """覆盖率报告生成器""" def __init__(self, output_dir: str = "coverage_reports"): self.output_dir = Path(output_dir) self.output_dir.mkdir(exist_ok=True) def generate_report(self, project_name: str = "CodeDetect") -> CoverageReport: """生成覆盖率报告""" print("🚀 开始生成覆盖率报告...") # 运行覆盖率测试 self._run_coverage_tests() # 解析覆盖率数据 coverage_data = self._parse_coverage_data() # 生成报告 report = self._create_report(project_name, coverage_data) # 生成多种格式的报告 self._generate_html_report(report) self._generate_json_report(report) self._generate_summary_report(report) return report def _run_coverage_tests(self): """运行覆盖率测试""" print("🧪 运行覆盖率测试...") try: # 运行pytest覆盖率测试 result = subprocess.run([ "python", "-m", "pytest", "tests/", "--cov=src/", "--cov-report=xml:coverage.xml", "--cov-report=html:htmlcov/", "--cov-report=term-missing", "-v" ], capture_output=True, text=True, timeout=300) if result.returncode != 0: print(f"⚠️ 覆盖率测试失败: {result.stderr}") # 创建一个最小的覆盖率文件 self._create_minimal_coverage_file() except Exception as e: print(f"⚠️ 覆盖率测试错误: {e}") self._create_minimal_coverage_file() def _create_minimal_coverage_file(self): """创建最小的覆盖率文件""" minimal_xml = """ . """ with open("coverage.xml", "w") as f: f.write(minimal_xml) def _parse_coverage_data(self) -> Dict[str, Any]: """解析覆盖率数据""" coverage_data = { "files": [], "overall": { "line_coverage": 0.0, "branch_coverage": 0.0, "total_lines": 0, "covered_lines": 0, "total_branches": 0, "covered_branches": 0 } } try: if Path("coverage.xml").exists(): tree = ET.parse("coverage.xml") root = tree.getroot() # 获取总体覆盖率 coverage_data["overall"]["line_coverage"] = float(root.get("line-rate", 0)) * 100 coverage_data["overall"]["branch_coverage"] = float(root.get("branch-rate", 0)) * 100 coverage_data["overall"]["total_lines"] = int(root.get("lines-valid", 0)) coverage_data["overall"]["covered_lines"] = int(root.get("lines-covered", 0)) coverage_data["overall"]["total_branches"] = int(root.get("branches-valid", 0)) coverage_data["overall"]["covered_branches"] = int(root.get("branches-covered", 0)) # 解析文件覆盖率 for package in root.findall(".//package"): for cls in package.findall(".//class"): file_data = { "file_path": cls.get("filename", "unknown"), "line_coverage": float(cls.get("line-rate", 0)) * 100, "branch_coverage": float(cls.get("branch-rate", 0)) * 100, "total_lines": int(cls.get("lines-valid", 0)), "covered_lines": int(cls.get("lines-covered", 0)), "total_branches": int(cls.get("branches-valid", 0)), "covered_branches": int(cls.get("branches-covered", 0)), "functions": [] } coverage_data["files"].append(file_data) except Exception as e: print(f"⚠️ 解析覆盖率数据失败: {e}") return coverage_data def _create_report(self, project_name: str, coverage_data: Dict[str, Any]) -> CoverageReport: """创建覆盖率报告""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 创建文件覆盖率数据 file_coverage = [] for file_data in coverage_data["files"]: file_coverage.append(CoverageData( file_path=file_data["file_path"], line_coverage=file_data["line_coverage"], branch_coverage=file_data["branch_coverage"], total_lines=file_data["total_lines"], covered_lines=file_data["covered_lines"], total_branches=file_data["total_branches"], covered_branches=file_data["covered_branches"], functions=file_data["functions"] )) # 创建报告 report = CoverageReport( project_name=project_name, timestamp=timestamp, overall_coverage=coverage_data["overall"], file_coverage=file_coverage, summary=self._create_summary(coverage_data) ) return report def _create_summary(self, coverage_data: Dict[str, Any]) -> Dict[str, Any]: """创建汇总信息""" overall = coverage_data["overall"] summary = { "total_files": len(coverage_data["files"]), "total_lines": overall["total_lines"], "covered_lines": overall["covered_lines"], "total_branches": overall["total_branches"], "covered_branches": overall["covered_branches"], "line_coverage_percent": overall["line_coverage"], "branch_coverage_percent": overall["branch_coverage"], "uncovered_lines": overall["total_lines"] - overall["covered_lines"], "uncovered_branches": overall["total_branches"] - overall["covered_branches"] } # 计算统计信息 if summary["total_files"] > 0: files_with_coverage = len([f for f in coverage_data["files"] if f["line_coverage"] > 0]) summary["files_with_coverage"] = files_with_coverage summary["files_without_coverage"] = summary["total_files"] - files_with_coverage else: summary["files_with_coverage"] = 0 summary["files_without_coverage"] = 0 return summary def _generate_html_report(self, report: CoverageReport): """生成HTML报告""" html_content = f""" {report.project_name} 覆盖率报告

{report.project_name} 覆盖率报告

生成时间: {report.timestamp}

总体统计

指标数值百分比
代码行覆盖率{report.summary['covered_lines']}/{report.summary['total_lines']}{report.summary['line_coverage_percent']:.1f}%
分支覆盖率{report.summary['covered_branches']}/{report.summary['total_branches']}{report.summary['branch_coverage_percent']:.1f}%
文件总数{report.summary['total_files']}-
有覆盖率的文件{report.summary['files_with_coverage']}-

文件覆盖率详情

""" for file_data in report.file_coverage: coverage_class = "coverage-high" if file_data.line_coverage >= 80 else "coverage-medium" if file_data.line_coverage >= 60 else "coverage-low" html_content += f"""

{file_data.file_path}

行覆盖率:

{file_data.line_coverage:.1f}% ({file_data.covered_lines}/{file_data.total_lines})

分支覆盖率: {file_data.branch_coverage:.1f}% ({file_data.covered_branches}/{file_data.total_branches})

""" html_content += """
""" # 保存HTML报告 html_path = self.output_dir / f"coverage_report_{report.timestamp.replace(' ', '_').replace(':', '-')}.html" with open(html_path, 'w', encoding='utf-8') as f: f.write(html_content) print(f"📄 HTML报告已生成: {html_path}") def _generate_json_report(self, report: CoverageReport): """生成JSON报告""" json_path = self.output_dir / f"coverage_report_{report.timestamp.replace(' ', '_').replace(':', '-')}.json" with open(json_path, 'w', encoding='utf-8') as f: json.dump(asdict(report), f, indent=2, ensure_ascii=False) print(f"📄 JSON报告已生成: {json_path}") def _generate_summary_report(self, report: CoverageReport): """生成汇总报告""" summary_content = f""" # {report.project_name} 覆盖率报告 **生成时间:** {report.timestamp} ## 总体统计 - **代码行覆盖率:** {report.summary['line_coverage_percent']:.1f}% ({report.summary['covered_lines']}/{report.summary['total_lines']}) - **分支覆盖率:** {report.summary['branch_coverage_percent']:.1f}% ({report.summary['covered_branches']}/{report.summary['total_branches']}) - **文件总数:** {report.summary['total_files']} - **有覆盖率的文件:** {report.summary['files_with_coverage']} - **未覆盖的行数:** {report.summary['uncovered_lines']} - **未覆盖的分支数:** {report.summary['uncovered_branches']} ## 文件覆盖率详情 """ for file_data in report.file_coverage: summary_content += f""" ### {file_data.file_path} - **行覆盖率:** {file_data.line_coverage:.1f}% ({file_data.covered_lines}/{file_data.total_lines}) - **分支覆盖率:** {file_data.branch_coverage:.1f}% ({file_data.covered_branches}/{file_data.total_branches}) """ # 保存汇总报告 summary_path = self.output_dir / f"coverage_summary_{report.timestamp.replace(' ', '_').replace(':', '-')}.md" with open(summary_path, 'w', encoding='utf-8') as f: f.write(summary_content) print(f"📄 汇总报告已生成: {summary_path}") def print_summary(self, report: CoverageReport): """打印汇总信息""" print("\n" + "="*60) print(f"📊 覆盖率报告 - {report.project_name}") print("="*60) print(f"时间: {report.timestamp}") print("-"*60) print(f"代码行覆盖率: {report.summary['line_coverage_percent']:.1f}% ({report.summary['covered_lines']}/{report.summary['total_lines']})") print(f"分支覆盖率: {report.summary['branch_coverage_percent']:.1f}% ({report.summary['covered_branches']}/{report.summary['total_branches']})") print(f"文件总数: {report.summary['total_files']}") print(f"有覆盖率的文件: {report.summary['files_with_coverage']}") print("-"*60) # 显示低覆盖率文件 low_coverage_files = [f for f in report.file_coverage if f.line_coverage < 60] if low_coverage_files: print("⚠️ 低覆盖率文件 (< 60%):") for file_data in low_coverage_files: print(f" - {file_data.file_path}: {file_data.line_coverage:.1f}%") def main(): """主函数""" parser = argparse.ArgumentParser(description='CodeDetect覆盖率报告生成器') parser.add_argument('--project', default='CodeDetect', help='项目名称') parser.add_argument('--output', default='coverage_reports', help='输出目录') parser.add_argument('--print-summary', action='store_true', help='打印汇总信息') args = parser.parse_args() reporter = CoverageReporter(args.output) report = reporter.generate_report(args.project) if args.print_summary: reporter.print_summary(report) if __name__ == "__main__": main()