#!/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()