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,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Java测试删除器
|
|
4
|
+
使用正则表达式解析Java测试文件
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from coverage_tool.test_removers.base import BaseSmartTestRemover, TestFunction
|
|
12
|
+
from coverage_tool.parsers import TestCase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class JavaSmartTestRemover(BaseSmartTestRemover):
|
|
16
|
+
"""Java智能测试删除器"""
|
|
17
|
+
|
|
18
|
+
# 预编译正则表达式提高性能
|
|
19
|
+
TEST_METHOD_PATTERN = re.compile(
|
|
20
|
+
r'(@Test(?:\([^)]*\))?\s*)?' # @Test 注解(可选)
|
|
21
|
+
r'(?:@\w+\s*)*' # 其他注解
|
|
22
|
+
r'(?:public\s+)?' # 访问修饰符
|
|
23
|
+
r'(?:void|static)?\s+' # 返回类型
|
|
24
|
+
r'(\w+)\s*' # 方法名
|
|
25
|
+
r'\([^)]*\)\s*' # 参数列表
|
|
26
|
+
r'\{', # 方法开始
|
|
27
|
+
re.MULTILINE | re.DOTALL
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def _is_build_failure(self, test_name: str) -> bool:
|
|
31
|
+
"""检查是否是Java构建失败的特殊测试名称"""
|
|
32
|
+
build_failure_patterns = [
|
|
33
|
+
'[build failed]',
|
|
34
|
+
'[compilation failed]',
|
|
35
|
+
'Compilation failure',
|
|
36
|
+
'cannot find symbol',
|
|
37
|
+
'package does not exist',
|
|
38
|
+
'class not found',
|
|
39
|
+
]
|
|
40
|
+
return any(pattern in test_name for pattern in build_failure_patterns)
|
|
41
|
+
|
|
42
|
+
def remove_all_tests_in_package(self, package_path: str) -> tuple:
|
|
43
|
+
"""
|
|
44
|
+
删除指定Java包内的所有测试文件
|
|
45
|
+
用于处理包级编译错误
|
|
46
|
+
"""
|
|
47
|
+
deleted_count = 0
|
|
48
|
+
deleted_files = []
|
|
49
|
+
|
|
50
|
+
# Java包路径格式: com.example.test
|
|
51
|
+
package_parts = package_path.split('.')
|
|
52
|
+
|
|
53
|
+
if len(package_parts) > 0:
|
|
54
|
+
# 将包路径转换为目录路径
|
|
55
|
+
for i in range(len(package_parts)):
|
|
56
|
+
subdir = '/'.join(package_parts[i:])
|
|
57
|
+
|
|
58
|
+
# 搜索可能的目录结构
|
|
59
|
+
search_paths = [
|
|
60
|
+
Path(self.project_dir) / subdir,
|
|
61
|
+
Path(self.project_dir) / 'src' / 'test' / 'java' / subdir,
|
|
62
|
+
Path(self.project_dir) / 'test' / subdir,
|
|
63
|
+
Path(self.project_dir) / 'tests' / subdir,
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
for dir_path in search_paths:
|
|
67
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
68
|
+
# 查找该目录下的所有测试文件
|
|
69
|
+
test_files = list(dir_path.glob("*Test.java")) + list(dir_path.glob("Test*.java"))
|
|
70
|
+
|
|
71
|
+
if test_files:
|
|
72
|
+
self.logger.info(f"[编译错误] 在包 {package_path} 目录 {dir_path} 发现 {len(test_files)} 个测试文件")
|
|
73
|
+
|
|
74
|
+
for test_file in test_files:
|
|
75
|
+
try:
|
|
76
|
+
# 备份文件
|
|
77
|
+
self.backup_manager.backup_file(str(test_file))
|
|
78
|
+
# 删除文件
|
|
79
|
+
test_file.unlink()
|
|
80
|
+
deleted_count += 1
|
|
81
|
+
deleted_files.append(str(test_file))
|
|
82
|
+
self.logger.info(f"[编译错误] ✓ 已删除测试文件: {test_file}")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.logger.error(f"[编译错误] 删除文件失败: {test_file}: {e}")
|
|
85
|
+
|
|
86
|
+
return deleted_count, deleted_files
|
|
87
|
+
|
|
88
|
+
# 如果没找到,尝试递归搜索
|
|
89
|
+
for search_pattern in ["**/*Test.java", "**/Test*.java"]:
|
|
90
|
+
for test_file in Path(self.project_dir).rglob(search_pattern):
|
|
91
|
+
try:
|
|
92
|
+
with open(test_file, 'r', encoding='utf-8') as f:
|
|
93
|
+
content = f.read()
|
|
94
|
+
# 检查文件是否包含该包的导入或引用
|
|
95
|
+
if f"package {package_path}" in content or f"import {package_path}" in content:
|
|
96
|
+
self.backup_manager.backup_file(str(test_file))
|
|
97
|
+
test_file.unlink()
|
|
98
|
+
deleted_count += 1
|
|
99
|
+
deleted_files.append(str(test_file))
|
|
100
|
+
self.logger.info(f"[编译错误] ✓ 已删除测试文件: {test_file}")
|
|
101
|
+
except:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
return deleted_count, deleted_files
|
|
105
|
+
|
|
106
|
+
def find_test_functions(self, file_path: str) -> List[TestFunction]:
|
|
107
|
+
"""查找Java测试方法"""
|
|
108
|
+
test_methods = []
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
112
|
+
content = f.read()
|
|
113
|
+
|
|
114
|
+
for match in self.TEST_METHOD_PATTERN.finditer(content):
|
|
115
|
+
has_test_annotation = match.group(1) is not None
|
|
116
|
+
method_name = match.group(2)
|
|
117
|
+
|
|
118
|
+
# 如果方法名以 test 开头或有@Test注解
|
|
119
|
+
if has_test_annotation or method_name.startswith('test'):
|
|
120
|
+
# 计算行号
|
|
121
|
+
start_pos = match.start()
|
|
122
|
+
line_num = content[:start_pos].count('\n') + 1
|
|
123
|
+
|
|
124
|
+
# 找到方法结束位置
|
|
125
|
+
brace_count = 0
|
|
126
|
+
end_pos = match.end()
|
|
127
|
+
for i, char in enumerate(content[match.end():]):
|
|
128
|
+
if char == '{':
|
|
129
|
+
brace_count += 1
|
|
130
|
+
elif char == '}':
|
|
131
|
+
brace_count -= 1
|
|
132
|
+
if brace_count == 0:
|
|
133
|
+
end_pos = match.end() + i + 1
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
end_line = content[:end_pos].count('\n') + 1
|
|
137
|
+
|
|
138
|
+
test_func = TestFunction(
|
|
139
|
+
name=method_name,
|
|
140
|
+
file_path=file_path,
|
|
141
|
+
start_line=line_num,
|
|
142
|
+
end_line=end_line,
|
|
143
|
+
decorators=['Test'] if has_test_annotation else []
|
|
144
|
+
)
|
|
145
|
+
test_methods.append(test_func)
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
self.logger.error(f"解析Java文件失败 {file_path}: {e}")
|
|
149
|
+
|
|
150
|
+
return test_methods
|
|
151
|
+
|
|
152
|
+
def _get_deletion_range(self, test_func: TestFunction, lines: List[str]) -> tuple:
|
|
153
|
+
"""
|
|
154
|
+
获取Java测试方法的删除范围
|
|
155
|
+
考虑注解
|
|
156
|
+
"""
|
|
157
|
+
start_idx = self._adjust_start_for_decorators(
|
|
158
|
+
test_func, lines, comment_prefixes=['//']
|
|
159
|
+
)
|
|
160
|
+
end_idx = test_func.end_line
|
|
161
|
+
return start_idx, end_idx
|
|
162
|
+
|
|
163
|
+
def find_test_file(self, test_case: TestCase) -> Optional[str]:
|
|
164
|
+
"""查找Java测试文件"""
|
|
165
|
+
classname = test_case.classname
|
|
166
|
+
test_name = test_case.name
|
|
167
|
+
|
|
168
|
+
# 处理特殊的构建失败错误
|
|
169
|
+
if self._is_build_failure(test_name):
|
|
170
|
+
self.logger.info(f"[编译错误] 检测到Java编译失败: {test_name}")
|
|
171
|
+
# 如果是编译失败,classname通常包含包路径
|
|
172
|
+
# 尝试从包名推断目录
|
|
173
|
+
package_parts = classname.split('.')
|
|
174
|
+
if len(package_parts) > 1:
|
|
175
|
+
# 尝试不同的包目录组合
|
|
176
|
+
for i in range(len(package_parts)):
|
|
177
|
+
subdir = '/'.join(package_parts[i:])
|
|
178
|
+
search_paths = [
|
|
179
|
+
Path(self.project_dir) / subdir,
|
|
180
|
+
Path(self.project_dir) / 'src' / 'test' / 'java' / subdir,
|
|
181
|
+
Path(self.project_dir) / 'test' / subdir,
|
|
182
|
+
]
|
|
183
|
+
for dir_path in search_paths:
|
|
184
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
185
|
+
# 查找该目录下的测试文件
|
|
186
|
+
for test_file in dir_path.glob("*Test.java"):
|
|
187
|
+
if test_file.exists():
|
|
188
|
+
self.logger.info(f"[编译错误] 找到包 {classname} 的测试文件: {test_file}")
|
|
189
|
+
return str(test_file)
|
|
190
|
+
|
|
191
|
+
parts = classname.split('.')
|
|
192
|
+
|
|
193
|
+
if len(parts) > 1:
|
|
194
|
+
# 完整的包路径
|
|
195
|
+
package_path = '/'.join(parts[:-1])
|
|
196
|
+
class_name = parts[-1]
|
|
197
|
+
patterns = [
|
|
198
|
+
f"{package_path}/{class_name}Test.java",
|
|
199
|
+
f"{package_path}/Test{class_name}.java",
|
|
200
|
+
f"test/{package_path}/{class_name}Test.java",
|
|
201
|
+
f"src/test/java/{package_path}/{class_name}Test.java",
|
|
202
|
+
]
|
|
203
|
+
else:
|
|
204
|
+
class_name = parts[0]
|
|
205
|
+
patterns = [
|
|
206
|
+
f"{class_name}Test.java",
|
|
207
|
+
f"Test{class_name}.java",
|
|
208
|
+
f"test/{class_name}Test.java",
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
for pattern in patterns:
|
|
212
|
+
file_path = Path(self.project_dir) / pattern
|
|
213
|
+
if file_path.exists():
|
|
214
|
+
return str(file_path)
|
|
215
|
+
|
|
216
|
+
return None
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Python测试删除器
|
|
4
|
+
使用AST解析Python测试文件
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import os
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from coverage_tool.test_removers.base import BaseSmartTestRemover, TestFunction
|
|
13
|
+
from coverage_tool.parsers import TestCase
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PythonSmartTestRemover(BaseSmartTestRemover):
|
|
17
|
+
"""Python智能测试删除器 - 使用AST解析"""
|
|
18
|
+
|
|
19
|
+
def find_test_functions(self, file_path: str) -> List[TestFunction]:
|
|
20
|
+
"""使用AST查找Python测试函数"""
|
|
21
|
+
test_functions = []
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
25
|
+
content = f.read()
|
|
26
|
+
|
|
27
|
+
tree = ast.parse(content)
|
|
28
|
+
|
|
29
|
+
for node in ast.walk(tree):
|
|
30
|
+
if isinstance(node, ast.FunctionDef):
|
|
31
|
+
# 检查是否是测试函数
|
|
32
|
+
if self._is_test_function(node):
|
|
33
|
+
# 获取函数在文件中的位置
|
|
34
|
+
start_line = node.lineno
|
|
35
|
+
end_line = node.end_lineno if hasattr(node, 'end_lineno') else start_line
|
|
36
|
+
|
|
37
|
+
# 获取装饰器
|
|
38
|
+
decorators = []
|
|
39
|
+
for decorator in node.decorator_list:
|
|
40
|
+
if isinstance(decorator, ast.Name):
|
|
41
|
+
decorators.append(decorator.id)
|
|
42
|
+
elif isinstance(decorator, ast.Call):
|
|
43
|
+
if isinstance(decorator.func, ast.Name):
|
|
44
|
+
decorators.append(decorator.func.id)
|
|
45
|
+
|
|
46
|
+
test_func = TestFunction(
|
|
47
|
+
name=node.name,
|
|
48
|
+
file_path=file_path,
|
|
49
|
+
start_line=start_line,
|
|
50
|
+
end_line=end_line or start_line,
|
|
51
|
+
decorators=decorators,
|
|
52
|
+
is_class_method=self._is_class_method(node, tree),
|
|
53
|
+
class_name=self._get_class_name(node, tree)
|
|
54
|
+
)
|
|
55
|
+
test_functions.append(test_func)
|
|
56
|
+
|
|
57
|
+
elif isinstance(node, ast.ClassDef):
|
|
58
|
+
# 处理测试类中的方法
|
|
59
|
+
if self._is_test_class(node):
|
|
60
|
+
for item in node.body:
|
|
61
|
+
if isinstance(item, ast.FunctionDef) and self._is_test_method(item):
|
|
62
|
+
start_line = item.lineno
|
|
63
|
+
end_line = item.end_lineno if hasattr(item, 'end_lineno') else start_line
|
|
64
|
+
|
|
65
|
+
test_func = TestFunction(
|
|
66
|
+
name=item.name,
|
|
67
|
+
file_path=file_path,
|
|
68
|
+
start_line=start_line,
|
|
69
|
+
end_line=end_line or start_line,
|
|
70
|
+
is_class_method=True,
|
|
71
|
+
class_name=node.name
|
|
72
|
+
)
|
|
73
|
+
test_functions.append(test_func)
|
|
74
|
+
|
|
75
|
+
except SyntaxError as e:
|
|
76
|
+
self.logger.error(f"Python语法错误 {file_path}: {e}")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self.logger.error(f"解析Python文件失败 {file_path}: {e}")
|
|
79
|
+
|
|
80
|
+
return test_functions
|
|
81
|
+
|
|
82
|
+
def _is_test_function(self, node: ast.FunctionDef) -> bool:
|
|
83
|
+
"""检查是否是测试函数"""
|
|
84
|
+
# 函数名以 test_ 开头
|
|
85
|
+
if node.name.startswith('test_'):
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
# 检查是否有测试相关的装饰器
|
|
89
|
+
test_decorators = {'pytest', 'fixture', 'parametrize'}
|
|
90
|
+
for decorator in node.decorator_list:
|
|
91
|
+
if isinstance(decorator, ast.Name):
|
|
92
|
+
if decorator.id.lower() in test_decorators or decorator.id.startswith('pytest'):
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def _is_test_class(self, node: ast.ClassDef) -> bool:
|
|
98
|
+
"""检查是否是测试类"""
|
|
99
|
+
# 类名以 Test 开头
|
|
100
|
+
if node.name.startswith('Test'):
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
# 检查继承
|
|
104
|
+
for base in node.bases:
|
|
105
|
+
if isinstance(base, ast.Name):
|
|
106
|
+
if 'test' in base.id.lower():
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def _is_test_method(self, node: ast.FunctionDef) -> bool:
|
|
112
|
+
"""检查是否是测试方法"""
|
|
113
|
+
return node.name.startswith('test_')
|
|
114
|
+
|
|
115
|
+
def _is_class_method(self, node: ast.FunctionDef, tree: ast.AST) -> bool:
|
|
116
|
+
"""检查函数是否是类方法"""
|
|
117
|
+
for parent in ast.walk(tree):
|
|
118
|
+
if isinstance(parent, ast.ClassDef):
|
|
119
|
+
for item in parent.body:
|
|
120
|
+
if isinstance(item, ast.FunctionDef) and item.name == node.name:
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def _get_class_name(self, node: ast.FunctionDef, tree: ast.AST) -> Optional[str]:
|
|
125
|
+
"""获取函数所属的类名"""
|
|
126
|
+
for parent in ast.walk(tree):
|
|
127
|
+
if isinstance(parent, ast.ClassDef):
|
|
128
|
+
for item in parent.body:
|
|
129
|
+
if isinstance(item, ast.FunctionDef) and item.name == node.name:
|
|
130
|
+
return parent.name
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def _get_deletion_range(self, test_func: TestFunction, lines: List[str]) -> tuple:
|
|
134
|
+
"""
|
|
135
|
+
获取Python测试函数的删除范围
|
|
136
|
+
考虑装饰器和空行
|
|
137
|
+
"""
|
|
138
|
+
start_idx = self._adjust_start_for_decorators(
|
|
139
|
+
test_func, lines, comment_prefixes=['#']
|
|
140
|
+
)
|
|
141
|
+
end_idx = test_func.end_line
|
|
142
|
+
return start_idx, end_idx
|
|
143
|
+
|
|
144
|
+
def find_test_file(self, test_case: TestCase) -> Optional[str]:
|
|
145
|
+
"""查找Python测试文件"""
|
|
146
|
+
classname = test_case.classname
|
|
147
|
+
|
|
148
|
+
# 可能的文件模式
|
|
149
|
+
patterns = [
|
|
150
|
+
f"{classname.replace('.', '/')}.py",
|
|
151
|
+
f"test_{classname.replace('.', '_')}.py",
|
|
152
|
+
f"{classname.replace('.', '_')}_test.py",
|
|
153
|
+
f"tests/{classname.replace('.', '/')}.py",
|
|
154
|
+
f"tests/test_{classname.replace('.', '_')}.py",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
for pattern in patterns:
|
|
158
|
+
file_path = Path(self.project_dir) / pattern
|
|
159
|
+
if file_path.exists():
|
|
160
|
+
return str(file_path)
|
|
161
|
+
|
|
162
|
+
# 搜索包含测试函数的文件
|
|
163
|
+
for test_file in Path(self.project_dir).rglob("*test*.py"):
|
|
164
|
+
try:
|
|
165
|
+
with open(test_file, 'r', encoding='utf-8') as f:
|
|
166
|
+
content = f.read()
|
|
167
|
+
if f"def {test_case.name}(" in content:
|
|
168
|
+
return str(test_file)
|
|
169
|
+
except:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
return None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
工具类集合包
|
|
4
|
+
提供文件备份、日志、依赖分析等通用功能
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .file_backup import FileBackup, FileBackupManager
|
|
8
|
+
from .dependency_graph import DependencyGraph
|
|
9
|
+
from .logger import Logger
|
|
10
|
+
from .progress import ProgressTracker
|
|
11
|
+
from .helpers import temporary_directory, safe_delete_file, find_files_by_patterns, parse_error_locations
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'FileBackup',
|
|
15
|
+
'FileBackupManager',
|
|
16
|
+
'DependencyGraph',
|
|
17
|
+
'Logger',
|
|
18
|
+
'ProgressTracker',
|
|
19
|
+
'temporary_directory',
|
|
20
|
+
'safe_delete_file',
|
|
21
|
+
'find_files_by_patterns',
|
|
22
|
+
'parse_error_locations',
|
|
23
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
依赖关系图模块
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Set
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DependencyGraph:
|
|
10
|
+
"""依赖关系图"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.dependencies: Dict[str, Set[str]] = {} # file -> set of files it depends on
|
|
14
|
+
self.dependents: Dict[str, Set[str]] = {} # file -> set of files that depend on it
|
|
15
|
+
|
|
16
|
+
def add_dependency(self, file_path: str, depends_on: str):
|
|
17
|
+
"""添加依赖关系"""
|
|
18
|
+
if file_path not in self.dependencies:
|
|
19
|
+
self.dependencies[file_path] = set()
|
|
20
|
+
self.dependencies[file_path].add(depends_on)
|
|
21
|
+
|
|
22
|
+
if depends_on not in self.dependents:
|
|
23
|
+
self.dependents[depends_on] = set()
|
|
24
|
+
self.dependents[depends_on].add(file_path)
|
|
25
|
+
|
|
26
|
+
def get_dependents(self, file_path: str) -> Set[str]:
|
|
27
|
+
"""获取依赖于该文件的文件列表"""
|
|
28
|
+
return self.dependents.get(file_path, set())
|
|
29
|
+
|
|
30
|
+
def get_dependencies(self, file_path: str) -> Set[str]:
|
|
31
|
+
"""获取该文件依赖的文件列表"""
|
|
32
|
+
return self.dependencies.get(file_path, set())
|
|
33
|
+
|
|
34
|
+
def get_impact_files(self, file_path: str) -> Set[str]:
|
|
35
|
+
"""获取删除该文件会影响的所有文件(递归)"""
|
|
36
|
+
impacted = set()
|
|
37
|
+
to_process = {file_path}
|
|
38
|
+
processed = set()
|
|
39
|
+
|
|
40
|
+
while to_process:
|
|
41
|
+
current = to_process.pop()
|
|
42
|
+
if current in processed:
|
|
43
|
+
continue
|
|
44
|
+
processed.add(current)
|
|
45
|
+
|
|
46
|
+
dependents = self.get_dependents(current)
|
|
47
|
+
for dependent in dependents:
|
|
48
|
+
if dependent != file_path:
|
|
49
|
+
impacted.add(dependent)
|
|
50
|
+
to_process.add(dependent)
|
|
51
|
+
|
|
52
|
+
return impacted
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
文件备份管理模块
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import hashlib
|
|
9
|
+
import tempfile
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class FileBackup:
|
|
17
|
+
"""文件备份记录"""
|
|
18
|
+
original_path: str
|
|
19
|
+
backup_path: str
|
|
20
|
+
checksum: str
|
|
21
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FileBackupManager:
|
|
25
|
+
"""文件备份管理器"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, backup_dir: Optional[str] = None):
|
|
28
|
+
self.backup_dir = backup_dir or tempfile.mkdtemp(prefix="coverage_backup_")
|
|
29
|
+
self.backups: Dict[str, FileBackup] = {}
|
|
30
|
+
self._ensure_backup_dir()
|
|
31
|
+
|
|
32
|
+
def _ensure_backup_dir(self):
|
|
33
|
+
"""确保备份目录存在"""
|
|
34
|
+
if not os.path.exists(self.backup_dir):
|
|
35
|
+
os.makedirs(self.backup_dir, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
def _calculate_checksum(self, file_path: str) -> str:
|
|
38
|
+
"""计算文件校验和"""
|
|
39
|
+
try:
|
|
40
|
+
with open(file_path, 'rb') as f:
|
|
41
|
+
return hashlib.md5(f.read()).hexdigest()
|
|
42
|
+
except:
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
def backup_file(self, file_path: str) -> Optional[FileBackup]:
|
|
46
|
+
"""备份单个文件"""
|
|
47
|
+
if not os.path.exists(file_path):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
# 计算相对路径
|
|
51
|
+
abs_path = os.path.abspath(file_path)
|
|
52
|
+
backup_file_path = os.path.join(
|
|
53
|
+
self.backup_dir,
|
|
54
|
+
abs_path.replace(os.sep, '_').replace(':', '_')
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 确保备份目录存在
|
|
58
|
+
os.makedirs(os.path.dirname(backup_file_path), exist_ok=True)
|
|
59
|
+
|
|
60
|
+
# 复制文件
|
|
61
|
+
shutil.copy2(abs_path, backup_file_path)
|
|
62
|
+
|
|
63
|
+
# 创建备份记录
|
|
64
|
+
backup = FileBackup(
|
|
65
|
+
original_path=abs_path,
|
|
66
|
+
backup_path=backup_file_path,
|
|
67
|
+
checksum=self._calculate_checksum(abs_path)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self.backups[abs_path] = backup
|
|
71
|
+
return backup
|
|
72
|
+
|
|
73
|
+
def backup_files(self, file_paths: List[str]) -> List[FileBackup]:
|
|
74
|
+
"""批量备份文件"""
|
|
75
|
+
backups = []
|
|
76
|
+
for file_path in file_paths:
|
|
77
|
+
backup = self.backup_file(file_path)
|
|
78
|
+
if backup:
|
|
79
|
+
backups.append(backup)
|
|
80
|
+
return backups
|
|
81
|
+
|
|
82
|
+
def restore_file(self, file_path: str) -> bool:
|
|
83
|
+
"""恢复单个文件"""
|
|
84
|
+
abs_path = os.path.abspath(file_path)
|
|
85
|
+
if abs_path not in self.backups:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
backup = self.backups[abs_path]
|
|
89
|
+
if os.path.exists(backup.backup_path):
|
|
90
|
+
shutil.copy2(backup.backup_path, abs_path)
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def restore_all(self) -> List[str]:
|
|
95
|
+
"""恢复所有备份的文件"""
|
|
96
|
+
restored = []
|
|
97
|
+
for file_path in self.backups:
|
|
98
|
+
if self.restore_file(file_path):
|
|
99
|
+
restored.append(file_path)
|
|
100
|
+
return restored
|
|
101
|
+
|
|
102
|
+
def cleanup(self):
|
|
103
|
+
"""清理备份目录"""
|
|
104
|
+
if os.path.exists(self.backup_dir):
|
|
105
|
+
shutil.rmtree(self.backup_dir, ignore_errors=True)
|
|
106
|
+
self.backups.clear()
|
|
107
|
+
|
|
108
|
+
def __enter__(self):
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
112
|
+
self.cleanup()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
辅助函数模块
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import re
|
|
9
|
+
import tempfile
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from .file_backup import FileBackupManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextmanager
|
|
18
|
+
def temporary_directory():
|
|
19
|
+
"""临时目录上下文管理器"""
|
|
20
|
+
temp_dir = tempfile.mkdtemp()
|
|
21
|
+
try:
|
|
22
|
+
yield temp_dir
|
|
23
|
+
finally:
|
|
24
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def safe_delete_file(file_path: str, backup_manager: Optional[FileBackupManager] = None) -> bool:
|
|
28
|
+
"""安全删除文件(先备份)"""
|
|
29
|
+
if not os.path.exists(file_path):
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# 先备份
|
|
33
|
+
if backup_manager:
|
|
34
|
+
backup_manager.backup_file(file_path)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
os.remove(file_path)
|
|
38
|
+
return True
|
|
39
|
+
except Exception as e:
|
|
40
|
+
print(f"删除文件失败 {file_path}: {e}")
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def find_files_by_patterns(directory: str, patterns: List[str]) -> List[str]:
|
|
45
|
+
"""根据多个模式查找文件"""
|
|
46
|
+
found_files = set()
|
|
47
|
+
path = Path(directory)
|
|
48
|
+
|
|
49
|
+
for pattern in patterns:
|
|
50
|
+
for file_path in path.rglob(pattern):
|
|
51
|
+
if file_path.is_file():
|
|
52
|
+
found_files.add(str(file_path))
|
|
53
|
+
|
|
54
|
+
return sorted(list(found_files))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_error_locations(errors: List[str], project_dir: str) -> Dict[str, List[int]]:
|
|
58
|
+
"""从错误信息中解析出错的文件和行号"""
|
|
59
|
+
error_locations: Dict[str, List[int]] = {}
|
|
60
|
+
|
|
61
|
+
# 各种编译错误模式
|
|
62
|
+
patterns = [
|
|
63
|
+
r'([^\s]+\.\w+):(\d+):', # file:line:col
|
|
64
|
+
r'([^\s]+\.\w+)\((\d+)\)', # file(line)
|
|
65
|
+
r'File "([^"]+)", line (\d+)', # Python style
|
|
66
|
+
r'([^\s]+\.\w+):(\d+)', # file:line
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
for error in errors:
|
|
70
|
+
for pattern in patterns:
|
|
71
|
+
matches = re.findall(pattern, error)
|
|
72
|
+
for match in matches:
|
|
73
|
+
if isinstance(match, tuple):
|
|
74
|
+
file_path, line_str = match
|
|
75
|
+
else:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# 规范化路径
|
|
79
|
+
if not os.path.isabs(file_path):
|
|
80
|
+
file_path = os.path.join(project_dir, file_path)
|
|
81
|
+
file_path = os.path.normpath(file_path)
|
|
82
|
+
|
|
83
|
+
if os.path.exists(file_path):
|
|
84
|
+
line_num = int(line_str)
|
|
85
|
+
if file_path not in error_locations:
|
|
86
|
+
error_locations[file_path] = []
|
|
87
|
+
error_locations[file_path].append(line_num)
|
|
88
|
+
|
|
89
|
+
return error_locations
|