coverage-tool 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. coverage_tool/__init__.py +7 -0
  2. coverage_tool/analyzers/__init__.py +14 -0
  3. coverage_tool/analyzers/dependency.py +513 -0
  4. coverage_tool/converters/__init__.py +60 -0
  5. coverage_tool/converters/base.py +83 -0
  6. coverage_tool/converters/cunit_converter.py +47 -0
  7. coverage_tool/converters/factory.py +36 -0
  8. coverage_tool/converters/generic_converter.py +62 -0
  9. coverage_tool/converters/go_converter.py +178 -0
  10. coverage_tool/converters/gtest_converter.py +63 -0
  11. coverage_tool/core/__init__.py +18 -0
  12. coverage_tool/core/config.py +66 -0
  13. coverage_tool/core/reporter.py +270 -0
  14. coverage_tool/example.py +102 -0
  15. coverage_tool/handlers/__init__.py +13 -0
  16. coverage_tool/handlers/compilation.py +322 -0
  17. coverage_tool/handlers/env_checker.py +312 -0
  18. coverage_tool/main.py +559 -0
  19. coverage_tool/parsers/__init__.py +15 -0
  20. coverage_tool/parsers/junit.py +172 -0
  21. coverage_tool/runners/__init__.py +26 -0
  22. coverage_tool/runners/base.py +249 -0
  23. coverage_tool/runners/c_runner.py +66 -0
  24. coverage_tool/runners/cpp_runner.py +65 -0
  25. coverage_tool/runners/factory.py +41 -0
  26. coverage_tool/runners/go_runner.py +158 -0
  27. coverage_tool/runners/java_runner.py +54 -0
  28. coverage_tool/runners/python_runner.py +99 -0
  29. coverage_tool/test_removers/__init__.py +38 -0
  30. coverage_tool/test_removers/base.py +233 -0
  31. coverage_tool/test_removers/c_remover.py +210 -0
  32. coverage_tool/test_removers/cpp_remover.py +272 -0
  33. coverage_tool/test_removers/factory.py +45 -0
  34. coverage_tool/test_removers/go_remover.py +112 -0
  35. coverage_tool/test_removers/java_remover.py +216 -0
  36. coverage_tool/test_removers/python_remover.py +172 -0
  37. coverage_tool/utils/__init__.py +23 -0
  38. coverage_tool/utils/dependency_graph.py +52 -0
  39. coverage_tool/utils/file_backup.py +112 -0
  40. coverage_tool/utils/helpers.py +89 -0
  41. coverage_tool/utils/logger.py +54 -0
  42. coverage_tool/utils/progress.py +41 -0
  43. coverage_tool-1.0.0.dist-info/METADATA +484 -0
  44. coverage_tool-1.0.0.dist-info/RECORD +47 -0
  45. coverage_tool-1.0.0.dist-info/WHEEL +5 -0
  46. coverage_tool-1.0.0.dist-info/entry_points.txt +2 -0
  47. coverage_tool-1.0.0.dist-info/top_level.txt +1 -0
coverage_tool/main.py ADDED
@@ -0,0 +1,559 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 多语言单测覆盖率统计工具
4
+ 支持 Python, Java, Go, C, C++
5
+ """
6
+
7
+ import argparse
8
+ import os
9
+ import sys
10
+ import tempfile
11
+ import shutil
12
+ from pathlib import Path
13
+ from datetime import datetime
14
+ from typing import List, Dict, Optional
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum
17
+
18
+ from coverage_tool.core import Language, LANGUAGE_CONFIGS
19
+ from coverage_tool.core import CoverageReporter, CoverageReport, CoverageStats, ReportFormat
20
+ from coverage_tool.runners.factory import RunnerFactory
21
+ from coverage_tool.runners.base import RunResult
22
+ from coverage_tool.parsers import JunitXMLParser, TestReport
23
+ from coverage_tool.handlers import SmartCompilationHandler, EnvironmentChecker
24
+ from coverage_tool.test_removers import SmartTestRemoverFactory, BaseSmartTestRemover
25
+ from coverage_tool.utils import Logger, ProgressTracker, FileBackupManager
26
+
27
+
28
+ class ExecutionPhase(Enum):
29
+ """执行阶段"""
30
+ ENV_CHECK = "环境检查"
31
+ COMPILATION = "编译"
32
+ TEST_EXECUTION = "测试执行"
33
+ TEST_CLEANUP = "测试清理"
34
+ COVERAGE_CALCULATION = "覆盖率计算"
35
+ REPORT_GENERATION = "报告生成"
36
+
37
+
38
+ @dataclass
39
+ class ExecutionResult:
40
+ """执行结果"""
41
+ success: bool
42
+ phase: ExecutionPhase
43
+ message: str
44
+ details: Dict = field(default_factory=dict)
45
+
46
+
47
+ class CoverageTool:
48
+ """覆盖率工具主类"""
49
+
50
+ def __init__(self, config: 'ToolConfig'):
51
+ self.config = config
52
+ self.logger = Logger(verbose=True)
53
+ self.backup_manager = FileBackupManager()
54
+ self.execution_results: List[ExecutionResult] = []
55
+ self.temp_dir = tempfile.mkdtemp(prefix="coverage_tool_")
56
+
57
+ def __enter__(self):
58
+ return self
59
+
60
+ def __exit__(self, exc_type, exc_val, exc_tb):
61
+ self.cleanup()
62
+
63
+ def cleanup(self):
64
+ """清理资源"""
65
+ if os.path.exists(self.temp_dir):
66
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
67
+ self.backup_manager.cleanup()
68
+
69
+ def run(self) -> CoverageReport:
70
+ """运行完整的覆盖率分析流程"""
71
+ self.logger.info("=" * 80)
72
+ self.logger.info(f"开始分析 {self.config.language.value} 项目: {self.config.target_dir}")
73
+ self.logger.info("=" * 80)
74
+
75
+ # 阶段1: 环境检查
76
+ if not self._check_environment():
77
+ return self._create_error_report("环境检查失败")
78
+
79
+ # 阶段2: 编译(带失败处理)
80
+ compile_success = self._compile_with_retry()
81
+ if not compile_success:
82
+ self.logger.warning("编译未完全成功,但继续尝试运行测试")
83
+
84
+ # 阶段3: 运行测试
85
+ test_result = self._run_tests()
86
+ if not test_result.junit_xml_path:
87
+ return self._create_error_report("测试执行失败或未生成报告")
88
+
89
+ # 阶段4: 解析测试报告
90
+ test_report = self._parse_test_report(test_result.junit_xml_path)
91
+ if not test_report:
92
+ return self._create_error_report("解析测试报告失败")
93
+
94
+ # 阶段5: 清理失败的测试
95
+ if test_report.total_failures > 0 or test_report.total_errors > 0:
96
+ test_report = self._cleanup_failed_tests(test_report)
97
+
98
+ # 阶段6: 生成报告
99
+ report = self._generate_report(test_report, test_result)
100
+
101
+ self.logger.info("=" * 80)
102
+ self.logger.info("分析完成")
103
+ self.logger.info("=" * 80)
104
+
105
+ return report
106
+
107
+ def _check_environment(self) -> bool:
108
+ """检查环境"""
109
+ self.logger.info(f"\n[{ExecutionPhase.ENV_CHECK.value}] 检查编译和测试环境...")
110
+
111
+ checker = EnvironmentChecker()
112
+ env_report = checker.check_language_environment(
113
+ self.config.language.value,
114
+ self.config.target_dir
115
+ )
116
+ checker.print_report(env_report)
117
+
118
+ if not env_report.all_passed:
119
+ self.logger.error("环境检查未通过,请先安装缺失的依赖")
120
+ self.execution_results.append(ExecutionResult(
121
+ success=False,
122
+ phase=ExecutionPhase.ENV_CHECK,
123
+ message="环境检查未通过",
124
+ details={'missing': env_report.critical_missing}
125
+ ))
126
+ return False
127
+
128
+ self.execution_results.append(ExecutionResult(
129
+ success=True,
130
+ phase=ExecutionPhase.ENV_CHECK,
131
+ message="环境检查通过"
132
+ ))
133
+ return True
134
+
135
+ def _compile_with_retry(self) -> bool:
136
+ """编译(支持失败重试)"""
137
+ self.logger.info(f"\n[{ExecutionPhase.COMPILATION.value}] 编译项目...")
138
+
139
+ runner = RunnerFactory.get_runner(self.config.language, self.logger)
140
+
141
+ # 首次编译尝试
142
+ success, errors = runner.compile(self.config.target_dir)
143
+
144
+ if success:
145
+ self.logger.info("✓ 编译成功")
146
+ self.execution_results.append(ExecutionResult(
147
+ success=True,
148
+ phase=ExecutionPhase.COMPILATION,
149
+ message="编译成功"
150
+ ))
151
+ return True
152
+
153
+ # 编译失败,尝试修复
154
+ if not self.config.delete_on_compile_failure:
155
+ self.logger.error(f"编译失败(未启用自动删除): {len(errors)} 个错误")
156
+ self.execution_results.append(ExecutionResult(
157
+ success=False,
158
+ phase=ExecutionPhase.COMPILATION,
159
+ message="编译失败",
160
+ details={'errors': errors[:10]}
161
+ ))
162
+ return False
163
+
164
+ self.logger.warning(f"编译失败,尝试自动修复... ({len(errors)} 个错误)")
165
+
166
+ # 使用智能编译处理器
167
+ handler = SmartCompilationHandler(self.config.target_dir, self.logger)
168
+
169
+ def compile_func(target_dir):
170
+ return runner.compile(target_dir)
171
+
172
+ success, deleted_files, remaining_errors = handler.handle_compilation_failure(
173
+ errors,
174
+ compile_func,
175
+ max_iterations=self.config.max_compile_iterations
176
+ )
177
+
178
+ self.execution_results.append(ExecutionResult(
179
+ success=success,
180
+ phase=ExecutionPhase.COMPILATION,
181
+ message="编译修复完成" if success else "编译修复失败",
182
+ details={
183
+ 'deleted_files': deleted_files,
184
+ 'remaining_errors': remaining_errors[:10] if remaining_errors else []
185
+ }
186
+ ))
187
+
188
+ if deleted_files:
189
+ self.logger.info(f"删除了 {len(deleted_files)} 个问题文件以修复编译")
190
+
191
+ return success
192
+
193
+ def _run_tests(self) -> RunResult:
194
+ """运行测试"""
195
+ self.logger.info(f"\n[{ExecutionPhase.TEST_EXECUTION.value}] 运行测试...")
196
+
197
+ runner = RunnerFactory.get_runner(self.config.language, self.logger)
198
+ junit_output = os.path.join(self.temp_dir, "junit_report.xml")
199
+
200
+ result = runner.run_tests(self.config.target_dir, junit_output)
201
+
202
+ if result.success:
203
+ self.logger.info(f"✓ 测试完成,耗时: {result.duration:.2f}s")
204
+ else:
205
+ self.logger.warning(f"测试返回非零状态: {result.return_code}")
206
+
207
+ self.execution_results.append(ExecutionResult(
208
+ success=result.success,
209
+ phase=ExecutionPhase.TEST_EXECUTION,
210
+ message="测试执行完成",
211
+ details={
212
+ 'duration': result.duration,
213
+ 'return_code': result.return_code,
214
+ 'junit_xml': result.junit_xml_path
215
+ }
216
+ ))
217
+
218
+ return result
219
+
220
+ def _parse_test_report(self, junit_xml_path: str) -> Optional[TestReport]:
221
+ """解析测试报告"""
222
+ if not os.path.exists(junit_xml_path):
223
+ self.logger.error(f"JUnit XML文件不存在: {junit_xml_path}")
224
+ return None
225
+
226
+ try:
227
+ test_report = JunitXMLParser.parse(junit_xml_path)
228
+
229
+ self.logger.info(f"\n测试用例统计:")
230
+ self.logger.info(f" 总数: {test_report.total_tests}")
231
+ self.logger.info(f" 通过: {test_report.total_tests - test_report.total_failures - test_report.total_errors - test_report.total_skipped}")
232
+ self.logger.info(f" 失败: {test_report.total_failures}")
233
+ self.logger.info(f" 错误: {test_report.total_errors}")
234
+ self.logger.info(f" 跳过: {test_report.total_skipped}")
235
+ self.logger.info(f" 通过率: {test_report.pass_rate:.2f}%")
236
+
237
+ return test_report
238
+
239
+ except Exception as e:
240
+ self.logger.error(f"解析测试报告失败: {e}")
241
+ return None
242
+
243
+ def _cleanup_failed_tests(self, test_report: TestReport) -> TestReport:
244
+ """清理失败的测试"""
245
+ self.logger.info(f"\n[{ExecutionPhase.TEST_CLEANUP.value}] 清理失败的测试...")
246
+ self.logger.info(f"发现 {test_report.total_failures} 个失败,{test_report.total_errors} 个错误")
247
+
248
+ remover = SmartTestRemoverFactory.get_remover(
249
+ self.config.language.value,
250
+ self.config.target_dir,
251
+ self.logger
252
+ )
253
+
254
+ max_iterations = self.config.max_cleanup_iterations
255
+ current_report = test_report
256
+ iteration = 0
257
+
258
+ for i in range(max_iterations):
259
+ iteration = i
260
+ if current_report.total_failures == 0 and current_report.total_errors == 0:
261
+ break
262
+
263
+ self.logger.info(f"\n清理迭代 {iteration + 1}/{max_iterations}...")
264
+
265
+ deleted_count, deleted_list = remover.remove_failed_tests(current_report)
266
+
267
+ if deleted_count == 0:
268
+ self.logger.warning("未能找到可清理的测试函数")
269
+ break
270
+
271
+ self.logger.info(f"已清理 {deleted_count} 个失败测试函数")
272
+
273
+ # 重新运行测试
274
+ runner = RunnerFactory.get_runner(self.config.language, self.logger)
275
+ new_junit_output = os.path.join(self.temp_dir, f"junit_report_{iteration}.xml")
276
+ test_result = runner.run_tests(self.config.target_dir, new_junit_output)
277
+
278
+ if os.path.exists(new_junit_output):
279
+ try:
280
+ current_report = JunitXMLParser.parse(new_junit_output)
281
+ self.logger.info(f"重新测试结果: 通过 {current_report.pass_rate:.2f}%")
282
+ except Exception as e:
283
+ self.logger.error(f"解析新测试报告失败: {e}")
284
+ break
285
+ else:
286
+ self.logger.warning("未生成新的测试报告")
287
+ break
288
+
289
+ self.execution_results.append(ExecutionResult(
290
+ success=True,
291
+ phase=ExecutionPhase.TEST_CLEANUP,
292
+ message="测试清理完成",
293
+ details={
294
+ 'iterations': iteration + 1,
295
+ 'final_failures': current_report.total_failures,
296
+ 'final_errors': current_report.total_errors
297
+ }
298
+ ))
299
+
300
+ return current_report
301
+
302
+ def _generate_report(self, test_report: TestReport, test_result: RunResult) -> CoverageReport:
303
+ """生成覆盖率报告"""
304
+ self.logger.info(f"\n[{ExecutionPhase.REPORT_GENERATION.value}] 生成报告...")
305
+
306
+ # 计算覆盖率
307
+ coverage_rate = test_result.coverage_rate
308
+ if coverage_rate == 0:
309
+ # 尝试重新获取
310
+ runner = RunnerFactory.get_runner(self.config.language, self.logger)
311
+ junit_output = os.path.join(self.temp_dir, "junit_report.xml")
312
+ coverage_rate = runner.get_coverage(junit_output)
313
+
314
+ self.logger.info(f"代码覆盖率: {coverage_rate:.2f}%")
315
+
316
+ # 统计文件数量
317
+ runner = RunnerFactory.get_runner(self.config.language, self.logger)
318
+ test_files = len(runner.find_files(
319
+ self.config.target_dir,
320
+ LANGUAGE_CONFIGS[self.config.language].test_file_pattern
321
+ ))
322
+ source_files = len(runner.find_files(
323
+ self.config.target_dir,
324
+ LANGUAGE_CONFIGS[self.config.language].source_file_pattern
325
+ ))
326
+
327
+ # 创建统计
328
+ stats = CoverageStats(
329
+ language=self.config.language.value,
330
+ total_tests=test_report.total_tests,
331
+ passed_tests=test_report.total_tests - test_report.total_failures - test_report.total_errors - test_report.total_skipped,
332
+ failed_tests=test_report.total_failures,
333
+ error_tests=test_report.total_errors,
334
+ skipped_tests=test_report.total_skipped,
335
+ pass_rate=test_report.pass_rate,
336
+ coverage_rate=coverage_rate,
337
+ duration=test_result.duration,
338
+ test_files=test_files,
339
+ source_files=source_files,
340
+ deleted_files=0 # 可以从execution_results计算
341
+ )
342
+
343
+ report = CoverageReport(
344
+ project_name=os.path.basename(self.config.target_dir),
345
+ stats=[stats],
346
+ total_duration=test_result.duration,
347
+ overall_pass_rate=stats.pass_rate,
348
+ overall_coverage=coverage_rate
349
+ )
350
+
351
+ # 生成报告文件
352
+ reporter = CoverageReporter(report)
353
+ output_path = f"{self.config.output_file}.{self.config.report_format.value}"
354
+ content = reporter.generate(output_path, self.config.report_format)
355
+
356
+ self.logger.info(f"报告已生成: {output_path}")
357
+
358
+ self.execution_results.append(ExecutionResult(
359
+ success=True,
360
+ phase=ExecutionPhase.REPORT_GENERATION,
361
+ message="报告生成完成",
362
+ details={'output_file': output_path}
363
+ ))
364
+
365
+ return report
366
+
367
+ def _create_error_report(self, message: str) -> CoverageReport:
368
+ """创建错误报告"""
369
+ self.logger.error(f"\n错误: {message}")
370
+
371
+ stats = CoverageStats(
372
+ language=self.config.language.value,
373
+ total_tests=0,
374
+ passed_tests=0,
375
+ failed_tests=0,
376
+ error_tests=0,
377
+ skipped_tests=0,
378
+ pass_rate=0.0,
379
+ coverage_rate=0.0,
380
+ duration=0.0,
381
+ test_files=0,
382
+ source_files=0,
383
+ deleted_files=0
384
+ )
385
+
386
+ return CoverageReport(
387
+ project_name=os.path.basename(self.config.target_dir),
388
+ stats=[stats],
389
+ total_duration=0.0,
390
+ overall_pass_rate=0.0,
391
+ overall_coverage=0.0
392
+ )
393
+
394
+
395
+ @dataclass
396
+ class ToolConfig:
397
+ """工具配置"""
398
+ language: Language
399
+ target_dir: str
400
+ output_file: str = "coverage_report"
401
+ report_format: ReportFormat = ReportFormat.TEXT
402
+ compile_timeout: int = 60
403
+ test_timeout: int = 300
404
+ delete_on_compile_failure: bool = True
405
+ max_compile_iterations: int = 10
406
+ max_cleanup_iterations: int = 10
407
+
408
+
409
+ def parse_args():
410
+ """解析命令行参数"""
411
+ parser = argparse.ArgumentParser(
412
+ description="多语言单测覆盖率统计工具",
413
+ formatter_class=argparse.RawDescriptionHelpFormatter,
414
+ epilog="""
415
+ 示例:
416
+ # Python项目
417
+ coverage-tool --language python --target ./src --output report
418
+
419
+ # Java项目
420
+ coverage-tool --language java --target ./project --format json
421
+
422
+ # Go项目
423
+ coverage-tool --language go --target ./module --format html
424
+
425
+ # C项目
426
+ coverage-tool --language c --target ./src --format text
427
+
428
+ # C++项目
429
+ coverage-tool --language cpp --target ./src --format csv
430
+ """
431
+ )
432
+
433
+ parser.add_argument(
434
+ "-l", "--language",
435
+ required=True,
436
+ choices=["python", "java", "go", "c", "cpp"],
437
+ help="编程语言"
438
+ )
439
+
440
+ parser.add_argument(
441
+ "-t", "--target",
442
+ required=True,
443
+ help="目标目录路径"
444
+ )
445
+
446
+ parser.add_argument(
447
+ "-o", "--output",
448
+ default="coverage_report",
449
+ help="输出报告文件名(默认: coverage_report)"
450
+ )
451
+
452
+ parser.add_argument(
453
+ "-f", "--format",
454
+ default="text",
455
+ choices=["text", "json", "csv", "html"],
456
+ help="报告格式(默认: text)"
457
+ )
458
+
459
+ parser.add_argument(
460
+ "--no-delete",
461
+ action="store_true",
462
+ help="编译失败时不删除文件"
463
+ )
464
+
465
+ parser.add_argument(
466
+ "--compile-timeout",
467
+ type=int,
468
+ default=60,
469
+ help="编译超时时间(秒,默认: 60)"
470
+ )
471
+
472
+ parser.add_argument(
473
+ "--test-timeout",
474
+ type=int,
475
+ default=300,
476
+ help="测试超时时间(秒,默认: 300)"
477
+ )
478
+
479
+ parser.add_argument(
480
+ "--max-compile-iterations",
481
+ type=int,
482
+ default=10,
483
+ help="最大编译修复迭代次数(默认: 10)"
484
+ )
485
+
486
+ parser.add_argument(
487
+ "--max-cleanup-iterations",
488
+ type=int,
489
+ default=10,
490
+ help="最大测试清理迭代次数(默认: 10)"
491
+ )
492
+
493
+ return parser.parse_args()
494
+
495
+
496
+ def main():
497
+ """主函数"""
498
+ args = parse_args()
499
+
500
+ # 转换参数
501
+ config = ToolConfig(
502
+ language=Language(args.language),
503
+ target_dir=os.path.abspath(args.target),
504
+ output_file=args.output,
505
+ report_format=ReportFormat(args.format),
506
+ compile_timeout=args.compile_timeout,
507
+ test_timeout=args.test_timeout,
508
+ delete_on_compile_failure=not args.no_delete,
509
+ max_compile_iterations=args.max_compile_iterations,
510
+ max_cleanup_iterations=args.max_cleanup_iterations
511
+ )
512
+
513
+ # 检查目标目录
514
+ if not os.path.exists(config.target_dir):
515
+ print(f"错误: 目标目录不存在: {config.target_dir}")
516
+ sys.exit(1)
517
+
518
+ # 运行工具
519
+ try:
520
+ with CoverageTool(config) as tool:
521
+ report = tool.run()
522
+
523
+ # 检查是否有问题
524
+ has_errors = False
525
+ error_messages = []
526
+
527
+ if report.stats and len(report.stats) > 0:
528
+ stat = report.stats[0]
529
+ if stat.total_tests == 0:
530
+ error_messages.append("未找到测试用例")
531
+ has_errors = True
532
+ if stat.coverage_rate == 0 and stat.total_tests > 0:
533
+ error_messages.append("覆盖率计算失败")
534
+ has_errors = True
535
+
536
+ # 如果有错误,显示警告
537
+ if has_errors:
538
+ print("\n" + "=" * 80)
539
+ print("⚠️ 执行完成,但存在以下问题:")
540
+ for msg in error_messages:
541
+ print(f" - {msg}")
542
+ print("=" * 80)
543
+ sys.exit(1)
544
+ else:
545
+ print("\n✓ 执行成功完成")
546
+ sys.exit(0)
547
+
548
+ except KeyboardInterrupt:
549
+ print("\n\n用户中断执行")
550
+ sys.exit(130)
551
+ except Exception as e:
552
+ print(f"\n错误: {e}")
553
+ import traceback
554
+ traceback.print_exc()
555
+ sys.exit(1)
556
+
557
+
558
+ if __name__ == "__main__":
559
+ main()
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 解析器包
4
+ 包含 JUnit XML 解析等功能
5
+ """
6
+
7
+ from .junit import JunitXMLParser, TestCase, TestSuite, TestReport, TestStatus
8
+
9
+ __all__ = [
10
+ 'JunitXMLParser',
11
+ 'TestCase',
12
+ 'TestSuite',
13
+ 'TestReport',
14
+ 'TestStatus',
15
+ ]