| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- #!/usr/bin/env python3
- """
- 测试覆盖率分析工具
- 用于生成测试覆盖率报告,确保达到80%的目标覆盖率
- """
- import os
- import subprocess
- import json
- import coverage
- from pathlib import Path
- import matplotlib.pyplot as plt
- import numpy as np
-
-
- class TestCoverageAnalyzer:
- """测试覆盖率分析器"""
-
- def __init__(self, project_root="/tmp/water-management-system"):
- self.project_root = Path(project_root)
- self.src_dir = self.project_root / "src"
- self.tests_dir = self.project_root / "tests"
- self.coverage_report = self.project_root / "coverage_report"
-
- # 创建覆盖率报告目录
- self.coverage_report.mkdir(exist_ok=True)
-
- def run_coverage_analysis(self):
- """运行覆盖率分析"""
- print("🔍 开始分析测试覆盖率...")
-
- # 设置覆盖率配置
- cov = coverage.Coverage(
- source=[str(self.src_dir)],
- omit=[
- "*/tests/*",
- "*/__init__.py",
- "*/migrations/*",
- "*/config/*"
- ]
- )
-
- # 开始覆盖率收集
- cov.start()
-
- # 运行所有测试
- self.run_tests()
-
- # 停止覆盖率收集
- cov.stop()
-
- # 生成覆盖率报告
- self.generate_reports(cov)
-
- return self.analyze_coverage_results(cov)
-
- def run_tests(self):
- """运行测试"""
- print("🧪 运行单元测试...")
-
- # 运行单元测试
- unit_tests_dir = self.tests_dir / "unit"
- for test_file in unit_tests_dir.glob("test_*.py"):
- cmd = ["python", str(test_file)]
- result = subprocess.run(cmd, capture_output=True, text=True)
- if result.returncode != 0:
- print(f"❌ 测试失败: {test_file}")
- print(result.stderr)
- else:
- print(f"✅ 测试通过: {test_file}")
-
- print("🧪 运行集成测试...")
-
- # 运行集成测试
- integration_test_file = self.tests_dir / "integration" / "test_full_integration.py"
- if integration_test_file.exists():
- cmd = ["python", str(integration_test_file)]
- result = subprocess.run(cmd, capture_output=True, text=True)
- if result.returncode != 0:
- print("❌ 集成测试失败")
- print(result.stderr)
- else:
- print("✅ 集成测试通过")
-
- def generate_reports(self, cov):
- """生成覆盖率报告"""
- print("📊 生成覆盖率报告...")
-
- # 生成HTML报告
- html_report = self.coverage_report / "index.html"
- cov.html_report(str(html_report))
-
- # 生成XML报告
- xml_report = self.coverage_report / "coverage.xml"
- cov.xml_report(outfile=str(xml_report))
-
- # 生成JSON报告
- json_report = self.coverage_report / "coverage.json"
- cov_data = cov.get_data()
-
- # 收集每个文件的覆盖率数据
- file_coverage = {}
- for filename in cov_data.measured_files():
- lines = cov_data.lines(filename)
- num_statements = len(lines)
- num_covered = sum(1 for line in lines if cov_data.line_hit(filename, line))
-
- coverage_percentage = (num_covered / num_statements * 100) if num_statements > 0 else 0
-
- # 转换为相对于src_dir的路径
- relative_path = str(filename).replace(str(self.src_dir) + "/", "")
-
- file_coverage[relative_path] = {
- "total_lines": num_statements,
- "covered_lines": num_covered,
- "coverage_percentage": coverage_percentage,
- "filename": filename
- }
-
- # 计算总体覆盖率
- total_lines = sum(data["total_lines"] for data in file_coverage.values())
- total_covered = sum(data["covered_lines"] for data in file_coverage.values())
- overall_coverage = (total_covered / total_lines * 100) if total_lines > 0 else 0
-
- coverage_summary = {
- "overall_coverage": overall_coverage,
- "total_files": len(file_coverage),
- "total_lines": total_lines,
- "total_covered": total_covered,
- "target_coverage": 80.0,
- "files": file_coverage
- }
-
- with open(json_report, 'w', encoding='utf-8') as f:
- json.dump(coverage_summary, f, indent=2, ensure_ascii=False)
-
- print(f"📊 覆盖率报告已生成:")
- print(f" HTML报告: {html_report}")
- print(f" XML报告: {xml_report}")
- print(f" JSON报告: {json_report}")
-
- return coverage_summary
-
- def analyze_coverage_results(self, cov):
- """分析覆盖率结果"""
- print("🔍 分析覆盖率结果...")
-
- # 获取覆盖率数据
- cov_data = cov.get_data()
-
- # 统计文件覆盖率
- file_stats = []
- for filename in cov_data.measured_files():
- relative_path = str(filename).replace(str(self.src_dir) + "/", "")
- lines = cov_data.lines(filename)
- num_statements = len(lines)
- num_covered = sum(1 for line in lines if cov_data.line_hit(filename, line))
-
- coverage_percentage = (num_covered / num_statements * 100) if num_statements > 0 else 0
-
- file_stats.append({
- "filename": relative_path,
- "coverage": coverage_percentage,
- "total_lines": num_statements,
- "covered_lines": num_covered
- })
-
- # 按覆盖率排序
- file_stats.sort(key=lambda x: x["coverage"], reverse=True)
-
- # 识别需要改进的文件
- low_coverage_files = [f for f in file_stats if f["coverage"] < 80]
-
- # 计算总体覆盖率
- total_lines = sum(f["total_lines"] for f in file_stats)
- total_covered = sum(f["covered_lines"] for f in file_stats)
- overall_coverage = (total_covered / total_lines * 100) if total_lines > 0 else 0
-
- # 生成可视化图表
- self.generate_coverage_chart(file_stats)
-
- # 生成详细分析报告
- self.generate_detailed_report(file_stats, overall_coverage, low_coverage_files)
-
- return {
- "overall_coverage": overall_coverage,
- "total_files": len(file_stats),
- "low_coverage_files": len(low_coverage_files),
- "files": file_stats
- }
-
- def generate_coverage_chart(self, file_stats):
- """生成覆盖率图表"""
- print("📈 生成覆盖率图表...")
-
- # 准备数据
- files = [f["filename"] for f in file_stats[:20]] # 只显示前20个文件
- coverages = [f["coverage"] for f in file_stats[:20]]
-
- # 创建图表
- plt.figure(figsize=(12, 8))
- bars = plt.bar(range(len(files)), coverages, color='skyblue', alpha=0.7)
-
- # 添加目标线
- plt.axhline(y=80, color='red', linestyle='--', linewidth=2, label='目标覆盖率 (80%)')
-
- # 标记低于目标的文件
- for i, (file, coverage) in enumerate(zip(files, coverages)):
- if coverage < 80:
- bars[i].set_color('orange')
-
- # 设置图表属性
- plt.xlabel('文件名')
- plt.ylabel('覆盖率 (%)')
- plt.title('代码覆盖率分析')
- plt.xticks(range(len(files)), files, rotation=45, ha='right')
- plt.legend()
- plt.tight_layout()
-
- # 保存图表
- chart_path = self.coverage_report / "coverage_chart.png"
- plt.savefig(chart_path, dpi=300, bbox_inches='tight')
- plt.close()
-
- print(f"📈 覆率图表已保存: {chart_path}")
-
- def generate_detailed_report(self, file_stats, overall_coverage, low_coverage_files):
- """生成详细分析报告"""
- print("📝 生成详细分析报告...")
-
- report_path = self.coverage_report / "detailed_report.txt"
-
- with open(report_path, 'w', encoding='utf-8') as f:
- f.write("=" * 60 + "\n")
- f.write("测试覆盖率详细分析报告\n")
- f.write("=" * 60 + "\n\n")
-
- f.write(f"总体覆盖率: {overall_coverage:.2f}%\n")
- f.write(f"目标覆盖率: 80.00%\n")
- f.write(f"状态: {'✅ 达标' if overall_coverage >= 80 else '❌ 未达标'}\n\n")
-
- f.write("文件覆盖率统计:\n")
- f.write("-" * 60 + "\n")
- f.write(f"{'文件名':<40} {'覆盖率':<10} {'总行数':<8} {'覆盖行数':<8} {'状态':<10}\n")
- f.write("-" * 60 + "\n")
-
- for file_stat in file_stats:
- status = "✅ 达标" if file_stat["coverage"] >= 80 else "❌ 未达标"
- f.write(f"{file_stat['filename']:<40} {file_stat['coverage']:<8.2f} "
- f"{file_stat['total_lines']:<8} {file_stat['covered_lines']:<8} {status:<10}\n")
-
- f.write("\n")
-
- if low_coverage_files:
- f.write("需要改进的文件 (覆盖率 < 80%):\n")
- f.write("-" * 60 + "\n")
- for file_stat in low_coverage_files:
- f.write(f"📁 {file_stat['filename']}: {file_stat['coverage']:.2f}%\n")
- f.write(f" 总行数: {file_stat['total_lines']}, 覆盖行数: {file_stat['covered_lines']}\n")
- f.write(f" 需要: {max(0, int(file_stat['total_lines'] * 0.8) - file_stat['covered_lines'])} 行额外覆盖\n")
- else:
- f.write("🎉 所有文件都达到目标覆盖率!\n")
-
- print(f"📝 详细报告已保存: {report_path}")
-
- def get_coverage_summary(self):
- """获取覆盖率摘要"""
- json_report = self.coverage_report / "coverage.json"
-
- if json_report.exists():
- with open(json_report, 'r', encoding='utf-8') as f:
- return json.load(f)
- else:
- return None
-
-
- def main():
- """主函数"""
- analyzer = TestCoverageAnalyzer()
-
- try:
- # 运行覆盖率分析
- results = analyzer.run_coverage_analysis()
-
- # 输出摘要
- print("\n" + "=" * 60)
- print("🎯 测试覆盖率分析完成")
- print("=" * 60)
- print(f"📊 总体覆盖率: {results['overall_coverage']:.2f}%")
- print(f"📁 测试文件数: {results['total_files']}")
- print(f"⚠️ 低覆盖率文件数: {results['low_coverage_files']}")
-
- target_met = results['overall_coverage'] >= 80
- print(f"🎯 目标达成: {'✅ 是' if target_met else '❌ 否'} (目标: 80%)")
-
- if not target_met:
- print("\n📋 改进建议:")
- print("1. 增加更多测试用例")
- print("2. 提高现有测试的覆盖范围")
- print("3. 重点改进低覆盖率文件的测试")
- print("4. 考虑使用覆盖率工具指导测试编写")
-
- print(f"\n📁 详细报告请查看: {analyzer.coverage_report}")
-
- return results
-
- except Exception as e:
- print(f"❌ 分析失败: {str(e)}")
- return None
-
-
- if __name__ == "__main__":
- main()
|