You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cbmc/codedetect/quality/coverage_report.py

369 lines
14 KiB

#!/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 = """<?xml version="1.0" ?>
<coverage version="5.0" timestamp="1620000000" line-rate="0.0" branch-rate="0.0" line-covered="0" lines-valid="0" branches-covered="0" branches-valid="0">
<sources>
<source>.</source>
</sources>
<packages>
<package name="src" line-rate="0.0" branch-rate="0.0" complexity="0.0">
<classes>
<class name="minimal" filename="src/minimal.py" line-rate="0.0" branch-rate="0.0" complexity="0.0">
<methods/>
<lines/>
</class>
</classes>
</package>
</packages>
</coverage>"""
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"""
<!DOCTYPE html>
<html>
<head>
<title>{report.project_name} 覆盖率报告</title>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.header {{ background-color: #f0f0f0; padding: 20px; border-radius: 5px; }}
.summary {{ background-color: #e8f5e8; padding: 15px; margin: 20px 0; border-radius: 5px; }}
.file-list {{ margin: 20px 0; }}
.file-item {{ border: 1px solid #ddd; margin: 10px 0; padding: 10px; border-radius: 5px; }}
.coverage-high {{ background-color: #d4edda; }}
.coverage-medium {{ background-color: #fff3cd; }}
.coverage-low {{ background-color: #f8d7da; }}
.progress-bar {{ width: 100px; height: 20px; background-color: #e0e0e0; border-radius: 10px; display: inline-block; }}
.progress-fill {{ height: 100%; background-color: #28a745; border-radius: 10px; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f2f2f2; }}
</style>
</head>
<body>
<div class="header">
<h1>{report.project_name} 覆盖率报告</h1>
<p>生成时间: {report.timestamp}</p>
</div>
<div class="summary">
<h2>总体统计</h2>
<table>
<tr><th>指标</th><th>数值</th><th>百分比</th></tr>
<tr><td>代码行覆盖率</td><td>{report.summary['covered_lines']}/{report.summary['total_lines']}</td><td>{report.summary['line_coverage_percent']:.1f}%</td></tr>
<tr><td>分支覆盖率</td><td>{report.summary['covered_branches']}/{report.summary['total_branches']}</td><td>{report.summary['branch_coverage_percent']:.1f}%</td></tr>
<tr><td>文件总数</td><td>{report.summary['total_files']}</td><td>-</td></tr>
<tr><td>有覆盖率的文件</td><td>{report.summary['files_with_coverage']}</td><td>-</td></tr>
</table>
</div>
<div class="file-list">
<h2>文件覆盖率详情</h2>
"""
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"""
<div class="file-item {coverage_class}">
<h3>{file_data.file_path}</h3>
<p>
<strong>行覆盖率:</strong>
<div class="progress-bar">
<div class="progress-fill" style="width: {file_data.line_coverage}%;"></div>
</div>
{file_data.line_coverage:.1f}% ({file_data.covered_lines}/{file_data.total_lines})
</p>
<p><strong>分支覆盖率:</strong> {file_data.branch_coverage:.1f}% ({file_data.covered_branches}/{file_data.total_branches})</p>
</div>
"""
html_content += """
</div>
</body>
</html>
"""
# 保存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()