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.
- coverage_tool/converters/gtest_converter.py +1 -1
- coverage_tool/example.py +2 -2
- coverage_tool/handlers/compilation.py +176 -209
- coverage_tool/main.py +1 -0
- coverage_tool/runners/base.py +4 -4
- coverage_tool/runners/c_runner.py +1 -1
- coverage_tool/runners/cpp_runner.py +1 -1
- coverage_tool/runners/go_runner.py +2 -2
- coverage_tool/runners/java_runner.py +68 -19
- {coverage_tool-1.0.0.dist-info → coverage_tool-1.0.2.dist-info}/METADATA +1 -1
- {coverage_tool-1.0.0.dist-info → coverage_tool-1.0.2.dist-info}/RECORD +14 -14
- {coverage_tool-1.0.0.dist-info → coverage_tool-1.0.2.dist-info}/WHEEL +0 -0
- {coverage_tool-1.0.0.dist-info → coverage_tool-1.0.2.dist-info}/entry_points.txt +0 -0
- {coverage_tool-1.0.0.dist-info → coverage_tool-1.0.2.dist-info}/top_level.txt +0 -0
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
|
|
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.
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
for
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
127
|
+
language: 编程语言
|
|
128
|
+
max_iterations: 最大尝试次数(建议设为1,因为只删单测文件)
|
|
129
|
+
|
|
151
130
|
Returns:
|
|
152
|
-
(编译是否成功,
|
|
131
|
+
(编译是否成功, 被删除的单测文件列表, 剩余的编译错误)
|
|
153
132
|
"""
|
|
154
|
-
self.logger.
|
|
155
|
-
|
|
133
|
+
self.logger.warning("=" * 60)
|
|
134
|
+
self.logger.warning("检测到编译失败,开始分析错误...")
|
|
135
|
+
self.logger.warning("=" * 60)
|
|
136
|
+
|
|
156
137
|
iteration = 0
|
|
157
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
for
|
|
185
|
-
|
|
186
|
-
if
|
|
187
|
-
self.logger.
|
|
188
|
-
|
|
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
|
|
194
|
-
if safe_delete_file(
|
|
195
|
-
deleted_in_iteration.append(
|
|
196
|
-
self.
|
|
197
|
-
self.logger.info(f"
|
|
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
|
-
|
|
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(
|
|
215
|
-
return True, self.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
coverage_tool/runners/base.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
coverage_tool/__init__.py,sha256=0j2DaSgKbtKY7LShLYx8VHbAhD09gm8aGQI__uo4CeU,123
|
|
2
|
-
coverage_tool/example.py,sha256=
|
|
3
|
-
coverage_tool/main.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
23
|
-
coverage_tool/runners/c_runner.py,sha256=
|
|
24
|
-
coverage_tool/runners/cpp_runner.py,sha256=
|
|
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=
|
|
27
|
-
coverage_tool/runners/java_runner.py,sha256=
|
|
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.
|
|
44
|
-
coverage_tool-1.0.
|
|
45
|
-
coverage_tool-1.0.
|
|
46
|
-
coverage_tool-1.0.
|
|
47
|
-
coverage_tool-1.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|