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
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
项目依赖分析器
|
|
4
|
+
根据项目配置文件智能分析所需的编译和测试环境
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import json
|
|
10
|
+
import xml.etree.ElementTree as ET
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import List, Dict, Set, Optional, Tuple
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
class DependencyType(Enum):
|
|
17
|
+
"""依赖类型"""
|
|
18
|
+
PYTHON_PACKAGE = "python_package"
|
|
19
|
+
JAVA_PACKAGE = "java_package"
|
|
20
|
+
GO_MODULE = "go_module"
|
|
21
|
+
NODE_PACKAGE = "node_package"
|
|
22
|
+
BUILD_TOOL = "build_tool"
|
|
23
|
+
COMPILER = "compiler"
|
|
24
|
+
TEST_FRAMEWORK = "test_framework"
|
|
25
|
+
COVERAGE_TOOL = "coverage_tool"
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Dependency:
|
|
29
|
+
"""依赖项"""
|
|
30
|
+
name: str
|
|
31
|
+
type: DependencyType
|
|
32
|
+
version: Optional[str] = None
|
|
33
|
+
required: bool = True
|
|
34
|
+
installed: bool = False
|
|
35
|
+
installed_version: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ProjectAnalysis:
|
|
39
|
+
"""项目分析结果"""
|
|
40
|
+
project_dir: str
|
|
41
|
+
language: str
|
|
42
|
+
dependencies: List[Dependency] = field(default_factory=list)
|
|
43
|
+
build_system: Optional[str] = None
|
|
44
|
+
test_framework: Optional[str] = None
|
|
45
|
+
coverage_tool: Optional[str] = None
|
|
46
|
+
missing_dependencies: List[str] = field(default_factory=list)
|
|
47
|
+
warnings: List[str] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
class DependencyAnalyzer:
|
|
50
|
+
"""依赖分析器"""
|
|
51
|
+
|
|
52
|
+
# 各语言的配置文件映射
|
|
53
|
+
CONFIG_PATTERNS = {
|
|
54
|
+
'python': [
|
|
55
|
+
('requirements.txt', 'pip'),
|
|
56
|
+
('setup.py', 'pip'),
|
|
57
|
+
('pyproject.toml', 'pip'),
|
|
58
|
+
('Pipfile', 'pip'),
|
|
59
|
+
('setup.cfg', 'pip'),
|
|
60
|
+
],
|
|
61
|
+
'java': [
|
|
62
|
+
('pom.xml', 'maven'),
|
|
63
|
+
('build.gradle', 'gradle'),
|
|
64
|
+
('build.gradle.kts', 'gradle'),
|
|
65
|
+
('pom.xml', 'java'),
|
|
66
|
+
],
|
|
67
|
+
'go': [
|
|
68
|
+
('go.mod', 'go'),
|
|
69
|
+
('go.sum', 'go'),
|
|
70
|
+
],
|
|
71
|
+
'c': [
|
|
72
|
+
('Makefile', 'make'),
|
|
73
|
+
('CMakeLists.txt', 'cmake'),
|
|
74
|
+
('*.mk', 'make'),
|
|
75
|
+
],
|
|
76
|
+
'cpp': [
|
|
77
|
+
('Makefile', 'make'),
|
|
78
|
+
('CMakeLists.txt', 'cmake'),
|
|
79
|
+
('*.cmake', 'cmake'),
|
|
80
|
+
],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# 关键依赖检测 (只检查系统级工具,不检查库依赖)
|
|
84
|
+
KEY_DEPENDENCIES = {
|
|
85
|
+
'python': [
|
|
86
|
+
('python', DependencyType.COMPILER),
|
|
87
|
+
],
|
|
88
|
+
'java': [
|
|
89
|
+
('java', DependencyType.COMPILER),
|
|
90
|
+
('maven', DependencyType.BUILD_TOOL),
|
|
91
|
+
],
|
|
92
|
+
'go': [
|
|
93
|
+
('go', DependencyType.COMPILER),
|
|
94
|
+
],
|
|
95
|
+
'c': [
|
|
96
|
+
('gcc', DependencyType.COMPILER),
|
|
97
|
+
('make', DependencyType.BUILD_TOOL),
|
|
98
|
+
],
|
|
99
|
+
'cpp': [
|
|
100
|
+
('g++', DependencyType.COMPILER),
|
|
101
|
+
('make', DependencyType.BUILD_TOOL),
|
|
102
|
+
],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def __init__(self, project_dir: str):
|
|
106
|
+
self.project_dir = project_dir
|
|
107
|
+
self.analysis: Optional[ProjectAnalysis] = None
|
|
108
|
+
|
|
109
|
+
def analyze(self, language: str) -> ProjectAnalysis:
|
|
110
|
+
"""分析项目依赖"""
|
|
111
|
+
self.analysis = ProjectAnalysis(
|
|
112
|
+
project_dir=self.project_dir,
|
|
113
|
+
language=language
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# 查找配置文件
|
|
117
|
+
self._find_config_files(language)
|
|
118
|
+
|
|
119
|
+
# 确保analysis不为None
|
|
120
|
+
if self.analysis is None:
|
|
121
|
+
self.analysis = ProjectAnalysis(
|
|
122
|
+
project_dir=self.project_dir,
|
|
123
|
+
language=language
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# 分析具体配置文件
|
|
127
|
+
self._parse_config_files(language)
|
|
128
|
+
|
|
129
|
+
# 检查关键依赖
|
|
130
|
+
self._check_key_dependencies(language)
|
|
131
|
+
|
|
132
|
+
return self.analysis
|
|
133
|
+
|
|
134
|
+
def _find_config_files(self, language: str):
|
|
135
|
+
"""查找配置文件"""
|
|
136
|
+
if self.analysis is None:
|
|
137
|
+
return
|
|
138
|
+
patterns = self.CONFIG_PATTERNS.get(language, [])
|
|
139
|
+
|
|
140
|
+
for pattern, build_system in patterns:
|
|
141
|
+
if '*' in pattern:
|
|
142
|
+
# 匹配通配符
|
|
143
|
+
files = Path(self.project_dir).glob(pattern)
|
|
144
|
+
for f in files:
|
|
145
|
+
self.analysis.build_system = build_system
|
|
146
|
+
else:
|
|
147
|
+
# 精确匹配
|
|
148
|
+
config_path = os.path.join(self.project_dir, pattern)
|
|
149
|
+
if os.path.exists(config_path):
|
|
150
|
+
self.analysis.build_system = build_system
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
def _parse_config_files(self, language: str):
|
|
154
|
+
"""解析配置文件"""
|
|
155
|
+
project_path = Path(self.project_dir)
|
|
156
|
+
|
|
157
|
+
if language == 'python':
|
|
158
|
+
self._parse_python_configs(project_path)
|
|
159
|
+
elif language == 'java':
|
|
160
|
+
self._parse_java_configs(project_path)
|
|
161
|
+
elif language == 'go':
|
|
162
|
+
self._parse_go_configs(project_path)
|
|
163
|
+
elif language in ['c', 'cpp']:
|
|
164
|
+
self._parse_c_cpp_configs(project_path)
|
|
165
|
+
|
|
166
|
+
def _parse_python_configs(self, project_path: Path):
|
|
167
|
+
"""解析Python配置文件"""
|
|
168
|
+
# requirements.txt
|
|
169
|
+
req_file = project_path / 'requirements.txt'
|
|
170
|
+
if req_file.exists():
|
|
171
|
+
self._parse_requirements(req_file)
|
|
172
|
+
|
|
173
|
+
# setup.py
|
|
174
|
+
setup_file = project_path / 'setup.py'
|
|
175
|
+
if setup_file.exists():
|
|
176
|
+
self._parse_setup_py(setup_file)
|
|
177
|
+
|
|
178
|
+
# pyproject.toml
|
|
179
|
+
pyproject_file = project_path / 'pyproject.toml'
|
|
180
|
+
if pyproject_file.exists():
|
|
181
|
+
self._parse_pyproject_toml(pyproject_file)
|
|
182
|
+
|
|
183
|
+
def _parse_requirements(self, file_path: Path):
|
|
184
|
+
"""解析requirements.txt"""
|
|
185
|
+
try:
|
|
186
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
187
|
+
for line in f:
|
|
188
|
+
line = line.strip()
|
|
189
|
+
if line and not line.startswith('#'):
|
|
190
|
+
# 解析版本要求
|
|
191
|
+
name, version = self._parse_package_line(line)
|
|
192
|
+
dep = Dependency(
|
|
193
|
+
name=name,
|
|
194
|
+
type=DependencyType.PYTHON_PACKAGE,
|
|
195
|
+
version=version,
|
|
196
|
+
required=False
|
|
197
|
+
)
|
|
198
|
+
self.analysis.dependencies.append(dep)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
self.analysis.warnings.append(f"无法解析 requirements.txt: {e}")
|
|
201
|
+
|
|
202
|
+
def _parse_setup_py(self, file_path: Path):
|
|
203
|
+
"""解析setup.py"""
|
|
204
|
+
try:
|
|
205
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
206
|
+
content = f.read()
|
|
207
|
+
|
|
208
|
+
# 查找install_requires
|
|
209
|
+
match = re.search(r'install_requires\s*=\s*\[(.*?)\]', content, re.DOTALL)
|
|
210
|
+
if match:
|
|
211
|
+
deps_str = match.group(1)
|
|
212
|
+
for line in deps_str.split('\n'):
|
|
213
|
+
line = line.strip().strip("'\"")
|
|
214
|
+
if line and line.startswith('['):
|
|
215
|
+
line = line.strip('[]')
|
|
216
|
+
if line:
|
|
217
|
+
name, version = self._parse_package_line(line)
|
|
218
|
+
dep = Dependency(
|
|
219
|
+
name=name,
|
|
220
|
+
type=DependencyType.PYTHON_PACKAGE,
|
|
221
|
+
version=version,
|
|
222
|
+
required=False
|
|
223
|
+
)
|
|
224
|
+
self.analysis.dependencies.append(dep)
|
|
225
|
+
|
|
226
|
+
# 查找test_requires
|
|
227
|
+
match = re.search(r'test_requires\s*=\s*\[(.*?)\]', content, re.DOTALL)
|
|
228
|
+
if match:
|
|
229
|
+
deps_str = match.group(1)
|
|
230
|
+
for line in deps_str.split('\n'):
|
|
231
|
+
line = line.strip().strip("'\"")
|
|
232
|
+
if line and line.startswith('['):
|
|
233
|
+
line = line.strip('[]')
|
|
234
|
+
if line:
|
|
235
|
+
name, version = self._parse_package_line(line)
|
|
236
|
+
dep = Dependency(
|
|
237
|
+
name=name,
|
|
238
|
+
type=DependencyType.TEST_FRAMEWORK,
|
|
239
|
+
version=version,
|
|
240
|
+
required=False
|
|
241
|
+
)
|
|
242
|
+
self.analysis.test_framework = name
|
|
243
|
+
self.analysis.dependencies.append(dep)
|
|
244
|
+
except Exception as e:
|
|
245
|
+
self.analysis.warnings.append(f"无法解析 setup.py: {e}")
|
|
246
|
+
|
|
247
|
+
def _parse_pyproject_toml(self, file_path: Path):
|
|
248
|
+
"""解析pyproject.toml"""
|
|
249
|
+
try:
|
|
250
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
251
|
+
content = f.read()
|
|
252
|
+
|
|
253
|
+
# 查找测试依赖
|
|
254
|
+
match = re.search(r'\[tool\.pytest\]', content)
|
|
255
|
+
if match:
|
|
256
|
+
self.analysis.test_framework = 'pytest'
|
|
257
|
+
|
|
258
|
+
# 查找依赖
|
|
259
|
+
match = re.search(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL)
|
|
260
|
+
if match:
|
|
261
|
+
deps_str = match.group(1)
|
|
262
|
+
for line in deps_str.split('\n'):
|
|
263
|
+
line = line.strip().strip("'\"")
|
|
264
|
+
if line:
|
|
265
|
+
name, version = self._parse_package_line(line)
|
|
266
|
+
dep = Dependency(
|
|
267
|
+
name=name,
|
|
268
|
+
type=DependencyType.PYTHON_PACKAGE,
|
|
269
|
+
version=version,
|
|
270
|
+
required=False
|
|
271
|
+
)
|
|
272
|
+
self.analysis.dependencies.append(dep)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
self.analysis.warnings.append(f"无法解析 pyproject.toml: {e}")
|
|
275
|
+
|
|
276
|
+
def _parse_package_line(self, line: str) -> Tuple[str, Optional[str]]:
|
|
277
|
+
"""解析包名和版本"""
|
|
278
|
+
line = line.strip()
|
|
279
|
+
if '>=' in line:
|
|
280
|
+
name, version = line.split('>=')
|
|
281
|
+
return name.strip(), f">={version.strip()}"
|
|
282
|
+
elif '<=' in line:
|
|
283
|
+
name, version = line.split('<=')
|
|
284
|
+
return name.strip(), f"<={version.strip()}"
|
|
285
|
+
elif '==' in line:
|
|
286
|
+
name, version = line.split('==')
|
|
287
|
+
return name.strip(), version.strip()
|
|
288
|
+
elif '>' in line:
|
|
289
|
+
name, version = line.split('>')
|
|
290
|
+
return name.strip(), f">{version.strip()}"
|
|
291
|
+
elif '<' in line:
|
|
292
|
+
name, version = line.split('<')
|
|
293
|
+
return name.strip(), f"<{version.strip()}"
|
|
294
|
+
else:
|
|
295
|
+
return line, None
|
|
296
|
+
|
|
297
|
+
def _parse_java_configs(self, project_path: Path):
|
|
298
|
+
"""解析Java配置文件"""
|
|
299
|
+
pom_file = project_path / 'pom.xml'
|
|
300
|
+
if pom_file.exists():
|
|
301
|
+
try:
|
|
302
|
+
tree = ET.parse(pom_file)
|
|
303
|
+
root = tree.getroot()
|
|
304
|
+
|
|
305
|
+
# 解析命名空间
|
|
306
|
+
ns = {'m': 'http://maven.apache.org/POM/4.0.0'}
|
|
307
|
+
|
|
308
|
+
# 获取坐标
|
|
309
|
+
group_id = root.find('m:groupId', ns) or root.find('groupId')
|
|
310
|
+
artifact_id = root.find('m:artifactId', ns) or root.find('artifactId')
|
|
311
|
+
version = root.find('m:version', ns) or root.find('version')
|
|
312
|
+
|
|
313
|
+
if artifact_id is not None:
|
|
314
|
+
self.analysis.build_system = 'maven'
|
|
315
|
+
|
|
316
|
+
# 查找测试依赖
|
|
317
|
+
for dep in root.findall('.//m:dependency', ns):
|
|
318
|
+
dep_group = dep.find('m:groupId', ns) or dep.find('groupId')
|
|
319
|
+
dep_artifact = dep.find('m:artifactId', ns) or dep.find('artifactId')
|
|
320
|
+
dep_version = dep.find('m:version', ns) or dep.find('version')
|
|
321
|
+
|
|
322
|
+
if dep_artifact is not None:
|
|
323
|
+
dep_name = dep_artifact.text or ""
|
|
324
|
+
is_test = False
|
|
325
|
+
|
|
326
|
+
# 检查是否是测试依赖
|
|
327
|
+
for scope in dep.findall('m:scope', ns) or dep.findall('scope'):
|
|
328
|
+
if scope.text == 'test':
|
|
329
|
+
is_test = True
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
# 检查测试框架
|
|
333
|
+
test_frameworks = ['junit', 'testng', 'mockito', 'assertj']
|
|
334
|
+
if any(tf in dep_name.lower() for tf in test_frameworks):
|
|
335
|
+
self.analysis.test_framework = dep_name
|
|
336
|
+
|
|
337
|
+
dep_type = DependencyType.TEST_FRAMEWORK if is_test else DependencyType.JAVA_PACKAGE
|
|
338
|
+
dep = Dependency(
|
|
339
|
+
name=dep_name,
|
|
340
|
+
type=dep_type,
|
|
341
|
+
version=dep_version.text if dep_version is not None else None,
|
|
342
|
+
required=False
|
|
343
|
+
)
|
|
344
|
+
self.analysis.dependencies.append(dep)
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
self.analysis.warnings.append(f"无法解析 pom.xml: {e}")
|
|
348
|
+
|
|
349
|
+
def _parse_go_configs(self, project_path: Path):
|
|
350
|
+
"""解析Go配置文件"""
|
|
351
|
+
go_mod_file = project_path / 'go.mod'
|
|
352
|
+
if go_mod_file.exists():
|
|
353
|
+
try:
|
|
354
|
+
with open(go_mod_file, 'r', encoding='utf-8') as f:
|
|
355
|
+
content = f.read()
|
|
356
|
+
|
|
357
|
+
# 查找go.mod版本
|
|
358
|
+
match = re.search(r'go\s+(\d+\.\d+)', content)
|
|
359
|
+
if match:
|
|
360
|
+
self.analysis.build_system = f"go {match.group(1)}"
|
|
361
|
+
|
|
362
|
+
# 查找测试框架
|
|
363
|
+
test_imports = ['testing', 'testify', 'goconvey', 'gocheck']
|
|
364
|
+
for line in content.split('\n'):
|
|
365
|
+
if 'require' in line.lower():
|
|
366
|
+
for test_pkg in test_imports:
|
|
367
|
+
if test_pkg in line.lower():
|
|
368
|
+
self.analysis.test_framework = test_pkg
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
except Exception as e:
|
|
372
|
+
self.analysis.warnings.append(f"无法解析 go.mod: {e}")
|
|
373
|
+
|
|
374
|
+
def _parse_c_cpp_configs(self, project_path: Path):
|
|
375
|
+
"""解析C/C++配置文件"""
|
|
376
|
+
# 检查Makefile
|
|
377
|
+
makefile = project_path / 'Makefile'
|
|
378
|
+
if makefile.exists():
|
|
379
|
+
self.analysis.build_system = 'make'
|
|
380
|
+
with open(makefile, 'r', encoding='utf-8') as f:
|
|
381
|
+
content = f.read()
|
|
382
|
+
|
|
383
|
+
# 检查测试框架
|
|
384
|
+
if 'gtest' in content.lower() or 'googletest' in content.lower():
|
|
385
|
+
self.analysis.test_framework = 'googletest'
|
|
386
|
+
elif 'catch' in content.lower():
|
|
387
|
+
self.analysis.test_framework = 'catch2'
|
|
388
|
+
elif 'unity' in content.lower():
|
|
389
|
+
self.analysis.test_framework = 'unity'
|
|
390
|
+
|
|
391
|
+
# 检查CMakeLists.txt
|
|
392
|
+
cmake_file = project_path / 'CMakeLists.txt'
|
|
393
|
+
if cmake_file.exists() and not self.analysis.build_system:
|
|
394
|
+
self.analysis.build_system = 'cmake'
|
|
395
|
+
|
|
396
|
+
with open(cmake_file, 'r', encoding='utf-8') as f:
|
|
397
|
+
content = f.read()
|
|
398
|
+
|
|
399
|
+
# 检查测试框架
|
|
400
|
+
if 'GoogleTest' in content or 'gtest' in content.lower():
|
|
401
|
+
self.analysis.test_framework = 'googletest'
|
|
402
|
+
elif 'Catch' in content:
|
|
403
|
+
self.analysis.test_framework = 'catch2'
|
|
404
|
+
|
|
405
|
+
def _check_key_dependencies(self, language: str):
|
|
406
|
+
"""检查关键依赖是否安装"""
|
|
407
|
+
key_deps = self.KEY_DEPENDENCIES.get(language, [])
|
|
408
|
+
|
|
409
|
+
for dep_name, dep_type in key_deps:
|
|
410
|
+
dep = Dependency(
|
|
411
|
+
name=dep_name,
|
|
412
|
+
type=dep_type,
|
|
413
|
+
required=True
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# 检查是否已安装
|
|
417
|
+
installed, version = self._check_dependency_installed(dep_name, language)
|
|
418
|
+
dep.installed = installed
|
|
419
|
+
dep.installed_version = version
|
|
420
|
+
|
|
421
|
+
self.analysis.dependencies.append(dep)
|
|
422
|
+
|
|
423
|
+
if not installed:
|
|
424
|
+
self.analysis.missing_dependencies.append(dep_name)
|
|
425
|
+
|
|
426
|
+
def _check_dependency_installed(self, dep_name: str, language: str) -> Tuple[bool, Optional[str]]:
|
|
427
|
+
"""检查依赖是否已安装"""
|
|
428
|
+
import subprocess
|
|
429
|
+
|
|
430
|
+
commands = {
|
|
431
|
+
'python': f'python3 --version 2>&1',
|
|
432
|
+
'java': f'java -version 2>&1',
|
|
433
|
+
'go': f'go version',
|
|
434
|
+
'gcc': f'gcc --version',
|
|
435
|
+
'g++': f'g++ --version',
|
|
436
|
+
'make': f'make --version',
|
|
437
|
+
'maven': f'mvn -version',
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
cmd = commands.get(dep_name)
|
|
441
|
+
if cmd:
|
|
442
|
+
try:
|
|
443
|
+
result = subprocess.run(
|
|
444
|
+
cmd,
|
|
445
|
+
shell=True,
|
|
446
|
+
capture_output=True,
|
|
447
|
+
text=True,
|
|
448
|
+
timeout=5
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if result.returncode == 0:
|
|
452
|
+
# 尝试提取版本
|
|
453
|
+
output = result.stdout or result.stderr
|
|
454
|
+
version_match = re.search(r'(\d+\.\d+\.?\d*)', output)
|
|
455
|
+
version = version_match.group(1) if version_match else None
|
|
456
|
+
return True, version
|
|
457
|
+
except:
|
|
458
|
+
pass
|
|
459
|
+
|
|
460
|
+
return False, None
|
|
461
|
+
|
|
462
|
+
def get_installation_suggestions(self) -> List[str]:
|
|
463
|
+
"""获取安装建议"""
|
|
464
|
+
suggestions = []
|
|
465
|
+
|
|
466
|
+
if not self.analysis:
|
|
467
|
+
return suggestions
|
|
468
|
+
|
|
469
|
+
dep_mapping = {
|
|
470
|
+
'pytest': {
|
|
471
|
+
'command': 'pip install pytest',
|
|
472
|
+
'description': 'Python测试框架'
|
|
473
|
+
},
|
|
474
|
+
'coverage': {
|
|
475
|
+
'command': 'pip install coverage',
|
|
476
|
+
'description': 'Python覆盖率工具'
|
|
477
|
+
},
|
|
478
|
+
'tox': {
|
|
479
|
+
'command': 'pip install tox',
|
|
480
|
+
'description': 'Python自动化测试工具'
|
|
481
|
+
},
|
|
482
|
+
'maven': {
|
|
483
|
+
'command': 'apt install maven # Ubuntu/Debian\nbrew install maven # macOS',
|
|
484
|
+
'description': 'Java构建工具'
|
|
485
|
+
},
|
|
486
|
+
'gcc': {
|
|
487
|
+
'command': 'apt install gcc # Ubuntu/Debian\nbrew install gcc # macOS',
|
|
488
|
+
'description': 'C/C++编译器'
|
|
489
|
+
},
|
|
490
|
+
'g++': {
|
|
491
|
+
'command': 'apt install g++ # Ubuntu/Debian\nbrew install gcc # macOS',
|
|
492
|
+
'description': 'C++编译器'
|
|
493
|
+
},
|
|
494
|
+
'make': {
|
|
495
|
+
'command': 'apt install make # Ubuntu/Debian\nbrew install make # macOS',
|
|
496
|
+
'description': '构建工具'
|
|
497
|
+
},
|
|
498
|
+
'lcov': {
|
|
499
|
+
'command': 'apt install lcov # Ubuntu/Debian',
|
|
500
|
+
'description': '代码覆盖率工具'
|
|
501
|
+
},
|
|
502
|
+
'googletest': {
|
|
503
|
+
'command': 'apt install libgtest-dev cmake\ncd /usr/src/gtest && cmake . && make && cp *.a /usr/lib/',
|
|
504
|
+
'description': 'C++测试框架'
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
for dep_name in self.analysis.missing_dependencies:
|
|
509
|
+
if dep_name in dep_mapping:
|
|
510
|
+
info = dep_mapping[dep_name]
|
|
511
|
+
suggestions.append(f"{dep_name}: {info['description']}\n 安装: {info['command']}")
|
|
512
|
+
|
|
513
|
+
return suggestions
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
JUnit XML 报告转换器包
|
|
4
|
+
将各种格式的测试输出转换为统一的 JUnit XML 格式
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from .base import TestCaseResult, TestSuiteResult, BaseConverter
|
|
10
|
+
from .go_converter import GoTestConverter, GoJUnitReportTool
|
|
11
|
+
from .gtest_converter import GTestConverter
|
|
12
|
+
from .cunit_converter import CUnitConverter
|
|
13
|
+
from .generic_converter import GenericConverter
|
|
14
|
+
from .factory import ConverterFactory
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# 保持向后兼容的 JUnitReportConverter 类
|
|
18
|
+
class JUnitReportConverter(BaseConverter):
|
|
19
|
+
"""JUnit XML 报告转换器 (向后兼容)"""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def convert_go_test(go_test_output: str, coverage_out: Optional[str] = None) -> str:
|
|
23
|
+
"""将 Go test JSON 输出转换为 JUnit XML"""
|
|
24
|
+
return GoTestConverter.convert(go_test_output, coverage_out)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def convert_cpp_gtest(gtest_output: str, xml_path: str) -> str:
|
|
28
|
+
"""转换 Google Test 输出为 JUnit XML"""
|
|
29
|
+
return GTestConverter.convert(gtest_output, xml_path)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def convert_cunit(cunit_output: str) -> str:
|
|
33
|
+
"""转换 CUnit 输出为 JUnit XML"""
|
|
34
|
+
return CUnitConverter.convert(cunit_output)
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def convert_generic_output(output: str, test_framework: str) -> str:
|
|
38
|
+
"""通用测试输出转换"""
|
|
39
|
+
return GenericConverter.convert(output, test_framework)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# 保持向后兼容的 GoJUnitReport 类
|
|
43
|
+
class GoJUnitReport(GoJUnitReportTool):
|
|
44
|
+
"""Go 测试 JUnit 报告生成器 (向后兼容)"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
'TestCaseResult',
|
|
50
|
+
'TestSuiteResult',
|
|
51
|
+
'BaseConverter',
|
|
52
|
+
'GoTestConverter',
|
|
53
|
+
'GoJUnitReportTool',
|
|
54
|
+
'GTestConverter',
|
|
55
|
+
'CUnitConverter',
|
|
56
|
+
'GenericConverter',
|
|
57
|
+
'ConverterFactory',
|
|
58
|
+
'JUnitReportConverter',
|
|
59
|
+
'GoJUnitReport',
|
|
60
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
JUnit XML 报告转换器 - 基础模块
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class TestCaseResult:
|
|
12
|
+
"""测试用例结果"""
|
|
13
|
+
name: str
|
|
14
|
+
classname: str
|
|
15
|
+
time: float
|
|
16
|
+
passed: bool = True
|
|
17
|
+
failed_message: Optional[str] = None
|
|
18
|
+
failed_type: Optional[str] = None
|
|
19
|
+
error_message: Optional[str] = None
|
|
20
|
+
error_type: Optional[str] = None
|
|
21
|
+
skipped: bool = False
|
|
22
|
+
skipped_message: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TestSuiteResult:
|
|
27
|
+
"""测试套件结果"""
|
|
28
|
+
name: str
|
|
29
|
+
tests: int = 0
|
|
30
|
+
failures: int = 0
|
|
31
|
+
errors: int = 0
|
|
32
|
+
skipped: int = 0
|
|
33
|
+
time: float = 0.0
|
|
34
|
+
test_cases: List[TestCaseResult] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BaseConverter:
|
|
38
|
+
"""报告转换器基类"""
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _create_junit_xml(suite_name: str, test_cases: List[TestCaseResult],
|
|
42
|
+
total_time: float) -> str:
|
|
43
|
+
"""创建 JUnit XML"""
|
|
44
|
+
tests = len(test_cases)
|
|
45
|
+
failures = sum(1 for tc in test_cases if not tc.passed and not tc.skipped)
|
|
46
|
+
errors = sum(1 for tc in test_cases if not tc.passed and tc.skipped)
|
|
47
|
+
skipped = sum(1 for tc in test_cases if tc.skipped)
|
|
48
|
+
|
|
49
|
+
lines = [
|
|
50
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
51
|
+
f'<testsuites name="Test Results" tests="{tests}" failures="{failures}" errors="{errors}" skipped="{skipped}">',
|
|
52
|
+
f' <testsuite name="{suite_name}" tests="{tests}" failures="{failures}" errors="{errors}" skipped="{skipped}" time="{total_time:.3f}">',
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
for case in test_cases:
|
|
56
|
+
status_attr = ''
|
|
57
|
+
content = ''
|
|
58
|
+
|
|
59
|
+
if case.skipped:
|
|
60
|
+
status_attr = f' status="skipped"'
|
|
61
|
+
content = f' <skipped message="{case.skipped_message or ""}"/>'
|
|
62
|
+
elif not case.passed:
|
|
63
|
+
status_attr = f' status="failure"'
|
|
64
|
+
if case.failed_message:
|
|
65
|
+
content = f' <failure message="{case.failed_message}" type="{case.failed_type or "failure"}"/>'
|
|
66
|
+
elif case.error_message:
|
|
67
|
+
content = f' <error message="{case.error_message}" type="{case.error_type or "error"}"/>'
|
|
68
|
+
|
|
69
|
+
lines.append(f' <testcase name="{case.name}" classname="{case.classname}" time="{case.time:.3f}"{status_attr}>')
|
|
70
|
+
lines.append(content)
|
|
71
|
+
lines.append(f' </testcase>')
|
|
72
|
+
|
|
73
|
+
lines.extend([
|
|
74
|
+
' </testsuite>',
|
|
75
|
+
'</testsuites>'
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
return '\n'.join(lines)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _create_basic_junit_xml(suite_name: str, test_cases: List[TestCaseResult]) -> str:
|
|
82
|
+
"""创建基本的 JUnit XML (用于错误情况)"""
|
|
83
|
+
return BaseConverter._create_junit_xml(suite_name, test_cases, 0.0)
|