test-coverage-analyzer 0.1.3__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.
- test_coverage_analyzer/__init__.py +30 -0
- test_coverage_analyzer/cli.py +213 -0
- test_coverage_analyzer/core.py +1514 -0
- test_coverage_analyzer-0.1.3.dist-info/METADATA +20 -0
- test_coverage_analyzer-0.1.3.dist-info/RECORD +8 -0
- test_coverage_analyzer-0.1.3.dist-info/WHEEL +5 -0
- test_coverage_analyzer-0.1.3.dist-info/entry_points.txt +2 -0
- test_coverage_analyzer-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Dict, Optional, Tuple
|
|
5
|
+
from dataclasses import dataclass, asdict
|
|
6
|
+
import ast
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class TestFileMatch:
|
|
11
|
+
"""测试文件匹配结果"""
|
|
12
|
+
source_file: str
|
|
13
|
+
test_file: str
|
|
14
|
+
language: str
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CoverageResult:
|
|
18
|
+
"""覆盖率结果"""
|
|
19
|
+
source_file: str
|
|
20
|
+
test_file: str
|
|
21
|
+
language: str
|
|
22
|
+
total_testable_elements: int
|
|
23
|
+
tested_elements: int
|
|
24
|
+
coverage_percentage: float
|
|
25
|
+
uncovered_elements: List[str]
|
|
26
|
+
code_similarity: float
|
|
27
|
+
cyclomatic_complexity: int
|
|
28
|
+
dependency_complexity: int
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class UnmatchedResult:
|
|
32
|
+
"""未匹配文件的分析结果"""
|
|
33
|
+
source_file: str
|
|
34
|
+
language: str
|
|
35
|
+
cyclomatic_complexity: int
|
|
36
|
+
dependency_complexity: int
|
|
37
|
+
|
|
38
|
+
class TestFileFinder:
|
|
39
|
+
"""多语言单元测试文件查找器"""
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
# 定义各种语言的源文件和测试文件匹配模式
|
|
43
|
+
self.language_patterns = {
|
|
44
|
+
'python': {
|
|
45
|
+
'source': r'.*\.py$',
|
|
46
|
+
'test': [
|
|
47
|
+
r'.*_test\.py$',
|
|
48
|
+
r'.*test_.*\.py$',
|
|
49
|
+
r'test_.*\.py$'
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
'java': {
|
|
53
|
+
'source': r'.*\.java$',
|
|
54
|
+
'test': [
|
|
55
|
+
r'.*Test\.java$',
|
|
56
|
+
r'Test.*\.java$',
|
|
57
|
+
r'.*Tests\.java$',
|
|
58
|
+
r'.*TestCase\.java$'
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
'go': {
|
|
62
|
+
'source': r'.*\.go$',
|
|
63
|
+
'test': [r'.*_test\.go$']
|
|
64
|
+
},
|
|
65
|
+
'cpp': {
|
|
66
|
+
'source': r'.*\.(cpp|cc|cxx|c\+\+)$',
|
|
67
|
+
'test': [
|
|
68
|
+
r'.*_test\.(cpp|cc|cxx|c\+\+)$',
|
|
69
|
+
r'.*test_.*\.(cpp|cc|cxx|c\+\+)$',
|
|
70
|
+
r'test_.*\.(cpp|cc|cxx|c\+\+)$',
|
|
71
|
+
r'.*Test\.(cpp|cc|cxx|c\+\+)$'
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
'javascript': {
|
|
75
|
+
'source': r'.*\.(js|jsx)$',
|
|
76
|
+
'test': [
|
|
77
|
+
r'.*\.(test|spec)\.(js|jsx)$',
|
|
78
|
+
r'.*test\.(js|jsx)$',
|
|
79
|
+
r'test_.*\.(js|jsx)$'
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# 要排除的文件模式
|
|
85
|
+
self.exclude_patterns = [
|
|
86
|
+
r'^\..*', # 以.开头的文件
|
|
87
|
+
r'.*\.md$', # markdown文件
|
|
88
|
+
r'.*\.txt$',
|
|
89
|
+
r'.*\.log$',
|
|
90
|
+
r'.*\.json$',
|
|
91
|
+
r'.*\.yaml$',
|
|
92
|
+
r'.*\.yml$',
|
|
93
|
+
r'.*\.xml$',
|
|
94
|
+
r'.*\.html$',
|
|
95
|
+
r'.*\.css$',
|
|
96
|
+
r'.*\.png$',
|
|
97
|
+
r'.*\.jpg$',
|
|
98
|
+
r'.*\.jpeg$',
|
|
99
|
+
r'.*\.gif$',
|
|
100
|
+
r'.*\.svg$',
|
|
101
|
+
r'.*\.pdf$',
|
|
102
|
+
r'.*\.zip$',
|
|
103
|
+
r'.*\.tar$',
|
|
104
|
+
r'.*\.gz$',
|
|
105
|
+
r'.*\.rar$'
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
# 特定语言需要排除的文件
|
|
109
|
+
self.language_specific_excludes = {
|
|
110
|
+
'python': [
|
|
111
|
+
r'.*__init__\.py$', # Python的__init__.py文件
|
|
112
|
+
r'.*config\.py$', # Python的配置文件
|
|
113
|
+
r'.*settings\.py$', # Python的设置文件
|
|
114
|
+
r'.*setup\.py$', # Python的setup文件
|
|
115
|
+
r'.*requirements\.py$',
|
|
116
|
+
r'.*requirements.*\.txt$',
|
|
117
|
+
r'.*Pipfile$',
|
|
118
|
+
r'.*pyproject\.toml$',
|
|
119
|
+
r'.*tox\.ini$',
|
|
120
|
+
r'.*Dockerfile$',
|
|
121
|
+
r'.*docker-compose\.yml$',
|
|
122
|
+
r'.*docker-compose\.yaml$'
|
|
123
|
+
],
|
|
124
|
+
'java': [
|
|
125
|
+
r'.*pom\.xml$', # Maven配置文件
|
|
126
|
+
r'.*build\.gradle$', # Gradle配置文件
|
|
127
|
+
r'.*settings\.gradle$',
|
|
128
|
+
r'.*application\.properties$', # Spring配置文件
|
|
129
|
+
r'.*application\.yml$',
|
|
130
|
+
r'.*application\.yaml$',
|
|
131
|
+
r'.*logback\.xml$',
|
|
132
|
+
r'.*log4j\.xml$',
|
|
133
|
+
r'.*config\.java$', # 配置类
|
|
134
|
+
r'.*Config\.java$',
|
|
135
|
+
r'.*Settings\.java$',
|
|
136
|
+
r'.*settings\.java$',
|
|
137
|
+
r'.*Constants\.java$', # 常量类
|
|
138
|
+
r'.*constants\.java$',
|
|
139
|
+
r'.*Dockerfile$',
|
|
140
|
+
r'.*docker-compose\.yml$',
|
|
141
|
+
r'.*docker-compose\.yaml$'
|
|
142
|
+
],
|
|
143
|
+
'go': [
|
|
144
|
+
r'.*go\.mod$',
|
|
145
|
+
r'.*go\.sum$',
|
|
146
|
+
r'.*config\.go$', # Go配置文件
|
|
147
|
+
r'.*config.*\.go$',
|
|
148
|
+
r'.*settings\.go$',
|
|
149
|
+
r'.*Dockerfile$',
|
|
150
|
+
r'.*docker-compose\.yml$',
|
|
151
|
+
r'.*docker-compose\.yaml$'
|
|
152
|
+
],
|
|
153
|
+
'cpp': [
|
|
154
|
+
r'.*CMakeLists\.txt$',
|
|
155
|
+
r'.*Makefile$',
|
|
156
|
+
r'.*makefile$',
|
|
157
|
+
r'.*config\.h$', # C++配置头文件
|
|
158
|
+
r'.*config\.cpp$',
|
|
159
|
+
r'.*settings\.h$',
|
|
160
|
+
r'.*settings\.cpp$',
|
|
161
|
+
r'.*constants\.h$',
|
|
162
|
+
r'.*constants\.cpp$',
|
|
163
|
+
r'.*Dockerfile$',
|
|
164
|
+
r'.*docker-compose\.yml$',
|
|
165
|
+
r'.*docker-compose\.yaml$'
|
|
166
|
+
],
|
|
167
|
+
'javascript': [
|
|
168
|
+
r'.*package\.json$',
|
|
169
|
+
r'.*package-lock\.json$',
|
|
170
|
+
r'.*yarn\.lock$',
|
|
171
|
+
r'.*webpack\.config\.js$', # Webpack配置
|
|
172
|
+
r'.*babel\.config\.js$', # Babel配置
|
|
173
|
+
r'.*jest\.config\.js$', # Jest配置
|
|
174
|
+
r'.*config\.js$', # 配置文件
|
|
175
|
+
r'.*config\.json$',
|
|
176
|
+
r'.*settings\.js$',
|
|
177
|
+
r'.*settings\.json$',
|
|
178
|
+
r'.*Dockerfile$',
|
|
179
|
+
r'.*docker-compose\.yml$',
|
|
180
|
+
r'.*docker-compose\.yaml$'
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# 编译正则表达式以提高性能
|
|
185
|
+
self.compiled_patterns = {}
|
|
186
|
+
for lang, patterns in self.language_patterns.items():
|
|
187
|
+
self.compiled_patterns[lang] = {
|
|
188
|
+
'source': re.compile(patterns['source']),
|
|
189
|
+
'test': [re.compile(t) for t in patterns['test']]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
self.compiled_exclude = [re.compile(p) for p in self.exclude_patterns]
|
|
193
|
+
self.compiled_language_excludes = {}
|
|
194
|
+
for lang, patterns in self.language_specific_excludes.items():
|
|
195
|
+
self.compiled_language_excludes[lang] = [re.compile(p) for p in patterns]
|
|
196
|
+
|
|
197
|
+
def _should_exclude(self, file_path: str, language: str = None) -> bool:
|
|
198
|
+
"""检查文件是否应该被排除"""
|
|
199
|
+
filename = os.path.basename(file_path)
|
|
200
|
+
|
|
201
|
+
# 检查通用排除模式
|
|
202
|
+
if any(pattern.match(filename) for pattern in self.compiled_exclude):
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
# 检查特定语言排除模式
|
|
206
|
+
if language and language in self.compiled_language_excludes:
|
|
207
|
+
if any(pattern.match(filename) for pattern in self.compiled_language_excludes[language]):
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
def _get_language(self, file_path: str) -> Optional[str]:
|
|
213
|
+
"""根据文件扩展名确定语言"""
|
|
214
|
+
for lang, patterns in self.compiled_patterns.items():
|
|
215
|
+
if patterns['source'].match(os.path.basename(file_path)):
|
|
216
|
+
return lang
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def _find_test_file(self, source_file: str, all_files: List[str]) -> Optional[str]:
|
|
220
|
+
"""为源文件查找对应的测试文件"""
|
|
221
|
+
source_path = Path(source_file)
|
|
222
|
+
source_name = source_path.stem # 不包含扩展名的文件名
|
|
223
|
+
source_dir = source_path.parent
|
|
224
|
+
|
|
225
|
+
# 根据源文件语言确定测试文件模式
|
|
226
|
+
lang = self._get_language(source_file)
|
|
227
|
+
if not lang:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
# 构建可能的测试文件名
|
|
231
|
+
possible_test_names = []
|
|
232
|
+
|
|
233
|
+
if lang == 'python':
|
|
234
|
+
# Python: test_*.py, *_test.py, *_tests.py
|
|
235
|
+
possible_test_names.extend([
|
|
236
|
+
f"test_{source_name}.py",
|
|
237
|
+
f"{source_name}_test.py",
|
|
238
|
+
f"{source_name}_tests.py"
|
|
239
|
+
])
|
|
240
|
+
elif lang == 'java':
|
|
241
|
+
# Java: *Test.java, Test*.java, *Tests.java
|
|
242
|
+
possible_test_names.extend([
|
|
243
|
+
f"{source_name}Test.java",
|
|
244
|
+
f"Test{source_name}.java",
|
|
245
|
+
f"{source_name}Tests.java",
|
|
246
|
+
f"{source_name}TestCase.java"
|
|
247
|
+
])
|
|
248
|
+
elif lang == 'go':
|
|
249
|
+
# Go: *_test.go
|
|
250
|
+
possible_test_names.append(f"{source_name}_test.go")
|
|
251
|
+
elif lang == 'cpp':
|
|
252
|
+
# C++: *_test.*, test_*, *Test.*
|
|
253
|
+
base_ext = source_path.suffix
|
|
254
|
+
possible_test_names.extend([
|
|
255
|
+
f"{source_name}_test{base_ext}",
|
|
256
|
+
f"test_{source_name}{base_ext}",
|
|
257
|
+
f"{source_name}Test{base_ext}"
|
|
258
|
+
])
|
|
259
|
+
elif lang == 'javascript':
|
|
260
|
+
# JS: *.test.*, *.spec.*, test_*
|
|
261
|
+
base_ext = source_path.suffix
|
|
262
|
+
possible_test_names.extend([
|
|
263
|
+
f"{source_name}.test{base_ext}",
|
|
264
|
+
f"{source_name}.spec{base_ext}",
|
|
265
|
+
f"test_{source_name}{base_ext}"
|
|
266
|
+
])
|
|
267
|
+
|
|
268
|
+
# 在同一目录下查找测试文件
|
|
269
|
+
for test_name in possible_test_names:
|
|
270
|
+
test_path = source_dir / test_name
|
|
271
|
+
if str(test_path) in all_files:
|
|
272
|
+
return str(test_path)
|
|
273
|
+
|
|
274
|
+
# 如果在同一目录没找到,检查所有文件中是否符合测试模式
|
|
275
|
+
for test_file in all_files:
|
|
276
|
+
test_filename = os.path.basename(test_file)
|
|
277
|
+
for test_pattern in self.compiled_patterns[lang]['test']:
|
|
278
|
+
if test_pattern.match(test_filename):
|
|
279
|
+
# 检查是否可能对应当前源文件
|
|
280
|
+
if self._is_matching_test(source_name, test_filename, lang):
|
|
281
|
+
return test_file
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
def _is_matching_test(self, source_name: str, test_name: str, lang: str) -> bool:
|
|
285
|
+
"""检查测试文件名是否可能对应源文件名"""
|
|
286
|
+
test_stem = Path(test_name).stem
|
|
287
|
+
|
|
288
|
+
if lang == 'python':
|
|
289
|
+
# 移除_test, test_等前缀后缀
|
|
290
|
+
if test_stem.endswith('_test'):
|
|
291
|
+
return test_stem[:-5] == source_name
|
|
292
|
+
elif test_stem.startswith('test_'):
|
|
293
|
+
return test_stem[5:] == source_name
|
|
294
|
+
elif lang == 'java':
|
|
295
|
+
# 移除Test, Tests, TestCase等后缀
|
|
296
|
+
if test_stem.endswith('Test'):
|
|
297
|
+
return test_stem[:-4] == source_name
|
|
298
|
+
elif test_stem.startswith('Test'):
|
|
299
|
+
return test_stem[4:] == source_name
|
|
300
|
+
elif test_stem.endswith('Tests'):
|
|
301
|
+
return test_stem[:-5] == source_name
|
|
302
|
+
elif test_stem.endswith('TestCase'):
|
|
303
|
+
return test_stem[:-8] == source_name
|
|
304
|
+
elif lang == 'go':
|
|
305
|
+
if test_stem.endswith('_test'):
|
|
306
|
+
return test_stem[:-5] == source_name
|
|
307
|
+
elif lang == 'cpp':
|
|
308
|
+
if test_stem.endswith('_test'):
|
|
309
|
+
return test_stem[:-5] == source_name
|
|
310
|
+
elif test_stem.startswith('test_'):
|
|
311
|
+
return test_stem[5:] == source_name
|
|
312
|
+
elif test_stem.endswith('Test'):
|
|
313
|
+
return test_stem[:-4] == source_name
|
|
314
|
+
elif lang == 'javascript':
|
|
315
|
+
if '.test.' in test_stem or test_stem.endswith('.test'):
|
|
316
|
+
return test_stem.replace('.test', '') == source_name
|
|
317
|
+
elif '.spec.' in test_stem or test_stem.endswith('.spec'):
|
|
318
|
+
return test_stem.replace('.spec', '') == source_name
|
|
319
|
+
elif test_stem.startswith('test_'):
|
|
320
|
+
return test_stem[5:] == source_name
|
|
321
|
+
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
def _get_all_files(self, path: str) -> List[str]:
|
|
325
|
+
"""获取路径下的所有文件"""
|
|
326
|
+
all_files = []
|
|
327
|
+
|
|
328
|
+
if os.path.isfile(path):
|
|
329
|
+
all_files.append(path)
|
|
330
|
+
elif os.path.isdir(path):
|
|
331
|
+
for root, dirs, files in os.walk(path):
|
|
332
|
+
# 过滤掉隐藏目录
|
|
333
|
+
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
|
334
|
+
for file in files:
|
|
335
|
+
file_path = os.path.join(root, file)
|
|
336
|
+
if not self._should_exclude(file_path):
|
|
337
|
+
all_files.append(file_path)
|
|
338
|
+
|
|
339
|
+
return all_files
|
|
340
|
+
|
|
341
|
+
def find_test_files(self, path: str) -> Tuple[List[TestFileMatch], List[Tuple[str, str]]]:
|
|
342
|
+
"""查找指定路径下所有源文件对应的测试文件,返回匹配和未匹配的列表"""
|
|
343
|
+
if not os.path.exists(path):
|
|
344
|
+
raise FileNotFoundError(f"路径不存在: {path}")
|
|
345
|
+
|
|
346
|
+
all_files = self._get_all_files(path)
|
|
347
|
+
|
|
348
|
+
# 过滤出源代码文件
|
|
349
|
+
source_files = []
|
|
350
|
+
for file_path in all_files:
|
|
351
|
+
lang = self._get_language(file_path)
|
|
352
|
+
if lang and not any(test_pattern.match(os.path.basename(file_path))
|
|
353
|
+
for test_pattern in self.compiled_patterns[lang]['test']):
|
|
354
|
+
# 检查是否需要排除特定语言的文件
|
|
355
|
+
if not self._should_exclude(file_path, lang):
|
|
356
|
+
source_files.append(file_path)
|
|
357
|
+
|
|
358
|
+
# 为每个源文件查找对应的测试文件
|
|
359
|
+
matches = []
|
|
360
|
+
unmatched = []
|
|
361
|
+
|
|
362
|
+
for source_file in source_files:
|
|
363
|
+
test_file = self._find_test_file(source_file, all_files)
|
|
364
|
+
lang = self._get_language(source_file)
|
|
365
|
+
if test_file:
|
|
366
|
+
matches.append(TestFileMatch(
|
|
367
|
+
source_file=source_file,
|
|
368
|
+
test_file=test_file,
|
|
369
|
+
language=lang
|
|
370
|
+
))
|
|
371
|
+
else:
|
|
372
|
+
unmatched.append((source_file, lang))
|
|
373
|
+
|
|
374
|
+
return matches, unmatched
|
|
375
|
+
|
|
376
|
+
class CodeAnalyzer:
|
|
377
|
+
"""代码分析器,用于计算代码复杂度和相似度"""
|
|
378
|
+
|
|
379
|
+
def calculate_complexity(self, source_code: str, language: str) -> Tuple[int, int]:
|
|
380
|
+
"""计算代码复杂度(圈复杂度和依赖复杂度)"""
|
|
381
|
+
try:
|
|
382
|
+
# 尝试使用lizard计算圈复杂度
|
|
383
|
+
import lizard
|
|
384
|
+
cyclomatic_complexity = self._calculate_lizard_complexity(source_code, language)
|
|
385
|
+
except ImportError:
|
|
386
|
+
# 如果lizard不可用,则使用内置方法
|
|
387
|
+
cyclomatic_complexity = self._calculate_builtin_cyclomatic_complexity(source_code, language)
|
|
388
|
+
|
|
389
|
+
dependency_complexity = self._calculate_dependency_complexity(source_code, language)
|
|
390
|
+
return cyclomatic_complexity, dependency_complexity
|
|
391
|
+
|
|
392
|
+
def calculate_file_complexity(self, file_path: str, language: str) -> Tuple[int, int]:
|
|
393
|
+
"""直接计算文件的复杂度"""
|
|
394
|
+
try:
|
|
395
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
396
|
+
source_code = f.read()
|
|
397
|
+
return self.calculate_complexity(source_code, language)
|
|
398
|
+
except Exception:
|
|
399
|
+
return 0, 0
|
|
400
|
+
|
|
401
|
+
def _calculate_lizard_complexity(self, source_code: str, language: str) -> int:
|
|
402
|
+
"""使用lizard计算圈复杂度"""
|
|
403
|
+
try:
|
|
404
|
+
import lizard
|
|
405
|
+
import tempfile
|
|
406
|
+
import os
|
|
407
|
+
|
|
408
|
+
# 创建临时文件以使用lizard分析
|
|
409
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix=f'.{language}', delete=False) as temp_file:
|
|
410
|
+
temp_file.write(source_code)
|
|
411
|
+
temp_file_path = temp_file.name
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
# 使用lizard分析代码
|
|
415
|
+
result = lizard.analyze_file(temp_file_path)
|
|
416
|
+
total_complexity = sum(func.cyclomatic_complexity for func in result.function_list)
|
|
417
|
+
|
|
418
|
+
# 如果没有函数,返回基于控制结构的简单复杂度
|
|
419
|
+
if not result.function_list:
|
|
420
|
+
return self._calculate_builtin_cyclomatic_complexity(source_code, language)
|
|
421
|
+
|
|
422
|
+
return max(1, total_complexity)
|
|
423
|
+
finally:
|
|
424
|
+
# 删除临时文件
|
|
425
|
+
os.unlink(temp_file_path)
|
|
426
|
+
except Exception:
|
|
427
|
+
# 如果lizard分析失败,使用内置方法
|
|
428
|
+
return self._calculate_builtin_cyclomatic_complexity(source_code, language)
|
|
429
|
+
|
|
430
|
+
def _calculate_builtin_cyclomatic_complexity(self, source_code: str, language: str) -> int:
|
|
431
|
+
"""使用内置方法计算圈复杂度"""
|
|
432
|
+
if language == 'python':
|
|
433
|
+
return self._calculate_python_cyclomatic_complexity(source_code)
|
|
434
|
+
elif language == 'java':
|
|
435
|
+
return self._calculate_java_cyclomatic_complexity(source_code)
|
|
436
|
+
elif language == 'go':
|
|
437
|
+
return self._calculate_go_cyclomatic_complexity(source_code)
|
|
438
|
+
elif language == 'cpp':
|
|
439
|
+
return self._calculate_cpp_cyclomatic_complexity(source_code)
|
|
440
|
+
elif language == 'javascript':
|
|
441
|
+
return self._calculate_js_cyclomatic_complexity(source_code)
|
|
442
|
+
else:
|
|
443
|
+
return 1
|
|
444
|
+
|
|
445
|
+
def _calculate_python_cyclomatic_complexity(self, source_code: str) -> int:
|
|
446
|
+
"""计算Python代码圈复杂度"""
|
|
447
|
+
try:
|
|
448
|
+
tree = ast.parse(source_code)
|
|
449
|
+
complexity = 1 # 基础复杂度
|
|
450
|
+
|
|
451
|
+
for node in ast.walk(tree):
|
|
452
|
+
# 增加圈复杂度的因素
|
|
453
|
+
if isinstance(node, (ast.If, ast.For, ast.While, ast.With, ast.Try, ast.ExceptHandler)):
|
|
454
|
+
complexity += 1
|
|
455
|
+
elif isinstance(node, ast.BoolOp): # and, or
|
|
456
|
+
complexity += len(node.values) - 1
|
|
457
|
+
elif isinstance(node, ast.IfExp): # 三元运算符
|
|
458
|
+
complexity += 1
|
|
459
|
+
|
|
460
|
+
return max(1, complexity)
|
|
461
|
+
except:
|
|
462
|
+
# 如果解析失败,返回基于行数的简单复杂度
|
|
463
|
+
return max(1, len(source_code.split('\n')))
|
|
464
|
+
|
|
465
|
+
def _calculate_java_cyclomatic_complexity(self, source_code: str) -> int:
|
|
466
|
+
"""计算Java代码圈复杂度"""
|
|
467
|
+
complexity = 1 # 基础复杂度
|
|
468
|
+
|
|
469
|
+
# 计算圈复杂度:控制结构
|
|
470
|
+
complexity += len(re.findall(r'\bif\s*\(', source_code))
|
|
471
|
+
complexity += len(re.findall(r'\bfor\s*\(', source_code))
|
|
472
|
+
complexity += len(re.findall(r'\bwhile\s*\(', source_code))
|
|
473
|
+
complexity += len(re.findall(r'\bswitch\s*\(', source_code))
|
|
474
|
+
complexity += len(re.findall(r'\btry\s*\{', source_code))
|
|
475
|
+
complexity += len(re.findall(r'\bcatch\s*\(', source_code))
|
|
476
|
+
|
|
477
|
+
# 计算布尔运算符(增加圈复杂度)
|
|
478
|
+
complexity += len(re.findall(r'&&|\|\|', source_code))
|
|
479
|
+
|
|
480
|
+
return max(1, complexity)
|
|
481
|
+
|
|
482
|
+
def _calculate_go_cyclomatic_complexity(self, source_code: str) -> int:
|
|
483
|
+
"""计算Go代码圈复杂度"""
|
|
484
|
+
complexity = 1 # 基础复杂度
|
|
485
|
+
|
|
486
|
+
# 计算圈复杂度:控制结构
|
|
487
|
+
complexity += len(re.findall(r'\bif\s*\(', source_code))
|
|
488
|
+
complexity += len(re.findall(r'\bfor\s*[^\{]*\{', source_code))
|
|
489
|
+
complexity += len(re.findall(r'\bswitch\s*[^\{]*\{', source_code))
|
|
490
|
+
complexity += len(re.findall(r'\bselect\s*\{', source_code))
|
|
491
|
+
|
|
492
|
+
# 计算布尔运算符
|
|
493
|
+
complexity += len(re.findall(r'&&|\|\|', source_code))
|
|
494
|
+
|
|
495
|
+
return max(1, complexity)
|
|
496
|
+
|
|
497
|
+
def _calculate_cpp_cyclomatic_complexity(self, source_code: str) -> int:
|
|
498
|
+
"""计算C++代码圈复杂度"""
|
|
499
|
+
complexity = 1 # 基础复杂度
|
|
500
|
+
|
|
501
|
+
# 计算圈复杂度:控制结构
|
|
502
|
+
complexity += len(re.findall(r'\bif\s*\(', source_code))
|
|
503
|
+
complexity += len(re.findall(r'\bfor\s*\(', source_code))
|
|
504
|
+
complexity += len(re.findall(r'\bwhile\s*\(', source_code))
|
|
505
|
+
complexity += len(re.findall(r'\bswitch\s*\(', source_code))
|
|
506
|
+
complexity += len(re.findall(r'\btry\s*\{', source_code))
|
|
507
|
+
complexity += len(re.findall(r'\bcatch\s*\(', source_code))
|
|
508
|
+
|
|
509
|
+
# 计算布尔运算符
|
|
510
|
+
complexity += len(re.findall(r'&&|\|\|', source_code))
|
|
511
|
+
|
|
512
|
+
return max(1, complexity)
|
|
513
|
+
|
|
514
|
+
def _calculate_js_cyclomatic_complexity(self, source_code: str) -> int:
|
|
515
|
+
"""计算JavaScript代码圈复杂度"""
|
|
516
|
+
complexity = 1 # 基础复杂度
|
|
517
|
+
|
|
518
|
+
# 计算圈复杂度:控制结构
|
|
519
|
+
complexity += len(re.findall(r'\bif\s*\(', source_code))
|
|
520
|
+
complexity += len(re.findall(r'\bfor\s*\(', source_code))
|
|
521
|
+
complexity += len(re.findall(r'\bwhile\s*\(', source_code))
|
|
522
|
+
complexity += len(re.findall(r'\bswitch\s*\(', source_code))
|
|
523
|
+
complexity += len(re.findall(r'\btry\s*\{', source_code))
|
|
524
|
+
complexity += len(re.findall(r'\bcatch\s*\(', source_code))
|
|
525
|
+
|
|
526
|
+
# 计算布尔运算符
|
|
527
|
+
complexity += len(re.findall(r'&&|\|\|', source_code))
|
|
528
|
+
|
|
529
|
+
return max(1, complexity)
|
|
530
|
+
|
|
531
|
+
def _calculate_dependency_complexity(self, source_code: str, language: str) -> int:
|
|
532
|
+
"""计算依赖复杂度"""
|
|
533
|
+
dependency_complexity = 0
|
|
534
|
+
|
|
535
|
+
if language == 'python':
|
|
536
|
+
# 计算导入模块数量(依赖复杂度)
|
|
537
|
+
import_count = len(re.findall(r'^\s*import\s+.*$', source_code, re.MULTILINE))
|
|
538
|
+
import_count += len(re.findall(r'^\s*from\s+.*\s+import\s+.*$', source_code, re.MULTILINE))
|
|
539
|
+
dependency_complexity += import_count
|
|
540
|
+
|
|
541
|
+
# 计算函数参数数量(增加依赖复杂度)
|
|
542
|
+
try:
|
|
543
|
+
tree = ast.parse(source_code)
|
|
544
|
+
for node in ast.walk(tree):
|
|
545
|
+
if isinstance(node, ast.FunctionDef):
|
|
546
|
+
dependency_complexity += len(node.args.args)
|
|
547
|
+
except:
|
|
548
|
+
pass
|
|
549
|
+
|
|
550
|
+
elif language == 'java':
|
|
551
|
+
# 计算依赖复杂度:导入语句数量
|
|
552
|
+
dependency_complexity += len(re.findall(r'^\s*import\s+.*;$', source_code, re.MULTILINE))
|
|
553
|
+
|
|
554
|
+
# 计算方法参数数量(增加依赖复杂度)
|
|
555
|
+
method_declarations = re.findall(r'\b\w+\s+\w+\s*\([^)]*\)', source_code)
|
|
556
|
+
for method in method_declarations:
|
|
557
|
+
param_count = method.count(',') + 1 if ',' in method else 1
|
|
558
|
+
if '(' in method and ')' in method:
|
|
559
|
+
param_part = method[method.find('(')+1:method.find(')')]
|
|
560
|
+
if param_part.strip():
|
|
561
|
+
param_count = param_part.count(',') + 1
|
|
562
|
+
else:
|
|
563
|
+
param_count = 0
|
|
564
|
+
dependency_complexity += param_count
|
|
565
|
+
|
|
566
|
+
elif language == 'go':
|
|
567
|
+
# 计算依赖复杂度:导入语句数量
|
|
568
|
+
dependency_complexity += len(re.findall(r'^\s*import\s+', source_code, re.MULTILINE))
|
|
569
|
+
|
|
570
|
+
# 计算函数参数数量
|
|
571
|
+
func_declarations = re.findall(r'func\s+\w+\s*\([^)]*\)', source_code)
|
|
572
|
+
for func in func_declarations:
|
|
573
|
+
param_part = func[func.find('(')+1:func.find(')')]
|
|
574
|
+
if param_part.strip():
|
|
575
|
+
# 简单计算参数数量
|
|
576
|
+
param_count = len([p for p in param_part.split(',') if p.strip()])
|
|
577
|
+
dependency_complexity += param_count
|
|
578
|
+
|
|
579
|
+
elif language == 'cpp':
|
|
580
|
+
# 计算依赖复杂度:包含语句数量
|
|
581
|
+
dependency_complexity += len(re.findall(r'^\s*#\s*include\s+', source_code, re.MULTILINE))
|
|
582
|
+
|
|
583
|
+
# 计算函数参数数量
|
|
584
|
+
func_declarations = re.findall(r'\b\w+\s+\w+\s*\([^)]*\)', source_code)
|
|
585
|
+
for func in func_declarations:
|
|
586
|
+
param_part = func[func.find('(')+1:func.find(')')]
|
|
587
|
+
if param_part.strip():
|
|
588
|
+
param_count = len([p for p in param_part.split(',') if p.strip()])
|
|
589
|
+
dependency_complexity += param_count
|
|
590
|
+
|
|
591
|
+
elif language == 'javascript':
|
|
592
|
+
# 计算依赖复杂度:import语句数量
|
|
593
|
+
dependency_complexity += len(re.findall(r'^\s*import\s+.*from', source_code, re.MULTILINE))
|
|
594
|
+
dependency_complexity += len(re.findall(r'^\s*require\s*\(', source_code, re.MULTILINE))
|
|
595
|
+
|
|
596
|
+
# 计算函数参数数量
|
|
597
|
+
func_declarations = re.findall(r'(?:function\s+\w+|const\s+\w+\s*=|var\s+\w+\s*=|let\s+\w+\s*=)\s*\([^)]*\)', source_code)
|
|
598
|
+
for func in func_declarations:
|
|
599
|
+
param_part = func[func.find('(')+1:func.find(')')]
|
|
600
|
+
if param_part.strip():
|
|
601
|
+
param_count = len([p for p in param_part.split(',') if p.strip()])
|
|
602
|
+
dependency_complexity += param_count
|
|
603
|
+
|
|
604
|
+
return max(0, dependency_complexity)
|
|
605
|
+
|
|
606
|
+
def calculate_similarity(self, source_code: str, test_code: str) -> float:
|
|
607
|
+
"""计算代码相似度"""
|
|
608
|
+
# 简单的Jaccard相似度计算
|
|
609
|
+
source_tokens = set(self._tokenize_code(source_code))
|
|
610
|
+
test_tokens = set(self._tokenize_code(test_code))
|
|
611
|
+
|
|
612
|
+
intersection = len(source_tokens.intersection(test_tokens))
|
|
613
|
+
union = len(source_tokens.union(test_tokens))
|
|
614
|
+
|
|
615
|
+
if union == 0:
|
|
616
|
+
return 0.0
|
|
617
|
+
|
|
618
|
+
return round(intersection / union * 100.0, 2)
|
|
619
|
+
|
|
620
|
+
def _tokenize_code(self, code: str) -> List[str]:
|
|
621
|
+
"""将代码分解为标记"""
|
|
622
|
+
# 去除注释和空白字符
|
|
623
|
+
lines = code.split('\n')
|
|
624
|
+
tokens = []
|
|
625
|
+
|
|
626
|
+
for line in lines:
|
|
627
|
+
# 移除行注释
|
|
628
|
+
if '//' in line:
|
|
629
|
+
line = line[:line.index('//')]
|
|
630
|
+
elif '#' in line:
|
|
631
|
+
line = line[:line.index('#')]
|
|
632
|
+
|
|
633
|
+
# 提取标识符、关键字、操作符等
|
|
634
|
+
token_pattern = r'\b\w+\b|[^\w\s]'
|
|
635
|
+
line_tokens = re.findall(token_pattern, line)
|
|
636
|
+
tokens.extend([t for t in line_tokens if t.strip()])
|
|
637
|
+
|
|
638
|
+
return tokens
|
|
639
|
+
|
|
640
|
+
class CoverageAnalyzer:
|
|
641
|
+
"""覆盖率分析器"""
|
|
642
|
+
|
|
643
|
+
def __init__(self):
|
|
644
|
+
self.language_parsers = {
|
|
645
|
+
'python': self._analyze_python_coverage,
|
|
646
|
+
'java': self._analyze_java_coverage,
|
|
647
|
+
'go': self._analyze_go_coverage,
|
|
648
|
+
'cpp': self._analyze_cpp_coverage,
|
|
649
|
+
'javascript': self._analyze_js_coverage
|
|
650
|
+
}
|
|
651
|
+
self.code_analyzer = CodeAnalyzer()
|
|
652
|
+
|
|
653
|
+
def analyze_coverage(self, source_file: str, test_file: str, language: str) -> CoverageResult:
|
|
654
|
+
"""分析源文件和测试文件的覆盖率"""
|
|
655
|
+
if language not in self.language_parsers:
|
|
656
|
+
return CoverageResult(
|
|
657
|
+
source_file=source_file,
|
|
658
|
+
test_file=test_file,
|
|
659
|
+
language=language,
|
|
660
|
+
total_testable_elements=0,
|
|
661
|
+
tested_elements=0,
|
|
662
|
+
coverage_percentage=0.0,
|
|
663
|
+
uncovered_elements=[],
|
|
664
|
+
code_similarity=0.0,
|
|
665
|
+
cyclomatic_complexity=0,
|
|
666
|
+
dependency_complexity=0
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
return self.language_parsers[language](source_file, test_file)
|
|
670
|
+
|
|
671
|
+
def analyze_unmatched_file(self, source_file: str, language: str) -> UnmatchedResult:
|
|
672
|
+
"""分析未匹配文件的复杂度"""
|
|
673
|
+
cyclomatic_complexity, dependency_complexity = self.code_analyzer.calculate_file_complexity(source_file, language)
|
|
674
|
+
return UnmatchedResult(
|
|
675
|
+
source_file=source_file,
|
|
676
|
+
language=language,
|
|
677
|
+
cyclomatic_complexity=cyclomatic_complexity,
|
|
678
|
+
dependency_complexity=dependency_complexity
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
def _analyze_python_coverage(self, source_file: str, test_file: str) -> CoverageResult:
|
|
682
|
+
"""分析Python文件的覆盖率"""
|
|
683
|
+
try:
|
|
684
|
+
with open(source_file, 'r', encoding='utf-8') as f:
|
|
685
|
+
source_code = f.read()
|
|
686
|
+
|
|
687
|
+
# 检查是否是特殊Python文件,如果是则不进行分析
|
|
688
|
+
filename = os.path.basename(source_file)
|
|
689
|
+
if filename == '__init__.py' or 'config' in filename.lower() or 'settings' in filename.lower():
|
|
690
|
+
# 返回空结果,因为这些文件不需要测试
|
|
691
|
+
return CoverageResult(
|
|
692
|
+
source_file=source_file,
|
|
693
|
+
test_file=test_file,
|
|
694
|
+
language='python',
|
|
695
|
+
total_testable_elements=0,
|
|
696
|
+
tested_elements=0,
|
|
697
|
+
coverage_percentage=0.0,
|
|
698
|
+
uncovered_elements=[],
|
|
699
|
+
code_similarity=0.0,
|
|
700
|
+
cyclomatic_complexity=0,
|
|
701
|
+
dependency_complexity=0
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
# 解析源文件中的可测试元素(公共函数、类方法等)
|
|
705
|
+
tree = ast.parse(source_code)
|
|
706
|
+
testable_elements = []
|
|
707
|
+
|
|
708
|
+
for node in ast.walk(tree):
|
|
709
|
+
if isinstance(node, ast.FunctionDef):
|
|
710
|
+
# 只考虑公共方法(非私有方法,即不以下划线开头)
|
|
711
|
+
if not node.name.startswith('_'):
|
|
712
|
+
testable_elements.append(node.name)
|
|
713
|
+
elif isinstance(node, ast.ClassDef):
|
|
714
|
+
# 类本身也可以被测试
|
|
715
|
+
testable_elements.append(f"class_{node.name}")
|
|
716
|
+
|
|
717
|
+
# 读取测试文件
|
|
718
|
+
with open(test_file, 'r', encoding='utf-8') as f:
|
|
719
|
+
test_code = f.read()
|
|
720
|
+
|
|
721
|
+
# 查找测试函数中调用的源元素
|
|
722
|
+
tested_elements = []
|
|
723
|
+
for element in testable_elements:
|
|
724
|
+
if element.startswith('class_'):
|
|
725
|
+
# 检查类是否在测试中被使用
|
|
726
|
+
class_name = element[6:] # 移除 'class_' 前缀
|
|
727
|
+
pattern = r'\b' + re.escape(class_name) + r'\b'
|
|
728
|
+
else:
|
|
729
|
+
# 检查函数是否在测试中被调用
|
|
730
|
+
pattern = r'\b' + re.escape(element) + r'\s*\('
|
|
731
|
+
|
|
732
|
+
if re.search(pattern, test_code):
|
|
733
|
+
tested_elements.append(element)
|
|
734
|
+
|
|
735
|
+
total = len(testable_elements)
|
|
736
|
+
tested = len(tested_elements)
|
|
737
|
+
coverage = (tested / total * 100) if total > 0 else 0.0
|
|
738
|
+
uncovered = [e for e in testable_elements if e not in tested_elements]
|
|
739
|
+
|
|
740
|
+
# 计算代码相似度和复杂度
|
|
741
|
+
similarity = self.code_analyzer.calculate_similarity(source_code, test_code)
|
|
742
|
+
cyclomatic_complexity, dependency_complexity = self.code_analyzer.calculate_complexity(source_code, 'python')
|
|
743
|
+
|
|
744
|
+
return CoverageResult(
|
|
745
|
+
source_file=source_file,
|
|
746
|
+
test_file=test_file,
|
|
747
|
+
language='python',
|
|
748
|
+
total_testable_elements=total,
|
|
749
|
+
tested_elements=tested,
|
|
750
|
+
coverage_percentage=coverage,
|
|
751
|
+
uncovered_elements=uncovered,
|
|
752
|
+
code_similarity=similarity,
|
|
753
|
+
cyclomatic_complexity=cyclomatic_complexity,
|
|
754
|
+
dependency_complexity=dependency_complexity
|
|
755
|
+
)
|
|
756
|
+
except Exception:
|
|
757
|
+
# 如果解析失败,返回基本结果
|
|
758
|
+
return CoverageResult(
|
|
759
|
+
source_file=source_file,
|
|
760
|
+
test_file=test_file,
|
|
761
|
+
language='python',
|
|
762
|
+
total_testable_elements=0,
|
|
763
|
+
tested_elements=0,
|
|
764
|
+
coverage_percentage=0.0,
|
|
765
|
+
uncovered_elements=[],
|
|
766
|
+
code_similarity=0.0,
|
|
767
|
+
cyclomatic_complexity=0,
|
|
768
|
+
dependency_complexity=0
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
def _analyze_java_coverage(self, source_file: str, test_file: str) -> CoverageResult:
|
|
772
|
+
"""分析Java文件的覆盖率"""
|
|
773
|
+
try:
|
|
774
|
+
with open(source_file, 'r', encoding='utf-8') as f:
|
|
775
|
+
source_code = f.read()
|
|
776
|
+
|
|
777
|
+
# 检查是否是配置类或常量类,如果是则不进行分析
|
|
778
|
+
filename = os.path.basename(source_file)
|
|
779
|
+
if 'Config' in filename or 'config' in filename.lower() or 'Constants' in filename or 'constants' in filename.lower() or 'Settings' in filename or 'settings' in filename.lower():
|
|
780
|
+
# 返回空结果,因为这些文件不需要测试
|
|
781
|
+
return CoverageResult(
|
|
782
|
+
source_file=source_file,
|
|
783
|
+
test_file=test_file,
|
|
784
|
+
language='java',
|
|
785
|
+
total_testable_elements=0,
|
|
786
|
+
tested_elements=0,
|
|
787
|
+
coverage_percentage=0.0,
|
|
788
|
+
uncovered_elements=[],
|
|
789
|
+
code_similarity=0.0,
|
|
790
|
+
cyclomatic_complexity=0,
|
|
791
|
+
dependency_complexity=0
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
# 提取Java公共方法(方法名)
|
|
795
|
+
testable_elements = []
|
|
796
|
+
|
|
797
|
+
# 提取公共方法(public关键字)
|
|
798
|
+
method_pattern = r'public\s+static\s+\w+\s+(\w+)\s*\([^)]*\)\s*[^{]*\{'
|
|
799
|
+
static_methods = re.findall(method_pattern, source_code)
|
|
800
|
+
|
|
801
|
+
# 提取公共方法(不带static关键字)
|
|
802
|
+
method_pattern2 = r'public\s+\w+\s+(\w+)\s*\([^)]*\)\s*[^{]*\{'
|
|
803
|
+
methods = re.findall(method_pattern2, source_code)
|
|
804
|
+
|
|
805
|
+
# 合并并去重
|
|
806
|
+
all_methods = list(set(static_methods + methods))
|
|
807
|
+
|
|
808
|
+
# 过滤掉构造函数(方法名与类名相同)和关键字
|
|
809
|
+
class_name = os.path.splitext(os.path.basename(source_file))[0]
|
|
810
|
+
all_methods = [m for m in all_methods if m != class_name and m not in ['if', 'for', 'while', 'switch', 'return', 'class', 'interface', 'enum', 'public', 'private', 'protected', 'static', 'final', 'abstract', 'default', 'import', 'package', 'new', 'try', 'catch', 'finally', 'throw', 'throws', 'extends', 'implements']]
|
|
811
|
+
testable_elements.extend(all_methods)
|
|
812
|
+
|
|
813
|
+
# 提取公共类(类本身也可以被测试)
|
|
814
|
+
class_pattern = r'(?:public\s+|abstract\s+|final\s+)?(?:class|interface|enum)\s+(\w+)'
|
|
815
|
+
classes = re.findall(class_pattern, source_code)
|
|
816
|
+
for cls in classes:
|
|
817
|
+
testable_elements.append(f"class_{cls}")
|
|
818
|
+
|
|
819
|
+
# 读取测试文件
|
|
820
|
+
with open(test_file, 'r', encoding='utf-8') as f:
|
|
821
|
+
test_code = f.read()
|
|
822
|
+
|
|
823
|
+
# 查找测试方法中调用的源元素
|
|
824
|
+
tested_elements = []
|
|
825
|
+
for element in testable_elements:
|
|
826
|
+
if element.startswith('class_'):
|
|
827
|
+
# 检查类是否在测试中被使用
|
|
828
|
+
class_name = element[6:] # 移除 'class_' 前缀
|
|
829
|
+
pattern = r'\b' + re.escape(class_name) + r'\b'
|
|
830
|
+
else:
|
|
831
|
+
# 检查方法是否在测试中被调用
|
|
832
|
+
pattern = r'\b' + re.escape(element) + r'\s*\('
|
|
833
|
+
|
|
834
|
+
if re.search(pattern, test_code):
|
|
835
|
+
tested_elements.append(element)
|
|
836
|
+
|
|
837
|
+
# 去重并过滤不需要测试的元素
|
|
838
|
+
tested_elements = list(set(tested_elements))
|
|
839
|
+
testable_elements = list(set(testable_elements))
|
|
840
|
+
|
|
841
|
+
# 过滤掉常见的不需要测试的元素
|
|
842
|
+
ignore_elements = {'main', 'log', 'logger', 'System', 'out', 'println', 'print', 'compile', 'matcher', 'matches', 'indexOf', 'substring', 'length', 'charAt', 'append', 'toString', 'intValue', 'doubleValue', 'booleanValue', 'equals', 'hashCode', 'getClass', 'clone', 'finalize', 'notify', 'notifyAll', 'wait', 'getClassLoader', 'getResource', 'getResources', 'getProtectionDomain', 'getSigners', 'wait', 'notify', 'notifyAll', 'equals', 'hashCode', 'toString', 'clone', 'finalize'}
|
|
843
|
+
testable_elements = [e for e in testable_elements if e not in ignore_elements]
|
|
844
|
+
tested_elements = [e for e in tested_elements if e not in ignore_elements]
|
|
845
|
+
|
|
846
|
+
total = len(testable_elements)
|
|
847
|
+
tested = len(tested_elements)
|
|
848
|
+
coverage = (tested / total * 100) if total > 0 else 0.0
|
|
849
|
+
uncovered = [e for e in testable_elements if e not in tested_elements]
|
|
850
|
+
|
|
851
|
+
# 计算代码相似度和复杂度
|
|
852
|
+
similarity = self.code_analyzer.calculate_similarity(source_code, test_code)
|
|
853
|
+
cyclomatic_complexity, dependency_complexity = self.code_analyzer.calculate_complexity(source_code, 'java')
|
|
854
|
+
|
|
855
|
+
return CoverageResult(
|
|
856
|
+
source_file=source_file,
|
|
857
|
+
test_file=test_file,
|
|
858
|
+
language='java',
|
|
859
|
+
total_testable_elements=total,
|
|
860
|
+
tested_elements=tested,
|
|
861
|
+
coverage_percentage=coverage,
|
|
862
|
+
uncovered_elements=uncovered,
|
|
863
|
+
code_similarity=similarity,
|
|
864
|
+
cyclomatic_complexity=cyclomatic_complexity,
|
|
865
|
+
dependency_complexity=dependency_complexity
|
|
866
|
+
)
|
|
867
|
+
except Exception as e:
|
|
868
|
+
return CoverageResult(
|
|
869
|
+
source_file=source_file,
|
|
870
|
+
test_file=test_file,
|
|
871
|
+
language='java',
|
|
872
|
+
total_testable_elements=0,
|
|
873
|
+
tested_elements=0,
|
|
874
|
+
coverage_percentage=0.0,
|
|
875
|
+
uncovered_elements=[],
|
|
876
|
+
code_similarity=0.0,
|
|
877
|
+
cyclomatic_complexity=0,
|
|
878
|
+
dependency_complexity=0
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
def _analyze_go_coverage(self, source_file: str, test_file: str) -> CoverageResult:
|
|
882
|
+
"""分析Go文件的覆盖率"""
|
|
883
|
+
try:
|
|
884
|
+
with open(source_file, 'r', encoding='utf-8') as f:
|
|
885
|
+
source_code = f.read()
|
|
886
|
+
|
|
887
|
+
# 检查是否是配置文件,如果是则不进行分析
|
|
888
|
+
filename = os.path.basename(source_file)
|
|
889
|
+
if 'config' in filename.lower() or 'settings' in filename.lower():
|
|
890
|
+
# 返回空结果,因为这些文件不需要测试
|
|
891
|
+
return CoverageResult(
|
|
892
|
+
source_file=source_file,
|
|
893
|
+
test_file=test_file,
|
|
894
|
+
language='go',
|
|
895
|
+
total_testable_elements=0,
|
|
896
|
+
tested_elements=0,
|
|
897
|
+
coverage_percentage=0.0,
|
|
898
|
+
uncovered_elements=[],
|
|
899
|
+
code_similarity=0.0,
|
|
900
|
+
cyclomatic_complexity=0,
|
|
901
|
+
dependency_complexity=0
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
# 提取Go公共函数和类型(大写字母开头)
|
|
905
|
+
testable_elements = []
|
|
906
|
+
|
|
907
|
+
# 提取公共函数(以大写字母开头)
|
|
908
|
+
func_pattern = r'func\s+([A-Z]\w*)\s*\([^)]*\)\s*[^{]*\{'
|
|
909
|
+
functions = re.findall(func_pattern, source_code)
|
|
910
|
+
functions = [f for f in functions if f not in ['if', 'for', 'range', 'switch', 'return', 'func', 'var', 'const', 'type', 'import', 'package', 'go', 'defer', 'select', 'case', 'default', 'break', 'continue', 'goto', 'fallthrough']]
|
|
911
|
+
testable_elements.extend(functions)
|
|
912
|
+
|
|
913
|
+
# 提取公共类型(以大写字母开头)
|
|
914
|
+
type_pattern = r'type\s+([A-Z]\w+)'
|
|
915
|
+
types = re.findall(type_pattern, source_code)
|
|
916
|
+
for t in types:
|
|
917
|
+
testable_elements.append(f"type_{t}")
|
|
918
|
+
|
|
919
|
+
# 读取测试文件
|
|
920
|
+
with open(test_file, 'r', encoding='utf-8') as f:
|
|
921
|
+
test_code = f.read()
|
|
922
|
+
|
|
923
|
+
# 查找测试函数中调用的源元素
|
|
924
|
+
tested_elements = []
|
|
925
|
+
for element in testable_elements:
|
|
926
|
+
if element.startswith('type_'):
|
|
927
|
+
# 检查类型是否在测试中被使用
|
|
928
|
+
type_name = element[5:] # 移除 'type_' 前缀
|
|
929
|
+
pattern = r'\b' + re.escape(type_name) + r'\b'
|
|
930
|
+
else:
|
|
931
|
+
# 检查函数是否在测试中被调用
|
|
932
|
+
pattern = r'\b' + re.escape(element) + r'\s*\('
|
|
933
|
+
|
|
934
|
+
if re.search(pattern, test_code):
|
|
935
|
+
tested_elements.append(element)
|
|
936
|
+
|
|
937
|
+
# 去重并过滤不需要测试的元素
|
|
938
|
+
tested_elements = list(set(tested_elements))
|
|
939
|
+
testable_elements = list(set(testable_elements))
|
|
940
|
+
|
|
941
|
+
# 过滤掉常见的不需要测试的元素
|
|
942
|
+
ignore_elements = {'main', 'len', 'append', 'copy', 'close', 'delete', 'make', 'new', 'panic', 'print', 'println', 'recover', 'cap', 'complex', 'imag', 'real', 'Type', 'Value', 'error', 'string', 'int', 'int64', 'int32', 'float64', 'bool', 'byte', 'rune', 'map', 'slice', 'chan', 'struct', 'interface', 'func', 'var', 'const', 'type', 'package', 'import', 'return', 'if', 'else', 'for', 'range', 'switch', 'case', 'default', 'break', 'continue', 'goto', 'fallthrough', 'defer', 'go', 'select'}
|
|
943
|
+
testable_elements = [e for e in testable_elements if e not in ignore_elements]
|
|
944
|
+
tested_elements = [e for e in tested_elements if e not in ignore_elements]
|
|
945
|
+
|
|
946
|
+
total = len(testable_elements)
|
|
947
|
+
tested = len(tested_elements)
|
|
948
|
+
coverage = (tested / total * 100) if total > 0 else 0.0
|
|
949
|
+
uncovered = [e for e in testable_elements if e not in tested_elements]
|
|
950
|
+
|
|
951
|
+
# 计算代码相似度和复杂度
|
|
952
|
+
similarity = self.code_analyzer.calculate_similarity(source_code, test_code)
|
|
953
|
+
cyclomatic_complexity, dependency_complexity = self.code_analyzer.calculate_complexity(source_code, 'go')
|
|
954
|
+
|
|
955
|
+
return CoverageResult(
|
|
956
|
+
source_file=source_file,
|
|
957
|
+
test_file=test_file,
|
|
958
|
+
language='go',
|
|
959
|
+
total_testable_elements=total,
|
|
960
|
+
tested_elements=tested,
|
|
961
|
+
coverage_percentage=coverage,
|
|
962
|
+
uncovered_elements=uncovered,
|
|
963
|
+
code_similarity=similarity,
|
|
964
|
+
cyclomatic_complexity=cyclomatic_complexity,
|
|
965
|
+
dependency_complexity=dependency_complexity
|
|
966
|
+
)
|
|
967
|
+
except Exception:
|
|
968
|
+
return CoverageResult(
|
|
969
|
+
source_file=source_file,
|
|
970
|
+
test_file=test_file,
|
|
971
|
+
language='go',
|
|
972
|
+
total_testable_elements=0,
|
|
973
|
+
tested_elements=0,
|
|
974
|
+
coverage_percentage=0.0,
|
|
975
|
+
uncovered_elements=[],
|
|
976
|
+
code_similarity=0.0,
|
|
977
|
+
cyclomatic_complexity=0,
|
|
978
|
+
dependency_complexity=0
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
def _analyze_cpp_coverage(self, source_file: str, test_file: str) -> CoverageResult:
|
|
982
|
+
"""分析C++文件的覆盖率"""
|
|
983
|
+
try:
|
|
984
|
+
with open(source_file, 'r', encoding='utf-8') as f:
|
|
985
|
+
source_code = f.read()
|
|
986
|
+
|
|
987
|
+
# 检查是否是配置文件,如果是则不进行分析
|
|
988
|
+
filename = os.path.basename(source_file)
|
|
989
|
+
if 'config' in filename.lower() or 'settings' in filename.lower() or 'constants' in filename.lower():
|
|
990
|
+
# 返回空结果,因为这些文件不需要测试
|
|
991
|
+
return CoverageResult(
|
|
992
|
+
source_file=source_file,
|
|
993
|
+
test_file=test_file,
|
|
994
|
+
language='cpp',
|
|
995
|
+
total_testable_elements=0,
|
|
996
|
+
tested_elements=0,
|
|
997
|
+
coverage_percentage=0.0,
|
|
998
|
+
uncovered_elements=[],
|
|
999
|
+
code_similarity=0.0,
|
|
1000
|
+
cyclomatic_complexity=0,
|
|
1001
|
+
dependency_complexity=0
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
# 提取C++公共函数和类
|
|
1005
|
+
testable_elements = []
|
|
1006
|
+
|
|
1007
|
+
# 提取公共函数(在public:部分或默认public的函数)
|
|
1008
|
+
# 匹配public:后的方法和不在private/protected部分的方法
|
|
1009
|
+
lines = source_code.split('\n')
|
|
1010
|
+
in_public_section = False
|
|
1011
|
+
for line in lines:
|
|
1012
|
+
if 'public:' in line:
|
|
1013
|
+
in_public_section = True
|
|
1014
|
+
elif 'private:' in line or 'protected:' in line:
|
|
1015
|
+
in_public_section = False
|
|
1016
|
+
elif in_public_section:
|
|
1017
|
+
# 在public部分查找函数
|
|
1018
|
+
func_match = re.search(r'\w+\s+(\w+)\s*\([^)]*\)\s*[^{]*\{', line)
|
|
1019
|
+
if func_match:
|
|
1020
|
+
func_name = func_match.group(1)
|
|
1021
|
+
if func_name not in ['if', 'for', 'while', 'switch', 'return', 'class', 'struct', 'public', 'private', 'protected', 'static', 'virtual', 'const', 'int', 'void', 'char', 'float', 'double', 'bool', 'string', 'auto', 'explicit', 'friend', 'inline', 'mutable', 'namespace', 'operator', 'private', 'protected', 'public', 'template', 'typename', 'using', 'volatile', 'delete', 'new', 'this', 'true', 'false', 'nullptr', 'sizeof', 'typeid', 'alignof', 'noexcept', 'override', 'final', 'decltype', 'constexpr', 'static_assert', 'thread_local']:
|
|
1022
|
+
testable_elements.append(func_name)
|
|
1023
|
+
|
|
1024
|
+
# 如果没有public关键字,也提取一些可能的公共函数
|
|
1025
|
+
if not testable_elements:
|
|
1026
|
+
# 查找不在private/protected部分的函数
|
|
1027
|
+
# 简化版本:提取所有非关键字的函数
|
|
1028
|
+
func_pattern = r'(\w+)\s*\([^)]*\)\s*[^{]*\{(?![\s\S]*private:|[\s\S]*protected:)'
|
|
1029
|
+
matches = re.findall(func_pattern, source_code)
|
|
1030
|
+
for match in matches:
|
|
1031
|
+
if match not in ['if', 'for', 'while', 'switch', 'return', 'class', 'struct', 'public', 'private', 'protected', 'static', 'virtual', 'const', 'int', 'void', 'char', 'float', 'double', 'bool', 'string', 'auto', 'explicit', 'friend', 'inline', 'mutable', 'namespace', 'operator', 'private', 'protected', 'public', 'template', 'typename', 'using', 'volatile', 'delete', 'new', 'this', 'true', 'false', 'nullptr', 'sizeof', 'typeid', 'alignof', 'noexcept', 'override', 'final', 'decltype', 'constexpr', 'static_assert', 'thread_local']:
|
|
1032
|
+
# 检查是否是大写字母开头(约定公共函数)
|
|
1033
|
+
if match and match[0].isupper():
|
|
1034
|
+
testable_elements.append(match)
|
|
1035
|
+
|
|
1036
|
+
# 提取类名
|
|
1037
|
+
class_pattern = r'(?:class|struct)\s+(\w+)'
|
|
1038
|
+
classes = re.findall(class_pattern, source_code)
|
|
1039
|
+
for cls in classes:
|
|
1040
|
+
testable_elements.append(f"class_{cls}")
|
|
1041
|
+
|
|
1042
|
+
# 读取测试文件
|
|
1043
|
+
with open(test_file, 'r', encoding='utf-8') as f:
|
|
1044
|
+
test_code = f.read()
|
|
1045
|
+
|
|
1046
|
+
# 查找测试函数中调用的源元素
|
|
1047
|
+
tested_elements = []
|
|
1048
|
+
for element in testable_elements:
|
|
1049
|
+
if element.startswith('class_'):
|
|
1050
|
+
# 检查类是否在测试中被使用
|
|
1051
|
+
class_name = element[6:] # 移除 'class_' 前缀
|
|
1052
|
+
pattern = r'\b' + re.escape(class_name) + r'\b'
|
|
1053
|
+
else:
|
|
1054
|
+
# 检查函数是否在测试中被调用
|
|
1055
|
+
pattern = r'\b' + re.escape(element) + r'\s*\('
|
|
1056
|
+
|
|
1057
|
+
if re.search(pattern, test_code):
|
|
1058
|
+
tested_elements.append(element)
|
|
1059
|
+
|
|
1060
|
+
# 去重并过滤不需要测试的元素
|
|
1061
|
+
tested_elements = list(set(tested_elements))
|
|
1062
|
+
testable_elements = list(set(testable_elements))
|
|
1063
|
+
|
|
1064
|
+
# 过滤掉常见的不需要测试的元素
|
|
1065
|
+
ignore_elements = {'main', 'cout', 'cin', 'cerr', 'clog', 'endl', 'size', 'length', 'empty', 'clear', 'push_back', 'pop_back', 'insert', 'erase', 'find', 'begin', 'end', 'rbegin', 'rend', 'front', 'back', 'data', 'c_str', 'substr', 'append', 'assign', 'compare', 'replace', 'swap', 'resize', 'reserve', 'capacity', 'max_size', 'shrink_to_fit', 'get', 'set', 'new', 'delete', 'sizeof', 'typeid', 'alignof', 'noexcept', 'static_cast', 'dynamic_cast', 'const_cast', 'reinterpret_cast', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue', 'goto', 'return', 'try', 'catch', 'throw', 'class', 'struct', 'union', 'enum', 'namespace', 'using', 'template', 'typename', 'explicit', 'friend', 'inline', 'virtual', 'override', 'final', 'static', 'extern', 'const', 'volatile', 'mutable', 'auto', 'register', 'thread_local', 'constexpr', 'decltype', 'int', 'char', 'float', 'double', 'bool', 'void', 'wchar_t', 'char16_t', 'char32_t', 'short', 'long', 'signed', 'unsigned', 'true', 'false', 'nullptr', 'this', 'operator', 'sizeof', 'typeid', 'alignof', 'noexcept'}
|
|
1066
|
+
testable_elements = [e for e in testable_elements if e not in ignore_elements]
|
|
1067
|
+
tested_elements = [e for e in tested_elements if e not in ignore_elements]
|
|
1068
|
+
|
|
1069
|
+
total = len(testable_elements)
|
|
1070
|
+
tested = len(tested_elements)
|
|
1071
|
+
coverage = (tested / total * 100) if total > 0 else 0.0
|
|
1072
|
+
uncovered = [e for e in testable_elements if e not in tested_elements]
|
|
1073
|
+
|
|
1074
|
+
# 计算代码相似度和复杂度
|
|
1075
|
+
similarity = self.code_analyzer.calculate_similarity(source_code, test_code)
|
|
1076
|
+
cyclomatic_complexity, dependency_complexity = self.code_analyzer.calculate_complexity(source_code, 'cpp')
|
|
1077
|
+
|
|
1078
|
+
return CoverageResult(
|
|
1079
|
+
source_file=source_file,
|
|
1080
|
+
test_file=test_file,
|
|
1081
|
+
language='cpp',
|
|
1082
|
+
total_testable_elements=total,
|
|
1083
|
+
tested_elements=tested,
|
|
1084
|
+
coverage_percentage=coverage,
|
|
1085
|
+
uncovered_elements=uncovered,
|
|
1086
|
+
code_similarity=similarity,
|
|
1087
|
+
cyclomatic_complexity=cyclomatic_complexity,
|
|
1088
|
+
dependency_complexity=dependency_complexity
|
|
1089
|
+
)
|
|
1090
|
+
except Exception:
|
|
1091
|
+
return CoverageResult(
|
|
1092
|
+
source_file=source_file,
|
|
1093
|
+
test_file=test_file,
|
|
1094
|
+
language='cpp',
|
|
1095
|
+
total_testable_elements=0,
|
|
1096
|
+
tested_elements=0,
|
|
1097
|
+
coverage_percentage=0.0,
|
|
1098
|
+
uncovered_elements=[],
|
|
1099
|
+
code_similarity=0.0,
|
|
1100
|
+
cyclomatic_complexity=0,
|
|
1101
|
+
dependency_complexity=0
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
def _analyze_js_coverage(self, source_file: str, test_file: str) -> CoverageResult:
|
|
1105
|
+
"""分析JavaScript文件的覆盖率"""
|
|
1106
|
+
try:
|
|
1107
|
+
with open(source_file, 'r', encoding='utf-8') as f:
|
|
1108
|
+
source_code = f.read()
|
|
1109
|
+
|
|
1110
|
+
# 检查是否是配置文件,如果是则不进行分析
|
|
1111
|
+
filename = os.path.basename(source_file)
|
|
1112
|
+
if 'config' in filename.lower() or 'settings' in filename.lower():
|
|
1113
|
+
# 返回空结果,因为这些文件不需要测试
|
|
1114
|
+
return CoverageResult(
|
|
1115
|
+
source_file=source_file,
|
|
1116
|
+
test_file=test_file,
|
|
1117
|
+
language='javascript',
|
|
1118
|
+
total_testable_elements=0,
|
|
1119
|
+
tested_elements=0,
|
|
1120
|
+
coverage_percentage=0.0,
|
|
1121
|
+
uncovered_elements=[],
|
|
1122
|
+
code_similarity=0.0,
|
|
1123
|
+
cyclomatic_complexity=0,
|
|
1124
|
+
dependency_complexity=0
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
# 提取JavaScript可测试元素(大写字母开头的函数和类)
|
|
1128
|
+
testable_elements = []
|
|
1129
|
+
|
|
1130
|
+
# 提取函数定义(大写字母开头)
|
|
1131
|
+
func_pattern = r'function\s+([A-Z]\w*)\s*\([^)]*\)|const\s+([A-Z]\w*)\s*=\s*[^=].*?=>|var\s+([A-Z]\w*)\s*=\s*[^=].*?=>|let\s+([A-Z]\w*)\s*=\s*[^=].*?=>'
|
|
1132
|
+
matches = re.findall(func_pattern, source_code)
|
|
1133
|
+
|
|
1134
|
+
# 提取函数名(从元组中获取非空值)
|
|
1135
|
+
for match in matches:
|
|
1136
|
+
for name in match:
|
|
1137
|
+
if name and not name.startswith('_') and name not in ['if', 'for', 'while', 'switch', 'return', 'function', 'const', 'var', 'let', 'class', 'new', 'this', 'super', 'import', 'export', 'default', 'static', 'async', 'await', 'try', 'catch', 'finally', 'throw', 'throws', 'extends', 'implements', 'instanceof', 'typeof', 'delete', 'in', 'of', 'with', 'do', 'break', 'continue', 'case', 'default', 'true', 'false', 'null', 'undefined', 'console', 'window', 'document', 'Math', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Function', 'Symbol', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Promise', 'Generator', 'Error', 'JSON', 'RegExp', 'isNaN', 'isFinite', 'parseInt', 'parseFloat', 'encodeURI', 'encodeURIComponent', 'decodeURI', 'decodeURIComponent', 'eval', 'escape', 'unescape', 'alert', 'prompt', 'confirm', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'require', 'module', 'exports', 'arguments', 'length', 'prototype', 'constructor', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString']:
|
|
1138
|
+
testable_elements.append(name)
|
|
1139
|
+
|
|
1140
|
+
# 提取类定义
|
|
1141
|
+
class_pattern = r'class\s+([A-Z]\w+)'
|
|
1142
|
+
classes = re.findall(class_pattern, source_code)
|
|
1143
|
+
for cls in classes:
|
|
1144
|
+
testable_elements.append(f"class_{cls}")
|
|
1145
|
+
|
|
1146
|
+
# 读取测试文件
|
|
1147
|
+
with open(test_file, 'r', encoding='utf-8') as f:
|
|
1148
|
+
test_code = f.read()
|
|
1149
|
+
|
|
1150
|
+
# 查找测试函数中调用的源元素
|
|
1151
|
+
tested_elements = []
|
|
1152
|
+
for element in testable_elements:
|
|
1153
|
+
if element.startswith('class_'):
|
|
1154
|
+
# 检查类是否在测试中被使用
|
|
1155
|
+
class_name = element[6:] # 移除 'class_' 前缀
|
|
1156
|
+
pattern = r'\b' + re.escape(class_name) + r'\b'
|
|
1157
|
+
else:
|
|
1158
|
+
# 检查函数是否在测试中被调用
|
|
1159
|
+
pattern = r'\b' + re.escape(element) + r'\s*\('
|
|
1160
|
+
|
|
1161
|
+
if re.search(pattern, test_code):
|
|
1162
|
+
tested_elements.append(element)
|
|
1163
|
+
|
|
1164
|
+
# 去重并过滤不需要测试的元素
|
|
1165
|
+
tested_elements = list(set(tested_elements))
|
|
1166
|
+
testable_elements = list(set(testable_elements))
|
|
1167
|
+
|
|
1168
|
+
# 过滤掉常见的不需要测试的元素
|
|
1169
|
+
ignore_elements = {'main', 'console', 'log', 'error', 'warn', 'info', 'debug', 'time', 'timeEnd', 'assert', 'dir', 'table', 'trace', 'group', 'groupEnd', 'count', 'clear', 'length', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf', 'match', 'replace', 'search', 'slice', 'split', 'substring', 'toLowerCase', 'toUpperCase', 'trim', 'substr', 'valueOf', 'includes', 'startsWith', 'endsWith', 'repeat', 'padStart', 'padEnd', 'trimStart', 'trimEnd', 'trimLeft', 'trimRight', 'matchAll', 'replaceAll', 'at', 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'concat', 'join', 'slice', 'indexOf', 'lastIndexOf', 'includes', 'forEach', 'map', 'filter', 'reduce', 'reduceRight', 'every', 'some', 'find', 'findIndex', 'flat', 'flatMap', 'entries', 'keys', 'values', 'copyWithin', 'fill', 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'concat', 'join', 'slice', 'indexOf', 'lastIndexOf', 'includes', 'forEach', 'map', 'filter', 'reduce', 'reduceRight', 'every', 'some', 'find', 'findIndex', 'flat', 'flatMap', 'entries', 'keys', 'values', 'copyWithin', 'fill', 'length', 'constructor', 'prototype', 'name', 'arguments', 'caller', 'callee', 'apply', 'bind', 'call', 'toString', 'valueOf', 'toLocaleString', 'hasInstance', 'isConcatSpreadable', 'iterator', 'match', 'replace', 'search', 'species', 'split', 'toPrimitive', 'toStringTag', 'unscopables', 'Math', 'abs', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh', 'atan2', 'ceil', 'cbrt', 'expm1', 'clz32', 'cos', 'cosh', 'exp', 'floor', 'fround', 'hypot', 'imul', 'log', 'log1p', 'log2', 'log10', 'max', 'min', 'pow', 'random', 'round', 'sign', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc', 'E', 'LN10', 'LN2', 'LOG10E', 'LOG2E', 'PI', 'SQRT1_2', 'SQRT2', 'Date', 'now', 'parse', 'UTC', 'getFullYear', 'getMonth', 'getDate', 'getDay', 'getHours', 'getMinutes', 'getSeconds', 'getMilliseconds', 'getTime', 'getYear', 'getUTCFullYear', 'getUTCMonth', 'getUTCDate', 'getUTCDay', 'getUTCHours', 'getUTCMinutes', 'getUTCSeconds', 'getUTCMilliseconds', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 'setYear', 'toDateString', 'toTimeString', 'toLocaleDateString', 'toLocaleTimeString', 'toISOString', 'toJSON', 'toGMTString', 'toUTCString', 'valueOf', 'getTimezoneOffset', 'getYear', 'setYear', 'toSource', 'toString', 'Object', 'assign', 'create', 'defineProperties', 'defineProperty', 'freeze', 'getOwnPropertyDescriptor', 'getOwnPropertyDescriptors', 'getOwnPropertyNames', 'getOwnPropertySymbols', 'getPrototypeOf', 'is', 'isExtensible', 'isFrozen', 'isSealed', 'keys', 'preventExtensions', 'propertyIsEnumerable', 'seal', 'setPrototypeOf', 'values', 'entries', 'fromEntries', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf', 'Array', 'isArray', 'from', 'of', 'concat', 'copyWithin', 'entries', 'every', 'fill', 'filter', 'find', 'findIndex', 'flat', 'flatMap', 'forEach', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'pop', 'push', 'reduce', 'reduceRight', 'reverse', 'shift', 'slice', 'some', 'sort', 'splice', 'unshift', 'values', 'length', 'Number', 'isFinite', 'isInteger', 'isNaN', 'isSafeInteger', 'parseFloat', 'parseInt', 'toExponential', 'toFixed', 'toPrecision', 'toString', 'valueOf', 'Boolean', 'toString', 'valueOf', 'String', 'fromCharCode', 'fromCodePoint', 'raw', 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'endsWith', 'includes', 'indexOf', 'lastIndexOf', 'localeCompare', 'match', 'matchAll', 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'replaceAll', 'search', 'slice', 'split', 'startsWith', 'substring', 'toLowerCase', 'toUpperCase', 'trim', 'trimStart', 'trimEnd', 'trimLeft', 'trimRight', 'toLocaleLowerCase', 'toLocaleUpperCase', 'valueOf', 'toString', 'Function', 'length', 'name', 'arguments', 'caller', 'constructor', 'apply', 'bind', 'call', 'toString', 'Symbol', 'asyncIterator', 'hasInstance', 'isConcatSpreadable', 'iterator', 'match', 'matchAll', 'replace', 'search', 'species', 'split', 'toPrimitive', 'toStringTag', 'unscopables', 'for', 'keyFor', 'Map', 'clear', 'delete', 'entries', 'forEach', 'get', 'has', 'keys', 'set', 'size', 'values', 'Set', 'add', 'clear', 'delete', 'entries', 'forEach', 'has', 'keys', 'size', 'values', 'WeakMap', 'delete', 'get', 'has', 'set', 'WeakSet', 'add', 'delete', 'has', 'Promise', 'all', 'race', 'reject', 'resolve', 'allSettled', 'any', 'Error', 'name', 'message', 'stack', 'JSON', 'parse', 'stringify', 'RegExp', 'exec', 'test', 'compile', 'flags', 'global', 'ignoreCase', 'multiline', 'source', 'sticky', 'unicode', 'lastIndex', 'toString', 'compile', 'exec', 'test', 'isNaN', 'isFinite', 'parseInt', 'parseFloat', 'encodeURI', 'encodeURIComponent', 'decodeURI', 'decodeURIComponent', 'eval', 'escape', 'unescape', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'require', 'module', 'exports', 'arguments', 'length', 'prototype', 'constructor', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'window', 'document', 'alert', 'prompt', 'confirm'}
|
|
1170
|
+
testable_elements = [e for e in testable_elements if e not in ignore_elements]
|
|
1171
|
+
tested_elements = [e for e in tested_elements if e not in ignore_elements]
|
|
1172
|
+
|
|
1173
|
+
total = len(testable_elements)
|
|
1174
|
+
tested = len(tested_elements)
|
|
1175
|
+
coverage = (tested / total * 100) if total > 0 else 0.0
|
|
1176
|
+
uncovered = [e for e in testable_elements if e not in tested_elements]
|
|
1177
|
+
|
|
1178
|
+
# 计算代码相似度和复杂度
|
|
1179
|
+
similarity = self.code_analyzer.calculate_similarity(source_code, test_code)
|
|
1180
|
+
cyclomatic_complexity, dependency_complexity = self.code_analyzer.calculate_complexity(source_code, 'javascript')
|
|
1181
|
+
|
|
1182
|
+
return CoverageResult(
|
|
1183
|
+
source_file=source_file,
|
|
1184
|
+
test_file=test_file,
|
|
1185
|
+
language='javascript',
|
|
1186
|
+
total_testable_elements=total,
|
|
1187
|
+
tested_elements=tested,
|
|
1188
|
+
coverage_percentage=coverage,
|
|
1189
|
+
uncovered_elements=uncovered,
|
|
1190
|
+
code_similarity=similarity,
|
|
1191
|
+
cyclomatic_complexity=cyclomatic_complexity,
|
|
1192
|
+
dependency_complexity=dependency_complexity
|
|
1193
|
+
)
|
|
1194
|
+
except Exception:
|
|
1195
|
+
return CoverageResult(
|
|
1196
|
+
source_file=source_file,
|
|
1197
|
+
test_file=test_file,
|
|
1198
|
+
language='javascript',
|
|
1199
|
+
total_testable_elements=0,
|
|
1200
|
+
tested_elements=0,
|
|
1201
|
+
coverage_percentage=0.0,
|
|
1202
|
+
uncovered_elements=[],
|
|
1203
|
+
code_similarity=0.0,
|
|
1204
|
+
cyclomatic_complexity=0,
|
|
1205
|
+
dependency_complexity=0
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
def save_results_to_txt(matches: List[TestFileMatch], unmatched: List[Tuple[str, str]], coverage_results: List[CoverageResult], unmatched_results: List[UnmatchedResult], output_path: str):
|
|
1209
|
+
"""将匹配结果和覆盖率分析保存到txt文件"""
|
|
1210
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
1211
|
+
f.write("单元测试文件查找及代码分析结果\n")
|
|
1212
|
+
f.write("=" * 80 + "\n\n")
|
|
1213
|
+
|
|
1214
|
+
if matches:
|
|
1215
|
+
f.write(f"找到 {len(matches)} 个源文件与测试文件的配对:\n")
|
|
1216
|
+
f.write("-" * 80 + "\n")
|
|
1217
|
+
|
|
1218
|
+
for i, match in enumerate(matches, 1):
|
|
1219
|
+
f.write(f"{i}. 语言: {match.language}\n")
|
|
1220
|
+
f.write(f" 源文件: {match.source_file}\n")
|
|
1221
|
+
f.write(f" 测试文件: {match.test_file}\n")
|
|
1222
|
+
|
|
1223
|
+
# 查找对应的覆盖率结果
|
|
1224
|
+
coverage_result = next((cr for cr in coverage_results if
|
|
1225
|
+
cr.source_file == match.source_file and
|
|
1226
|
+
cr.test_file == match.test_file), None)
|
|
1227
|
+
|
|
1228
|
+
if coverage_result:
|
|
1229
|
+
f.write(f" 可测试元素总数: {coverage_result.total_testable_elements}\n")
|
|
1230
|
+
f.write(f" 已测试元素: {coverage_result.tested_elements}\n")
|
|
1231
|
+
f.write(f" 覆盖率: {coverage_result.coverage_percentage:.2f}%\n")
|
|
1232
|
+
f.write(f" 代码相似度: {coverage_result.code_similarity:.2f}%\n")
|
|
1233
|
+
f.write(f" 圈复杂度: {coverage_result.cyclomatic_complexity}\n")
|
|
1234
|
+
f.write(f" 依赖复杂度: {coverage_result.dependency_complexity}\n")
|
|
1235
|
+
|
|
1236
|
+
if coverage_result.uncovered_elements:
|
|
1237
|
+
f.write(f" 未覆盖元素: {', '.join(coverage_result.uncovered_elements)}\n")
|
|
1238
|
+
else:
|
|
1239
|
+
f.write(" 未覆盖元素: 无\n")
|
|
1240
|
+
else:
|
|
1241
|
+
f.write(" 覆盖率分析: 无法分析\n")
|
|
1242
|
+
|
|
1243
|
+
f.write("\n")
|
|
1244
|
+
else:
|
|
1245
|
+
f.write("未找到任何源文件与测试文件的配对\n\n")
|
|
1246
|
+
|
|
1247
|
+
if unmatched:
|
|
1248
|
+
f.write(f"未找到测试文件的源文件 ({len(unmatched)} 个):\n")
|
|
1249
|
+
f.write("-" * 80 + "\n")
|
|
1250
|
+
|
|
1251
|
+
for i, (source_file, lang) in enumerate(unmatched, 1):
|
|
1252
|
+
f.write(f"{i}. 语言: {lang}\n")
|
|
1253
|
+
f.write(f" 源文件: {source_file}\n")
|
|
1254
|
+
|
|
1255
|
+
# 查找对应的未匹配结果
|
|
1256
|
+
unmatched_result = next((ur for ur in unmatched_results if
|
|
1257
|
+
ur.source_file == source_file), None)
|
|
1258
|
+
|
|
1259
|
+
if unmatched_result:
|
|
1260
|
+
f.write(f" 圈复杂度: {unmatched_result.cyclomatic_complexity}\n")
|
|
1261
|
+
f.write(f" 依赖复杂度: {unmatched_result.dependency_complexity}\n")
|
|
1262
|
+
else:
|
|
1263
|
+
f.write(" 复杂度分析: 无法分析\n")
|
|
1264
|
+
|
|
1265
|
+
f.write("\n")
|
|
1266
|
+
else:
|
|
1267
|
+
f.write("所有源文件都找到了对应的测试文件\n\n")
|
|
1268
|
+
|
|
1269
|
+
# 输出覆盖率统计摘要
|
|
1270
|
+
if coverage_results or unmatched_results:
|
|
1271
|
+
f.write("复杂度统计摘要:\n")
|
|
1272
|
+
f.write("-" * 80 + "\n")
|
|
1273
|
+
|
|
1274
|
+
# 匹配文件的统计
|
|
1275
|
+
if coverage_results:
|
|
1276
|
+
avg_cyclomatic_matched = sum(cr.cyclomatic_complexity for cr in coverage_results) / len(coverage_results)
|
|
1277
|
+
avg_dependency_matched = sum(cr.dependency_complexity for cr in coverage_results) / len(coverage_results)
|
|
1278
|
+
f.write(f"已匹配文件 ({len(coverage_results)} 个):\n")
|
|
1279
|
+
f.write(f" 平均圈复杂度: {avg_cyclomatic_matched:.2f}\n")
|
|
1280
|
+
f.write(f" 平均依赖复杂度: {avg_dependency_matched:.2f}\n")
|
|
1281
|
+
|
|
1282
|
+
# 未匹配文件的统计
|
|
1283
|
+
if unmatched_results:
|
|
1284
|
+
avg_cyclomatic_unmatched = sum(ur.cyclomatic_complexity for ur in unmatched_results) / len(unmatched_results)
|
|
1285
|
+
avg_dependency_unmatched = sum(ur.dependency_complexity for ur in unmatched_results) / len(unmatched_results)
|
|
1286
|
+
f.write(f"\n未匹配文件 ({len(unmatched_results)} 个):\n")
|
|
1287
|
+
f.write(f" 平均圈复杂度: {avg_cyclomatic_unmatched:.2f}\n")
|
|
1288
|
+
f.write(f" 平均依赖复杂度: {avg_dependency_unmatched:.2f}\n")
|
|
1289
|
+
|
|
1290
|
+
# 整体覆盖率统计
|
|
1291
|
+
if coverage_results:
|
|
1292
|
+
total_elements = sum(cr.total_testable_elements for cr in coverage_results)
|
|
1293
|
+
total_tested = sum(cr.tested_elements for cr in coverage_results)
|
|
1294
|
+
avg_coverage = sum(cr.coverage_percentage for cr in coverage_results) / len(coverage_results) if coverage_results else 0
|
|
1295
|
+
avg_similarity = sum(cr.code_similarity for cr in coverage_results) / len(coverage_results) if coverage_results else 0
|
|
1296
|
+
|
|
1297
|
+
f.write(f"\n覆盖率统计:\n")
|
|
1298
|
+
f.write(f" 总可测试元素数: {total_elements}\n")
|
|
1299
|
+
f.write(f" 已测试元素数: {total_tested}\n")
|
|
1300
|
+
f.write(f" 平均覆盖率: {avg_coverage:.2f}%\n")
|
|
1301
|
+
f.write(f" 平均代码相似度: {avg_similarity:.2f}%\n")
|
|
1302
|
+
|
|
1303
|
+
# 按语言统计
|
|
1304
|
+
f.write("\n按语言统计:\n")
|
|
1305
|
+
lang_stats = {}
|
|
1306
|
+
|
|
1307
|
+
# 处理匹配文件的统计
|
|
1308
|
+
for cr in coverage_results:
|
|
1309
|
+
lang = cr.language
|
|
1310
|
+
if lang not in lang_stats:
|
|
1311
|
+
lang_stats[lang] = {
|
|
1312
|
+
'matched_count': 0, 'unmatched_count': 0,
|
|
1313
|
+
'avg_cyclomatic_matched': 0, 'avg_dependency_matched': 0,
|
|
1314
|
+
'avg_cyclomatic_unmatched': 0, 'avg_dependency_unmatched': 0,
|
|
1315
|
+
'coverage_sum': 0
|
|
1316
|
+
}
|
|
1317
|
+
lang_stats[lang]['matched_count'] += 1
|
|
1318
|
+
lang_stats[lang]['avg_cyclomatic_matched'] += cr.cyclomatic_complexity
|
|
1319
|
+
lang_stats[lang]['avg_dependency_matched'] += cr.dependency_complexity
|
|
1320
|
+
lang_stats[lang]['coverage_sum'] += cr.coverage_percentage
|
|
1321
|
+
|
|
1322
|
+
# 处理未匹配文件的统计
|
|
1323
|
+
for ur in unmatched_results:
|
|
1324
|
+
lang = ur.language
|
|
1325
|
+
if lang not in lang_stats:
|
|
1326
|
+
lang_stats[lang] = {
|
|
1327
|
+
'matched_count': 0, 'unmatched_count': 0,
|
|
1328
|
+
'avg_cyclomatic_matched': 0, 'avg_dependency_matched': 0,
|
|
1329
|
+
'avg_cyclomatic_unmatched': 0, 'avg_dependency_unmatched': 0,
|
|
1330
|
+
'coverage_sum': 0
|
|
1331
|
+
}
|
|
1332
|
+
lang_stats[lang]['unmatched_count'] += 1
|
|
1333
|
+
lang_stats[lang]['avg_cyclomatic_unmatched'] += ur.cyclomatic_complexity
|
|
1334
|
+
lang_stats[lang]['avg_dependency_unmatched'] += ur.dependency_complexity
|
|
1335
|
+
|
|
1336
|
+
# 输出各语言统计
|
|
1337
|
+
for lang, stats in lang_stats.items():
|
|
1338
|
+
f.write(f"\n {lang}:\n")
|
|
1339
|
+
f.write(f" 已匹配文件数: {stats['matched_count']}\n")
|
|
1340
|
+
f.write(f" 未匹配文件数: {stats['unmatched_count']}\n")
|
|
1341
|
+
|
|
1342
|
+
if stats['matched_count'] > 0:
|
|
1343
|
+
avg_cyclo_matched = stats['avg_cyclomatic_matched'] / stats['matched_count']
|
|
1344
|
+
avg_dep_matched = stats['avg_dependency_matched'] / stats['matched_count']
|
|
1345
|
+
avg_cov = stats['coverage_sum'] / stats['matched_count']
|
|
1346
|
+
f.write(f" 已匹配文件平均圈复杂度: {avg_cyclo_matched:.2f}\n")
|
|
1347
|
+
f.write(f" 已匹配文件平均依赖复杂度: {avg_dep_matched:.2f}\n")
|
|
1348
|
+
f.write(f" 平均覆盖率: {avg_cov:.2f}%\n")
|
|
1349
|
+
|
|
1350
|
+
if stats['unmatched_count'] > 0:
|
|
1351
|
+
avg_cyclo_unmatched = stats['avg_cyclomatic_unmatched'] / stats['unmatched_count']
|
|
1352
|
+
avg_dep_unmatched = stats['avg_dependency_unmatched'] / stats['unmatched_count']
|
|
1353
|
+
f.write(f" 未匹配文件平均圈复杂度: {avg_cyclo_unmatched:.2f}\n")
|
|
1354
|
+
f.write(f" 未匹配文件平均依赖复杂度: {avg_dep_unmatched:.2f}\n")
|
|
1355
|
+
|
|
1356
|
+
def save_results_to_json(matches: List[TestFileMatch], unmatched: List[Tuple[str, str]],
|
|
1357
|
+
coverage_results: List[CoverageResult], unmatched_results: List[UnmatchedResult]):
|
|
1358
|
+
"""将匹配结果和覆盖率分析保存到JSON文件"""
|
|
1359
|
+
# 转换数据为字典格式
|
|
1360
|
+
matches_dict = [asdict(match) for match in matches]
|
|
1361
|
+
coverage_dict = [asdict(cr) for cr in coverage_results]
|
|
1362
|
+
unmatched_dict = [asdict(ur) for ur in unmatched_results]
|
|
1363
|
+
|
|
1364
|
+
# 构建最终JSON结构
|
|
1365
|
+
result = {
|
|
1366
|
+
"matches": matches_dict,
|
|
1367
|
+
"unmatched": unmatched_dict,
|
|
1368
|
+
"coverage_results": coverage_dict,
|
|
1369
|
+
"summary": {
|
|
1370
|
+
"total_matches": len(matches),
|
|
1371
|
+
"total_unmatched": len(unmatched),
|
|
1372
|
+
"total_coverage_analyzed": len(coverage_results),
|
|
1373
|
+
"total_unmatched_analyzed": len(unmatched_results),
|
|
1374
|
+
# 统计覆盖率摘要
|
|
1375
|
+
"coverage_summary": {
|
|
1376
|
+
"total_testable_elements": sum(cr["total_testable_elements"] for cr in coverage_dict),
|
|
1377
|
+
"total_tested_elements": sum(cr["tested_elements"] for cr in coverage_dict),
|
|
1378
|
+
"avg_coverage_percentage": (sum(cr["coverage_percentage"] for cr in coverage_dict) / len(coverage_dict)) if coverage_dict else 0,
|
|
1379
|
+
"avg_code_similarity": (sum(cr["code_similarity"] for cr in coverage_dict) / len(coverage_dict)) if coverage_dict else 0,
|
|
1380
|
+
"avg_cyclomatic_complexity_matched": (sum(cr["cyclomatic_complexity"] for cr in coverage_dict) / len(coverage_dict)) if coverage_dict else 0,
|
|
1381
|
+
"avg_dependency_complexity_matched": (sum(cr["dependency_complexity"] for cr in coverage_dict) / len(coverage_dict)) if coverage_dict else 0
|
|
1382
|
+
},
|
|
1383
|
+
# 未匹配文件的复杂度统计
|
|
1384
|
+
"unmatched_summary": {
|
|
1385
|
+
"avg_cyclomatic_complexity": (sum(ur["cyclomatic_complexity"] for ur in unmatched_dict) / len(unmatched_dict)) if unmatched_dict else 0,
|
|
1386
|
+
"avg_dependency_complexity": (sum(ur["dependency_complexity"] for ur in unmatched_dict) / len(unmatched_dict)) if unmatched_dict else 0
|
|
1387
|
+
},
|
|
1388
|
+
# 按语言统计
|
|
1389
|
+
"language_stats": {}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
# 按语言统计详细信息
|
|
1394
|
+
lang_stats = {}
|
|
1395
|
+
|
|
1396
|
+
# 处理匹配文件
|
|
1397
|
+
for cr in coverage_dict:
|
|
1398
|
+
lang = cr["language"]
|
|
1399
|
+
if lang not in lang_stats:
|
|
1400
|
+
lang_stats[lang] = {
|
|
1401
|
+
"matched": {
|
|
1402
|
+
"count": 0,
|
|
1403
|
+
"total_testable": 0,
|
|
1404
|
+
"total_tested": 0,
|
|
1405
|
+
"coverage_sum": 0,
|
|
1406
|
+
"cyclomatic_sum": 0,
|
|
1407
|
+
"dependency_sum": 0,
|
|
1408
|
+
"similarity_sum": 0
|
|
1409
|
+
},
|
|
1410
|
+
"unmatched": {
|
|
1411
|
+
"count": 0,
|
|
1412
|
+
"cyclomatic_sum": 0,
|
|
1413
|
+
"dependency_sum": 0
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
lang_stats[lang]["matched"]["count"] += 1
|
|
1417
|
+
lang_stats[lang]["matched"]["total_testable"] += cr["total_testable_elements"]
|
|
1418
|
+
lang_stats[lang]["matched"]["total_tested"] += cr["tested_elements"]
|
|
1419
|
+
lang_stats[lang]["matched"]["coverage_sum"] += cr["coverage_percentage"]
|
|
1420
|
+
lang_stats[lang]["matched"]["cyclomatic_sum"] += cr["cyclomatic_complexity"]
|
|
1421
|
+
lang_stats[lang]["matched"]["dependency_sum"] += cr["dependency_complexity"]
|
|
1422
|
+
lang_stats[lang]["matched"]["similarity_sum"] += cr["code_similarity"]
|
|
1423
|
+
|
|
1424
|
+
# 处理未匹配文件
|
|
1425
|
+
for ur in unmatched_dict:
|
|
1426
|
+
lang = ur["language"]
|
|
1427
|
+
if lang not in lang_stats:
|
|
1428
|
+
lang_stats[lang] = {
|
|
1429
|
+
"matched": {
|
|
1430
|
+
"count": 0,
|
|
1431
|
+
"total_testable": 0,
|
|
1432
|
+
"total_tested": 0,
|
|
1433
|
+
"coverage_sum": 0,
|
|
1434
|
+
"cyclomatic_sum": 0,
|
|
1435
|
+
"dependency_sum": 0,
|
|
1436
|
+
"similarity_sum": 0
|
|
1437
|
+
},
|
|
1438
|
+
"unmatched": {
|
|
1439
|
+
"count": 0,
|
|
1440
|
+
"cyclomatic_sum": 0,
|
|
1441
|
+
"dependency_sum": 0
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
lang_stats[lang]["unmatched"]["count"] += 1
|
|
1445
|
+
lang_stats[lang]["unmatched"]["cyclomatic_sum"] += ur["cyclomatic_complexity"]
|
|
1446
|
+
lang_stats[lang]["unmatched"]["dependency_sum"] += ur["dependency_complexity"]
|
|
1447
|
+
|
|
1448
|
+
# 计算各语言的平均值
|
|
1449
|
+
for lang, stats in lang_stats.items():
|
|
1450
|
+
result["summary"]["language_stats"][lang] = {
|
|
1451
|
+
"total_files": stats["matched"]["count"] + stats["unmatched"]["count"],
|
|
1452
|
+
"matched_files": stats["matched"]["count"],
|
|
1453
|
+
"unmatched_files": stats["unmatched"]["count"],
|
|
1454
|
+
"matched_stats": {
|
|
1455
|
+
"avg_coverage": stats["matched"]["coverage_sum"] / stats["matched"]["count"] if stats["matched"]["count"] > 0 else 0,
|
|
1456
|
+
"avg_cyclomatic": stats["matched"]["cyclomatic_sum"] / stats["matched"]["count"] if stats["matched"]["count"] > 0 else 0,
|
|
1457
|
+
"avg_dependency": stats["matched"]["dependency_sum"] / stats["matched"]["count"] if stats["matched"]["count"] > 0 else 0,
|
|
1458
|
+
"avg_similarity": stats["matched"]["similarity_sum"] / stats["matched"]["count"] if stats["matched"]["count"] > 0 else 0,
|
|
1459
|
+
"total_testable_elements": stats["matched"]["total_testable"],
|
|
1460
|
+
"coverage_ratio": (stats["matched"]["total_tested"] / stats["matched"]["total_testable"] * 100) if stats["matched"]["total_testable"] > 0 else 0
|
|
1461
|
+
},
|
|
1462
|
+
"unmatched_stats": {
|
|
1463
|
+
"avg_cyclomatic": stats["unmatched"]["cyclomatic_sum"] / stats["unmatched"]["count"] if stats["unmatched"]["count"] > 0 else 0,
|
|
1464
|
+
"avg_dependency": stats["unmatched"]["dependency_sum"] / stats["unmatched"]["count"] if stats["unmatched"]["count"] > 0 else 0
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
return result
|
|
1469
|
+
# 写入JSON文件
|
|
1470
|
+
# with open(output_path, 'w', encoding='utf-8') as f:
|
|
1471
|
+
# json.dump(result, f, ensure_ascii=False, indent=2)
|
|
1472
|
+
|
|
1473
|
+
def process_coverage_data(json_data):
|
|
1474
|
+
# Step 1: 提取 coverage_results 和 unmatched
|
|
1475
|
+
coverage_results = json_data.get("coverage_results", [])
|
|
1476
|
+
unmatched = json_data.get("unmatched", [])
|
|
1477
|
+
|
|
1478
|
+
# Step 2: 将 unmatched 转换为与 coverage_results 结构一致的格式
|
|
1479
|
+
# 填充缺失字段为合理默认值
|
|
1480
|
+
normalized_unmatched = []
|
|
1481
|
+
for item in unmatched:
|
|
1482
|
+
normalized_item = {
|
|
1483
|
+
"source_file": item["source_file"],
|
|
1484
|
+
"test_file": None, # 无对应测试文件
|
|
1485
|
+
"language": item["language"],
|
|
1486
|
+
"total_testable_elements": 0,
|
|
1487
|
+
"tested_elements": 0,
|
|
1488
|
+
"coverage_percentage": 0.0,
|
|
1489
|
+
"uncovered_elements": [], # 或可设为 None,但保持列表一致性
|
|
1490
|
+
"code_similarity": 0.0,
|
|
1491
|
+
"cyclomatic_complexity": item["cyclomatic_complexity"],
|
|
1492
|
+
"dependency_complexity": item["dependency_complexity"]
|
|
1493
|
+
}
|
|
1494
|
+
normalized_unmatched.append(normalized_item)
|
|
1495
|
+
|
|
1496
|
+
# Step 3: 合并 coverage_results 和 normalized_unmatched
|
|
1497
|
+
combined = coverage_results + normalized_unmatched
|
|
1498
|
+
|
|
1499
|
+
# Step 4: 按覆盖率和圈复杂度升序排序(由易到难)
|
|
1500
|
+
combined_sorted = sorted(
|
|
1501
|
+
combined,
|
|
1502
|
+
key=lambda x: (x["coverage_percentage"], x["cyclomatic_complexity"])
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
# Step 5: 返回新结构(不含 matches,只保留处理后的结果)
|
|
1506
|
+
result = {
|
|
1507
|
+
"coverage_results": combined_sorted
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
return result
|
|
1511
|
+
|
|
1512
|
+
def to_json(output_path, result):
|
|
1513
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
1514
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|