cli-test-framework 0.2.4__tar.gz → 0.3.0__tar.gz

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 (61) hide show
  1. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/CHANGELOG.md +60 -0
  2. {cli_test_framework-0.2.4/src/cli_test_framework.egg-info → cli_test_framework-0.3.0}/PKG-INFO +3 -3
  3. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/docs/user_manual.md +153 -6
  4. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/setup.py +3 -3
  5. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/__init__.py +9 -1
  6. cli_test_framework-0.3.0/src/cli_test_framework/core/base_runner.py +81 -0
  7. cli_test_framework-0.3.0/src/cli_test_framework/core/parallel_runner.py +125 -0
  8. cli_test_framework-0.3.0/src/cli_test_framework/core/setup.py +134 -0
  9. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/runners/json_runner.py +3 -0
  10. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/runners/parallel_json_runner.py +3 -0
  11. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/runners/yaml_runner.py +3 -0
  12. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0/src/cli_test_framework.egg-info}/PKG-INFO +3 -3
  13. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework.egg-info/SOURCES.txt +6 -1
  14. cli_test_framework-0.3.0/tests/__pycache__/test_setup_module.cpython-312-pytest-7.4.4.pyc +0 -0
  15. cli_test_framework-0.3.0/tests/fixtures/test_with_setup.json +42 -0
  16. cli_test_framework-0.3.0/tests/fixtures/test_with_setup.yaml +42 -0
  17. cli_test_framework-0.3.0/tests/test_setup_module.py +352 -0
  18. cli_test_framework-0.2.4/src/cli_test_framework/core/base_runner.py +0 -56
  19. cli_test_framework-0.2.4/src/cli_test_framework/core/parallel_runner.py +0 -118
  20. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/MANIFEST.in +0 -0
  21. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/README.md +0 -0
  22. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/pyproject.toml +0 -0
  23. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/setup.cfg +0 -0
  24. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/cli.py +0 -0
  25. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/commands/__init__.py +0 -0
  26. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/commands/compare.py +0 -0
  27. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/core/__init__.py +0 -0
  28. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/core/assertions.py +0 -0
  29. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/core/process_worker.py +0 -0
  30. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/core/test_case.py +0 -0
  31. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/__init__.py +0 -0
  32. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/base_comparator.py +0 -0
  33. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/binary_comparator.py +0 -0
  34. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/csv_comparator.py +0 -0
  35. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/factory.py +0 -0
  36. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/h5_comparator.py +0 -0
  37. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/json_comparator.py +0 -0
  38. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/result.py +0 -0
  39. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/text_comparator.py +0 -0
  40. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/file_comparator/xml_comparator.py +0 -0
  41. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/runners/__init__.py +0 -0
  42. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/utils/__init__.py +0 -0
  43. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/utils/path_resolver.py +0 -0
  44. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework/utils/report_generator.py +0 -0
  45. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework.egg-info/dependency_links.txt +0 -0
  46. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework.egg-info/entry_points.txt +0 -0
  47. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework.egg-info/requires.txt +0 -0
  48. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/src/cli_test_framework.egg-info/top_level.txt +0 -0
  49. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/__init__.py +0 -0
  50. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  51. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  52. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/fixtures/test_cases.json +0 -0
  53. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/fixtures/test_cases.yaml +0 -0
  54. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/fixtures/test_cases1.json +0 -0
  55. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/performance_test.py +0 -0
  56. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/test1.py +0 -0
  57. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/test_comprehensive_space.py +0 -0
  58. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/test_parallel_runner.py +0 -0
  59. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/test_parallel_space.py +0 -0
  60. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/test_report.txt +0 -0
  61. {cli_test_framework-0.2.4 → cli_test_framework-0.3.0}/tests/test_runners.py +0 -0
@@ -1,5 +1,65 @@
1
1
  # 变更日志
2
2
 
3
+ ## [v0.3.0] - 2025-06-07
4
+
5
+ ### 🎉 重大新功能 - Setup模块系统
6
+
7
+ #### 插件化Setup架构
8
+ - **BaseSetup抽象基类**:定义了setup插件的标准接口,支持setup()和teardown()方法
9
+ - **SetupManager管理器**:统一管理多个setup插件的生命周期
10
+ - **可扩展设计**:用户可以轻松创建自定义setup插件
11
+
12
+ #### 内置环境变量插件
13
+ - **EnvironmentSetup插件**:自动设置和管理环境变量
14
+ - **智能恢复机制**:测试完成后自动恢复原始环境状态,支持新增和覆盖的环境变量
15
+ - **配置文件集成**:直接在JSON/YAML测试配置中定义环境变量
16
+
17
+ #### 全面Runner集成
18
+ - **JSONRunner**:完全支持setup配置的加载和执行
19
+ - **YAMLRunner**:完全支持setup配置的加载和执行
20
+ - **ParallelJSONRunner**:完全支持setup配置,在并行模式下正确处理setup/teardown
21
+ - **自动生命周期**:setup和teardown在测试前后自动执行
22
+
23
+ #### 配置文件语法扩展
24
+ ```json
25
+ {
26
+ "setup": {
27
+ "environment_variables": {
28
+ "TEST_ENV": "development",
29
+ "API_URL": "http://localhost:8080"
30
+ }
31
+ },
32
+ "test_cases": [...]
33
+ }
34
+ ```
35
+
36
+ ### 🔧 改进和修复
37
+ - **Python版本要求**:从Python 3.6+更新为Python 3.9+(由于依赖版本要求)
38
+ - **错误处理增强**:改进了setup/teardown的异常处理,使用try-finally确保清理总是执行
39
+ - **并行安全**:Setup模块在并行执行模式下的线程安全处理
40
+ - **包导出修复**:移除了错误的FileComparator导出
41
+
42
+ ### 🧪 测试覆盖
43
+ - **新增16个单元测试**:覆盖setup模块所有核心功能
44
+ - **集成测试**:确保与所有runner的兼容性
45
+ - **边界条件测试**:测试空配置、错误情况等边界条件
46
+
47
+ ### 📚 文档更新
48
+ - **用户手册扩展**:新增完整的Setup模块章节,包含详细使用说明和最佳实践
49
+ - **示例代码**:提供了完整的使用示例和自定义插件示例
50
+ - **API文档**:完整的setup模块API文档和类型注解
51
+
52
+ ### ⚠️ 破坏性变更
53
+ - **Python版本要求**:从3.6+更新为3.9+
54
+ - **包结构调整**:setup模块相关类现在从cli_test_framework包导出
55
+
56
+ ### 🚀 迁移指南
57
+ - **现有代码兼容**:现有的测试配置文件无需修改即可正常工作
58
+ - **新功能使用**:在配置文件中添加setup部分即可使用环境变量设置
59
+ - **自定义插件**:继承BaseSetup类创建自定义setup插件
60
+
61
+ ---
62
+
3
63
  ## [v2.0.0] - 2025-05-27
4
64
 
5
65
  ### 🚀 重大新功能
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-test-framework
3
- Version: 0.2.4
4
- Summary: A small command line testing framework in Python with file comparison capabilities.
3
+ Version: 0.3.0
4
+ Summary: A powerful command line testing framework in Python with setup modules, parallel execution, and file comparison capabilities.
5
5
  Home-page: https://github.com/yourusername/cli-test-framework
6
6
  Author: Xiaotong Wang
7
7
  Author-email: xiaotongwang98@gmail.com
@@ -15,7 +15,7 @@ Classifier: Development Status :: 4 - Beta
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: Topic :: Software Development :: Testing
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
- Requires-Python: >=3.6
18
+ Requires-Python: >=3.9
19
19
  Description-Content-Type: text/markdown
20
20
  Requires-Dist: dukpy==0.5.0
21
21
  Requires-Dist: h5py>=3.8.0
@@ -5,12 +5,13 @@
5
5
  2. [Installation](#installation)
6
6
  3. [Basic Usage](#basic-usage)
7
7
  4. [Test Case Definition](#test-case-definition)
8
- 5. [Parallel Testing](#parallel-testing)
9
- 6. [File Comparison](#file-comparison)
10
- 7. [Advanced Features](#advanced-features)
11
- 8. [Troubleshooting](#troubleshooting)
12
- 9. [API Reference](#api-reference)
13
- 10. [Examples](#examples)
8
+ 5. [Setup Module](#setup-module)
9
+ 6. [Parallel Testing](#parallel-testing)
10
+ 7. [File Comparison](#file-comparison)
11
+ 8. [Advanced Features](#advanced-features)
12
+ 9. [Troubleshooting](#troubleshooting)
13
+ 10. [API Reference](#api-reference)
14
+ 11. [Examples](#examples)
14
15
 
15
16
  ## Introduction
16
17
 
@@ -121,6 +122,152 @@ test_cases:
121
122
  - ".*regex pattern.*"
122
123
  ```
123
124
 
125
+ ## Setup Module
126
+
127
+ The Setup Module provides a plugin-based system for executing pre-test setup tasks and post-test cleanup. It's designed to be extensible and supports multiple setup plugins running in sequence.
128
+
129
+ ### Key Features
130
+ - **Plugin Architecture**: Extensible design allowing custom setup plugins
131
+ - **Built-in Environment Plugin**: Set environment variables for tests
132
+ - **Full Runner Support**: Works with JSONRunner, YAMLRunner, and ParallelJSONRunner
133
+ - **Lifecycle Management**: Automatic setup and teardown with proper cleanup
134
+
135
+ ### Environment Variable Setup
136
+
137
+ #### JSON Configuration
138
+ ```json
139
+ {
140
+ "setup": {
141
+ "environment_variables": {
142
+ "TEST_ENV": "development",
143
+ "DEBUG_MODE": "true",
144
+ "API_URL": "http://localhost:8080",
145
+ "DATABASE_URL": "sqlite:///test.db"
146
+ }
147
+ },
148
+ "test_cases": [
149
+ {
150
+ "name": "Test with environment variables",
151
+ "command": "python",
152
+ "args": ["-c", "import os; print(f'Env: {os.environ.get(\"TEST_ENV\")}')"],
153
+ "expected": {
154
+ "return_code": 0,
155
+ "output_contains": ["Env: development"]
156
+ }
157
+ }
158
+ ]
159
+ }
160
+ ```
161
+
162
+ #### YAML Configuration
163
+ ```yaml
164
+ setup:
165
+ environment_variables:
166
+ TEST_ENV: "production"
167
+ DATABASE_URL: "postgresql://localhost:5432/test"
168
+ MAX_CONNECTIONS: "10"
169
+ TIMEOUT_SECONDS: "30"
170
+
171
+ test_cases:
172
+ - name: "Test database environment"
173
+ command: "python"
174
+ args:
175
+ - "-c"
176
+ - "import os; print(f'DB: {os.environ.get(\"DATABASE_URL\")}')"
177
+ expected:
178
+ return_code: 0
179
+ output_contains:
180
+ - "DB: postgresql://localhost:5432/test"
181
+ ```
182
+
183
+ ### Custom Setup Plugins
184
+
185
+ #### Creating Custom Plugins
186
+ ```python
187
+ from cli_test_framework import BaseSetup
188
+
189
+ class DatabaseSetup(BaseSetup):
190
+ def setup(self):
191
+ """Initialize test database"""
192
+ print("Setting up test database...")
193
+ # Your database initialization code here
194
+
195
+ def teardown(self):
196
+ """Clean up test database"""
197
+ print("Cleaning up test database...")
198
+ # Your database cleanup code here
199
+
200
+ class ServiceSetup(BaseSetup):
201
+ def setup(self):
202
+ """Start test services"""
203
+ self.service_port = self.config.get('port', 8080)
204
+ print(f"Starting test service on port {self.service_port}")
205
+ # Your service startup code here
206
+
207
+ def teardown(self):
208
+ """Stop test services"""
209
+ print("Stopping test services...")
210
+ # Your service shutdown code here
211
+ ```
212
+
213
+ #### Using Custom Plugins
214
+ ```python
215
+ from cli_test_framework import JSONRunner
216
+
217
+ # Create runner
218
+ runner = JSONRunner("test_cases.json")
219
+
220
+ # Add custom setup plugins
221
+ db_setup = DatabaseSetup({"connection": "test_db"})
222
+ service_setup = ServiceSetup({"port": 9090})
223
+
224
+ runner.setup_manager.add_setup(db_setup)
225
+ runner.setup_manager.add_setup(service_setup)
226
+
227
+ # Run tests (setup will be executed automatically)
228
+ success = runner.run_tests()
229
+ ```
230
+
231
+ ### Execution Flow
232
+
233
+ 1. **Load Configuration**: Read setup configuration from test file
234
+ 2. **Setup Phase**: Execute all setup plugins in order
235
+ - Environment variables are set
236
+ - Custom setups are executed
237
+ - Setup status is reported
238
+ 3. **Test Execution**: Run all test cases with setup environment
239
+ 4. **Teardown Phase**: Clean up all setups in reverse order
240
+ - Environment variables are restored
241
+ - Custom cleanups are executed
242
+ - Cleanup is guaranteed even if tests fail
243
+
244
+ ### Best Practices
245
+
246
+ 1. **Idempotent Operations**: Make setup operations safe to run multiple times
247
+ 2. **Proper Cleanup**: Always implement teardown to avoid side effects
248
+ 3. **Error Handling**: Setup failures stop test execution, teardown failures don't
249
+ 4. **Resource Management**: Use try-finally patterns in custom plugins
250
+ 5. **Configuration Validation**: Check required configuration parameters in setup
251
+
252
+ ### Integration Examples
253
+
254
+ #### With JSON Runner
255
+ ```bash
256
+ cli-test test_with_setup.json
257
+ ```
258
+
259
+ #### With YAML Runner
260
+ ```bash
261
+ cli-test test_with_setup.yaml --runner yaml
262
+ ```
263
+
264
+ #### With Parallel Runner
265
+ ```bash
266
+ cli-test test_with_setup.json --runner parallel --max-workers 4
267
+ ```
268
+
269
+ Note: In parallel mode, setup and teardown run in the main thread to ensure environment consistency.
270
+
124
271
  ## Parallel Testing
125
272
 
126
273
  ### Thread Mode
@@ -8,10 +8,10 @@ with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f:
8
8
 
9
9
  setup(
10
10
  name="cli-test-framework",
11
- version="0.2.4",
11
+ version="0.3.0",
12
12
  author="Xiaotong Wang",
13
13
  author_email="xiaotongwang98@gmail.com",
14
- description="A small command line testing framework in Python with file comparison capabilities.",
14
+ description="A powerful command line testing framework in Python with setup modules, parallel execution, and file comparison capabilities.",
15
15
  long_description=long_description,
16
16
  long_description_content_type="text/markdown",
17
17
  url="https://github.com/yourusername/cli-test-framework",
@@ -39,7 +39,7 @@ setup(
39
39
  "Topic :: Software Development :: Testing",
40
40
  "Topic :: Software Development :: Libraries :: Python Modules",
41
41
  ],
42
- python_requires='>=3.6',
42
+ python_requires='>=3.9',
43
43
  project_urls={
44
44
  'Documentation': 'https://github.com/yourusername/cli-test-framework/docs/user_manual.md',
45
45
  'Source': 'https://github.com/yourusername/cli-test-framework',
@@ -5,7 +5,7 @@ This package provides tools for testing command-line applications and scripts
5
5
  with support for parallel execution and advanced file comparison capabilities.
6
6
  """
7
7
 
8
- __version__ = "0.2.4"
8
+ __version__ = "0.3.0"
9
9
  __author__ = "Xiaotong Wang"
10
10
  __email__ = "xiaotongwang98@gmail.com"
11
11
 
@@ -13,9 +13,17 @@ __email__ = "xiaotongwang98@gmail.com"
13
13
  from .runners.json_runner import JSONRunner
14
14
  from .runners.parallel_json_runner import ParallelJSONRunner
15
15
  from .runners.yaml_runner import YAMLRunner
16
+ from .core.test_case import TestCase
17
+ from .core.assertions import Assertions
18
+ from .core.setup import BaseSetup, EnvironmentSetup, SetupManager
16
19
 
17
20
  __all__ = [
18
21
  'JSONRunner',
19
22
  'ParallelJSONRunner',
20
23
  'YAMLRunner',
24
+ 'TestCase',
25
+ 'Assertions',
26
+ 'BaseSetup',
27
+ 'EnvironmentSetup',
28
+ 'SetupManager'
21
29
  ]
@@ -0,0 +1,81 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+ from typing import List, Dict, Any, Optional
4
+ from .test_case import TestCase
5
+ from .assertions import Assertions
6
+ from .setup import SetupManager, EnvironmentSetup
7
+
8
+ class BaseRunner(ABC):
9
+ def __init__(self, config_file: str, workspace: Optional[str] = None):
10
+ if workspace:
11
+ self.workspace = Path(workspace)
12
+ else:
13
+ self.workspace = Path(__file__).parent.parent.parent
14
+ self.config_path = self.workspace / config_file
15
+ self.test_cases: List[TestCase] = []
16
+ self.results: Dict[str, Any] = {
17
+ "total": 0,
18
+ "passed": 0,
19
+ "failed": 0,
20
+ "details": []
21
+ }
22
+ self.assertions = Assertions()
23
+ self.setup_manager = SetupManager()
24
+
25
+ @abstractmethod
26
+ def load_test_cases(self) -> None:
27
+ """Load test cases from configuration file"""
28
+ pass
29
+
30
+ def load_setup_from_config(self, config: Dict[str, Any]) -> None:
31
+ """从配置文件加载setup配置"""
32
+ setup_config = config.get("setup", {})
33
+
34
+ # 处理环境变量设置
35
+ if "environment_variables" in setup_config:
36
+ env_setup = EnvironmentSetup({"environment_variables": setup_config["environment_variables"]})
37
+ self.setup_manager.add_setup(env_setup)
38
+
39
+ # 这里可以扩展支持其他类型的setup插件
40
+ # 例如:
41
+ # if "custom_setups" in setup_config:
42
+ # for custom_setup_config in setup_config["custom_setups"]:
43
+ # # 动态加载自定义setup插件
44
+ # pass
45
+
46
+ def run_tests(self) -> bool:
47
+ """Run all test cases and return whether all tests passed"""
48
+ try:
49
+ self.load_test_cases()
50
+ self.results["total"] = len(self.test_cases)
51
+
52
+ # 执行setup任务
53
+ self.setup_manager.setup_all()
54
+
55
+ print(f"\nStarting test execution... Total tests: {self.results['total']}")
56
+ print("=" * 50)
57
+
58
+ for i, case in enumerate(self.test_cases, 1):
59
+ print(f"\nRunning test {i}/{self.results['total']}: {case.name}")
60
+ result = self.run_single_test(case)
61
+ self.results["details"].append(result)
62
+ if result["status"] == "passed":
63
+ self.results["passed"] += 1
64
+ print(f"✓ Test passed: {case.name}")
65
+ else:
66
+ self.results["failed"] += 1
67
+ print(f"✗ Test failed: {case.name}")
68
+ if result["message"]:
69
+ print(f" Error: {result['message']}")
70
+
71
+ print("\n" + "=" * 50)
72
+ print(f"Test execution completed. Passed: {self.results['passed']}, Failed: {self.results['failed']}")
73
+ return self.results["failed"] == 0
74
+ finally:
75
+ # 确保teardown总是被执行
76
+ self.setup_manager.teardown_all()
77
+
78
+ @abstractmethod
79
+ def run_single_test(self, case: TestCase) -> Dict[str, str]:
80
+ """Run a single test case and return the result"""
81
+ pass
@@ -0,0 +1,125 @@
1
+ from abc import ABC
2
+ from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
3
+ from typing import List, Dict, Any, Optional, Union
4
+ import time
5
+ import threading
6
+ from .base_runner import BaseRunner
7
+ from .test_case import TestCase
8
+ from .process_worker import run_test_in_process
9
+
10
+ class ParallelRunner(BaseRunner):
11
+ """并行测试运行器基类,支持多线程和多进程执行"""
12
+
13
+ def __init__(self, config_file: str, workspace: Optional[str] = None,
14
+ max_workers: Optional[int] = None,
15
+ execution_mode: str = "thread"):
16
+ """
17
+ 初始化并行运行器
18
+
19
+ Args:
20
+ config_file: 配置文件路径
21
+ workspace: 工作目录
22
+ max_workers: 最大并发数,默认为CPU核心数
23
+ execution_mode: 执行模式,'thread'(线程) 或 'process'(进程)
24
+ """
25
+ super().__init__(config_file, workspace)
26
+ self.max_workers = max_workers
27
+ self.execution_mode = execution_mode
28
+ self.lock = threading.Lock() # 用于线程安全的结果更新
29
+
30
+ def run_tests(self) -> bool:
31
+ """并行运行所有测试用例"""
32
+ try:
33
+ self.load_test_cases()
34
+ self.results["total"] = len(self.test_cases)
35
+
36
+ # 执行setup任务
37
+ self.setup_manager.setup_all()
38
+
39
+ print(f"\nStarting parallel test execution... Total tests: {self.results['total']}")
40
+ print(f"Execution mode: {self.execution_mode}, Max workers: {self.max_workers or 'auto'}")
41
+ print("=" * 50)
42
+
43
+ start_time = time.time()
44
+
45
+ if self.execution_mode == "process":
46
+ executor_class = ProcessPoolExecutor
47
+ else:
48
+ executor_class = ThreadPoolExecutor
49
+
50
+ with executor_class(max_workers=self.max_workers) as executor:
51
+ # 提交所有测试任务
52
+ if self.execution_mode == "process":
53
+ # 进程模式:使用独立的工作器函数
54
+ future_to_case = {
55
+ executor.submit(
56
+ run_test_in_process,
57
+ i,
58
+ {
59
+ "name": case.name,
60
+ "command": case.command,
61
+ "args": case.args,
62
+ "expected": case.expected
63
+ },
64
+ str(self.workspace) if self.workspace else None
65
+ ): (i, case)
66
+ for i, case in enumerate(self.test_cases, 1)
67
+ }
68
+ else:
69
+ # 线程模式:使用实例方法
70
+ future_to_case = {
71
+ executor.submit(self._run_test_with_index, i, case): (i, case)
72
+ for i, case in enumerate(self.test_cases, 1)
73
+ }
74
+
75
+ # 收集结果
76
+ for future in as_completed(future_to_case):
77
+ test_index, case = future_to_case[future]
78
+ try:
79
+ result = future.result()
80
+ self._update_results(result, test_index, case)
81
+ except Exception as exc:
82
+ error_result = {
83
+ "name": case.name,
84
+ "status": "failed",
85
+ "message": f"Test execution failed: {str(exc)}",
86
+ "output": "",
87
+ "command": "",
88
+ "return_code": None
89
+ }
90
+ self._update_results(error_result, test_index, case)
91
+
92
+ end_time = time.time()
93
+ execution_time = end_time - start_time
94
+
95
+ print("\n" + "=" * 50)
96
+ print(f"Parallel test execution completed in {execution_time:.2f} seconds")
97
+ print(f"Passed: {self.results['passed']}, Failed: {self.results['failed']}")
98
+ return self.results["failed"] == 0
99
+ finally:
100
+ # 确保teardown总是被执行
101
+ self.setup_manager.teardown_all()
102
+
103
+ def _run_test_with_index(self, test_index: int, case: TestCase) -> Dict[str, Any]:
104
+ """运行单个测试并返回结果(包含索引信息)"""
105
+ print(f"[Worker] Running test {test_index}: {case.name}")
106
+ result = self.run_single_test(case)
107
+ return result
108
+
109
+ def _update_results(self, result: Dict[str, Any], test_index: int, case: TestCase) -> None:
110
+ """线程安全地更新测试结果"""
111
+ with self.lock:
112
+ self.results["details"].append(result)
113
+ if result["status"] == "passed":
114
+ self.results["passed"] += 1
115
+ print(f"✓ Test {test_index} passed: {case.name}")
116
+ else:
117
+ self.results["failed"] += 1
118
+ print(f"✗ Test {test_index} failed: {case.name}")
119
+ if result["message"]:
120
+ print(f" Error: {result['message']}")
121
+
122
+ def run_tests_sequential(self) -> bool:
123
+ """回退到顺序执行模式"""
124
+ print("Falling back to sequential execution...")
125
+ return super().run_tests()
@@ -0,0 +1,134 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Any, Optional
3
+ import os
4
+
5
+
6
+ class BaseSetup(ABC):
7
+ """测试前置任务的基类,允许用户以插件形式自定义"""
8
+
9
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
10
+ """
11
+ 初始化Setup
12
+
13
+ Args:
14
+ config: 配置参数字典
15
+ """
16
+ self.config = config or {}
17
+
18
+ @abstractmethod
19
+ def setup(self) -> None:
20
+ """
21
+ 执行前置任务设置
22
+ 子类必须实现此方法
23
+ """
24
+ pass
25
+
26
+ @abstractmethod
27
+ def teardown(self) -> None:
28
+ """
29
+ 执行后置清理任务
30
+ 子类必须实现此方法
31
+ """
32
+ pass
33
+
34
+ def get_name(self) -> str:
35
+ """返回Setup的名称"""
36
+ return self.__class__.__name__
37
+
38
+
39
+ class EnvironmentSetup(BaseSetup):
40
+ """内置的环境变量设置插件"""
41
+
42
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
43
+ super().__init__(config)
44
+ self._original_env = {} # 存储原始环境变量值
45
+ self._added_env = set() # 存储新添加的环境变量
46
+
47
+ def setup(self) -> None:
48
+ """设置环境变量"""
49
+ env_vars = self.config.get('environment_variables', {})
50
+
51
+ if not env_vars:
52
+ return
53
+
54
+ print(f"Setting up environment variables...")
55
+
56
+ for key, value in env_vars.items():
57
+ # 保存原始值
58
+ if key in os.environ:
59
+ self._original_env[key] = os.environ[key]
60
+ else:
61
+ self._added_env.add(key)
62
+
63
+ # 设置新值
64
+ os.environ[key] = str(value)
65
+ print(f" {key} = {value}")
66
+
67
+ def teardown(self) -> None:
68
+ """恢复环境变量"""
69
+ if not self._original_env and not self._added_env:
70
+ return
71
+
72
+ print(f"Restoring environment variables...")
73
+
74
+ # 恢复原始值
75
+ for key, value in self._original_env.items():
76
+ os.environ[key] = value
77
+ print(f" Restored {key}")
78
+
79
+ # 删除新添加的环境变量
80
+ for key in self._added_env:
81
+ if key in os.environ:
82
+ del os.environ[key]
83
+ print(f" Removed {key}")
84
+
85
+ # 清空记录
86
+ self._original_env.clear()
87
+ self._added_env.clear()
88
+
89
+
90
+ class SetupManager:
91
+ """Setup管理器,负责管理多个Setup插件"""
92
+
93
+ def __init__(self):
94
+ self.setups = []
95
+
96
+ def add_setup(self, setup: BaseSetup) -> None:
97
+ """添加Setup插件"""
98
+ self.setups.append(setup)
99
+
100
+ def setup_all(self) -> None:
101
+ """执行所有Setup的前置任务"""
102
+ if not self.setups:
103
+ return
104
+
105
+ print("\n" + "=" * 50)
106
+ print("Executing setup tasks...")
107
+ print("=" * 50)
108
+
109
+ for setup in self.setups:
110
+ try:
111
+ print(f"\nRunning setup: {setup.get_name()}")
112
+ setup.setup()
113
+ except Exception as e:
114
+ print(f"Error in setup {setup.get_name()}: {str(e)}")
115
+ raise
116
+
117
+ def teardown_all(self) -> None:
118
+ """执行所有Setup的后置清理任务(逆序执行)"""
119
+ if not self.setups:
120
+ return
121
+
122
+ print("\n" + "=" * 50)
123
+ print("Executing teardown tasks...")
124
+ print("=" * 50)
125
+
126
+ # 逆序执行teardown
127
+ for setup in reversed(self.setups):
128
+ try:
129
+ print(f"\nRunning teardown: {setup.get_name()}")
130
+ setup.teardown()
131
+ except Exception as e:
132
+ print(f"Error in teardown {setup.get_name()}: {str(e)}")
133
+ # teardown错误不应该阻止其他teardown的执行
134
+ continue
@@ -18,6 +18,9 @@ class JSONRunner(BaseRunner):
18
18
  with open(self.config_path, 'r', encoding='utf-8') as f:
19
19
  config = json.load(f)
20
20
 
21
+ # 加载setup配置
22
+ self.load_setup_from_config(config)
23
+
21
24
  required_fields = ["name", "command", "args", "expected"]
22
25
  for case in config["test_cases"]:
23
26
  if not all(field in case for field in required_fields):
@@ -31,6 +31,9 @@ class ParallelJSONRunner(ParallelRunner):
31
31
  with open(self.config_path, 'r', encoding='utf-8') as f:
32
32
  config = json.load(f)
33
33
 
34
+ # 加载setup配置
35
+ self.load_setup_from_config(config)
36
+
34
37
  required_fields = ["name", "command", "args", "expected"]
35
38
  for case in config["test_cases"]:
36
39
  if not all(field in case for field in required_fields):
@@ -17,6 +17,9 @@ class YAMLRunner(BaseRunner):
17
17
  with open(self.config_path, 'r', encoding='utf-8') as f:
18
18
  config = yaml.safe_load(f)
19
19
 
20
+ # 加载setup配置
21
+ self.load_setup_from_config(config)
22
+
20
23
  required_fields = ["name", "command", "args", "expected"]
21
24
  for case in config["test_cases"]:
22
25
  if not all(field in case for field in required_fields):