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.
Files changed (47) hide show
  1. coverage_tool/__init__.py +7 -0
  2. coverage_tool/analyzers/__init__.py +14 -0
  3. coverage_tool/analyzers/dependency.py +513 -0
  4. coverage_tool/converters/__init__.py +60 -0
  5. coverage_tool/converters/base.py +83 -0
  6. coverage_tool/converters/cunit_converter.py +47 -0
  7. coverage_tool/converters/factory.py +36 -0
  8. coverage_tool/converters/generic_converter.py +62 -0
  9. coverage_tool/converters/go_converter.py +178 -0
  10. coverage_tool/converters/gtest_converter.py +63 -0
  11. coverage_tool/core/__init__.py +18 -0
  12. coverage_tool/core/config.py +66 -0
  13. coverage_tool/core/reporter.py +270 -0
  14. coverage_tool/example.py +102 -0
  15. coverage_tool/handlers/__init__.py +13 -0
  16. coverage_tool/handlers/compilation.py +322 -0
  17. coverage_tool/handlers/env_checker.py +312 -0
  18. coverage_tool/main.py +559 -0
  19. coverage_tool/parsers/__init__.py +15 -0
  20. coverage_tool/parsers/junit.py +172 -0
  21. coverage_tool/runners/__init__.py +26 -0
  22. coverage_tool/runners/base.py +249 -0
  23. coverage_tool/runners/c_runner.py +66 -0
  24. coverage_tool/runners/cpp_runner.py +65 -0
  25. coverage_tool/runners/factory.py +41 -0
  26. coverage_tool/runners/go_runner.py +158 -0
  27. coverage_tool/runners/java_runner.py +54 -0
  28. coverage_tool/runners/python_runner.py +99 -0
  29. coverage_tool/test_removers/__init__.py +38 -0
  30. coverage_tool/test_removers/base.py +233 -0
  31. coverage_tool/test_removers/c_remover.py +210 -0
  32. coverage_tool/test_removers/cpp_remover.py +272 -0
  33. coverage_tool/test_removers/factory.py +45 -0
  34. coverage_tool/test_removers/go_remover.py +112 -0
  35. coverage_tool/test_removers/java_remover.py +216 -0
  36. coverage_tool/test_removers/python_remover.py +172 -0
  37. coverage_tool/utils/__init__.py +23 -0
  38. coverage_tool/utils/dependency_graph.py +52 -0
  39. coverage_tool/utils/file_backup.py +112 -0
  40. coverage_tool/utils/helpers.py +89 -0
  41. coverage_tool/utils/logger.py +54 -0
  42. coverage_tool/utils/progress.py +41 -0
  43. coverage_tool-1.0.0.dist-info/METADATA +484 -0
  44. coverage_tool-1.0.0.dist-info/RECORD +47 -0
  45. coverage_tool-1.0.0.dist-info/WHEEL +5 -0
  46. coverage_tool-1.0.0.dist-info/entry_points.txt +2 -0
  47. coverage_tool-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CUnit 测试输出转换器
4
+ """
5
+
6
+ import re
7
+
8
+ from .base import BaseConverter, TestCaseResult
9
+
10
+
11
+ class CUnitConverter(BaseConverter):
12
+ """CUnit 测试输出转换器"""
13
+
14
+ @staticmethod
15
+ def convert(cunit_output: str) -> str:
16
+ """转换 CUnit 输出为 JUnit XML"""
17
+ test_cases = []
18
+
19
+ # CUnit 输出解析
20
+ pattern = re.compile(
21
+ r'(PASS|FAIL|SKIP):\s+(\w+)\s+\(([^)]+)\)',
22
+ re.MULTILINE
23
+ )
24
+
25
+ for match in pattern.finditer(cunit_output):
26
+ status, name, message = match.groups()
27
+
28
+ case = TestCaseResult(
29
+ name=name,
30
+ classname="cunit",
31
+ time=0.0,
32
+ passed=status == 'PASS',
33
+ skipped=status == 'SKIP'
34
+ )
35
+
36
+ if status == 'FAIL':
37
+ case.passed = False
38
+ case.failed_message = message
39
+ case.failed_type = 'failure'
40
+
41
+ test_cases.append(case)
42
+
43
+ return CUnitConverter._create_junit_xml(
44
+ "CUnit Tests",
45
+ test_cases,
46
+ 0.0
47
+ )
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 转换器工厂
4
+ """
5
+
6
+ from typing import Dict, Type
7
+
8
+ from .base import BaseConverter
9
+ from .go_converter import GoTestConverter
10
+ from .gtest_converter import GTestConverter
11
+ from .cunit_converter import CUnitConverter
12
+ from .generic_converter import GenericConverter
13
+
14
+
15
+ class ConverterFactory:
16
+ """转换器工厂"""
17
+
18
+ _converters: Dict[str, Type[BaseConverter]] = {
19
+ 'go': GoTestConverter,
20
+ 'gtest': GTestConverter,
21
+ 'google': GTestConverter,
22
+ 'cunit': CUnitConverter,
23
+ }
24
+
25
+ @classmethod
26
+ def get_converter(cls, test_framework: str):
27
+ """获取对应测试框架的转换器"""
28
+ converter_class = cls._converters.get(test_framework)
29
+ if converter_class is None:
30
+ return GenericConverter
31
+ return converter_class
32
+
33
+ @classmethod
34
+ def register_converter(cls, test_framework: str, converter_class: type):
35
+ """注册新的转换器"""
36
+ cls._converters[test_framework] = converter_class
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 通用测试输出转换器
4
+ """
5
+
6
+ import re
7
+ from typing import List
8
+
9
+ from .base import BaseConverter, TestCaseResult
10
+ from .go_converter import GoTestConverter
11
+ from .gtest_converter import GTestConverter
12
+ from .cunit_converter import CUnitConverter
13
+
14
+
15
+ class GenericConverter(BaseConverter):
16
+ """通用测试输出转换器"""
17
+
18
+ @staticmethod
19
+ def convert(output: str, test_framework: str) -> str:
20
+ """通用测试输出转换"""
21
+ test_cases: List[TestCaseResult] = []
22
+
23
+ if test_framework == 'go':
24
+ return GoTestConverter.convert(output)
25
+ elif test_framework in ['gtest', 'google']:
26
+ return GTestConverter.convert(output, '')
27
+ elif test_framework == 'cunit':
28
+ return CUnitConverter.convert(output)
29
+
30
+ # 通用解析:查找 PASS/FAIL 模式
31
+ patterns = {
32
+ 'PASS': r'PASS:\s+(\w+)',
33
+ 'FAIL': r'FAIL:\s+(\w+)',
34
+ 'OK': r'OK\s+-\s+(\w+)',
35
+ 'ERROR': r'ERROR\s+-\s+(\w+)',
36
+ }
37
+
38
+ passed_tests = []
39
+ failed_tests = []
40
+
41
+ for status, pattern in patterns.items():
42
+ matches = re.findall(pattern, output)
43
+ if 'PASS' in status:
44
+ passed_tests.extend(matches)
45
+ elif 'FAIL' in status or 'ERROR' in status:
46
+ failed_tests.extend(matches)
47
+
48
+ test_cases.extend([
49
+ TestCaseResult(name=name, classname="test", time=0.0, passed=True)
50
+ for name in passed_tests
51
+ ])
52
+ test_cases.extend([
53
+ TestCaseResult(name=name, classname="test", time=0.0, passed=False,
54
+ failed_message="Test failed", failed_type='failure')
55
+ for name in failed_tests
56
+ ])
57
+
58
+ return GenericConverter._create_junit_xml(
59
+ f"{test_framework.title()} Tests",
60
+ test_cases,
61
+ 0.0
62
+ )
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Go 测试输出转换器
4
+ """
5
+
6
+ import json
7
+ import subprocess
8
+ import os
9
+ from typing import Optional
10
+
11
+ from .base import BaseConverter, TestCaseResult
12
+
13
+
14
+ class GoTestConverter(BaseConverter):
15
+ """Go 测试输出转换器"""
16
+
17
+ @staticmethod
18
+ def convert(go_test_output: str, coverage_out: Optional[str] = None) -> str:
19
+ """将 Go test JSON 输出转换为 JUnit XML"""
20
+ test_cases = []
21
+ total_time = 0.0
22
+ current_test = {}
23
+
24
+ try:
25
+ # Go test JSON 输出是 NDJSON 格式(每行一个JSON对象)
26
+ for line in go_test_output.strip().split('\n'):
27
+ if not line.strip():
28
+ continue
29
+ data = json.loads(line)
30
+ event = data.get('Action', '') # Go test 使用 Action 字段
31
+
32
+ # 处理测试开始/运行事件
33
+ if event in ['start', 'run']:
34
+ test_name = data.get('Test', '')
35
+ if isinstance(test_name, dict):
36
+ test_name = test_name.get('Name', '')
37
+ if test_name:
38
+ current_test = {
39
+ 'name': test_name,
40
+ 'package': data.get('Package', 'main'),
41
+ 'elapsed': data.get('Elapsed', 0),
42
+ }
43
+
44
+ # 处理测试结果
45
+ elif event == 'pass':
46
+ if current_test.get('name'):
47
+ case = TestCaseResult(
48
+ name=current_test['name'],
49
+ classname=current_test.get('package', 'main'),
50
+ time=float(current_test.get('elapsed', 0)),
51
+ passed=True
52
+ )
53
+ test_cases.append(case)
54
+ total_time += case.time
55
+ current_test = {}
56
+
57
+ # 处理测试失败
58
+ elif event == 'fail':
59
+ if current_test.get('name'):
60
+ case = TestCaseResult(
61
+ name=current_test['name'],
62
+ classname=current_test.get('package', 'main'),
63
+ time=float(current_test.get('elapsed', 0)),
64
+ passed=False,
65
+ failed_message=data.get('Output', 'Test failed'),
66
+ failed_type='failure'
67
+ )
68
+ test_cases.append(case)
69
+ total_time += case.time
70
+ current_test = {}
71
+
72
+ # 处理测试跳过
73
+ elif event == 'skip':
74
+ if current_test.get('name'):
75
+ case = TestCaseResult(
76
+ name=current_test['name'],
77
+ classname=current_test.get('package', 'main'),
78
+ time=float(current_test.get('elapsed', 0)),
79
+ skipped=True,
80
+ skipped_message=data.get('Skip', '')
81
+ )
82
+ test_cases.append(case)
83
+ total_time += case.time
84
+ current_test = {}
85
+
86
+ except json.JSONDecodeError:
87
+ return GoTestConverter._create_basic_junit_xml(
88
+ "Go Test",
89
+ [TestCaseResult(name="Parse Error", classname="go_test", time=0, passed=False,
90
+ failed_message="Failed to parse test output")]
91
+ )
92
+
93
+ return GoTestConverter._create_junit_xml(
94
+ "Go Tests",
95
+ test_cases,
96
+ total_time
97
+ )
98
+
99
+
100
+ class GoJUnitReportTool:
101
+ """Go 测试 JUnit 报告工具"""
102
+
103
+ @staticmethod
104
+ def ensure_tool_installed() -> bool:
105
+ """确保 go-junit-report 工具已安装"""
106
+ # 检查是否已安装
107
+ result = subprocess.run(
108
+ ["which", "go-junit-report"],
109
+ capture_output=True,
110
+ text=True
111
+ )
112
+ if result.returncode == 0:
113
+ return True
114
+
115
+ # 尝试安装
116
+ print("正在安装 go-junit-report...")
117
+ install_result = subprocess.run(
118
+ ["go", "install", "github.com/jstemmer/go-junit-report@latest"],
119
+ capture_output=True,
120
+ text=True,
121
+ timeout=120
122
+ )
123
+
124
+ if install_result.returncode == 0:
125
+ # 添加到 PATH
126
+ go_path = subprocess.run(
127
+ ["go", "env", "GOPATH"],
128
+ capture_output=True,
129
+ text=True
130
+ ).stdout.strip()
131
+
132
+ gopath_bin = os.path.join(go_path, "bin")
133
+ if gopath_bin not in os.environ.get('PATH', ''):
134
+ os.environ['PATH'] = f"{gopath_bin}:{os.environ.get('PATH', '')}"
135
+
136
+ print("✓ go-junit-report 安装成功")
137
+ return True
138
+ else:
139
+ print(f"✗ go-junit-report 安装失败: {install_result.stderr}")
140
+ return False
141
+
142
+ @staticmethod
143
+ def generate_report(go_test_output: str, xml_path: str) -> bool:
144
+ """使用 go-junit-report 生成报告"""
145
+ # 先确保工具已安装
146
+ if not GoJUnitReportTool.ensure_tool_installed():
147
+ return False
148
+
149
+ try:
150
+ report_cmd = [
151
+ "go-junit-report",
152
+ "-xmlout", xml_path
153
+ ]
154
+
155
+ result = subprocess.run(
156
+ report_cmd,
157
+ input=go_test_output,
158
+ capture_output=True,
159
+ text=True,
160
+ timeout=60
161
+ )
162
+
163
+ if result.returncode == 0 and os.path.exists(xml_path):
164
+ return True
165
+
166
+ if result.returncode != 0:
167
+ print(f"go-junit-report 错误: {result.stderr}")
168
+ except FileNotFoundError:
169
+ print("go-junit-report 未找到,尝试使用内置转换...")
170
+ except Exception as e:
171
+ print(f"go-junit-report 执行错误: {e}")
172
+
173
+ return False
174
+
175
+ @staticmethod
176
+ def manual_convert(go_test_output: str, xml_path: str) -> str:
177
+ """手动将 Go test 输出转换为 JUnit XML(备用方案)"""
178
+ return GoTestConverter.convert(go_test_output)
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Google Test 输出转换器
4
+ """
5
+
6
+ import os
7
+ import re
8
+
9
+ from .base import BaseConverter, TestCaseResult
10
+
11
+
12
+ class GTestConverter(BaseConverter):
13
+ """Google Test 输出转换器"""
14
+
15
+ @staticmethod
16
+ def convert(gtest_output: str, xml_path: str) -> str:
17
+ """转换 Google Test 输出为 JUnit XML"""
18
+ # Google Test 支持直接输出 XML
19
+ # 如果已经有 XML 文件,直接返回
20
+ if os.path.exists(xml_path):
21
+ with open(xml_path, 'r') as f:
22
+ return f.read()
23
+
24
+ # 解析文本输出
25
+ test_cases = []
26
+ total_time = 0.0
27
+
28
+ # Google Test 输出格式解析
29
+ pattern = re.compile(
30
+ r'\[(?:PASSED|FAILED|SKIPPED)\]\s+(?:\[.*?\]\s+)?(\w+)\s+\(([^)]+)\)',
31
+ re.MULTILINE
32
+ )
33
+
34
+ for match in pattern.finditer(gtest_output):
35
+ status = match.group(0)
36
+ name = match.group(1)
37
+ details = match.group(2)
38
+
39
+ case = TestCaseResult(
40
+ name=name,
41
+ classname="gtest",
42
+ time=0.0,
43
+ passed="PASSED" in status,
44
+ skipped="SKIPPED" in status
45
+ )
46
+
47
+ if "PASSED" in status:
48
+ case.passed = True
49
+ elif "FAILED" in status:
50
+ case.passed = False
51
+ case.failed_message = details
52
+ case.failed_type = 'failure'
53
+ elif "SKIPPED" in status:
54
+ case.skipped = True
55
+ case.skipped_message = details
56
+
57
+ test_cases.append(case)
58
+
59
+ return GTestConverter._create_junit_xml(
60
+ "Google Tests",
61
+ test_cases,
62
+ total_time
63
+ )
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 核心模块包
4
+ 包含配置、主程序和报告生成等核心功能
5
+ """
6
+
7
+ from .config import Language, CoverageConfig, LANGUAGE_CONFIGS
8
+ from .reporter import CoverageReporter, CoverageReport, CoverageStats, ReportFormat
9
+
10
+ __all__ = [
11
+ 'Language',
12
+ 'CoverageConfig',
13
+ 'LANGUAGE_CONFIGS',
14
+ 'CoverageReporter',
15
+ 'CoverageReport',
16
+ 'CoverageStats',
17
+ 'ReportFormat',
18
+ ]
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 多语言单测统计工具配置
4
+ 支持 Python, Java, Go, C, C++
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import List, Dict, Optional
9
+ from enum import Enum
10
+
11
+ class Language(Enum):
12
+ PYTHON = "python"
13
+ JAVA = "java"
14
+ GO = "go"
15
+ C = "c"
16
+ CPP = "cpp"
17
+
18
+ @dataclass
19
+ class CoverageConfig:
20
+ """单测覆盖率配置"""
21
+ language: Language
22
+ test_command: str
23
+ coverage_command: str
24
+ test_file_pattern: str
25
+ source_file_pattern: str
26
+ output_format: str = "junit"
27
+ coverage_output: str = "coverage.xml"
28
+
29
+ # 各语言配置
30
+ LANGUAGE_CONFIGS: Dict[Language, CoverageConfig] = {
31
+ Language.PYTHON: CoverageConfig(
32
+ language=Language.PYTHON,
33
+ test_command="python -m pytest --junitxml={junit_output}",
34
+ coverage_command="python -m coverage run --source={source} -m pytest --junitxml={junit_output}",
35
+ test_file_pattern="**/*test*.py",
36
+ source_file_pattern="**/*.py"
37
+ ),
38
+ Language.JAVA: CoverageConfig(
39
+ language=Language.JAVA,
40
+ test_command="mvn test -Dreport=XML",
41
+ coverage_command="mvn test jacoco:report -Dreport=XML",
42
+ test_file_pattern="**/*Test.java",
43
+ source_file_pattern="**/*.java"
44
+ ),
45
+ Language.GO: CoverageConfig(
46
+ language=Language.GO,
47
+ test_command="go test -v -cover -coverprofile=coverage.out {packages}",
48
+ coverage_command="go test -v -cover -coverprofile=coverage.out {packages}",
49
+ test_file_pattern="**/*_test.go",
50
+ source_file_pattern="**/*.go"
51
+ ),
52
+ Language.C: CoverageConfig(
53
+ language=Language.C,
54
+ test_command="make test",
55
+ coverage_command="make coverage",
56
+ test_file_pattern="**/test_*.c,**/*_test.c,**/*test*.c,**/check_*.c,**/tests/*.c",
57
+ source_file_pattern="**/*.c"
58
+ ),
59
+ Language.CPP: CoverageConfig(
60
+ language=Language.CPP,
61
+ test_command="make test",
62
+ coverage_command="make coverage",
63
+ test_file_pattern="**/test_*.cpp,**/*_test.cpp,**/*test*.cpp,**/Test*.cpp,**/*.test.cpp,**/*.cc",
64
+ source_file_pattern="**/*.cpp,**/*.cc,**/*.hpp"
65
+ )
66
+ }