coverage-tool 1.0.0__py3-none-any.whl → 1.0.2__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.
@@ -18,7 +18,7 @@ class GTestConverter(BaseConverter):
18
18
  # Google Test 支持直接输出 XML
19
19
  # 如果已经有 XML 文件,直接返回
20
20
  if os.path.exists(xml_path):
21
- with open(xml_path, 'r') as f:
21
+ with open(xml_path, 'r', encoding='utf-8') as f:
22
22
  return f.read()
23
23
 
24
24
  # 解析文本输出
coverage_tool/example.py CHANGED
@@ -14,7 +14,7 @@ def create_sample_python_project(project_dir):
14
14
  """创建一个示例Python项目"""
15
15
  # 创建主模块
16
16
  main_file = os.path.join(project_dir, "main.py")
17
- with open(main_file, 'w') as f:
17
+ with open(main_file, 'w', encoding='utf-8') as f:
18
18
  f.write("""
19
19
  def add(a, b):
20
20
  return a + b
@@ -28,7 +28,7 @@ def multiply(a, b):
28
28
 
29
29
  # 创建测试文件
30
30
  test_file = os.path.join(project_dir, "test_main.py")
31
- with open(test_file, 'w') as f:
31
+ with open(test_file, 'w', encoding='utf-8') as f:
32
32
  f.write("""
33
33
  import pytest
34
34
  from main import add, subtract, multiply
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  智能编译失败处理器
4
- 支持渐进式删除策略和依赖分析
4
+ 只处理单测文件的删除,业务代码错误直接告警用户
5
5
  """
6
6
 
7
7
  import os
@@ -15,236 +15,211 @@ from enum import Enum
15
15
  from coverage_tool.utils import FileBackupManager, DependencyGraph, Logger, parse_error_locations, safe_delete_file
16
16
 
17
17
 
18
- class FilePriority(Enum):
19
- """文件优先级"""
20
- LOW = 1 # 可以安全删除的文件(如测试文件、示例文件)
21
- MEDIUM = 2 # 普通源文件
22
- HIGH = 3 # 核心文件(如main文件、配置文件)
23
- CRITICAL = 4 # 绝对不能删除的文件
24
-
25
-
26
- @dataclass
27
- class FileInfo:
28
- """文件信息"""
29
- path: str
30
- priority: FilePriority
31
- error_count: int = 0
32
- is_test_file: bool = False
33
- is_source_file: bool = False
34
- dependents: List[str] = field(default_factory=list)
18
+ class SmartCompilationHandler:
19
+ """智能编译失败处理器 - 只删除单测文件"""
35
20
 
21
+ TEST_FILE_PATTERNS = {
22
+ 'python': [
23
+ r'.*_test\.py$',
24
+ r'.*test_.+\.py$',
25
+ r'.*_tests\.py$',
26
+ r'test_.+\.py$',
27
+ r'.*/tests/.+\.py$',
28
+ r'.*/test/.+\.py$',
29
+ ],
30
+ 'java': [
31
+ r'.*Test\.java$',
32
+ r'.*Tests\.java$',
33
+ r'.*TestCase\.java$',
34
+ ],
35
+ 'go': [
36
+ r'.*_test\.go$',
37
+ ],
38
+ 'c': [
39
+ r'.*_test\.c$',
40
+ r'.*test_.+\.c$',
41
+ r'.*/tests/.+\.c$',
42
+ r'.*/test/.+\.c$',
43
+ ],
44
+ 'cpp': [
45
+ r'.*_test\.cpp$',
46
+ r'.*test_.+\.cpp$',
47
+ r'.*_test\.cc$',
48
+ r'.*test_.+\.cc$',
49
+ r'.*/tests/.+\.cpp$',
50
+ r'.*/test/.+\.cpp$',
51
+ ],
52
+ }
36
53
 
37
- class SmartCompilationHandler:
38
- """智能编译失败处理器"""
39
-
40
54
  def __init__(self, target_dir: str, logger: Optional[Logger] = None):
41
55
  self.target_dir = target_dir
42
56
  self.logger = logger or Logger()
43
57
  self.backup_manager = FileBackupManager()
44
- self.dependency_graph = DependencyGraph()
45
- self.deleted_files: List[str] = []
46
- self.compilation_history: List[Dict] = []
47
-
48
- # 文件优先级模式
49
- self.priority_patterns = {
50
- FilePriority.LOW: [
51
- r'.*test.*\.(py|java|go|c|cpp|h|hpp)$',
52
- r'.*example.*\.(py|java|go|c|cpp)$',
53
- r'.*demo.*\.(py|java|go|c|cpp)$',
54
- r'.*mock.*\.(py|java|go|c|cpp)$',
55
- r'.*stub.*\.(py|java|go|c|cpp)$',
56
- ],
57
- FilePriority.HIGH: [
58
- r'^main\.(py|java|go|c|cpp)$',
59
- r'^index\.(py|java|go|c|cpp)$',
60
- r'^app\.(py|java|go|c|cpp)$',
61
- r'^setup\.py$',
62
- r'^[Rr]eadme\.md$',
63
- r'^go\.mod$',
64
- r'^pom\.xml$',
65
- r'^build\.gradle$',
66
- r'^Makefile$',
67
- r'^CMakeLists\.txt$',
68
- ],
69
- FilePriority.CRITICAL: [
70
- r'.*\.git.*',
71
- r'.*\.svn.*',
72
- ]
73
- }
74
-
75
- def analyze_compilation_errors(self, errors: List[str]) -> List[FileInfo]:
76
- """分析编译错误,提取问题文件"""
77
- error_locations = parse_error_locations(errors, self.target_dir)
78
-
79
- file_infos = []
80
- for file_path, line_nums in error_locations.items():
81
- if os.path.exists(file_path):
82
- info = FileInfo(
83
- path=file_path,
84
- priority=self._determine_priority(file_path),
85
- error_count=len(line_nums),
86
- is_test_file=self._is_test_file(file_path),
87
- is_source_file=self._is_source_file(file_path)
88
- )
89
- file_infos.append(info)
90
-
91
- # 按优先级和错误数量排序(优先级低的先删除)
92
- file_infos.sort(key=lambda x: (x.priority.value, -x.error_count))
93
-
94
- return file_infos
95
-
96
- def _determine_priority(self, file_path: str) -> FilePriority:
97
- """确定文件优先级"""
58
+ self.deleted_test_files: List[str] = []
59
+
60
+ def _is_test_file(self, file_path: str, language: str = 'python') -> bool:
61
+ """检查是否是单测文件"""
98
62
  file_name = os.path.basename(file_path)
99
-
100
- # 检查CRITICAL模式
101
- for pattern in self.priority_patterns[FilePriority.CRITICAL]:
102
- if re.match(pattern, file_name, re.IGNORECASE):
103
- return FilePriority.CRITICAL
104
-
105
- # 检查HIGH模式
106
- for pattern in self.priority_patterns[FilePriority.HIGH]:
63
+ patterns = self.TEST_FILE_PATTERNS.get(language, self.TEST_FILE_PATTERNS['python'])
64
+ for pattern in patterns:
107
65
  if re.match(pattern, file_name, re.IGNORECASE):
108
- return FilePriority.HIGH
109
-
110
- # 检查LOW模式
111
- for pattern in self.priority_patterns[FilePriority.LOW]:
112
- if re.match(pattern, file_name, re.IGNORECASE):
113
- return FilePriority.LOW
114
-
115
- return FilePriority.MEDIUM
116
-
117
- def _is_test_file(self, file_path: str) -> bool:
118
- """检查是否是测试文件"""
119
- test_patterns = [
120
- r'.*test.*\.(py|java|go|c|cpp)$',
121
- r'.*_test\.(py|java|go|c|cpp)$',
122
- r'test_.*\.py$',
123
- r'.*Test\.java$',
66
+ return True
67
+ return False
68
+
69
+ def _is_business_code_error(self, error: str) -> bool:
70
+ """检查是否是业务代码错误(非单测代码错误)"""
71
+ business_error_keywords = [
72
+ 'undefined reference',
73
+ 'undefined symbol',
74
+ 'cannot find symbol',
75
+ 'package does not exist',
76
+ 'class not found',
77
+ 'method not found',
78
+ 'variable not declared',
79
+ 'syntax error',
80
+ 'type error',
81
+ 'compile error',
124
82
  ]
125
- file_name = os.path.basename(file_path)
126
- for pattern in test_patterns:
127
- if re.match(pattern, file_name, re.IGNORECASE):
83
+ error_lower = error.lower()
84
+ return any(keyword in error_lower for keyword in business_error_keywords)
85
+
86
+ def _extract_error_directory(self, error: str) -> Optional[str]:
87
+ """从错误信息中提取报错文件所在目录"""
88
+ file_path = parse_error_locations([error], self.target_dir)
89
+ if file_path:
90
+ first_file = list(file_path.keys())[0]
91
+ return os.path.dirname(first_file)
92
+ return None
93
+
94
+ def _find_test_files_in_directory(self, directory: str, language: str) -> List[str]:
95
+ """查找指定目录下的所有单测文件"""
96
+ if not os.path.exists(directory) or not os.path.isdir(directory):
97
+ return []
98
+
99
+ test_files = []
100
+ for root, dirs, files in os.walk(directory):
101
+ for file in files:
102
+ file_path = os.path.join(root, file)
103
+ if self._is_test_file(file_path, language):
104
+ test_files.append(file_path)
105
+ return test_files
106
+
107
+ def _has_source_code_error(self, errors: List[str]) -> bool:
108
+ """检查错误中是否包含业务代码错误"""
109
+ for error in errors:
110
+ if self._is_business_code_error(error):
128
111
  return True
129
112
  return False
130
-
131
- def _is_source_file(self, file_path: str) -> bool:
132
- """检查是否是源文件"""
133
- source_extensions = ['.py', '.java', '.go', '.c', '.cpp', '.h', '.hpp']
134
- ext = os.path.splitext(file_path)[1].lower()
135
- return ext in source_extensions
136
-
113
+
137
114
  def handle_compilation_failure(
138
- self,
139
- errors: List[str],
115
+ self,
116
+ errors: List[str],
140
117
  compile_func,
141
- max_iterations: int = 10
118
+ language: str = 'python',
119
+ max_iterations: int = 1
142
120
  ) -> Tuple[bool, List[str], List[str]]:
143
121
  """
144
- 处理编译失败,使用渐进式删除策略
145
-
122
+ 处理编译失败 - 只删除单测文件,业务代码错误直接终止
123
+
146
124
  Args:
147
125
  errors: 编译错误列表
148
126
  compile_func: 编译函数,接收target_dir,返回(success, errors)
149
- max_iterations: 最大尝试次数
150
-
127
+ language: 编程语言
128
+ max_iterations: 最大尝试次数(建议设为1,因为只删单测文件)
129
+
151
130
  Returns:
152
- (编译是否成功, 被删除的文件列表, 剩余的编译错误)
131
+ (编译是否成功, 被删除的单测文件列表, 剩余的编译错误)
153
132
  """
154
- self.logger.info(f"开始处理编译失败,共 {len(errors)} 个错误")
155
-
133
+ self.logger.warning("=" * 60)
134
+ self.logger.warning("检测到编译失败,开始分析错误...")
135
+ self.logger.warning("=" * 60)
136
+
156
137
  iteration = 0
157
- current_errors = errors.copy()
158
-
159
- while iteration < max_iterations and current_errors:
138
+
139
+ while iteration < max_iterations:
160
140
  iteration += 1
161
141
  self.logger.info(f"编译修复迭代 {iteration}/{max_iterations}")
162
-
163
- # 分析错误
164
- file_infos = self.analyze_compilation_errors(current_errors)
165
-
166
- if not file_infos:
167
- self.logger.warning("无法从错误信息中识别问题文件")
168
- break
169
-
170
- # 过滤掉不能删除的文件
171
- deletable_files = [
172
- info for info in file_infos
173
- if info.priority not in [FilePriority.CRITICAL, FilePriority.HIGH]
174
- ]
175
-
176
- if not deletable_files:
177
- self.logger.warning("没有可安全删除的文件")
142
+
143
+ error_locations = parse_error_locations(errors, self.target_dir)
144
+
145
+ if not error_locations:
146
+ self.logger.warning("无法从错误信息中识别问题文件位置")
178
147
  break
179
-
180
- # 选择要删除的文件(每次最多删除3个,避免过度删除)
181
- files_to_delete = deletable_files[:3]
182
-
183
- # 检查依赖关系
184
- for file_info in files_to_delete:
185
- impacted = self.dependency_graph.get_impact_files(file_info.path)
186
- if impacted:
187
- self.logger.warning(
188
- f"删除 {file_info.path} 会影响 {len(impacted)} 个文件"
189
- )
190
-
191
- # 备份并删除文件
148
+
149
+ error_dirs = set(os.path.dirname(path) for path in error_locations.keys())
150
+ self.logger.info(f"检测到 {len(error_dirs)} 个报错目录")
151
+
152
+ all_test_files = []
153
+ for error_dir in error_dirs:
154
+ test_files = self._find_test_files_in_directory(error_dir, language)
155
+ if test_files:
156
+ self.logger.info(f"目录 {error_dir} 找到 {len(test_files)} 个单测文件")
157
+ all_test_files.extend(test_files)
158
+
159
+ all_test_files = list(set(all_test_files))
160
+
161
+ if not all_test_files:
162
+ self.logger.warning("报错目录下未找到单测文件")
163
+ self.logger.warning("=" * 60)
164
+ self.logger.warning("编译错误来自业务代码,请手动修复以下问题后重试:")
165
+ for i, error in enumerate(errors[:5], 1):
166
+ self.logger.warning(f" [{i}] {error.strip()}")
167
+ if len(errors) > 5:
168
+ self.logger.warning(f" ... 共 {len(errors)} 个错误")
169
+ self.logger.warning("=" * 60)
170
+ return False, self.deleted_test_files, errors
171
+
172
+ self.logger.info(f"准备删除 {len(all_test_files)} 个单测文件")
173
+
192
174
  deleted_in_iteration = []
193
- for file_info in files_to_delete:
194
- if safe_delete_file(file_info.path, self.backup_manager):
195
- deleted_in_iteration.append(file_info.path)
196
- self.deleted_files.append(file_info.path)
197
- self.logger.info(f"已删除文件: {file_info.path}")
198
-
175
+ for test_file in all_test_files:
176
+ if safe_delete_file(test_file, self.backup_manager):
177
+ deleted_in_iteration.append(test_file)
178
+ self.deleted_test_files.append(test_file)
179
+ self.logger.info(f"已删除单测文件: {test_file}")
180
+
199
181
  if not deleted_in_iteration:
200
- self.logger.warning("本轮未能删除任何文件")
182
+ self.logger.warning("未能删除任何单测文件")
201
183
  break
202
-
203
- # 记录编译历史
204
- self.compilation_history.append({
205
- 'iteration': iteration,
206
- 'deleted_files': deleted_in_iteration,
207
- 'error_count_before': len(current_errors)
208
- })
209
-
210
- # 尝试重新编译
184
+
185
+ self.logger.info(f"已删除 {len(deleted_in_iteration)} 个单测文件,重新编译...")
186
+
211
187
  success, new_errors = compile_func(self.target_dir)
212
-
188
+
213
189
  if success:
214
- self.logger.info(f"编译成功!共进行了 {iteration} 轮修复")
215
- return True, self.deleted_files, []
216
-
217
- # 检查错误是否减少
218
- if len(new_errors) >= len(current_errors):
219
- self.logger.warning(
220
- f"错误数量未减少 ({len(current_errors)} -> {len(new_errors)}),"
221
- "可能需要更激进的删除策略"
222
- )
223
-
224
- current_errors = new_errors
225
-
226
- # 达到最大迭代次数仍未成功
227
- self.logger.error(f"编译修复失败,已达到最大迭代次数 {max_iterations}")
228
- return False, self.deleted_files, current_errors
229
-
190
+ self.logger.info("编译成功!")
191
+ return True, self.deleted_test_files, []
192
+
193
+ errors = new_errors
194
+
195
+ if iteration >= max_iterations:
196
+ self.logger.warning("=" * 60)
197
+ self.logger.warning("已删除所有相关单测文件,但编译仍未成功")
198
+ self.logger.warning("编译错误来自业务代码,请手动修复以下问题后重试:")
199
+ for i, error in enumerate(errors[:10], 1):
200
+ self.logger.warning(f" [{i}] {error.strip()}")
201
+ if len(errors) > 10:
202
+ self.logger.warning(f" ... 共 {len(errors)} 个错误")
203
+ self.logger.warning("=" * 60)
204
+ return False, self.deleted_test_files, errors
205
+
206
+ self.logger.error("编译修复失败")
207
+ return False, self.deleted_test_files, errors
208
+
230
209
  def restore_deleted_files(self) -> List[str]:
231
- """恢复所有已删除的文件"""
210
+ """恢复所有已删除的单测文件"""
232
211
  restored = []
233
- for file_path in self.deleted_files:
212
+ for file_path in self.deleted_test_files:
234
213
  if self.backup_manager.restore_file(file_path):
235
214
  restored.append(file_path)
236
215
  self.logger.info(f"已恢复文件: {file_path}")
237
- self.deleted_files.clear()
216
+ self.deleted_test_files.clear()
238
217
  return restored
239
-
218
+
240
219
  def get_deleted_files(self) -> List[str]:
241
220
  """获取已删除的文件列表"""
242
- return self.deleted_files.copy()
243
-
244
- def get_compilation_history(self) -> List[Dict]:
245
- """获取编译历史记录"""
246
- return self.compilation_history.copy()
247
-
221
+ return self.deleted_test_files.copy()
222
+
248
223
  def cleanup(self):
249
224
  """清理资源"""
250
225
  self.backup_manager.cleanup()
@@ -252,8 +227,7 @@ class SmartCompilationHandler:
252
227
 
253
228
  class CompilationErrorParser:
254
229
  """编译错误解析器"""
255
-
256
- # 各种语言的编译错误模式
230
+
257
231
  ERROR_PATTERNS = {
258
232
  'python': [
259
233
  (r'File "([^"]+\.py)", line (\d+)', 'file_and_line'),
@@ -282,18 +256,12 @@ class CompilationErrorParser:
282
256
  (r'undefined reference to', 'link_error'),
283
257
  ]
284
258
  }
285
-
259
+
286
260
  @staticmethod
287
261
  def parse_errors(errors: List[str], language: str, project_dir: str) -> Dict[str, List[Dict]]:
288
- """
289
- 解析编译错误
290
-
291
- Returns:
292
- 按文件分类的错误信息
293
- """
294
262
  patterns = CompilationErrorParser.ERROR_PATTERNS.get(language, [])
295
263
  file_errors: Dict[str, List[Dict]] = {}
296
-
264
+
297
265
  for error in errors:
298
266
  for pattern, error_type in patterns:
299
267
  matches = re.findall(pattern, error)
@@ -304,19 +272,18 @@ class CompilationErrorParser:
304
272
  else:
305
273
  file_path = match
306
274
  line_num = 0
307
-
308
- # 规范化路径
275
+
309
276
  if not os.path.isabs(file_path):
310
277
  file_path = os.path.join(project_dir, file_path)
311
278
  file_path = os.path.normpath(file_path)
312
-
279
+
313
280
  if file_path not in file_errors:
314
281
  file_errors[file_path] = []
315
-
282
+
316
283
  file_errors[file_path].append({
317
284
  'line': line_num,
318
285
  'type': error_type,
319
286
  'message': error
320
287
  })
321
-
288
+
322
289
  return file_errors
coverage_tool/main.py CHANGED
@@ -172,6 +172,7 @@ class CoverageTool:
172
172
  success, deleted_files, remaining_errors = handler.handle_compilation_failure(
173
173
  errors,
174
174
  compile_func,
175
+ language=self.config.language.value,
175
176
  max_iterations=self.config.max_compile_iterations
176
177
  )
177
178
 
@@ -77,7 +77,7 @@ class CoverageExtractor:
77
77
  def _parse_go_coverage_file(coverage_out_path: str) -> float:
78
78
  """直接解析Go coverage.out文件"""
79
79
  try:
80
- with open(coverage_out_path, 'r') as f:
80
+ with open(coverage_out_path, 'r', encoding='utf-8') as f:
81
81
  lines = f.readlines()
82
82
 
83
83
  if not lines or not lines[0].startswith('mode:'):
@@ -108,7 +108,7 @@ class CoverageExtractor:
108
108
  def _extract_gcov_coverage(coverage_out_path: str) -> float:
109
109
  """提取gcov覆盖率"""
110
110
  try:
111
- with open(coverage_out_path, 'r') as f:
111
+ with open(coverage_out_path, 'r', encoding='utf-8') as f:
112
112
  content = f.read()
113
113
 
114
114
  # 尝试多种模式匹配
@@ -134,7 +134,7 @@ class CoverageExtractor:
134
134
  def _extract_generic_coverage(coverage_out_path: str) -> float:
135
135
  """通用覆盖率提取"""
136
136
  try:
137
- with open(coverage_out_path, 'r') as f:
137
+ with open(coverage_out_path, 'r', encoding='utf-8') as f:
138
138
  content = f.read()
139
139
 
140
140
  # 查找百分比
@@ -153,7 +153,7 @@ class CoverageExtractor:
153
153
  return 0.0
154
154
 
155
155
  try:
156
- with open(jacoco_html_path, 'r') as f:
156
+ with open(jacoco_html_path, 'r', encoding='utf-8') as f:
157
157
  content = f.read()
158
158
 
159
159
  # 查找Total行中的覆盖率
@@ -25,7 +25,7 @@ class CRunner(BaseRunner):
25
25
  xml_content = JUnitReportConverter.convert_generic_output(
26
26
  result.stdout + result.stderr, 'cunit'
27
27
  )
28
- with open(output_file, 'w') as f:
28
+ with open(output_file, 'w', encoding='utf-8') as f:
29
29
  f.write(xml_content)
30
30
  result.junit_xml_path = output_file
31
31
  except Exception as e:
@@ -25,7 +25,7 @@ class CPPRunner(BaseRunner):
25
25
  xml_content = JUnitReportConverter.convert_generic_output(
26
26
  result.stdout + result.stderr, 'gtest'
27
27
  )
28
- with open(output_file, 'w') as f:
28
+ with open(output_file, 'w', encoding='utf-8') as f:
29
29
  f.write(xml_content)
30
30
  result.junit_xml_path = output_file
31
31
  except Exception as e:
@@ -82,7 +82,7 @@ class GoRunner(BaseRunner):
82
82
  )
83
83
 
84
84
  if junit_result.returncode == 0 or junit_result.stdout:
85
- with open(output_file, 'w') as f:
85
+ with open(output_file, 'w', encoding='utf-8') as f:
86
86
  f.write(junit_result.stdout)
87
87
  result.junit_xml_path = output_file
88
88
  junit_xml_generated = True
@@ -96,7 +96,7 @@ class GoRunner(BaseRunner):
96
96
  if not junit_xml_generated:
97
97
  try:
98
98
  xml_content = JUnitReportConverter.convert_go_test(result.stdout)
99
- with open(output_file, 'w') as f:
99
+ with open(output_file, 'w', encoding='utf-8') as f:
100
100
  f.write(xml_content)
101
101
  result.junit_xml_path = output_file
102
102
  self.logger.info("[报告生成] ✓ 使用内置转换器生成 JUnit XML")
@@ -4,6 +4,8 @@ Java单测运行器
4
4
  """
5
5
 
6
6
  import os
7
+ import glob
8
+ import xml.etree.ElementTree as ET
7
9
  from typing import List, Tuple
8
10
 
9
11
  from .base import BaseRunner, RunResult, CoverageExtractor
@@ -11,44 +13,91 @@ from .base import BaseRunner, RunResult, CoverageExtractor
11
13
 
12
14
  class JavaRunner(BaseRunner):
13
15
  """Java单测运行器"""
14
-
16
+
15
17
  def run_tests(self, target_dir: str, output_file: str) -> RunResult:
16
18
  """运行Java测试"""
17
- # 使用Maven运行测试
18
- cmd = f"mvn test -Dtest.output={output_file}"
19
+ cmd = "mvn test"
19
20
  result = self._run_command(cmd, target_dir)
20
-
21
- # Maven通常生成surefire报告
21
+
22
22
  surefire_report = os.path.join(target_dir, "target", "surefire-reports")
23
23
  if os.path.exists(surefire_report):
24
- # 转换surefire报告为JUnit XML
25
24
  self._convert_surefire_reports(surefire_report, output_file)
26
-
25
+
26
+ if not os.path.exists(output_file):
27
+ junit_files = glob.glob(os.path.join(surefire_report, "*.xml"))
28
+ if junit_files:
29
+ self._merge_junit_reports(junit_files, output_file)
30
+
27
31
  result.junit_xml_path = output_file if os.path.exists(output_file) else None
28
- result.coverage_rate = self.get_coverage(output_file)
29
-
32
+ result.coverage_rate = self.get_coverage(target_dir)
33
+
30
34
  return result
31
-
35
+
36
+ def _merge_junit_reports(self, junit_files: List[str], output_file: str):
37
+ """合并多个JUnit XML报告为一个"""
38
+ total_tests = 0
39
+ total_failures = 0
40
+ total_errors = 0
41
+ total_skipped = 0
42
+ test_suites = []
43
+
44
+ for junit_file in junit_files:
45
+ try:
46
+ tree = ET.parse(junit_file)
47
+ root = tree.getroot()
48
+
49
+ if root.tag == 'testsuites':
50
+ for suite in root.findall('testsuite'):
51
+ test_suites.append(suite)
52
+ elif root.tag == 'testsuite':
53
+ test_suites.append(root)
54
+ except Exception as e:
55
+ self.logger.warning(f"解析JUnit报告失败: {junit_file}: {e}")
56
+
57
+ xml_declaration = '<?xml version="1.0" encoding="UTF-8"?>\n'
58
+ testsuites_elem = ET.Element('testsuites')
59
+
60
+ for suite in test_suites:
61
+ testsuites_elem.append(suite)
62
+ total_tests += int(suite.get('tests', 0))
63
+ total_failures += int(suite.get('failures', 0))
64
+ total_errors += int(suite.get('errors', 0))
65
+ total_skipped += int(suite.get('skipped', 0))
66
+
67
+ testsuites_elem.set('name', 'Merged Test Results')
68
+ testsuites_elem.set('tests', str(total_tests))
69
+ testsuites_elem.set('failures', str(total_failures))
70
+ testsuites_elem.set('errors', str(total_errors))
71
+ testsuites_elem.set('skipped', str(total_skipped))
72
+
73
+ tree = ET.ElementTree(testsuites_elem)
74
+ tree.write(output_file, encoding='UTF-8', xml_declaration=False)
75
+
76
+ with open(output_file, 'r', encoding='utf-8') as f:
77
+ content = f.read()
78
+ with open(output_file, 'w', encoding='utf-8') as f:
79
+ f.write(xml_declaration + content)
80
+
32
81
  def _convert_surefire_reports(self, surefire_dir: str, output_file: str):
33
82
  """转换Surefire报告为JUnit XML"""
34
- # 这里简化处理,实际应该解析XML并合并
35
- pass
36
-
37
- def get_coverage(self, output_file: str) -> float:
83
+ junit_files = glob.glob(os.path.join(surefire_dir, "TEST-*.xml"))
84
+ if junit_files:
85
+ self._merge_junit_reports(junit_files, output_file)
86
+
87
+ def get_coverage(self, target_dir: str) -> float:
38
88
  """获取Java覆盖率(JaCoCo)"""
39
- # JaCoCo报告通常在 target/site/jacoco/index.html
40
- jacoco_html = os.path.join("target", "site", "jacoco", "index.html")
89
+ jacoco_html = os.path.join(target_dir, "target", "site", "jacoco", "index.html")
41
90
  return CoverageExtractor.extract_from_jacoco(jacoco_html)
42
-
91
+
43
92
  def compile(self, target_dir: str) -> Tuple[bool, List[str]]:
44
93
  """编译Java项目"""
45
94
  cmd = "mvn compile"
46
95
  result = self._run_command(cmd, target_dir)
47
-
96
+
48
97
  errors = []
49
98
  if not result.success:
50
99
  for line in result.stderr.split('\n'):
51
100
  if 'error:' in line.lower() or 'cannot find symbol' in line.lower():
52
101
  errors.append(line.strip())
53
-
102
+
54
103
  return result.success, errors
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coverage-tool
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Multi-language unit test coverage analysis tool
5
5
  Author-email: Coverage Tool <coverage@tool.dev>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  coverage_tool/__init__.py,sha256=0j2DaSgKbtKY7LShLYx8VHbAhD09gm8aGQI__uo4CeU,123
2
- coverage_tool/example.py,sha256=8d0VAve0F54wDXgKIpIgE5kn-ZzMEKDED6_74TfQMvo,2712
3
- coverage_tool/main.py,sha256=cyHcWEcX3fK35kyLnr1_Nwf0Ug_svcLqG8K3wXbCk14,19416
2
+ coverage_tool/example.py,sha256=EvY25Hfos49FU5BQp1Y5RaCJwyfNNlMx_Fcl4yD_5Pc,2748
3
+ coverage_tool/main.py,sha256=kbxkhHdI6dRtkiPk_1naidVLqp3p-ClS-Eak3p_5EKo,19465
4
4
  coverage_tool/analyzers/__init__.py,sha256=GlcdK3CsmPtnGN0P13K0F6i-dMbL03wVRepIPW3U0O0,265
5
5
  coverage_tool/analyzers/dependency.py,sha256=l6HywXXdBY506F6yzaFH-aXYYrinbWKuG3LTfT2CGJc,18759
6
6
  coverage_tool/converters/__init__.py,sha256=h3wSGZgs1x_tKMjq3ZLAJepmcBgc3fR_-cGnQydZFhM,1830
@@ -9,22 +9,22 @@ coverage_tool/converters/cunit_converter.py,sha256=_a9Tg_nSit6XlG7x7IA0oxjcGAJEK
9
9
  coverage_tool/converters/factory.py,sha256=Qzg4ZmfX9YeNx4BcPJu3n37XLcszy6ysu2B4NHs1-6w,1003
10
10
  coverage_tool/converters/generic_converter.py,sha256=35o6M5KyHxxOlX8al-nrN0dTT9v12PQF-Nc4hF7bHW8,1906
11
11
  coverage_tool/converters/go_converter.py,sha256=KoNQ4bqZrv1zeDuGWm7GUhri6QFMqolnG-GV0YIF490,6287
12
- coverage_tool/converters/gtest_converter.py,sha256=uTn65IvLEGy7OUTBk80uMx9o-mQ147Az0_Za7n575fI,1728
12
+ coverage_tool/converters/gtest_converter.py,sha256=GIkGe7bkKK8oJUBC1QhxNkgctjqDnPZo4vzDKEBs100,1746
13
13
  coverage_tool/core/__init__.py,sha256=rNM16navIyNkoMKVvJsEy2zVF7jAcyHV81nOsP1gKgI,415
14
14
  coverage_tool/core/config.py,sha256=QmBxj72gcwhI-bEUDMDd7mCUPxJdn7NiO7DdfingaOA,2133
15
15
  coverage_tool/core/reporter.py,sha256=GOsOhu8JX0u9sBHhEdbHHfrEFEfvFXQDVOyTLdeeD0w,9500
16
16
  coverage_tool/handlers/__init__.py,sha256=6iAHa2i3LhqpDWqt32x98qx3-PbVeNcsDqQX6jHhm0U,253
17
- coverage_tool/handlers/compilation.py,sha256=7rvrMGOs9nHPF1op4RXgFzXVs1wL4Skl_pBKclQotOI,11981
17
+ coverage_tool/handlers/compilation.py,sha256=RfGr0lCWFb5ncNvVWuzHkTk1_h-mlXNAn7S3mv8PkBw,10907
18
18
  coverage_tool/handlers/env_checker.py,sha256=L6JJE39C-9DxKKtVC_ZUi-0ONxnjREZhQKiaj8t5Uoo,12665
19
19
  coverage_tool/parsers/__init__.py,sha256=7Wmg-Tf_rAo-uD9PWZJv5gGwSeWWuzcAAO2qMm6O3QE,264
20
20
  coverage_tool/parsers/junit.py,sha256=COjG9_YWvlAsX3Ztd11ttFRRDZnpjg-BZWWwr4egVTM,5555
21
21
  coverage_tool/runners/__init__.py,sha256=w-Lnwfh-4jOdjxdSepKO-YSg4GqHmPtK6Ut6wQeQkJU,584
22
- coverage_tool/runners/base.py,sha256=hyaFSIFoqW35O9Vou8iI3S2S0dTWCMvhn_92spz4lv0,8078
23
- coverage_tool/runners/c_runner.py,sha256=DZPtEVUZXSNQLuA1nDdI449ODImT1Tkfx8q28HyC_Ko,2166
24
- coverage_tool/runners/cpp_runner.py,sha256=f_L1FVuLRJH4DByj6Oo7uVGodOJFUDUEosZreqf70Ko,2154
22
+ coverage_tool/runners/base.py,sha256=bPsTFc1I-NCXKjJ0ILqYUoh54iAUylaR2dFnsUZfrYU,8150
23
+ coverage_tool/runners/c_runner.py,sha256=bAakBnI2gvuz0Cc8Paj84NJCECBe94VDOtBFv25GVgI,2184
24
+ coverage_tool/runners/cpp_runner.py,sha256=pormwZXQVRxdkceTjUNbXxYKp5-kYVP9vLW6qXyROXI,2172
25
25
  coverage_tool/runners/factory.py,sha256=f_oM_AQfj3Yge1Ou_XSyERbcynluBLWb4s85m7YgYWs,1187
26
- coverage_tool/runners/go_runner.py,sha256=FEOPpi-IMI5zu_zbzuaJ60vS0aoYJCTbRtW2q1TJEC0,6397
27
- coverage_tool/runners/java_runner.py,sha256=BEzaR5-4npIzmPSCO8avzktgsDp_OwjSgQxT3A8-uSI,1936
26
+ coverage_tool/runners/go_runner.py,sha256=3ZU3YJ5nOGHti2c-KfoX-7EQVSHBoXoDEuePPM6SCsE,6433
27
+ coverage_tool/runners/java_runner.py,sha256=yF-lMjQ2NzpOi1v9nVDBQqBLN3XM_DB4PiG1PPQK71M,3826
28
28
  coverage_tool/runners/python_runner.py,sha256=ZTOjY93ZY1vfuROM9-CX80d03AaAUA5fJhcprgWQ3Vk,3731
29
29
  coverage_tool/test_removers/__init__.py,sha256=rPbFy8DRohFctpZgBz-QR-mld0rxqBg4G4KHjKQqGgI,958
30
30
  coverage_tool/test_removers/base.py,sha256=LYvG0ZRcsbsI4p_h3HvwvStfO1-6i_ARzaupNFCE3BE,8911
@@ -40,8 +40,8 @@ coverage_tool/utils/file_backup.py,sha256=SSrVGAPlO-Lz4cx-dFLhRwonbL8V0h0siBm0ZP
40
40
  coverage_tool/utils/helpers.py,sha256=t2aWyMtV_r1EGFli40q1H1gGmibNrpjd5pFlhXNEX0w,2587
41
41
  coverage_tool/utils/logger.py,sha256=FGRPw39iXCT-VwrY4VOLpL_z2hALeYv4QAcYNC7SUsA,1596
42
42
  coverage_tool/utils/progress.py,sha256=o8t-FvrEMN4oYSK-cq_ywnkMES5szCwNhl546i0JpUU,1312
43
- coverage_tool-1.0.0.dist-info/METADATA,sha256=R5Q1L9EajJkA1YU94J1Fj4ZkNmoJWoZ71bIk13yaacU,13751
44
- coverage_tool-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
45
- coverage_tool-1.0.0.dist-info/entry_points.txt,sha256=gpUjpKt5x923IqF_YogfB-DMj_AhMyGivppNG61H7ig,58
46
- coverage_tool-1.0.0.dist-info/top_level.txt,sha256=QXoWKxLBHkNWJAlCFZjqCuunXV-6uWjUhc0ni4enaYo,14
47
- coverage_tool-1.0.0.dist-info/RECORD,,
43
+ coverage_tool-1.0.2.dist-info/METADATA,sha256=0aD4WA8aZJauxacL_rMmQuXQLz5DzYmK_qwEqsCHXGs,13751
44
+ coverage_tool-1.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
45
+ coverage_tool-1.0.2.dist-info/entry_points.txt,sha256=gpUjpKt5x923IqF_YogfB-DMj_AhMyGivppNG61H7ig,58
46
+ coverage_tool-1.0.2.dist-info/top_level.txt,sha256=QXoWKxLBHkNWJAlCFZjqCuunXV-6uWjUhc0ni4enaYo,14
47
+ coverage_tool-1.0.2.dist-info/RECORD,,