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,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
JUnit XML 解析器
|
|
4
|
+
解析各语言生成的JUnit XML测试报告
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import xml.etree.ElementTree as ET
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import List, Dict, Optional
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
class TestStatus(Enum):
|
|
14
|
+
PASSED = "passed"
|
|
15
|
+
FAILED = "failed"
|
|
16
|
+
ERROR = "error"
|
|
17
|
+
SKIPPED = "skipped"
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TestCase:
|
|
21
|
+
"""单个测试用例"""
|
|
22
|
+
name: str
|
|
23
|
+
classname: str
|
|
24
|
+
time: float
|
|
25
|
+
status: TestStatus
|
|
26
|
+
failure_message: Optional[str] = None
|
|
27
|
+
failure_type: Optional[str] = None
|
|
28
|
+
error_message: Optional[str] = None
|
|
29
|
+
error_type: Optional[str] = None
|
|
30
|
+
skipped_message: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class TestSuite:
|
|
34
|
+
"""测试套件"""
|
|
35
|
+
name: str
|
|
36
|
+
tests: int
|
|
37
|
+
failures: int
|
|
38
|
+
errors: int
|
|
39
|
+
skipped: int
|
|
40
|
+
time: float
|
|
41
|
+
test_cases: List[TestCase] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class TestReport:
|
|
45
|
+
"""完整测试报告"""
|
|
46
|
+
suites: List[TestSuite]
|
|
47
|
+
total_tests: int = 0
|
|
48
|
+
total_failures: int = 0
|
|
49
|
+
total_errors: int = 0
|
|
50
|
+
total_skipped: int = 0
|
|
51
|
+
total_time: float = 0.0
|
|
52
|
+
|
|
53
|
+
def __post_init__(self):
|
|
54
|
+
for suite in self.suites:
|
|
55
|
+
self.total_tests += suite.tests
|
|
56
|
+
self.total_failures += suite.failures
|
|
57
|
+
self.total_errors += suite.errors
|
|
58
|
+
self.total_skipped += suite.skipped
|
|
59
|
+
self.total_time += suite.time
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def pass_rate(self) -> float:
|
|
63
|
+
if self.total_tests == 0:
|
|
64
|
+
return 0.0
|
|
65
|
+
passed = self.total_tests - self.total_failures - self.total_errors - self.total_skipped
|
|
66
|
+
return (passed / self.total_tests) * 100
|
|
67
|
+
|
|
68
|
+
class JunitXMLParser:
|
|
69
|
+
"""JUnit XML解析器"""
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def parse(xml_path: str) -> TestReport:
|
|
73
|
+
"""解析JUnit XML文件"""
|
|
74
|
+
tree = ET.parse(xml_path)
|
|
75
|
+
root = tree.getroot()
|
|
76
|
+
|
|
77
|
+
# 兼容不同格式的根节点
|
|
78
|
+
if root.tag == 'testsuites':
|
|
79
|
+
return JunitXMLParser._parse_testsuites(root)
|
|
80
|
+
elif root.tag == 'testsuite':
|
|
81
|
+
return JunitXMLParser._parse_testsuite(root)
|
|
82
|
+
else:
|
|
83
|
+
raise ValueError(f"未知的XML根节点: {root.tag}")
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _parse_testsuites(root: ET.Element) -> TestReport:
|
|
87
|
+
"""解析testsuites根节点"""
|
|
88
|
+
suites = []
|
|
89
|
+
for suite_elem in root.findall('testsuite'):
|
|
90
|
+
suite = JunitXMLParser._parse_single_testsuite(suite_elem)
|
|
91
|
+
suites.append(suite)
|
|
92
|
+
return TestReport(suites=suites)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _parse_testsuite(root: ET.Element) -> TestReport:
|
|
96
|
+
"""解析testsuite根节点"""
|
|
97
|
+
suite = JunitXMLParser._parse_single_testsuite(root)
|
|
98
|
+
return TestReport(suites=[suite])
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _parse_single_testsuite(suite_elem: ET.Element) -> TestSuite:
|
|
102
|
+
"""解析单个testsuite"""
|
|
103
|
+
name = suite_elem.get('name', '')
|
|
104
|
+
tests = int(suite_elem.get('tests', 0))
|
|
105
|
+
failures = int(suite_elem.get('failures', 0))
|
|
106
|
+
errors = int(suite_elem.get('errors', 0))
|
|
107
|
+
skipped = int(suite_elem.get('skipped', 0))
|
|
108
|
+
time = float(suite_elem.get('time', 0.0))
|
|
109
|
+
|
|
110
|
+
test_cases = []
|
|
111
|
+
for case_elem in suite_elem.findall('testcase'):
|
|
112
|
+
test_case = JunitXMLParser._parse_testcase(case_elem)
|
|
113
|
+
test_cases.append(test_case)
|
|
114
|
+
|
|
115
|
+
return TestSuite(
|
|
116
|
+
name=name,
|
|
117
|
+
tests=tests,
|
|
118
|
+
failures=failures,
|
|
119
|
+
errors=errors,
|
|
120
|
+
skipped=skipped,
|
|
121
|
+
time=time,
|
|
122
|
+
test_cases=test_cases
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _parse_testcase(case_elem: ET.Element) -> TestCase:
|
|
127
|
+
"""解析单个testcase"""
|
|
128
|
+
name = case_elem.get('name', '')
|
|
129
|
+
classname = case_elem.get('classname', '')
|
|
130
|
+
time = float(case_elem.get('time', 0.0))
|
|
131
|
+
|
|
132
|
+
# 检查状态
|
|
133
|
+
failure_elem = case_elem.find('failure')
|
|
134
|
+
error_elem = case_elem.find('error')
|
|
135
|
+
skipped_elem = case_elem.find('skipped')
|
|
136
|
+
|
|
137
|
+
if failure_elem is not None:
|
|
138
|
+
status = TestStatus.FAILED
|
|
139
|
+
failure_message = failure_elem.get('message', '')
|
|
140
|
+
failure_type = failure_elem.get('type', '')
|
|
141
|
+
elif error_elem is not None:
|
|
142
|
+
status = TestStatus.ERROR
|
|
143
|
+
error_message = error_elem.get('message', '')
|
|
144
|
+
error_type = error_elem.get('type', '')
|
|
145
|
+
elif skipped_elem is not None:
|
|
146
|
+
status = TestStatus.SKIPPED
|
|
147
|
+
skipped_message = skipped_elem.get('message', '')
|
|
148
|
+
else:
|
|
149
|
+
status = TestStatus.PASSED
|
|
150
|
+
|
|
151
|
+
return TestCase(
|
|
152
|
+
name=name,
|
|
153
|
+
classname=classname,
|
|
154
|
+
time=time,
|
|
155
|
+
status=status,
|
|
156
|
+
failure_message=failure_elem.get('message') if failure_elem else None,
|
|
157
|
+
failure_type=failure_elem.get('type') if failure_elem else None,
|
|
158
|
+
error_message=error_elem.get('message') if error_elem else None,
|
|
159
|
+
error_type=error_elem.get('type') if error_elem else None,
|
|
160
|
+
skipped_message=skipped_elem.get('message') if skipped_elem else None
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def parse_string(xml_string: str) -> TestReport:
|
|
165
|
+
"""从字符串解析JUnit XML"""
|
|
166
|
+
root = ET.fromstring(xml_string)
|
|
167
|
+
if root.tag == 'testsuites':
|
|
168
|
+
return JunitXMLParser._parse_testsuites(root)
|
|
169
|
+
elif root.tag == 'testsuite':
|
|
170
|
+
return JunitXMLParser._parse_testsuite(root)
|
|
171
|
+
else:
|
|
172
|
+
raise ValueError(f"未知的XML根节点: {root.tag}")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
多语言单测运行器包
|
|
4
|
+
支持 Python, Java, Go, C, C++
|
|
5
|
+
统一覆盖率计算和增强错误处理
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import RunResult, BaseRunner, CoverageExtractor
|
|
9
|
+
from .factory import RunnerFactory
|
|
10
|
+
from .python_runner import PythonRunner
|
|
11
|
+
from .java_runner import JavaRunner
|
|
12
|
+
from .go_runner import GoRunner
|
|
13
|
+
from .c_runner import CRunner
|
|
14
|
+
from .cpp_runner import CPPRunner
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
'RunResult',
|
|
18
|
+
'BaseRunner',
|
|
19
|
+
'CoverageExtractor',
|
|
20
|
+
'RunnerFactory',
|
|
21
|
+
'PythonRunner',
|
|
22
|
+
'JavaRunner',
|
|
23
|
+
'GoRunner',
|
|
24
|
+
'CRunner',
|
|
25
|
+
'CPPRunner',
|
|
26
|
+
]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
运行器基类和共享组件
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import re
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import List, Dict, Optional, Tuple, Any
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from coverage_tool.core import CoverageConfig
|
|
16
|
+
from coverage_tool.utils import Logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RunResult:
|
|
21
|
+
"""运行结果"""
|
|
22
|
+
success: bool
|
|
23
|
+
return_code: int
|
|
24
|
+
stdout: str
|
|
25
|
+
stderr: str
|
|
26
|
+
duration: float
|
|
27
|
+
junit_xml_path: Optional[str] = None
|
|
28
|
+
coverage_rate: float = 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CoverageExtractor:
|
|
32
|
+
"""覆盖率提取器 - 统一处理不同语言的覆盖率"""
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def extract_from_coverage_out(coverage_out_path: str, language: str) -> float:
|
|
36
|
+
"""从coverage.out文件提取覆盖率"""
|
|
37
|
+
if not os.path.exists(coverage_out_path):
|
|
38
|
+
return 0.0
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
if language == 'go':
|
|
42
|
+
return CoverageExtractor._extract_go_coverage(coverage_out_path)
|
|
43
|
+
elif language in ['c', 'cpp']:
|
|
44
|
+
return CoverageExtractor._extract_gcov_coverage(coverage_out_path)
|
|
45
|
+
else:
|
|
46
|
+
return CoverageExtractor._extract_generic_coverage(coverage_out_path)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"提取覆盖率失败: {e}")
|
|
49
|
+
return 0.0
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _extract_go_coverage(coverage_out_path: str) -> float:
|
|
53
|
+
"""提取Go覆盖率"""
|
|
54
|
+
# 使用 go tool cover
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
f"go tool cover -func={coverage_out_path}",
|
|
57
|
+
shell=True,
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if result.returncode == 0:
|
|
63
|
+
for line in result.stdout.split('\n'):
|
|
64
|
+
if 'total:' in line.lower():
|
|
65
|
+
parts = line.split()
|
|
66
|
+
for part in reversed(parts):
|
|
67
|
+
if '%' in part:
|
|
68
|
+
try:
|
|
69
|
+
return float(part.replace('%', ''))
|
|
70
|
+
except:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
# 备用方案:直接解析文件
|
|
74
|
+
return CoverageExtractor._parse_go_coverage_file(coverage_out_path)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _parse_go_coverage_file(coverage_out_path: str) -> float:
|
|
78
|
+
"""直接解析Go coverage.out文件"""
|
|
79
|
+
try:
|
|
80
|
+
with open(coverage_out_path, 'r') as f:
|
|
81
|
+
lines = f.readlines()
|
|
82
|
+
|
|
83
|
+
if not lines or not lines[0].startswith('mode:'):
|
|
84
|
+
return 0.0
|
|
85
|
+
|
|
86
|
+
total_statements = 0
|
|
87
|
+
executed_statements = 0
|
|
88
|
+
|
|
89
|
+
for line in lines[1:]:
|
|
90
|
+
parts = line.strip().split()
|
|
91
|
+
if len(parts) >= 3:
|
|
92
|
+
try:
|
|
93
|
+
hits = int(parts[-1])
|
|
94
|
+
total_statements += 1
|
|
95
|
+
if hits > 0:
|
|
96
|
+
executed_statements += 1
|
|
97
|
+
except:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
if total_statements > 0:
|
|
101
|
+
return (executed_statements / total_statements) * 100
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"解析Go覆盖率文件失败: {e}")
|
|
104
|
+
|
|
105
|
+
return 0.0
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _extract_gcov_coverage(coverage_out_path: str) -> float:
|
|
109
|
+
"""提取gcov覆盖率"""
|
|
110
|
+
try:
|
|
111
|
+
with open(coverage_out_path, 'r') as f:
|
|
112
|
+
content = f.read()
|
|
113
|
+
|
|
114
|
+
# 尝试多种模式匹配
|
|
115
|
+
patterns = [
|
|
116
|
+
r'lines[\s\w:]+(\d+\.?\d*)%',
|
|
117
|
+
r'Total[\s\w:]+(\d+\.?\d*)%',
|
|
118
|
+
r'(\d+\.?\d*)%',
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for pattern in patterns:
|
|
122
|
+
match = re.search(pattern, content, re.IGNORECASE)
|
|
123
|
+
if match:
|
|
124
|
+
try:
|
|
125
|
+
return float(match.group(1))
|
|
126
|
+
except:
|
|
127
|
+
continue
|
|
128
|
+
except Exception as e:
|
|
129
|
+
print(f"提取gcov覆盖率失败: {e}")
|
|
130
|
+
|
|
131
|
+
return 0.0
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _extract_generic_coverage(coverage_out_path: str) -> float:
|
|
135
|
+
"""通用覆盖率提取"""
|
|
136
|
+
try:
|
|
137
|
+
with open(coverage_out_path, 'r') as f:
|
|
138
|
+
content = f.read()
|
|
139
|
+
|
|
140
|
+
# 查找百分比
|
|
141
|
+
match = re.search(r'(\d+\.?\d*)%', content)
|
|
142
|
+
if match:
|
|
143
|
+
return float(match.group(1))
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print(f"提取覆盖率失败: {e}")
|
|
146
|
+
|
|
147
|
+
return 0.0
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def extract_from_jacoco(jacoco_html_path: str) -> float:
|
|
151
|
+
"""从JaCoCo HTML报告提取覆盖率"""
|
|
152
|
+
if not os.path.exists(jacoco_html_path):
|
|
153
|
+
return 0.0
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
with open(jacoco_html_path, 'r') as f:
|
|
157
|
+
content = f.read()
|
|
158
|
+
|
|
159
|
+
# 查找Total行中的覆盖率
|
|
160
|
+
match = re.search(r'Total.*?(\d+)%<', content, re.DOTALL)
|
|
161
|
+
if match:
|
|
162
|
+
return float(match.group(1))
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(f"提取JaCoCo覆盖率失败: {e}")
|
|
165
|
+
|
|
166
|
+
return 0.0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class BaseRunner(ABC):
|
|
170
|
+
"""单测运行器基类"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, config: CoverageConfig, logger: Optional[Logger] = None):
|
|
173
|
+
self.config = config
|
|
174
|
+
self.logger = logger or Logger()
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
def run_tests(self, target_dir: str, output_file: str) -> RunResult:
|
|
178
|
+
"""运行测试"""
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
@abstractmethod
|
|
182
|
+
def get_coverage(self, output_file: str) -> float:
|
|
183
|
+
"""获取覆盖率"""
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
@abstractmethod
|
|
187
|
+
def compile(self, target_dir: str) -> Tuple[bool, List[str]]:
|
|
188
|
+
"""编译项目"""
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
def find_files(self, target_dir: str, pattern: str) -> List[str]:
|
|
192
|
+
"""查找文件"""
|
|
193
|
+
# 清理模式:移除开头的 **/ 或 **,确保是相对模式
|
|
194
|
+
clean_pattern = pattern
|
|
195
|
+
if clean_pattern.startswith("**/"):
|
|
196
|
+
clean_pattern = clean_pattern[3:]
|
|
197
|
+
elif clean_pattern.startswith("**"):
|
|
198
|
+
clean_pattern = clean_pattern[2:]
|
|
199
|
+
|
|
200
|
+
# 确保模式不为空且是相对路径
|
|
201
|
+
if not clean_pattern or clean_pattern.startswith("/"):
|
|
202
|
+
clean_pattern = "*" + clean_pattern if clean_pattern else "*"
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
return [str(p) for p in Path(target_dir).rglob(clean_pattern) if p.is_file()]
|
|
206
|
+
except Exception as e:
|
|
207
|
+
self.logger.error(f"查找文件失败: {e}")
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
def _run_command(self, cmd: str, cwd: str, timeout: int = 300) -> RunResult:
|
|
211
|
+
"""执行命令"""
|
|
212
|
+
start_time = time.time()
|
|
213
|
+
try:
|
|
214
|
+
result = subprocess.run(
|
|
215
|
+
cmd,
|
|
216
|
+
shell=True,
|
|
217
|
+
capture_output=True,
|
|
218
|
+
text=True,
|
|
219
|
+
cwd=cwd,
|
|
220
|
+
timeout=timeout
|
|
221
|
+
)
|
|
222
|
+
duration = time.time() - start_time
|
|
223
|
+
return RunResult(
|
|
224
|
+
success=result.returncode == 0,
|
|
225
|
+
return_code=result.returncode,
|
|
226
|
+
stdout=result.stdout,
|
|
227
|
+
stderr=result.stderr,
|
|
228
|
+
duration=duration
|
|
229
|
+
)
|
|
230
|
+
except subprocess.TimeoutExpired:
|
|
231
|
+
duration = time.time() - start_time
|
|
232
|
+
self.logger.error(f"命令执行超时: {cmd}")
|
|
233
|
+
return RunResult(
|
|
234
|
+
success=False,
|
|
235
|
+
return_code=-1,
|
|
236
|
+
stdout="",
|
|
237
|
+
stderr=f"命令执行超时({timeout}秒)",
|
|
238
|
+
duration=duration
|
|
239
|
+
)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
duration = time.time() - start_time
|
|
242
|
+
self.logger.error(f"命令执行异常: {e}")
|
|
243
|
+
return RunResult(
|
|
244
|
+
success=False,
|
|
245
|
+
return_code=-1,
|
|
246
|
+
stdout="",
|
|
247
|
+
stderr=str(e),
|
|
248
|
+
duration=duration
|
|
249
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
C单测运行器
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from coverage_tool.converters import JUnitReportConverter
|
|
10
|
+
from .base import BaseRunner, RunResult, CoverageExtractor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CRunner(BaseRunner):
|
|
14
|
+
"""C单测运行器"""
|
|
15
|
+
|
|
16
|
+
def run_tests(self, target_dir: str, output_file: str) -> RunResult:
|
|
17
|
+
"""运行C测试"""
|
|
18
|
+
# 尝试使用make运行测试
|
|
19
|
+
cmd = "make test"
|
|
20
|
+
result = self._run_command(cmd, target_dir)
|
|
21
|
+
|
|
22
|
+
# 尝试转换输出为JUnit XML
|
|
23
|
+
if result.stdout or result.stderr:
|
|
24
|
+
try:
|
|
25
|
+
xml_content = JUnitReportConverter.convert_generic_output(
|
|
26
|
+
result.stdout + result.stderr, 'cunit'
|
|
27
|
+
)
|
|
28
|
+
with open(output_file, 'w') as f:
|
|
29
|
+
f.write(xml_content)
|
|
30
|
+
result.junit_xml_path = output_file
|
|
31
|
+
except Exception as e:
|
|
32
|
+
self.logger.error(f"转换C测试输出失败: {e}")
|
|
33
|
+
|
|
34
|
+
result.coverage_rate = self.get_coverage(output_file)
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
def get_coverage(self, output_file: str) -> float:
|
|
39
|
+
"""获取C覆盖率(gcov)"""
|
|
40
|
+
# 查找gcov输出文件
|
|
41
|
+
temp_dir = os.path.dirname(output_file) if output_file else "."
|
|
42
|
+
|
|
43
|
+
coverage_files = [
|
|
44
|
+
os.path.join(temp_dir, "coverage.out"),
|
|
45
|
+
os.path.join(temp_dir, "coverage.txt"),
|
|
46
|
+
os.path.join(os.getcwd(), "coverage.out"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
for cov_file in coverage_files:
|
|
50
|
+
if os.path.exists(cov_file):
|
|
51
|
+
return CoverageExtractor.extract_from_coverage_out(cov_file, 'c')
|
|
52
|
+
|
|
53
|
+
return 0.0
|
|
54
|
+
|
|
55
|
+
def compile(self, target_dir: str) -> tuple[bool, list[str]]:
|
|
56
|
+
"""编译C项目"""
|
|
57
|
+
cmd = "make"
|
|
58
|
+
result = self._run_command(cmd, target_dir)
|
|
59
|
+
|
|
60
|
+
errors = []
|
|
61
|
+
if not result.success:
|
|
62
|
+
for line in result.stderr.split('\n'):
|
|
63
|
+
if 'error:' in line.lower() or 'undefined reference' in line.lower():
|
|
64
|
+
errors.append(line.strip())
|
|
65
|
+
|
|
66
|
+
return result.success, errors
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
C++单测运行器
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from coverage_tool.converters import JUnitReportConverter
|
|
10
|
+
from .base import BaseRunner, RunResult, CoverageExtractor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CPPRunner(BaseRunner):
|
|
14
|
+
"""C++单测运行器"""
|
|
15
|
+
|
|
16
|
+
def run_tests(self, target_dir: str, output_file: str) -> RunResult:
|
|
17
|
+
"""运行C++测试"""
|
|
18
|
+
# 尝试使用make运行测试
|
|
19
|
+
cmd = "make test"
|
|
20
|
+
result = self._run_command(cmd, target_dir)
|
|
21
|
+
|
|
22
|
+
# 尝试转换输出为JUnit XML
|
|
23
|
+
if result.stdout or result.stderr:
|
|
24
|
+
try:
|
|
25
|
+
xml_content = JUnitReportConverter.convert_generic_output(
|
|
26
|
+
result.stdout + result.stderr, 'gtest'
|
|
27
|
+
)
|
|
28
|
+
with open(output_file, 'w') as f:
|
|
29
|
+
f.write(xml_content)
|
|
30
|
+
result.junit_xml_path = output_file
|
|
31
|
+
except Exception as e:
|
|
32
|
+
self.logger.error(f"转换C++测试输出失败: {e}")
|
|
33
|
+
|
|
34
|
+
result.coverage_rate = self.get_coverage(output_file)
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
def get_coverage(self, output_file: str) -> float:
|
|
39
|
+
"""获取C++覆盖率(gcov/lcov)"""
|
|
40
|
+
temp_dir = os.path.dirname(output_file) if output_file else "."
|
|
41
|
+
|
|
42
|
+
coverage_files = [
|
|
43
|
+
os.path.join(temp_dir, "coverage.out"),
|
|
44
|
+
os.path.join(temp_dir, "coverage.txt"),
|
|
45
|
+
os.path.join(os.getcwd(), "coverage.out"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
for cov_file in coverage_files:
|
|
49
|
+
if os.path.exists(cov_file):
|
|
50
|
+
return CoverageExtractor.extract_from_coverage_out(cov_file, 'cpp')
|
|
51
|
+
|
|
52
|
+
return 0.0
|
|
53
|
+
|
|
54
|
+
def compile(self, target_dir: str) -> tuple[bool, list[str]]:
|
|
55
|
+
"""编译C++项目"""
|
|
56
|
+
cmd = "make"
|
|
57
|
+
result = self._run_command(cmd, target_dir)
|
|
58
|
+
|
|
59
|
+
errors = []
|
|
60
|
+
if not result.success:
|
|
61
|
+
for line in result.stderr.split('\n'):
|
|
62
|
+
if 'error:' in line.lower() or 'undefined reference' in line.lower():
|
|
63
|
+
errors.append(line.strip())
|
|
64
|
+
|
|
65
|
+
return result.success, errors
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
运行器工厂
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from coverage_tool.core import Language, LANGUAGE_CONFIGS
|
|
9
|
+
from coverage_tool.utils import Logger
|
|
10
|
+
from .base import BaseRunner
|
|
11
|
+
from .python_runner import PythonRunner
|
|
12
|
+
from .java_runner import JavaRunner
|
|
13
|
+
from .go_runner import GoRunner
|
|
14
|
+
from .c_runner import CRunner
|
|
15
|
+
from .cpp_runner import CPPRunner
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RunnerFactory:
|
|
19
|
+
"""运行器工厂"""
|
|
20
|
+
|
|
21
|
+
_runners = {
|
|
22
|
+
Language.PYTHON: PythonRunner,
|
|
23
|
+
Language.JAVA: JavaRunner,
|
|
24
|
+
Language.GO: GoRunner,
|
|
25
|
+
Language.C: CRunner,
|
|
26
|
+
Language.CPP: CPPRunner,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_runner(cls, language: Language, logger: Optional[Logger] = None) -> BaseRunner:
|
|
31
|
+
"""获取对应语言的运行器"""
|
|
32
|
+
config = LANGUAGE_CONFIGS[language]
|
|
33
|
+
runner_class = cls._runners.get(language)
|
|
34
|
+
if runner_class is None:
|
|
35
|
+
raise ValueError(f"不支持的语言: {language}")
|
|
36
|
+
return runner_class(config, logger)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def register_runner(cls, language: Language, runner_class: type):
|
|
40
|
+
"""注册新的运行器"""
|
|
41
|
+
cls._runners[language] = runner_class
|