智慧水务管理系统 - 精河县供水工程综合管理平台

test_coverage.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. #!/usr/bin/env python3
  2. """
  3. 测试覆盖率分析工具
  4. 用于生成测试覆盖率报告,确保达到80%的目标覆盖率
  5. """
  6. import os
  7. import subprocess
  8. import json
  9. import coverage
  10. from pathlib import Path
  11. import matplotlib.pyplot as plt
  12. import numpy as np
  13. class TestCoverageAnalyzer:
  14. """测试覆盖率分析器"""
  15. def __init__(self, project_root="/tmp/water-management-system"):
  16. self.project_root = Path(project_root)
  17. self.src_dir = self.project_root / "src"
  18. self.tests_dir = self.project_root / "tests"
  19. self.coverage_report = self.project_root / "coverage_report"
  20. # 创建覆盖率报告目录
  21. self.coverage_report.mkdir(exist_ok=True)
  22. def run_coverage_analysis(self):
  23. """运行覆盖率分析"""
  24. print("🔍 开始分析测试覆盖率...")
  25. # 设置覆盖率配置
  26. cov = coverage.Coverage(
  27. source=[str(self.src_dir)],
  28. omit=[
  29. "*/tests/*",
  30. "*/__init__.py",
  31. "*/migrations/*",
  32. "*/config/*"
  33. ]
  34. )
  35. # 开始覆盖率收集
  36. cov.start()
  37. # 运行所有测试
  38. self.run_tests()
  39. # 停止覆盖率收集
  40. cov.stop()
  41. # 生成覆盖率报告
  42. self.generate_reports(cov)
  43. return self.analyze_coverage_results(cov)
  44. def run_tests(self):
  45. """运行测试"""
  46. print("🧪 运行单元测试...")
  47. # 运行单元测试
  48. unit_tests_dir = self.tests_dir / "unit"
  49. for test_file in unit_tests_dir.glob("test_*.py"):
  50. cmd = ["python", str(test_file)]
  51. result = subprocess.run(cmd, capture_output=True, text=True)
  52. if result.returncode != 0:
  53. print(f"❌ 测试失败: {test_file}")
  54. print(result.stderr)
  55. else:
  56. print(f"✅ 测试通过: {test_file}")
  57. print("🧪 运行集成测试...")
  58. # 运行集成测试
  59. integration_test_file = self.tests_dir / "integration" / "test_full_integration.py"
  60. if integration_test_file.exists():
  61. cmd = ["python", str(integration_test_file)]
  62. result = subprocess.run(cmd, capture_output=True, text=True)
  63. if result.returncode != 0:
  64. print("❌ 集成测试失败")
  65. print(result.stderr)
  66. else:
  67. print("✅ 集成测试通过")
  68. def generate_reports(self, cov):
  69. """生成覆盖率报告"""
  70. print("📊 生成覆盖率报告...")
  71. # 生成HTML报告
  72. html_report = self.coverage_report / "index.html"
  73. cov.html_report(str(html_report))
  74. # 生成XML报告
  75. xml_report = self.coverage_report / "coverage.xml"
  76. cov.xml_report(outfile=str(xml_report))
  77. # 生成JSON报告
  78. json_report = self.coverage_report / "coverage.json"
  79. cov_data = cov.get_data()
  80. # 收集每个文件的覆盖率数据
  81. file_coverage = {}
  82. for filename in cov_data.measured_files():
  83. lines = cov_data.lines(filename)
  84. num_statements = len(lines)
  85. num_covered = sum(1 for line in lines if cov_data.line_hit(filename, line))
  86. coverage_percentage = (num_covered / num_statements * 100) if num_statements > 0 else 0
  87. # 转换为相对于src_dir的路径
  88. relative_path = str(filename).replace(str(self.src_dir) + "/", "")
  89. file_coverage[relative_path] = {
  90. "total_lines": num_statements,
  91. "covered_lines": num_covered,
  92. "coverage_percentage": coverage_percentage,
  93. "filename": filename
  94. }
  95. # 计算总体覆盖率
  96. total_lines = sum(data["total_lines"] for data in file_coverage.values())
  97. total_covered = sum(data["covered_lines"] for data in file_coverage.values())
  98. overall_coverage = (total_covered / total_lines * 100) if total_lines > 0 else 0
  99. coverage_summary = {
  100. "overall_coverage": overall_coverage,
  101. "total_files": len(file_coverage),
  102. "total_lines": total_lines,
  103. "total_covered": total_covered,
  104. "target_coverage": 80.0,
  105. "files": file_coverage
  106. }
  107. with open(json_report, 'w', encoding='utf-8') as f:
  108. json.dump(coverage_summary, f, indent=2, ensure_ascii=False)
  109. print(f"📊 覆盖率报告已生成:")
  110. print(f" HTML报告: {html_report}")
  111. print(f" XML报告: {xml_report}")
  112. print(f" JSON报告: {json_report}")
  113. return coverage_summary
  114. def analyze_coverage_results(self, cov):
  115. """分析覆盖率结果"""
  116. print("🔍 分析覆盖率结果...")
  117. # 获取覆盖率数据
  118. cov_data = cov.get_data()
  119. # 统计文件覆盖率
  120. file_stats = []
  121. for filename in cov_data.measured_files():
  122. relative_path = str(filename).replace(str(self.src_dir) + "/", "")
  123. lines = cov_data.lines(filename)
  124. num_statements = len(lines)
  125. num_covered = sum(1 for line in lines if cov_data.line_hit(filename, line))
  126. coverage_percentage = (num_covered / num_statements * 100) if num_statements > 0 else 0
  127. file_stats.append({
  128. "filename": relative_path,
  129. "coverage": coverage_percentage,
  130. "total_lines": num_statements,
  131. "covered_lines": num_covered
  132. })
  133. # 按覆盖率排序
  134. file_stats.sort(key=lambda x: x["coverage"], reverse=True)
  135. # 识别需要改进的文件
  136. low_coverage_files = [f for f in file_stats if f["coverage"] < 80]
  137. # 计算总体覆盖率
  138. total_lines = sum(f["total_lines"] for f in file_stats)
  139. total_covered = sum(f["covered_lines"] for f in file_stats)
  140. overall_coverage = (total_covered / total_lines * 100) if total_lines > 0 else 0
  141. # 生成可视化图表
  142. self.generate_coverage_chart(file_stats)
  143. # 生成详细分析报告
  144. self.generate_detailed_report(file_stats, overall_coverage, low_coverage_files)
  145. return {
  146. "overall_coverage": overall_coverage,
  147. "total_files": len(file_stats),
  148. "low_coverage_files": len(low_coverage_files),
  149. "files": file_stats
  150. }
  151. def generate_coverage_chart(self, file_stats):
  152. """生成覆盖率图表"""
  153. print("📈 生成覆盖率图表...")
  154. # 准备数据
  155. files = [f["filename"] for f in file_stats[:20]] # 只显示前20个文件
  156. coverages = [f["coverage"] for f in file_stats[:20]]
  157. # 创建图表
  158. plt.figure(figsize=(12, 8))
  159. bars = plt.bar(range(len(files)), coverages, color='skyblue', alpha=0.7)
  160. # 添加目标线
  161. plt.axhline(y=80, color='red', linestyle='--', linewidth=2, label='目标覆盖率 (80%)')
  162. # 标记低于目标的文件
  163. for i, (file, coverage) in enumerate(zip(files, coverages)):
  164. if coverage < 80:
  165. bars[i].set_color('orange')
  166. # 设置图表属性
  167. plt.xlabel('文件名')
  168. plt.ylabel('覆盖率 (%)')
  169. plt.title('代码覆盖率分析')
  170. plt.xticks(range(len(files)), files, rotation=45, ha='right')
  171. plt.legend()
  172. plt.tight_layout()
  173. # 保存图表
  174. chart_path = self.coverage_report / "coverage_chart.png"
  175. plt.savefig(chart_path, dpi=300, bbox_inches='tight')
  176. plt.close()
  177. print(f"📈 覆率图表已保存: {chart_path}")
  178. def generate_detailed_report(self, file_stats, overall_coverage, low_coverage_files):
  179. """生成详细分析报告"""
  180. print("📝 生成详细分析报告...")
  181. report_path = self.coverage_report / "detailed_report.txt"
  182. with open(report_path, 'w', encoding='utf-8') as f:
  183. f.write("=" * 60 + "\n")
  184. f.write("测试覆盖率详细分析报告\n")
  185. f.write("=" * 60 + "\n\n")
  186. f.write(f"总体覆盖率: {overall_coverage:.2f}%\n")
  187. f.write(f"目标覆盖率: 80.00%\n")
  188. f.write(f"状态: {'✅ 达标' if overall_coverage >= 80 else '❌ 未达标'}\n\n")
  189. f.write("文件覆盖率统计:\n")
  190. f.write("-" * 60 + "\n")
  191. f.write(f"{'文件名':<40} {'覆盖率':<10} {'总行数':<8} {'覆盖行数':<8} {'状态':<10}\n")
  192. f.write("-" * 60 + "\n")
  193. for file_stat in file_stats:
  194. status = "✅ 达标" if file_stat["coverage"] >= 80 else "❌ 未达标"
  195. f.write(f"{file_stat['filename']:<40} {file_stat['coverage']:<8.2f} "
  196. f"{file_stat['total_lines']:<8} {file_stat['covered_lines']:<8} {status:<10}\n")
  197. f.write("\n")
  198. if low_coverage_files:
  199. f.write("需要改进的文件 (覆盖率 < 80%):\n")
  200. f.write("-" * 60 + "\n")
  201. for file_stat in low_coverage_files:
  202. f.write(f"📁 {file_stat['filename']}: {file_stat['coverage']:.2f}%\n")
  203. f.write(f" 总行数: {file_stat['total_lines']}, 覆盖行数: {file_stat['covered_lines']}\n")
  204. f.write(f" 需要: {max(0, int(file_stat['total_lines'] * 0.8) - file_stat['covered_lines'])} 行额外覆盖\n")
  205. else:
  206. f.write("🎉 所有文件都达到目标覆盖率!\n")
  207. print(f"📝 详细报告已保存: {report_path}")
  208. def get_coverage_summary(self):
  209. """获取覆盖率摘要"""
  210. json_report = self.coverage_report / "coverage.json"
  211. if json_report.exists():
  212. with open(json_report, 'r', encoding='utf-8') as f:
  213. return json.load(f)
  214. else:
  215. return None
  216. def main():
  217. """主函数"""
  218. analyzer = TestCoverageAnalyzer()
  219. try:
  220. # 运行覆盖率分析
  221. results = analyzer.run_coverage_analysis()
  222. # 输出摘要
  223. print("\n" + "=" * 60)
  224. print("🎯 测试覆盖率分析完成")
  225. print("=" * 60)
  226. print(f"📊 总体覆盖率: {results['overall_coverage']:.2f}%")
  227. print(f"📁 测试文件数: {results['total_files']}")
  228. print(f"⚠️ 低覆盖率文件数: {results['low_coverage_files']}")
  229. target_met = results['overall_coverage'] >= 80
  230. print(f"🎯 目标达成: {'✅ 是' if target_met else '❌ 否'} (目标: 80%)")
  231. if not target_met:
  232. print("\n📋 改进建议:")
  233. print("1. 增加更多测试用例")
  234. print("2. 提高现有测试的覆盖范围")
  235. print("3. 重点改进低覆盖率文件的测试")
  236. print("4. 考虑使用覆盖率工具指导测试编写")
  237. print(f"\n📁 详细报告请查看: {analyzer.coverage_report}")
  238. return results
  239. except Exception as e:
  240. print(f"❌ 分析失败: {str(e)}")
  241. return None
  242. if __name__ == "__main__":
  243. main()