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.
- coverage_tool/__init__.py +7 -0
- coverage_tool/analyzers/__init__.py +14 -0
- coverage_tool/analyzers/dependency.py +513 -0
- coverage_tool/converters/__init__.py +60 -0
- coverage_tool/converters/base.py +83 -0
- coverage_tool/converters/cunit_converter.py +47 -0
- coverage_tool/converters/factory.py +36 -0
- coverage_tool/converters/generic_converter.py +62 -0
- coverage_tool/converters/go_converter.py +178 -0
- coverage_tool/converters/gtest_converter.py +63 -0
- coverage_tool/core/__init__.py +18 -0
- coverage_tool/core/config.py +66 -0
- coverage_tool/core/reporter.py +270 -0
- coverage_tool/example.py +102 -0
- coverage_tool/handlers/__init__.py +13 -0
- coverage_tool/handlers/compilation.py +322 -0
- coverage_tool/handlers/env_checker.py +312 -0
- coverage_tool/main.py +559 -0
- coverage_tool/parsers/__init__.py +15 -0
- coverage_tool/parsers/junit.py +172 -0
- coverage_tool/runners/__init__.py +26 -0
- coverage_tool/runners/base.py +249 -0
- coverage_tool/runners/c_runner.py +66 -0
- coverage_tool/runners/cpp_runner.py +65 -0
- coverage_tool/runners/factory.py +41 -0
- coverage_tool/runners/go_runner.py +158 -0
- coverage_tool/runners/java_runner.py +54 -0
- coverage_tool/runners/python_runner.py +99 -0
- coverage_tool/test_removers/__init__.py +38 -0
- coverage_tool/test_removers/base.py +233 -0
- coverage_tool/test_removers/c_remover.py +210 -0
- coverage_tool/test_removers/cpp_remover.py +272 -0
- coverage_tool/test_removers/factory.py +45 -0
- coverage_tool/test_removers/go_remover.py +112 -0
- coverage_tool/test_removers/java_remover.py +216 -0
- coverage_tool/test_removers/python_remover.py +172 -0
- coverage_tool/utils/__init__.py +23 -0
- coverage_tool/utils/dependency_graph.py +52 -0
- coverage_tool/utils/file_backup.py +112 -0
- coverage_tool/utils/helpers.py +89 -0
- coverage_tool/utils/logger.py +54 -0
- coverage_tool/utils/progress.py +41 -0
- coverage_tool-1.0.0.dist-info/METADATA +484 -0
- coverage_tool-1.0.0.dist-info/RECORD +47 -0
- coverage_tool-1.0.0.dist-info/WHEEL +5 -0
- coverage_tool-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|