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,158 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Go单测运行器
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
from typing import List, Tuple, Optional
|
|
10
|
+
|
|
11
|
+
from coverage_tool.converters import JUnitReportConverter
|
|
12
|
+
from .base import BaseRunner, RunResult, CoverageExtractor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GoRunner(BaseRunner):
|
|
16
|
+
"""Go单测运行器"""
|
|
17
|
+
|
|
18
|
+
def _ensure_go_junit_report(self) -> bool:
|
|
19
|
+
"""确保 go-junit-report 工具已安装,未安装时自动安装"""
|
|
20
|
+
# 检查是否已安装
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
["which", "go-junit-report"],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True
|
|
25
|
+
)
|
|
26
|
+
if result.returncode == 0:
|
|
27
|
+
self.logger.debug("go-junit-report 已安装")
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
# 未安装,尝试自动安装
|
|
31
|
+
self.logger.info("[工具安装] go-junit-report 未安装,正在自动安装...")
|
|
32
|
+
install_result = subprocess.run(
|
|
33
|
+
["go", "install", "github.com/jstemmer/go-junit-report/v2@latest"],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
timeout=120
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if install_result.returncode == 0:
|
|
40
|
+
# 获取 GOPATH 并添加到 PATH
|
|
41
|
+
go_path_result = subprocess.run(
|
|
42
|
+
["go", "env", "GOPATH"],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True
|
|
45
|
+
)
|
|
46
|
+
if go_path_result.returncode == 0:
|
|
47
|
+
go_path = go_path_result.stdout.strip()
|
|
48
|
+
gopath_bin = os.path.join(go_path, "bin")
|
|
49
|
+
current_path = os.environ.get('PATH', '')
|
|
50
|
+
if gopath_bin not in current_path:
|
|
51
|
+
os.environ['PATH'] = f"{gopath_bin}:{current_path}"
|
|
52
|
+
|
|
53
|
+
self.logger.info("[工具安装] ✓ go-junit-report 安装成功")
|
|
54
|
+
return True
|
|
55
|
+
else:
|
|
56
|
+
self.logger.warning(f"[工具安装] ✗ go-junit-report 安装失败: {install_result.stderr}")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def run_tests(self, target_dir: str, output_file: str) -> RunResult:
|
|
60
|
+
"""运行Go测试"""
|
|
61
|
+
# 确保 go-junit-report 已安装(Go依赖由go test自动下载)
|
|
62
|
+
has_junit_tool = self._ensure_go_junit_report()
|
|
63
|
+
|
|
64
|
+
# 运行测试(标准输出格式,不是-json)
|
|
65
|
+
cmd = "go test -v -cover -coverprofile=coverage.out ./... 2>&1"
|
|
66
|
+
result = self._run_command(cmd, target_dir)
|
|
67
|
+
|
|
68
|
+
# 使用 go-junit-report 生成 JUnit XML
|
|
69
|
+
if result.stdout:
|
|
70
|
+
junit_xml_generated = False
|
|
71
|
+
|
|
72
|
+
if has_junit_tool:
|
|
73
|
+
try:
|
|
74
|
+
# 使用 go-junit-report 转换
|
|
75
|
+
report_cmd = ["go-junit-report", "-set-exit-code"]
|
|
76
|
+
junit_result = subprocess.run(
|
|
77
|
+
report_cmd,
|
|
78
|
+
input=result.stdout,
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
timeout=60
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if junit_result.returncode == 0 or junit_result.stdout:
|
|
85
|
+
with open(output_file, 'w') as f:
|
|
86
|
+
f.write(junit_result.stdout)
|
|
87
|
+
result.junit_xml_path = output_file
|
|
88
|
+
junit_xml_generated = True
|
|
89
|
+
self.logger.info("[报告生成] ✓ 使用 go-junit-report 生成 JUnit XML")
|
|
90
|
+
else:
|
|
91
|
+
self.logger.warning(f"[报告生成] go-junit-report 执行失败: {junit_result.stderr}")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.logger.warning(f"[报告生成] go-junit-report 执行错误: {e}")
|
|
94
|
+
|
|
95
|
+
# 如果 go-junit-report 失败或未安装,使用内置转换作为备用
|
|
96
|
+
if not junit_xml_generated:
|
|
97
|
+
try:
|
|
98
|
+
xml_content = JUnitReportConverter.convert_go_test(result.stdout)
|
|
99
|
+
with open(output_file, 'w') as f:
|
|
100
|
+
f.write(xml_content)
|
|
101
|
+
result.junit_xml_path = output_file
|
|
102
|
+
self.logger.info("[报告生成] ✓ 使用内置转换器生成 JUnit XML")
|
|
103
|
+
except Exception as e:
|
|
104
|
+
self.logger.error(f"转换Go测试输出失败: {e}")
|
|
105
|
+
|
|
106
|
+
# 复制coverage.out到临时目录
|
|
107
|
+
project_coverage = os.path.join(target_dir, "coverage.out")
|
|
108
|
+
if os.path.exists(project_coverage):
|
|
109
|
+
temp_dir = os.path.dirname(output_file)
|
|
110
|
+
if temp_dir:
|
|
111
|
+
temp_coverage = os.path.join(temp_dir, "coverage.out")
|
|
112
|
+
shutil.copy2(project_coverage, temp_coverage)
|
|
113
|
+
|
|
114
|
+
result.coverage_rate = self.get_coverage(output_file, target_dir)
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
def get_coverage(self, output_file: str, target_dir: Optional[str] = None) -> float:
|
|
119
|
+
"""获取Go覆盖率"""
|
|
120
|
+
# 尝试多个可能的 coverage.out 位置
|
|
121
|
+
possible_paths = []
|
|
122
|
+
|
|
123
|
+
# 1. 临时目录
|
|
124
|
+
if output_file:
|
|
125
|
+
temp_dir = os.path.dirname(output_file)
|
|
126
|
+
if temp_dir:
|
|
127
|
+
possible_paths.append(os.path.join(temp_dir, "coverage.out"))
|
|
128
|
+
|
|
129
|
+
# 2. 目标项目目录(最重要)
|
|
130
|
+
if target_dir:
|
|
131
|
+
possible_paths.append(os.path.join(target_dir, "coverage.out"))
|
|
132
|
+
|
|
133
|
+
# 3. 当前工作目录
|
|
134
|
+
possible_paths.append(os.path.join(os.getcwd(), "coverage.out"))
|
|
135
|
+
|
|
136
|
+
# 尝试每个路径
|
|
137
|
+
for coverage_path in possible_paths:
|
|
138
|
+
if os.path.exists(coverage_path):
|
|
139
|
+
self.logger.debug(f"找到Go覆盖率文件: {coverage_path}")
|
|
140
|
+
coverage = CoverageExtractor.extract_from_coverage_out(coverage_path, 'go')
|
|
141
|
+
if coverage > 0:
|
|
142
|
+
return coverage
|
|
143
|
+
|
|
144
|
+
self.logger.warning("未找到Go覆盖率文件 (coverage.out)")
|
|
145
|
+
return 0.0
|
|
146
|
+
|
|
147
|
+
def compile(self, target_dir: str) -> Tuple[bool, List[str]]:
|
|
148
|
+
"""编译Go项目"""
|
|
149
|
+
cmd = "go build ./"
|
|
150
|
+
result = self._run_command(cmd, target_dir)
|
|
151
|
+
|
|
152
|
+
errors = []
|
|
153
|
+
if not result.success:
|
|
154
|
+
for line in result.stderr.split('\n'):
|
|
155
|
+
if line.strip() and ('#' in line or 'error' in line.lower()):
|
|
156
|
+
errors.append(line.strip())
|
|
157
|
+
|
|
158
|
+
return result.success, errors
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Java单测运行器
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import List, Tuple
|
|
8
|
+
|
|
9
|
+
from .base import BaseRunner, RunResult, CoverageExtractor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JavaRunner(BaseRunner):
|
|
13
|
+
"""Java单测运行器"""
|
|
14
|
+
|
|
15
|
+
def run_tests(self, target_dir: str, output_file: str) -> RunResult:
|
|
16
|
+
"""运行Java测试"""
|
|
17
|
+
# 使用Maven运行测试
|
|
18
|
+
cmd = f"mvn test -Dtest.output={output_file}"
|
|
19
|
+
result = self._run_command(cmd, target_dir)
|
|
20
|
+
|
|
21
|
+
# Maven通常生成surefire报告
|
|
22
|
+
surefire_report = os.path.join(target_dir, "target", "surefire-reports")
|
|
23
|
+
if os.path.exists(surefire_report):
|
|
24
|
+
# 转换surefire报告为JUnit XML
|
|
25
|
+
self._convert_surefire_reports(surefire_report, output_file)
|
|
26
|
+
|
|
27
|
+
result.junit_xml_path = output_file if os.path.exists(output_file) else None
|
|
28
|
+
result.coverage_rate = self.get_coverage(output_file)
|
|
29
|
+
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
def _convert_surefire_reports(self, surefire_dir: str, output_file: str):
|
|
33
|
+
"""转换Surefire报告为JUnit XML"""
|
|
34
|
+
# 这里简化处理,实际应该解析XML并合并
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def get_coverage(self, output_file: str) -> float:
|
|
38
|
+
"""获取Java覆盖率(JaCoCo)"""
|
|
39
|
+
# JaCoCo报告通常在 target/site/jacoco/index.html
|
|
40
|
+
jacoco_html = os.path.join("target", "site", "jacoco", "index.html")
|
|
41
|
+
return CoverageExtractor.extract_from_jacoco(jacoco_html)
|
|
42
|
+
|
|
43
|
+
def compile(self, target_dir: str) -> Tuple[bool, List[str]]:
|
|
44
|
+
"""编译Java项目"""
|
|
45
|
+
cmd = "mvn compile"
|
|
46
|
+
result = self._run_command(cmd, target_dir)
|
|
47
|
+
|
|
48
|
+
errors = []
|
|
49
|
+
if not result.success:
|
|
50
|
+
for line in result.stderr.split('\n'):
|
|
51
|
+
if 'error:' in line.lower() or 'cannot find symbol' in line.lower():
|
|
52
|
+
errors.append(line.strip())
|
|
53
|
+
|
|
54
|
+
return result.success, errors
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Python单测运行器
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
from typing import List, Tuple
|
|
10
|
+
|
|
11
|
+
from .base import BaseRunner, RunResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PythonRunner(BaseRunner):
|
|
15
|
+
"""Python单测运行器"""
|
|
16
|
+
|
|
17
|
+
def run_tests(self, target_dir: str, output_file: str) -> RunResult:
|
|
18
|
+
"""运行Python测试"""
|
|
19
|
+
output_dir = os.path.dirname(output_file) if output_file else target_dir
|
|
20
|
+
|
|
21
|
+
# 使用 coverage 运行测试,同时生成 JUnit XML
|
|
22
|
+
cmd = f"python3 -m coverage run --source={target_dir} -m pytest --junitxml={output_file} -v {target_dir}"
|
|
23
|
+
result = self._run_command(cmd, target_dir)
|
|
24
|
+
result.junit_xml_path = output_file if os.path.exists(output_file) else None
|
|
25
|
+
|
|
26
|
+
# 复制 .coverage 文件到输出目录(如果需要)
|
|
27
|
+
project_coverage = os.path.join(target_dir, ".coverage")
|
|
28
|
+
if os.path.exists(project_coverage) and output_dir != target_dir:
|
|
29
|
+
temp_coverage = os.path.join(output_dir, ".coverage")
|
|
30
|
+
try:
|
|
31
|
+
shutil.copy2(project_coverage, temp_coverage)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
self.logger.debug(f"复制覆盖率文件失败: {e}")
|
|
34
|
+
|
|
35
|
+
# 计算覆盖率
|
|
36
|
+
result.coverage_rate = self.get_coverage(output_file)
|
|
37
|
+
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
def get_coverage(self, output_file: str) -> float:
|
|
41
|
+
"""获取Python覆盖率"""
|
|
42
|
+
output_dir = os.path.dirname(output_file) if output_file else "."
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# 检查是否存在 .coverage 数据文件
|
|
46
|
+
coverage_file = os.path.join(output_dir, ".coverage")
|
|
47
|
+
|
|
48
|
+
if not os.path.exists(coverage_file):
|
|
49
|
+
# 如果没有覆盖率数据,尝试运行测试收集
|
|
50
|
+
self.logger.debug("未找到覆盖率数据,尝试收集...")
|
|
51
|
+
return 0.0
|
|
52
|
+
|
|
53
|
+
# 生成覆盖率报告
|
|
54
|
+
cmd = "python3 -m coverage report --omit='*test*'"
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
cmd,
|
|
57
|
+
shell=True,
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
cwd=output_dir
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if result.returncode == 0:
|
|
64
|
+
for line in result.stdout.split('\n'):
|
|
65
|
+
if 'TOTAL' in line:
|
|
66
|
+
parts = line.split()
|
|
67
|
+
if len(parts) >= 2:
|
|
68
|
+
try:
|
|
69
|
+
coverage = float(parts[-1].replace('%', ''))
|
|
70
|
+
self.logger.info(f"从覆盖率报告读取: {coverage:.2f}%")
|
|
71
|
+
return coverage
|
|
72
|
+
except:
|
|
73
|
+
pass
|
|
74
|
+
else:
|
|
75
|
+
self.logger.warning(f"生成覆盖率报告失败: {result.stderr}")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
self.logger.error(f"获取Python覆盖率失败: {e}")
|
|
78
|
+
|
|
79
|
+
return 0.0
|
|
80
|
+
|
|
81
|
+
def compile(self, target_dir: str) -> Tuple[bool, List[str]]:
|
|
82
|
+
"""Python语法检查"""
|
|
83
|
+
errors = []
|
|
84
|
+
py_files = self.find_files(target_dir, "*.py")
|
|
85
|
+
|
|
86
|
+
for py_file in py_files:
|
|
87
|
+
try:
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
f"python3 -m py_compile {py_file}",
|
|
90
|
+
shell=True,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
text=True
|
|
93
|
+
)
|
|
94
|
+
if result.returncode != 0:
|
|
95
|
+
errors.append(f"语法错误: {py_file} - {result.stderr}")
|
|
96
|
+
except Exception as e:
|
|
97
|
+
errors.append(f"检查失败: {py_file} - {str(e)}")
|
|
98
|
+
|
|
99
|
+
return len(errors) == 0, errors
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
测试删除器包
|
|
4
|
+
|
|
5
|
+
重构后的模块化测试删除器,支持多种编程语言。
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from test_removers import SmartTestRemoverFactory
|
|
9
|
+
|
|
10
|
+
remover = SmartTestRemoverFactory.get_remover('python', '/path/to/project')
|
|
11
|
+
test_functions = remover.find_test_functions('/path/to/test_file.py')
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .base import (
|
|
15
|
+
BaseSmartTestRemover,
|
|
16
|
+
TestFunction,
|
|
17
|
+
TestRemovalStrategy,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .python_remover import PythonSmartTestRemover
|
|
21
|
+
from .java_remover import JavaSmartTestRemover
|
|
22
|
+
from .go_remover import GoSmartTestRemover
|
|
23
|
+
from .c_remover import CSmartTestRemover
|
|
24
|
+
from .cpp_remover import CPPSmartTestRemover
|
|
25
|
+
|
|
26
|
+
from .factory import SmartTestRemoverFactory
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
'BaseSmartTestRemover',
|
|
30
|
+
'TestFunction',
|
|
31
|
+
'TestRemovalStrategy',
|
|
32
|
+
'PythonSmartTestRemover',
|
|
33
|
+
'JavaSmartTestRemover',
|
|
34
|
+
'GoSmartTestRemover',
|
|
35
|
+
'CSmartTestRemover',
|
|
36
|
+
'CPPSmartTestRemover',
|
|
37
|
+
'SmartTestRemoverFactory',
|
|
38
|
+
]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
测试删除器基础模块
|
|
4
|
+
包含公共数据结构和基类
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
from coverage_tool.utils import FileBackupManager, Logger
|
|
14
|
+
from coverage_tool.parsers import TestStatus, TestReport, TestCase
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TestFunction:
|
|
19
|
+
"""测试函数信息"""
|
|
20
|
+
name: str
|
|
21
|
+
file_path: str
|
|
22
|
+
start_line: int
|
|
23
|
+
end_line: int
|
|
24
|
+
decorators: List[str] = field(default_factory=list)
|
|
25
|
+
is_class_method: bool = False
|
|
26
|
+
class_name: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestRemovalStrategy(Enum):
|
|
30
|
+
"""测试删除策略"""
|
|
31
|
+
DELETE_FUNCTION = 1 # 仅删除测试函数
|
|
32
|
+
DELETE_FILE = 2 # 删除整个测试文件
|
|
33
|
+
COMMENT_OUT = 3 # 注释掉测试(保留代码)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BaseSmartTestRemover(ABC):
|
|
37
|
+
"""智能测试删除器基类"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, project_dir: str, logger: Optional[Logger] = None):
|
|
40
|
+
self.project_dir = project_dir
|
|
41
|
+
self.logger = logger or Logger()
|
|
42
|
+
self.backup_manager = FileBackupManager()
|
|
43
|
+
self.deleted_tests: List[TestFunction] = []
|
|
44
|
+
self.removal_strategy = TestRemovalStrategy.DELETE_FUNCTION
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def find_test_functions(self, file_path: str) -> List[TestFunction]:
|
|
48
|
+
"""查找文件中的所有测试函数"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def find_test_file(self, test_case: TestCase) -> Optional[str]:
|
|
53
|
+
"""根据测试用例找到对应的测试文件"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def _get_deletion_range(self, test_func: TestFunction, lines: List[str]) -> tuple:
|
|
57
|
+
"""
|
|
58
|
+
获取要删除的行范围
|
|
59
|
+
子类可以覆盖此方法以调整删除范围(例如考虑装饰器/注解)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
(start_idx, end_idx) - 行索引(0-based)
|
|
63
|
+
"""
|
|
64
|
+
start_idx = test_func.start_line - 1
|
|
65
|
+
end_idx = test_func.end_line
|
|
66
|
+
return start_idx, end_idx
|
|
67
|
+
|
|
68
|
+
def _adjust_start_for_decorators(self, test_func: TestFunction, lines: List[str],
|
|
69
|
+
comment_prefixes: Optional[List[str]] = None) -> int:
|
|
70
|
+
"""
|
|
71
|
+
向上查找装饰器/注解,调整删除起始行
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
test_func: 测试函数信息
|
|
75
|
+
lines: 文件行列表
|
|
76
|
+
comment_prefixes: 注释前缀列表(如 ['#', '//'])
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
调整后的起始行索引(0-based)
|
|
80
|
+
"""
|
|
81
|
+
start_idx = test_func.start_line - 1
|
|
82
|
+
|
|
83
|
+
while start_idx > 0:
|
|
84
|
+
line = lines[start_idx - 1].strip()
|
|
85
|
+
# 检查是否是装饰器/注解行
|
|
86
|
+
if line.startswith('@'):
|
|
87
|
+
start_idx -= 1
|
|
88
|
+
# 检查是否是空行或注释行
|
|
89
|
+
elif line == '':
|
|
90
|
+
start_idx -= 1
|
|
91
|
+
elif comment_prefixes:
|
|
92
|
+
is_comment = any(line.startswith(prefix) for prefix in comment_prefixes)
|
|
93
|
+
if is_comment:
|
|
94
|
+
start_idx -= 1
|
|
95
|
+
else:
|
|
96
|
+
break
|
|
97
|
+
else:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
return start_idx
|
|
101
|
+
|
|
102
|
+
def remove_test_function(self, test_func: TestFunction) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
删除单个测试函数 - 通用实现
|
|
105
|
+
|
|
106
|
+
这是通用的文件行删除逻辑,适用于大多数语言。
|
|
107
|
+
对于需要特殊处理的场景,子类可以覆盖 _get_deletion_range 方法。
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
with open(test_func.file_path, 'r', encoding='utf-8') as f:
|
|
111
|
+
lines = f.readlines()
|
|
112
|
+
|
|
113
|
+
# 获取删除范围(可以由子类调整)
|
|
114
|
+
start_idx, end_idx = self._get_deletion_range(test_func, lines)
|
|
115
|
+
|
|
116
|
+
# 删除指定行范围
|
|
117
|
+
new_lines = lines[:start_idx] + lines[end_idx:]
|
|
118
|
+
|
|
119
|
+
# 写回文件
|
|
120
|
+
with open(test_func.file_path, 'w', encoding='utf-8') as f:
|
|
121
|
+
f.writelines(new_lines)
|
|
122
|
+
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.logger.error(f"删除测试函数失败 {test_func.file_path}:{test_func.name}: {e}")
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def remove_failed_tests(self, test_report: TestReport) -> tuple:
|
|
130
|
+
"""
|
|
131
|
+
删除所有失败的测试
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
(删除数量, 删除的测试列表)
|
|
135
|
+
"""
|
|
136
|
+
total_deleted = 0
|
|
137
|
+
deleted_list = []
|
|
138
|
+
|
|
139
|
+
for suite in test_report.suites:
|
|
140
|
+
for test_case in suite.test_cases:
|
|
141
|
+
if test_case.status in [TestStatus.FAILED, TestStatus.ERROR]:
|
|
142
|
+
file_path = self.find_test_file(test_case)
|
|
143
|
+
|
|
144
|
+
if not file_path or not os.path.exists(file_path):
|
|
145
|
+
self.logger.warning(f"找不到测试文件: {test_case.classname}.{test_case.name}")
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# 处理特殊的构建失败
|
|
149
|
+
if self._is_build_failure(test_case.name):
|
|
150
|
+
self.logger.info(f"[编译错误] 包 {test_case.classname} 构建失败,准备删除该包的所有测试文件")
|
|
151
|
+
pkg_deleted_count, pkg_deleted_files = self.remove_all_tests_in_package(test_case.classname)
|
|
152
|
+
if pkg_deleted_count > 0:
|
|
153
|
+
total_deleted += pkg_deleted_count
|
|
154
|
+
for f in pkg_deleted_files:
|
|
155
|
+
deleted_list.append(f"{f}:BUILD_FAILURE_DELETED")
|
|
156
|
+
self.logger.info(f"[编译错误] ✓ 已删除 {pkg_deleted_count} 个测试文件解决包级编译错误")
|
|
157
|
+
else:
|
|
158
|
+
self.logger.warning(f"[编译错误] 未能找到包 {test_case.classname} 的测试文件")
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# 查找测试函数
|
|
162
|
+
test_funcs = self.find_test_functions(file_path)
|
|
163
|
+
target_func = None
|
|
164
|
+
|
|
165
|
+
for func in test_funcs:
|
|
166
|
+
if func.name == test_case.name or self._match_test_name(func.name, test_case.name):
|
|
167
|
+
target_func = func
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
if target_func:
|
|
171
|
+
# 备份文件
|
|
172
|
+
self.backup_manager.backup_file(file_path)
|
|
173
|
+
|
|
174
|
+
# 删除测试函数
|
|
175
|
+
if self.remove_test_function(target_func):
|
|
176
|
+
total_deleted += 1
|
|
177
|
+
deleted_list.append(f"{file_path}:{target_func.name}")
|
|
178
|
+
self.deleted_tests.append(target_func)
|
|
179
|
+
self.logger.info(f"已删除测试函数: {file_path}:{target_func.name}")
|
|
180
|
+
else:
|
|
181
|
+
self.logger.error(f"删除测试函数失败: {file_path}:{target_func.name}")
|
|
182
|
+
else:
|
|
183
|
+
self.logger.warning(f"在文件 {file_path} 中找不到测试函数: {test_case.name}")
|
|
184
|
+
|
|
185
|
+
return total_deleted, deleted_list
|
|
186
|
+
|
|
187
|
+
def _is_build_failure(self, test_name: str) -> bool:
|
|
188
|
+
"""检查是否是构建失败的特殊测试名称"""
|
|
189
|
+
build_failure_patterns = [
|
|
190
|
+
'[build failed]',
|
|
191
|
+
'[no test files]',
|
|
192
|
+
'[setup failed]',
|
|
193
|
+
]
|
|
194
|
+
return any(pattern in test_name for pattern in build_failure_patterns)
|
|
195
|
+
|
|
196
|
+
def remove_all_tests_in_package(self, package_path: str) -> tuple:
|
|
197
|
+
"""
|
|
198
|
+
删除指定包内的所有测试文件
|
|
199
|
+
用于处理包级编译错误
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
(删除数量, 删除的文件列表)
|
|
203
|
+
"""
|
|
204
|
+
# 基类提供默认实现,子类应覆盖以提供语言特定的逻辑
|
|
205
|
+
return 0, []
|
|
206
|
+
|
|
207
|
+
def _match_test_name(self, func_name: str, test_name: str) -> bool:
|
|
208
|
+
"""匹配测试名称(处理不同的命名方式)"""
|
|
209
|
+
# 完全匹配
|
|
210
|
+
if func_name == test_name:
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
# 忽略大小写匹配
|
|
214
|
+
if func_name.lower() == test_name.lower():
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
# 处理 Go 的测试名称(TestXxx/subtest 格式)
|
|
218
|
+
if '/' in test_name:
|
|
219
|
+
main_test = test_name.split('/')[0]
|
|
220
|
+
if func_name == main_test:
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
def restore_all(self) -> List[str]:
|
|
226
|
+
"""恢复所有修改"""
|
|
227
|
+
restored = self.backup_manager.restore_all()
|
|
228
|
+
self.deleted_tests.clear()
|
|
229
|
+
return restored
|
|
230
|
+
|
|
231
|
+
def cleanup(self):
|
|
232
|
+
"""清理资源"""
|
|
233
|
+
self.backup_manager.cleanup()
|