cli-test-framework 0.3.7__tar.gz → 0.4.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.
- {cli_test_framework-0.3.7/src/cli_test_framework.egg-info → cli_test_framework-0.4.0}/PKG-INFO +1 -1
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/setup.py +1 -1
- cli_test_framework-0.4.0/src/cli_test_framework/core/execution.py +67 -0
- cli_test_framework-0.4.0/src/cli_test_framework/core/process_worker.py +43 -0
- cli_test_framework-0.4.0/src/cli_test_framework/core/types.py +45 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/json_comparator.py +5 -4
- cli_test_framework-0.4.0/src/cli_test_framework/runners/json_runner.py +63 -0
- cli_test_framework-0.4.0/src/cli_test_framework/runners/parallel_json_runner.py +79 -0
- cli_test_framework-0.4.0/src/cli_test_framework/runners/yaml_runner.py +62 -0
- cli_test_framework-0.4.0/src/cli_test_framework/utils/__init__.py +19 -0
- cli_test_framework-0.4.0/src/cli_test_framework/utils/path_resolver.py +229 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0/src/cli_test_framework.egg-info}/PKG-INFO +1 -1
- cli_test_framework-0.4.0/src/cli_test_framework.egg-info/SOURCES.txt +92 -0
- cli_test_framework-0.4.0/tests/README.md +15 -0
- cli_test_framework-0.4.0/tests/__pycache__/__init__.cpython-39.pyc +0 -0
- cli_test_framework-0.4.0/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/__pycache__/conftest.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/__pycache__/run_all.cpython-312.pyc +0 -0
- cli_test_framework-0.4.0/tests/__pycache__/run_all.cpython-39.pyc +0 -0
- cli_test_framework-0.4.0/tests/conftest.py +15 -0
- cli_test_framework-0.3.7/tests/test_filter_demo.py → cli_test_framework-0.4.0/tests/demos/h5_filter_demo.py +37 -33
- cli_test_framework-0.4.0/tests/demos/manual_report_example.py +32 -0
- cli_test_framework-0.3.7/tests/performance_test.py → cli_test_framework-0.4.0/tests/demos/perf_parallel.py +45 -58
- cli_test_framework-0.4.0/tests/e2e/__init__.py +2 -0
- cli_test_framework-0.4.0/tests/e2e/__pycache__/__init__.cpython-312.pyc +0 -0
- cli_test_framework-0.4.0/tests/e2e/__pycache__/__init__.cpython-39.pyc +0 -0
- cli_test_framework-0.4.0/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/e2e/__pycache__/test_user_flows.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/e2e/test_user_flows.py +83 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_json_compare.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_json_compare.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_text_compare.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_text_compare.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/test_binary_compare.py +31 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/test_h5_compare.py +107 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/test_json_compare.py +48 -0
- cli_test_framework-0.4.0/tests/integration/file_compare/test_text_compare.py +40 -0
- cli_test_framework-0.4.0/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/parallel/test_parallel_runner.py +111 -0
- cli_test_framework-0.4.0/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/integration/path_handling/test_spaces_in_paths.py +85 -0
- cli_test_framework-0.4.0/tests/run_all.py +62 -0
- cli_test_framework-0.4.0/tests/unit/core/__pycache__/test_setup.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/unit/core/__pycache__/test_setup.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.3.7/tests/test_setup_module.py → cli_test_framework-0.4.0/tests/unit/core/test_setup.py +81 -157
- cli_test_framework-0.4.0/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-39-pytest-8.3.4.pyc +0 -0
- cli_test_framework-0.4.0/tests/unit/runners/test_json_yaml_runner.py +83 -0
- cli_test_framework-0.3.7/CHANGELOG.md +0 -211
- cli_test_framework-0.3.7/src/cli_test_framework/core/process_worker.py +0 -93
- cli_test_framework-0.3.7/src/cli_test_framework/runners/json_runner.py +0 -99
- cli_test_framework-0.3.7/src/cli_test_framework/runners/parallel_json_runner.py +0 -118
- cli_test_framework-0.3.7/src/cli_test_framework/runners/yaml_runner.py +0 -96
- cli_test_framework-0.3.7/src/cli_test_framework/utils/__init__.py +0 -11
- cli_test_framework-0.3.7/src/cli_test_framework/utils/path_resolver.py +0 -205
- cli_test_framework-0.3.7/src/cli_test_framework.egg-info/SOURCES.txt +0 -58
- cli_test_framework-0.3.7/tests/test1.py +0 -30
- cli_test_framework-0.3.7/tests/test_comprehensive_space.py +0 -118
- cli_test_framework-0.3.7/tests/test_parallel_runner.py +0 -170
- cli_test_framework-0.3.7/tests/test_parallel_space.py +0 -93
- cli_test_framework-0.3.7/tests/test_runners.py +0 -32
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/MANIFEST.in +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/README.md +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/docs/user_manual.md +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/pyproject.toml +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/setup.cfg +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/__init__.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/cli.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/commands/__init__.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/commands/compare.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/core/__init__.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/core/assertions.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/core/base_runner.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/core/parallel_runner.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/core/setup.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/core/test_case.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/__init__.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/base_comparator.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/binary_comparator.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/csv_comparator.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/factory.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/h5_comparator.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/result.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/text_comparator.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/xml_comparator.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/runners/__init__.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework/utils/report_generator.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework.egg-info/dependency_links.txt +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework.egg-info/entry_points.txt +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework.egg-info/requires.txt +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/src/cli_test_framework.egg-info/top_level.txt +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/__init__.py +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/__pycache__/test_setup_module.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/fixtures/test_cases.json +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/fixtures/test_cases.yaml +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/fixtures/test_cases1.json +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/fixtures/test_with_setup.json +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/fixtures/test_with_setup.yaml +0 -0
- {cli_test_framework-0.3.7 → cli_test_framework-0.4.0}/tests/test_report.txt +0 -0
{cli_test_framework-0.3.7/src/cli_test_framework.egg-info → cli_test_framework-0.4.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-test-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
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/ozil111/cli-test-framework
|
|
6
6
|
Author: Xiaotong Wang
|
|
@@ -8,7 +8,7 @@ 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.
|
|
11
|
+
version="0.4.0",
|
|
12
12
|
author="Xiaotong Wang",
|
|
13
13
|
author_email="xiaotongwang98@gmail.com",
|
|
14
14
|
description="A powerful command line testing framework in Python with setup modules, parallel execution, and file comparison capabilities.",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .assertions import Assertions
|
|
6
|
+
from .types import ExpectedResult, TestCaseData, TestResultData
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def validate_result(expected: ExpectedResult, actual: TestResultData) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Pure validation logic. Raises AssertionError on mismatch.
|
|
12
|
+
"""
|
|
13
|
+
assertions = Assertions()
|
|
14
|
+
|
|
15
|
+
if "return_code" in expected:
|
|
16
|
+
assertions.return_code_equals(actual["return_code"], expected["return_code"])
|
|
17
|
+
|
|
18
|
+
if "output_contains" in expected:
|
|
19
|
+
for text in expected["output_contains"]:
|
|
20
|
+
assertions.contains(actual["output"], text)
|
|
21
|
+
|
|
22
|
+
if "output_matches" in expected and expected["output_matches"]:
|
|
23
|
+
assertions.matches(actual["output"], expected["output_matches"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def execute_single_test_case(case: TestCaseData, workspace: Optional[str] = None) -> TestResultData:
|
|
27
|
+
"""
|
|
28
|
+
Stateless execution of a single test case.
|
|
29
|
+
"""
|
|
30
|
+
start_time = time.time()
|
|
31
|
+
full_command = f"{case['command']} {' '.join(case['args'])}".strip()
|
|
32
|
+
|
|
33
|
+
result: TestResultData = {
|
|
34
|
+
"name": case["name"],
|
|
35
|
+
"status": "failed",
|
|
36
|
+
"message": "",
|
|
37
|
+
"command": full_command,
|
|
38
|
+
"output": "",
|
|
39
|
+
"return_code": None,
|
|
40
|
+
"duration": 0.0,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
process = subprocess.run(
|
|
45
|
+
full_command,
|
|
46
|
+
cwd=workspace if workspace else None,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
check=False,
|
|
50
|
+
shell=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
output = process.stdout + process.stderr
|
|
54
|
+
result["output"] = output
|
|
55
|
+
result["return_code"] = process.returncode
|
|
56
|
+
|
|
57
|
+
validate_result(case["expected"], result)
|
|
58
|
+
result["status"] = "passed"
|
|
59
|
+
except AssertionError as exc:
|
|
60
|
+
result["message"] = str(exc)
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
result["message"] = f"Execution error: {str(exc)}"
|
|
63
|
+
finally:
|
|
64
|
+
result["duration"] = time.time() - start_time
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
进程工作器模块
|
|
3
|
+
用于多进程并行测试执行,避免序列化问题
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
from .execution import execute_single_test_case
|
|
8
|
+
from .types import TestCaseData
|
|
9
|
+
|
|
10
|
+
def run_test_in_process(test_index: int, case_data: Dict[str, Any], workspace: str = None) -> Dict[str, Any]:
|
|
11
|
+
"""
|
|
12
|
+
在独立进程中运行单个测试用例
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
test_index: 测试索引
|
|
16
|
+
case_data: 测试用例数据字典
|
|
17
|
+
workspace: 工作目录
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
测试结果字典
|
|
21
|
+
"""
|
|
22
|
+
case: TestCaseData = {
|
|
23
|
+
"name": case_data["name"],
|
|
24
|
+
"command": case_data["command"],
|
|
25
|
+
"args": case_data["args"],
|
|
26
|
+
"expected": case_data["expected"],
|
|
27
|
+
"description": case_data.get("description"),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
command_preview = f"{case['command']} {' '.join(case['args'])}".strip()
|
|
31
|
+
print(f" [Process Worker {test_index}] Executing command: {command_preview}")
|
|
32
|
+
|
|
33
|
+
result = execute_single_test_case(case, workspace)
|
|
34
|
+
|
|
35
|
+
if result["output"].strip():
|
|
36
|
+
print(f" [Process Worker {test_index}] Command output for {case['name']}:")
|
|
37
|
+
for line in result["output"].splitlines():
|
|
38
|
+
print(f" {line}")
|
|
39
|
+
|
|
40
|
+
if result["status"] != "passed" and result.get("message"):
|
|
41
|
+
print(f" [Process Worker {test_index}] Error for {case['name']}: {result['message']}")
|
|
42
|
+
|
|
43
|
+
return result
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExpectedResult(TypedDict, total=False):
|
|
5
|
+
"""Expectation configuration for a single test case."""
|
|
6
|
+
|
|
7
|
+
return_code: Optional[int]
|
|
8
|
+
output_contains: List[str]
|
|
9
|
+
output_matches: Optional[str]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCaseData(TypedDict):
|
|
13
|
+
"""Input data shape for a test case after解析/路径处理."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
command: str
|
|
17
|
+
args: List[str]
|
|
18
|
+
expected: ExpectedResult
|
|
19
|
+
description: Optional[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SetupConfig(TypedDict):
|
|
23
|
+
"""Setup configuration (currently environment variables only)."""
|
|
24
|
+
|
|
25
|
+
environment_variables: Dict[str, str]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestSuiteConfig(TypedDict):
|
|
29
|
+
"""Top-level configuration for a suite loaded from JSON/YAML."""
|
|
30
|
+
|
|
31
|
+
setup: Optional[SetupConfig]
|
|
32
|
+
test_cases: List[TestCaseData]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestResultData(TypedDict):
|
|
36
|
+
"""Normalized result produced by executing a single test case."""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
status: str # 'passed', 'failed'
|
|
40
|
+
message: str
|
|
41
|
+
command: str
|
|
42
|
+
output: str
|
|
43
|
+
return_code: Optional[int]
|
|
44
|
+
duration: float
|
|
45
|
+
|
|
@@ -53,8 +53,8 @@ class JsonComparator(TextComparator):
|
|
|
53
53
|
json_text = ''.join(text_content)
|
|
54
54
|
try:
|
|
55
55
|
json_data = json.loads(json_text)
|
|
56
|
-
if self.key_field:
|
|
57
|
-
# Only keep the specified key field(s)
|
|
56
|
+
if self.key_field and isinstance(json_data, dict):
|
|
57
|
+
# Only keep the specified key field(s) when top-level is a mapping
|
|
58
58
|
key_fields = self.key_field if isinstance(self.key_field, list) else [self.key_field]
|
|
59
59
|
filtered_data = {key: json_data[key] for key in key_fields if key in json_data}
|
|
60
60
|
return filtered_data
|
|
@@ -79,8 +79,9 @@ class JsonComparator(TextComparator):
|
|
|
79
79
|
self._compare_json_key_based(content1, content2, "", differences)
|
|
80
80
|
else:
|
|
81
81
|
self._compare_json_exact(content1, content2, "", differences)
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
|
|
83
|
+
identical = len(differences) == 0
|
|
84
|
+
return identical, differences
|
|
84
85
|
|
|
85
86
|
def _compare_json_exact(self, obj1, obj2, path, differences, max_diffs=10):
|
|
86
87
|
"""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from ..core.base_runner import BaseRunner
|
|
3
|
+
from ..core.test_case import TestCase
|
|
4
|
+
from ..core.execution import execute_single_test_case
|
|
5
|
+
from ..core.types import TestCaseData
|
|
6
|
+
from ..utils.path_resolver import PathResolver, parse_command_string, resolve_paths
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
class JSONRunner(BaseRunner):
|
|
11
|
+
def __init__(self, config_file="test_cases.json", workspace: Optional[str] = None):
|
|
12
|
+
super().__init__(config_file, workspace)
|
|
13
|
+
# Backward-compatible attribute for tests that patch path_resolver
|
|
14
|
+
self.path_resolver = PathResolver(self.workspace)
|
|
15
|
+
|
|
16
|
+
def load_test_cases(self) -> None:
|
|
17
|
+
"""Load test cases from a JSON file."""
|
|
18
|
+
try:
|
|
19
|
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
20
|
+
config = json.load(f)
|
|
21
|
+
|
|
22
|
+
# 加载setup配置
|
|
23
|
+
self.load_setup_from_config(config)
|
|
24
|
+
|
|
25
|
+
required_fields = ["name", "command", "args", "expected"]
|
|
26
|
+
for case in config["test_cases"]:
|
|
27
|
+
if not all(field in case for field in required_fields):
|
|
28
|
+
raise ValueError(f"Test case {case.get('name', 'unnamed')} is missing required fields")
|
|
29
|
+
|
|
30
|
+
# 使用智能命令解析,正确处理包含空格的路径
|
|
31
|
+
# Use resolver attribute (keeps backward compatibility with tests monkeypatching it)
|
|
32
|
+
case["command"] = self.path_resolver.parse_command_string(case["command"])
|
|
33
|
+
case["args"] = self.path_resolver.resolve_paths(case["args"])
|
|
34
|
+
self.test_cases.append(TestCase(**case))
|
|
35
|
+
|
|
36
|
+
print(f"Successfully loaded {len(self.test_cases)} test cases")
|
|
37
|
+
# print(self.test_cases)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
sys.exit(f"Failed to load configuration file: {str(e)}")
|
|
40
|
+
|
|
41
|
+
def run_single_test(self, case: TestCase) -> Dict[str, str]:
|
|
42
|
+
case_data: TestCaseData = {
|
|
43
|
+
"name": case.name,
|
|
44
|
+
"command": case.command,
|
|
45
|
+
"args": case.args,
|
|
46
|
+
"expected": case.expected,
|
|
47
|
+
"description": case.description or None,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
command_preview = f"{case_data['command']} {' '.join(case_data['args'])}".strip()
|
|
51
|
+
print(f" Executing command: {command_preview}")
|
|
52
|
+
|
|
53
|
+
result = execute_single_test_case(case_data, str(self.workspace) if self.workspace else None)
|
|
54
|
+
|
|
55
|
+
if result["output"].strip():
|
|
56
|
+
print(" Command output:")
|
|
57
|
+
for line in result["output"].splitlines():
|
|
58
|
+
print(f" {line}")
|
|
59
|
+
|
|
60
|
+
if result["status"] != "passed" and result.get("message"):
|
|
61
|
+
print(f" Error: {result['message']}")
|
|
62
|
+
|
|
63
|
+
return result
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from ..core.parallel_runner import ParallelRunner
|
|
3
|
+
from ..core.test_case import TestCase
|
|
4
|
+
from ..core.execution import execute_single_test_case
|
|
5
|
+
from ..core.types import TestCaseData
|
|
6
|
+
from ..utils.path_resolver import PathResolver, parse_command_string, resolve_paths
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
class ParallelJSONRunner(ParallelRunner):
|
|
12
|
+
"""并行JSON测试运行器"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config_file="test_cases.json", workspace: Optional[str] = None,
|
|
15
|
+
max_workers: Optional[int] = None, execution_mode: str = "thread"):
|
|
16
|
+
"""
|
|
17
|
+
初始化并行JSON运行器
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
config_file: JSON配置文件路径
|
|
21
|
+
workspace: 工作目录
|
|
22
|
+
max_workers: 最大并发数
|
|
23
|
+
execution_mode: 执行模式,'thread' 或 'process'
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(config_file, workspace, max_workers, execution_mode)
|
|
26
|
+
# Backward-compatible attribute for potential external patches/tests
|
|
27
|
+
self.path_resolver = PathResolver(self.workspace)
|
|
28
|
+
self._print_lock = threading.Lock() # 用于控制输出顺序
|
|
29
|
+
|
|
30
|
+
def load_test_cases(self) -> None:
|
|
31
|
+
"""从JSON文件加载测试用例"""
|
|
32
|
+
try:
|
|
33
|
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
34
|
+
config = json.load(f)
|
|
35
|
+
|
|
36
|
+
# 加载setup配置
|
|
37
|
+
self.load_setup_from_config(config)
|
|
38
|
+
|
|
39
|
+
required_fields = ["name", "command", "args", "expected"]
|
|
40
|
+
for case in config["test_cases"]:
|
|
41
|
+
if not all(field in case for field in required_fields):
|
|
42
|
+
raise ValueError(f"Test case {case.get('name', 'unnamed')} is missing required fields")
|
|
43
|
+
|
|
44
|
+
# Use resolver attribute (keeps backward compatibility with tests monkeypatching it)
|
|
45
|
+
case["command"] = self.path_resolver.parse_command_string(case["command"])
|
|
46
|
+
case["args"] = self.path_resolver.resolve_paths(case["args"])
|
|
47
|
+
self.test_cases.append(TestCase(**case))
|
|
48
|
+
|
|
49
|
+
print(f"Successfully loaded {len(self.test_cases)} test cases")
|
|
50
|
+
except Exception as e:
|
|
51
|
+
sys.exit(f"Failed to load configuration file: {str(e)}")
|
|
52
|
+
|
|
53
|
+
def run_single_test(self, case: TestCase) -> Dict[str, Any]:
|
|
54
|
+
"""运行单个测试用例(线程安全版本)"""
|
|
55
|
+
case_data: TestCaseData = {
|
|
56
|
+
"name": case.name,
|
|
57
|
+
"command": case.command,
|
|
58
|
+
"args": case.args,
|
|
59
|
+
"expected": case.expected,
|
|
60
|
+
"description": case.description or None,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
command_preview = f"{case_data['command']} {' '.join(case_data['args'])}".strip()
|
|
64
|
+
with self._print_lock:
|
|
65
|
+
print(f" [Worker] Executing command: {command_preview}")
|
|
66
|
+
|
|
67
|
+
result = execute_single_test_case(case_data, str(self.workspace) if self.workspace else None)
|
|
68
|
+
|
|
69
|
+
if result["output"].strip():
|
|
70
|
+
with self._print_lock:
|
|
71
|
+
print(f" [Worker] Command output for {case.name}:")
|
|
72
|
+
for line in result["output"].splitlines():
|
|
73
|
+
print(f" {line}")
|
|
74
|
+
|
|
75
|
+
if result["status"] != "passed" and result.get("message"):
|
|
76
|
+
with self._print_lock:
|
|
77
|
+
print(f" [Worker] Error for {case.name}: {result['message']}")
|
|
78
|
+
|
|
79
|
+
return result
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from ..core.base_runner import BaseRunner
|
|
3
|
+
from ..core.test_case import TestCase
|
|
4
|
+
from ..core.execution import execute_single_test_case
|
|
5
|
+
from ..core.types import TestCaseData
|
|
6
|
+
from ..utils.path_resolver import PathResolver, parse_command_string, resolve_paths
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
class YAMLRunner(BaseRunner):
|
|
10
|
+
def __init__(self, config_file="test_cases.yaml", workspace: Optional[str] = None):
|
|
11
|
+
super().__init__(config_file, workspace)
|
|
12
|
+
# Backward-compatible attribute for tests that patch path_resolver
|
|
13
|
+
self.path_resolver = PathResolver(self.workspace)
|
|
14
|
+
|
|
15
|
+
def load_test_cases(self):
|
|
16
|
+
"""Load test cases from a YAML file."""
|
|
17
|
+
try:
|
|
18
|
+
import yaml
|
|
19
|
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
20
|
+
config = yaml.safe_load(f)
|
|
21
|
+
|
|
22
|
+
# 加载setup配置
|
|
23
|
+
self.load_setup_from_config(config)
|
|
24
|
+
|
|
25
|
+
required_fields = ["name", "command", "args", "expected"]
|
|
26
|
+
for case in config["test_cases"]:
|
|
27
|
+
if not all(field in case for field in required_fields):
|
|
28
|
+
raise ValueError(f"Test case {case.get('name', 'unnamed')} is missing required fields")
|
|
29
|
+
|
|
30
|
+
# Use resolver attribute (keeps backward compatibility with tests monkeypatching it)
|
|
31
|
+
case["command"] = self.path_resolver.parse_command_string(case["command"])
|
|
32
|
+
case["args"] = self.path_resolver.resolve_paths(case["args"])
|
|
33
|
+
self.test_cases.append(TestCase(**case))
|
|
34
|
+
|
|
35
|
+
print(f"Successfully loaded {len(self.test_cases)} test cases")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
sys.exit(f"Failed to load configuration file: {str(e)}")
|
|
38
|
+
|
|
39
|
+
def run_single_test(self, case: TestCase) -> Dict[str, Any]:
|
|
40
|
+
"""Run a single test case and return the result"""
|
|
41
|
+
case_data: TestCaseData = {
|
|
42
|
+
"name": case.name,
|
|
43
|
+
"command": case.command,
|
|
44
|
+
"args": case.args,
|
|
45
|
+
"expected": case.expected,
|
|
46
|
+
"description": case.description or None,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
command_preview = f"{case_data['command']} {' '.join(case_data['args'])}".strip()
|
|
50
|
+
print(f" Executing command: {command_preview}")
|
|
51
|
+
|
|
52
|
+
result = execute_single_test_case(case_data, str(self.workspace) if self.workspace else None)
|
|
53
|
+
|
|
54
|
+
if result["output"].strip():
|
|
55
|
+
print(" Command output:")
|
|
56
|
+
for line in result["output"].splitlines():
|
|
57
|
+
print(f" {line}")
|
|
58
|
+
|
|
59
|
+
if result["status"] != "passed" and result.get("message"):
|
|
60
|
+
print(f" Error: {result['message']}")
|
|
61
|
+
|
|
62
|
+
return result
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# File: /python-test-framework/python-test-framework/src/utils/__init__.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Utility functions for the CLI Testing Framework
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .path_resolver import (
|
|
8
|
+
PathResolver,
|
|
9
|
+
parse_command_string,
|
|
10
|
+
resolve_paths,
|
|
11
|
+
resolve_command,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'PathResolver',
|
|
16
|
+
'parse_command_string',
|
|
17
|
+
'resolve_paths',
|
|
18
|
+
'resolve_command',
|
|
19
|
+
]
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
import shlex
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
WorkspaceLike = Union[str, Path]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _as_workspace_path(workspace: WorkspaceLike) -> Path:
|
|
11
|
+
return workspace if isinstance(workspace, Path) else Path(workspace)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def resolve_paths(args: List[str], workspace: WorkspaceLike) -> List[str]:
|
|
15
|
+
"""Resolve relative paths in args against the workspace."""
|
|
16
|
+
workspace_path = _as_workspace_path(workspace)
|
|
17
|
+
resolved_args = []
|
|
18
|
+
for arg in args:
|
|
19
|
+
if not arg.startswith("--"):
|
|
20
|
+
if not Path(arg).is_absolute():
|
|
21
|
+
resolved_args.append(str(workspace_path / arg))
|
|
22
|
+
else:
|
|
23
|
+
resolved_args.append(arg)
|
|
24
|
+
else:
|
|
25
|
+
resolved_args.append(arg)
|
|
26
|
+
return resolved_args
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_command(command: str, workspace: WorkspaceLike) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Resolve command path:
|
|
32
|
+
- absolute path: keep
|
|
33
|
+
- available in PATH: keep
|
|
34
|
+
- known system command: keep
|
|
35
|
+
- otherwise treat as relative to workspace
|
|
36
|
+
"""
|
|
37
|
+
workspace_path = _as_workspace_path(workspace)
|
|
38
|
+
|
|
39
|
+
if Path(command).is_absolute():
|
|
40
|
+
return command
|
|
41
|
+
|
|
42
|
+
if shutil.which(command) is not None:
|
|
43
|
+
return command
|
|
44
|
+
|
|
45
|
+
system_commands = {
|
|
46
|
+
'echo', 'ping', 'dir', 'ls', 'cat', 'grep', 'find', 'sort',
|
|
47
|
+
'head', 'tail', 'wc', 'curl', 'wget', 'git', 'python', 'node',
|
|
48
|
+
'npm', 'pip', 'java', 'javac', 'gcc', 'make', 'cmake', 'docker',
|
|
49
|
+
'kubectl', 'helm', 'terraform', 'ansible', 'ssh', 'scp', 'rsync'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if command in system_commands:
|
|
53
|
+
return command
|
|
54
|
+
|
|
55
|
+
return str(workspace_path / command)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_command_string(command_string: str, workspace: WorkspaceLike) -> str:
|
|
59
|
+
"""
|
|
60
|
+
Parse command string with smart handling of quoted paths and relative segments.
|
|
61
|
+
"""
|
|
62
|
+
workspace_path = _as_workspace_path(workspace)
|
|
63
|
+
|
|
64
|
+
if '"' in command_string or "'" in command_string:
|
|
65
|
+
try:
|
|
66
|
+
parts = shlex.split(command_string, posix=True)
|
|
67
|
+
|
|
68
|
+
if not parts:
|
|
69
|
+
return command_string
|
|
70
|
+
|
|
71
|
+
command_part = parts[0]
|
|
72
|
+
remaining_parts = parts[1:]
|
|
73
|
+
|
|
74
|
+
if Path(command_part).is_absolute():
|
|
75
|
+
resolved_command = command_part
|
|
76
|
+
else:
|
|
77
|
+
resolved_command = resolve_command(command_part, workspace_path)
|
|
78
|
+
|
|
79
|
+
# Preserve scripts passed via "-c" as a single argument to avoid losing quotes
|
|
80
|
+
resolved_parts = []
|
|
81
|
+
if "-c" in remaining_parts:
|
|
82
|
+
c_index = remaining_parts.index("-c")
|
|
83
|
+
before_c = remaining_parts[:c_index]
|
|
84
|
+
script_body = " ".join(remaining_parts[c_index + 1 :])
|
|
85
|
+
for part in before_c:
|
|
86
|
+
if part.startswith("-"):
|
|
87
|
+
resolved_parts.append(part)
|
|
88
|
+
elif ('.' in part or '/' in part or '\\' in part) and not part.isdigit():
|
|
89
|
+
if not Path(part).is_absolute():
|
|
90
|
+
resolved_parts.append(str(workspace_path / part))
|
|
91
|
+
else:
|
|
92
|
+
resolved_parts.append(part)
|
|
93
|
+
else:
|
|
94
|
+
resolved_parts.append(part)
|
|
95
|
+
resolved_parts.append("-c")
|
|
96
|
+
resolved_parts.append(script_body)
|
|
97
|
+
else:
|
|
98
|
+
for part in remaining_parts:
|
|
99
|
+
if part.startswith('-'):
|
|
100
|
+
resolved_parts.append(part)
|
|
101
|
+
elif ('.' in part or '/' in part or '\\' in part) and not part.isdigit():
|
|
102
|
+
if not Path(part).is_absolute():
|
|
103
|
+
resolved_parts.append(str(workspace_path / part))
|
|
104
|
+
else:
|
|
105
|
+
resolved_parts.append(part)
|
|
106
|
+
else:
|
|
107
|
+
resolved_parts.append(part)
|
|
108
|
+
|
|
109
|
+
return f"{resolved_command} {' '.join(resolved_parts)}"
|
|
110
|
+
|
|
111
|
+
except ValueError:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
if _starts_with_absolute_path(command_string):
|
|
115
|
+
return _parse_absolute_path_command(command_string, workspace_path)
|
|
116
|
+
else:
|
|
117
|
+
parts = command_string.split()
|
|
118
|
+
if not parts:
|
|
119
|
+
return command_string
|
|
120
|
+
|
|
121
|
+
if len(parts) == 1:
|
|
122
|
+
return resolve_command(parts[0], workspace_path)
|
|
123
|
+
else:
|
|
124
|
+
command_part = parts[0]
|
|
125
|
+
remaining_parts = parts[1:]
|
|
126
|
+
|
|
127
|
+
resolved_command = resolve_command(command_part, workspace_path)
|
|
128
|
+
|
|
129
|
+
if "-c" in remaining_parts:
|
|
130
|
+
c_index = remaining_parts.index("-c")
|
|
131
|
+
before_c = remaining_parts[:c_index]
|
|
132
|
+
script_body = " ".join(remaining_parts[c_index + 1 :])
|
|
133
|
+
resolved_parts = before_c + ["-c", script_body]
|
|
134
|
+
else:
|
|
135
|
+
resolved_parts = []
|
|
136
|
+
for part in remaining_parts:
|
|
137
|
+
if part.startswith('-'):
|
|
138
|
+
resolved_parts.append(part)
|
|
139
|
+
elif ('.' in part or '/' in part or '\\' in part) and not part.isdigit():
|
|
140
|
+
if not Path(part).is_absolute():
|
|
141
|
+
resolved_parts.append(str(workspace_path / part))
|
|
142
|
+
else:
|
|
143
|
+
resolved_parts.append(part)
|
|
144
|
+
else:
|
|
145
|
+
resolved_parts.append(part)
|
|
146
|
+
|
|
147
|
+
return f"{resolved_command} {' '.join(resolved_parts)}"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _starts_with_absolute_path(command_string: str) -> bool:
|
|
151
|
+
"""检查命令字符串是否以绝对路径开头"""
|
|
152
|
+
if os.name == 'nt':
|
|
153
|
+
return (len(command_string) >= 3 and
|
|
154
|
+
command_string[1:3] == ':\\') or command_string.startswith('\\\\')
|
|
155
|
+
else:
|
|
156
|
+
return command_string.startswith('/')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _parse_absolute_path_command(command_string: str, workspace: WorkspaceLike) -> str:
|
|
160
|
+
workspace_path = _as_workspace_path(workspace)
|
|
161
|
+
|
|
162
|
+
if os.name == 'nt':
|
|
163
|
+
exe_extensions = ['.exe', '.bat', '.cmd', '.com']
|
|
164
|
+
|
|
165
|
+
for ext in exe_extensions:
|
|
166
|
+
if ext in command_string:
|
|
167
|
+
ext_pos = command_string.find(ext)
|
|
168
|
+
if ext_pos != -1:
|
|
169
|
+
command_end = ext_pos + len(ext)
|
|
170
|
+
command_part = command_string[:command_end]
|
|
171
|
+
remaining = command_string[command_end:].strip()
|
|
172
|
+
|
|
173
|
+
if remaining:
|
|
174
|
+
remaining_parts = remaining.split()
|
|
175
|
+
resolved_parts = []
|
|
176
|
+
|
|
177
|
+
for part in remaining_parts:
|
|
178
|
+
if part.startswith('-'):
|
|
179
|
+
resolved_parts.append(part)
|
|
180
|
+
elif ('.' in part or '/' in part or '\\' in part) and not part.isdigit():
|
|
181
|
+
if not Path(part).is_absolute():
|
|
182
|
+
resolved_parts.append(str(workspace_path / part))
|
|
183
|
+
else:
|
|
184
|
+
resolved_parts.append(part)
|
|
185
|
+
else:
|
|
186
|
+
resolved_parts.append(part)
|
|
187
|
+
|
|
188
|
+
return f"{command_part} {' '.join(resolved_parts)}"
|
|
189
|
+
else:
|
|
190
|
+
return command_part
|
|
191
|
+
|
|
192
|
+
parts = command_string.split()
|
|
193
|
+
if not parts:
|
|
194
|
+
return command_string
|
|
195
|
+
|
|
196
|
+
command_part = parts[0]
|
|
197
|
+
remaining_parts = parts[1:]
|
|
198
|
+
|
|
199
|
+
resolved_parts = []
|
|
200
|
+
for part in remaining_parts:
|
|
201
|
+
if part.startswith('-'):
|
|
202
|
+
resolved_parts.append(part)
|
|
203
|
+
elif ('.' in part or '/' in part or '\\' in part) and not part.isdigit():
|
|
204
|
+
if not Path(part).is_absolute():
|
|
205
|
+
resolved_parts.append(str(workspace_path / part))
|
|
206
|
+
else:
|
|
207
|
+
resolved_parts.append(part)
|
|
208
|
+
else:
|
|
209
|
+
resolved_parts.append(part)
|
|
210
|
+
|
|
211
|
+
return f"{command_part} {' '.join(resolved_parts)}"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class PathResolver:
|
|
215
|
+
"""
|
|
216
|
+
Backwards-compatible wrapper; delegates to pure functions.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(self, workspace: Path):
|
|
220
|
+
self.workspace = workspace
|
|
221
|
+
|
|
222
|
+
def resolve_paths(self, args: List[str]) -> List[str]:
|
|
223
|
+
return resolve_paths(args, self.workspace)
|
|
224
|
+
|
|
225
|
+
def resolve_command(self, command: str) -> str:
|
|
226
|
+
return resolve_command(command, self.workspace)
|
|
227
|
+
|
|
228
|
+
def parse_command_string(self, command_string: str) -> str:
|
|
229
|
+
return parse_command_string(command_string, self.workspace)
|
{cli_test_framework-0.3.7 → cli_test_framework-0.4.0/src/cli_test_framework.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-test-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
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/ozil111/cli-test-framework
|
|
6
6
|
Author: Xiaotong Wang
|