cli-test-framework 0.4.4__tar.gz → 0.5.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.4.4/src/cli_test_framework.egg-info → cli_test_framework-0.5.0}/PKG-INFO +5 -3
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/setup.py +6 -4
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/core/base_runner.py +63 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/core/parallel_runner.py +10 -1
- cli_test_framework-0.5.0/src/cli_test_framework/core/process_worker.py +111 -0
- cli_test_framework-0.5.0/src/cli_test_framework/core/test_case.py +45 -0
- cli_test_framework-0.5.0/src/cli_test_framework/runners/json_runner.py +93 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/runners/parallel_json_runner.py +130 -36
- cli_test_framework-0.5.0/src/cli_test_framework/runners/yaml_runner.py +92 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0/src/cli_test_framework.egg-info}/PKG-INFO +5 -3
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework.egg-info/SOURCES.txt +10 -0
- cli_test_framework-0.5.0/src/cli_test_framework.egg-info/requires.txt +11 -0
- cli_test_framework-0.5.0/tests/__pycache__/conftest.cpython-312-pytest-9.0.3.pyc +0 -0
- cli_test_framework-0.5.0/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-9.0.3.pyc +0 -0
- cli_test_framework-0.5.0/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-312-pytest-9.0.3.pyc +0 -0
- cli_test_framework-0.5.0/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-312-pytest-9.0.3.pyc +0 -0
- cli_test_framework-0.5.0/tests/integration/file_compare/__pycache__/test_json_compare.cpython-312-pytest-9.0.3.pyc +0 -0
- cli_test_framework-0.5.0/tests/integration/file_compare/__pycache__/test_text_compare.cpython-312-pytest-9.0.3.pyc +0 -0
- cli_test_framework-0.5.0/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-312-pytest-9.0.3.pyc +0 -0
- cli_test_framework-0.5.0/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-312-pytest-9.0.3.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/run_all.py +4 -2
- cli_test_framework-0.5.0/tests/unit/core/__pycache__/test_setup.cpython-312-pytest-9.0.3.pyc +0 -0
- cli_test_framework-0.5.0/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-9.0.3.pyc +0 -0
- cli_test_framework-0.4.4/src/cli_test_framework/core/process_worker.py +0 -45
- cli_test_framework-0.4.4/src/cli_test_framework/core/test_case.py +0 -25
- cli_test_framework-0.4.4/src/cli_test_framework/runners/json_runner.py +0 -65
- cli_test_framework-0.4.4/src/cli_test_framework/runners/yaml_runner.py +0 -64
- cli_test_framework-0.4.4/src/cli_test_framework.egg-info/requires.txt +0 -5
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/MANIFEST.in +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/README.md +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/docs/user_manual.md +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/pyproject.toml +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/setup.cfg +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/__init__.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/cli.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/commands/__init__.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/commands/compare.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/core/__init__.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/core/assertions.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/core/execution.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/core/setup.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/core/types.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/__init__.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/base_comparator.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/binary_comparator.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/csv_comparator.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/factory.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/h5_comparator.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/json_comparator.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/result.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/text_comparator.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/xml_comparator.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/runners/__init__.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/utils/__init__.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/utils/path_resolver.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/utils/report_generator.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework.egg-info/dependency_links.txt +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework.egg-info/entry_points.txt +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework.egg-info/top_level.txt +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/README.md +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/__init__.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/__pycache__/__init__.cpython-39.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/__pycache__/conftest.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/__pycache__/run_all.cpython-312.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/__pycache__/run_all.cpython-39.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/__pycache__/test_setup_module.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/conftest.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/demos/h5_filter_demo.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/demos/manual_report_example.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/demos/perf_parallel.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/e2e/__init__.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/e2e/__pycache__/__init__.cpython-312.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/e2e/__pycache__/__init__.cpython-39.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/e2e/__pycache__/test_user_flows.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/e2e/test_user_flows.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/fixtures/test_cases.json +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/fixtures/test_cases.yaml +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/fixtures/test_cases1.json +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/fixtures/test_with_setup.json +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/fixtures/test_with_setup.yaml +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_json_compare.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_json_compare.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_text_compare.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_text_compare.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/test_binary_compare.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/test_h5_compare.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/test_json_compare.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/file_compare/test_text_compare.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/parallel/test_parallel_runner.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/integration/path_handling/test_spaces_in_paths.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/test_report.txt +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/unit/core/__pycache__/test_setup.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/unit/core/__pycache__/test_setup.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/unit/core/test_setup.py +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/tests/unit/runners/test_json_yaml_runner.py +0 -0
{cli_test_framework-0.4.4/src/cli_test_framework.egg-info → cli_test_framework-0.5.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.5.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
|
|
@@ -18,8 +18,10 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Requires-Python: >=3.9
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
Requires-Dist: dukpy==0.5.0
|
|
21
|
-
Requires-Dist: h5py
|
|
22
|
-
Requires-Dist:
|
|
21
|
+
Requires-Dist: h5py<4.0.0,>=3.8.0; python_version < "3.12"
|
|
22
|
+
Requires-Dist: h5py<4.0.0,>=3.10.0; python_version >= "3.12"
|
|
23
|
+
Requires-Dist: numpy<2.0.0,>=1.21.0; python_version < "3.12"
|
|
24
|
+
Requires-Dist: numpy<2.0.0,>=1.26.0; python_version >= "3.12"
|
|
23
25
|
Requires-Dist: setuptools>=75.8.0
|
|
24
26
|
Requires-Dist: wheel>=0.45.1
|
|
25
27
|
Dynamic: author
|
|
@@ -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.5.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.",
|
|
@@ -19,8 +19,10 @@ setup(
|
|
|
19
19
|
package_dir={"": "src"},
|
|
20
20
|
install_requires=[
|
|
21
21
|
"dukpy==0.5.0",
|
|
22
|
-
"h5py>=3.8.0",
|
|
23
|
-
"
|
|
22
|
+
"h5py>=3.8.0,<4.0.0; python_version<'3.12'",
|
|
23
|
+
"h5py>=3.10.0,<4.0.0; python_version>='3.12'",
|
|
24
|
+
"numpy>=1.21.0,<2.0.0; python_version<'3.12'",
|
|
25
|
+
"numpy>=1.26.0,<2.0.0; python_version>='3.12'",
|
|
24
26
|
"setuptools>=75.8.0",
|
|
25
27
|
"wheel>=0.45.1"
|
|
26
28
|
],
|
|
@@ -45,4 +47,4 @@ setup(
|
|
|
45
47
|
'Source': 'https://github.com/ozil111/cli-test-framework',
|
|
46
48
|
'Tracker': 'https://github.com/ozil111/cli-test-framework/issues',
|
|
47
49
|
},
|
|
48
|
-
)
|
|
50
|
+
)
|
{cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/core/base_runner.py
RENAMED
|
@@ -4,6 +4,7 @@ from typing import List, Dict, Any, Optional
|
|
|
4
4
|
from .test_case import TestCase
|
|
5
5
|
from .assertions import Assertions
|
|
6
6
|
from .setup import SetupManager, EnvironmentSetup
|
|
7
|
+
from .execution import execute_single_test_case
|
|
7
8
|
|
|
8
9
|
class BaseRunner(ABC):
|
|
9
10
|
def __init__(self, config_file: str, workspace: Optional[str] = None):
|
|
@@ -75,6 +76,68 @@ class BaseRunner(ABC):
|
|
|
75
76
|
# 确保teardown总是被执行
|
|
76
77
|
self.setup_manager.teardown_all()
|
|
77
78
|
|
|
79
|
+
def _run_sequence(self, case: TestCase) -> Dict[str, Any]:
|
|
80
|
+
"""Run a sequence test case with multiple steps (fail-fast)."""
|
|
81
|
+
combined_output = ""
|
|
82
|
+
total_duration = 0.0
|
|
83
|
+
all_passed = True
|
|
84
|
+
last_result = None
|
|
85
|
+
failed_step = None
|
|
86
|
+
|
|
87
|
+
for i, step in enumerate(case.steps):
|
|
88
|
+
step_name = f"{case.name} [step {i+1}/{len(case.steps)}]"
|
|
89
|
+
case_data = {
|
|
90
|
+
"name": step_name,
|
|
91
|
+
"command": step.command,
|
|
92
|
+
"args": step.args,
|
|
93
|
+
"expected": step.expected,
|
|
94
|
+
"description": None,
|
|
95
|
+
"timeout": step.timeout,
|
|
96
|
+
"resources": None,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
command_preview = f"{step.command} {' '.join(step.args)}".strip()
|
|
100
|
+
print(f" Executing step {i+1}/{len(case.steps)}: {command_preview}")
|
|
101
|
+
|
|
102
|
+
result = execute_single_test_case(
|
|
103
|
+
case_data, str(self.workspace) if self.workspace else None
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if result["output"].strip():
|
|
107
|
+
print(" Command output:")
|
|
108
|
+
for line in result["output"].splitlines():
|
|
109
|
+
print(f" {line}")
|
|
110
|
+
|
|
111
|
+
combined_output += result["output"]
|
|
112
|
+
total_duration += result["duration"]
|
|
113
|
+
last_result = result
|
|
114
|
+
|
|
115
|
+
if result["status"] != "passed":
|
|
116
|
+
all_passed = False
|
|
117
|
+
failed_step = i + 1
|
|
118
|
+
if result.get("message"):
|
|
119
|
+
print(f" Error at step {i+1}: {result['message']}")
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
status = "passed" if all_passed else last_result["status"]
|
|
123
|
+
message = ""
|
|
124
|
+
if not all_passed:
|
|
125
|
+
message = f"Failed at step {failed_step}/{len(case.steps)}: {last_result['message']}"
|
|
126
|
+
|
|
127
|
+
command_summary = " -> ".join(
|
|
128
|
+
f"{s.command} {' '.join(s.args)}".strip() for s in case.steps
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"name": case.name,
|
|
133
|
+
"status": status,
|
|
134
|
+
"message": message,
|
|
135
|
+
"command": command_summary,
|
|
136
|
+
"output": combined_output,
|
|
137
|
+
"return_code": last_result["return_code"] if last_result else None,
|
|
138
|
+
"duration": total_duration,
|
|
139
|
+
}
|
|
140
|
+
|
|
78
141
|
@abstractmethod
|
|
79
142
|
def run_single_test(self, case: TestCase) -> Dict[str, str]:
|
|
80
143
|
"""Run a single test case and return the result"""
|
{cli_test_framework-0.4.4 → cli_test_framework-0.5.0}/src/cli_test_framework/core/parallel_runner.py
RENAMED
|
@@ -61,7 +61,16 @@ class ParallelRunner(BaseRunner):
|
|
|
61
61
|
"args": case.args,
|
|
62
62
|
"expected": case.expected,
|
|
63
63
|
"timeout": case.timeout,
|
|
64
|
-
"resources": case.resources
|
|
64
|
+
"resources": case.resources,
|
|
65
|
+
"steps": [
|
|
66
|
+
{
|
|
67
|
+
"command": s.command,
|
|
68
|
+
"args": s.args,
|
|
69
|
+
"expected": s.expected,
|
|
70
|
+
"timeout": s.timeout,
|
|
71
|
+
}
|
|
72
|
+
for s in case.steps
|
|
73
|
+
] if case.steps else None,
|
|
65
74
|
},
|
|
66
75
|
str(self.workspace) if self.workspace else None
|
|
67
76
|
): (i, case)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
进程工作器模块
|
|
3
|
+
用于多进程并行测试执行,避免序列化问题
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, List
|
|
7
|
+
from .execution import execute_single_test_case
|
|
8
|
+
from .types import TestCaseData
|
|
9
|
+
|
|
10
|
+
def _run_sequence_in_process(test_index: int, case_data: Dict[str, Any], workspace: str = None) -> Dict[str, Any]:
|
|
11
|
+
"""Run a sequence test case with multiple steps (fail-fast) in a process worker."""
|
|
12
|
+
steps: List[Dict[str, Any]] = case_data["steps"]
|
|
13
|
+
combined_output = ""
|
|
14
|
+
total_duration = 0.0
|
|
15
|
+
all_passed = True
|
|
16
|
+
last_result = None
|
|
17
|
+
failed_step = None
|
|
18
|
+
|
|
19
|
+
for i, step in enumerate(steps):
|
|
20
|
+
step_name = f"{case_data['name']} [step {i+1}/{len(steps)}]"
|
|
21
|
+
step_case: TestCaseData = {
|
|
22
|
+
"name": step_name,
|
|
23
|
+
"command": step["command"],
|
|
24
|
+
"args": step["args"],
|
|
25
|
+
"expected": step["expected"],
|
|
26
|
+
"description": None,
|
|
27
|
+
"timeout": step.get("timeout"),
|
|
28
|
+
"resources": None,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
command_preview = f"{step['command']} {' '.join(step['args'])}".strip()
|
|
32
|
+
print(f" [Process Worker {test_index}] Executing step {i+1}/{len(steps)}: {command_preview}")
|
|
33
|
+
|
|
34
|
+
result = execute_single_test_case(step_case, workspace)
|
|
35
|
+
|
|
36
|
+
if result["output"].strip():
|
|
37
|
+
print(f" [Process Worker {test_index}] Command output for {step_name}:")
|
|
38
|
+
for line in result["output"].splitlines():
|
|
39
|
+
print(f" {line}")
|
|
40
|
+
|
|
41
|
+
combined_output += result["output"]
|
|
42
|
+
total_duration += result["duration"]
|
|
43
|
+
last_result = result
|
|
44
|
+
|
|
45
|
+
if result["status"] != "passed":
|
|
46
|
+
all_passed = False
|
|
47
|
+
failed_step = i + 1
|
|
48
|
+
if result.get("message"):
|
|
49
|
+
print(f" [Process Worker {test_index}] Error at step {i+1}: {result['message']}")
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
status = "passed" if all_passed else last_result["status"]
|
|
53
|
+
message = ""
|
|
54
|
+
if not all_passed:
|
|
55
|
+
message = f"Failed at step {failed_step}/{len(steps)}: {last_result['message']}"
|
|
56
|
+
|
|
57
|
+
command_summary = " -> ".join(
|
|
58
|
+
f"{s['command']} {' '.join(s['args'])}".strip() for s in steps
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"name": case_data["name"],
|
|
63
|
+
"status": status,
|
|
64
|
+
"message": message,
|
|
65
|
+
"command": command_summary,
|
|
66
|
+
"output": combined_output,
|
|
67
|
+
"return_code": last_result["return_code"] if last_result else None,
|
|
68
|
+
"duration": total_duration,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def run_test_in_process(test_index: int, case_data: Dict[str, Any], workspace: str = None) -> Dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
在独立进程中运行单个测试用例
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
test_index: 测试索引
|
|
77
|
+
case_data: 测试用例数据字典
|
|
78
|
+
workspace: 工作目录
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
测试结果字典
|
|
82
|
+
"""
|
|
83
|
+
# Sequence mode
|
|
84
|
+
if case_data.get("steps"):
|
|
85
|
+
return _run_sequence_in_process(test_index, case_data, workspace)
|
|
86
|
+
|
|
87
|
+
# Single command mode
|
|
88
|
+
case: TestCaseData = {
|
|
89
|
+
"name": case_data["name"],
|
|
90
|
+
"command": case_data["command"],
|
|
91
|
+
"args": case_data["args"],
|
|
92
|
+
"expected": case_data["expected"],
|
|
93
|
+
"description": case_data.get("description"),
|
|
94
|
+
"timeout": case_data.get("timeout"),
|
|
95
|
+
"resources": case_data.get("resources"),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
command_preview = f"{case['command']} {' '.join(case['args'])}".strip()
|
|
99
|
+
print(f" [Process Worker {test_index}] Executing command: {command_preview}")
|
|
100
|
+
|
|
101
|
+
result = execute_single_test_case(case, workspace)
|
|
102
|
+
|
|
103
|
+
if result["output"].strip():
|
|
104
|
+
print(f" [Process Worker {test_index}] Command output for {case['name']}:")
|
|
105
|
+
for line in result["output"].splitlines():
|
|
106
|
+
print(f" {line}")
|
|
107
|
+
|
|
108
|
+
if result["status"] != "passed" and result.get("message"):
|
|
109
|
+
print(f" [Process Worker {test_index}] Error for {case['name']}: {result['message']}")
|
|
110
|
+
|
|
111
|
+
return result
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Dict, Any, Optional
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class TestCaseStep:
|
|
6
|
+
"""A single step within a sequence test case."""
|
|
7
|
+
command: str
|
|
8
|
+
args: List[str]
|
|
9
|
+
expected: Dict[str, Any]
|
|
10
|
+
timeout: Optional[float] = None
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TestCase:
|
|
14
|
+
name: str
|
|
15
|
+
command: str = ""
|
|
16
|
+
args: List[str] = field(default_factory=list)
|
|
17
|
+
expected: Dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
description: str = ""
|
|
19
|
+
timeout: Optional[float] = None
|
|
20
|
+
resources: Optional[Dict[str, Any]] = None
|
|
21
|
+
steps: Optional[List[TestCaseStep]] = None
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
24
|
+
"""Convert test case to dictionary format"""
|
|
25
|
+
print("Convert test case to dictionary format")
|
|
26
|
+
print(self.command)
|
|
27
|
+
result = {
|
|
28
|
+
"name": self.name,
|
|
29
|
+
"command": self.command,
|
|
30
|
+
"args": self.args,
|
|
31
|
+
"expected": self.expected,
|
|
32
|
+
"timeout": self.timeout,
|
|
33
|
+
"resources": self.resources,
|
|
34
|
+
}
|
|
35
|
+
if self.steps is not None:
|
|
36
|
+
result["steps"] = [
|
|
37
|
+
{
|
|
38
|
+
"command": s.command,
|
|
39
|
+
"args": s.args,
|
|
40
|
+
"expected": s.expected,
|
|
41
|
+
"timeout": s.timeout,
|
|
42
|
+
}
|
|
43
|
+
for s in self.steps
|
|
44
|
+
]
|
|
45
|
+
return result
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from ..core.base_runner import BaseRunner
|
|
3
|
+
from ..core.test_case import TestCase, TestCaseStep
|
|
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
|
+
for case in config["test_cases"]:
|
|
26
|
+
if "steps" in case:
|
|
27
|
+
# Sequence mode: case has ordered steps
|
|
28
|
+
steps = []
|
|
29
|
+
for step in case["steps"]:
|
|
30
|
+
step_required = ["command", "args", "expected"]
|
|
31
|
+
if not all(field in step for field in step_required):
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"Step in test case '{case.get('name', 'unnamed')}' is missing required fields"
|
|
34
|
+
)
|
|
35
|
+
step["command"] = self.path_resolver.parse_command_string(step["command"])
|
|
36
|
+
step["args"] = self.path_resolver.resolve_paths(step["args"])
|
|
37
|
+
steps.append(TestCaseStep(**{
|
|
38
|
+
"command": step["command"],
|
|
39
|
+
"args": step["args"],
|
|
40
|
+
"expected": step["expected"],
|
|
41
|
+
"timeout": step.get("timeout"),
|
|
42
|
+
}))
|
|
43
|
+
self.test_cases.append(TestCase(
|
|
44
|
+
name=case["name"],
|
|
45
|
+
steps=steps,
|
|
46
|
+
description=case.get("description", ""),
|
|
47
|
+
resources=case.get("resources"),
|
|
48
|
+
))
|
|
49
|
+
else:
|
|
50
|
+
# Single command mode (backward compatible)
|
|
51
|
+
required_fields = ["name", "command", "args", "expected"]
|
|
52
|
+
if not all(field in case for field in required_fields):
|
|
53
|
+
raise ValueError(f"Test case {case.get('name', 'unnamed')} is missing required fields")
|
|
54
|
+
|
|
55
|
+
# 使用智能命令解析,正确处理包含空格的路径
|
|
56
|
+
# Use resolver attribute (keeps backward compatibility with tests monkeypatching it)
|
|
57
|
+
case["command"] = self.path_resolver.parse_command_string(case["command"])
|
|
58
|
+
case["args"] = self.path_resolver.resolve_paths(case["args"])
|
|
59
|
+
self.test_cases.append(TestCase(**case))
|
|
60
|
+
|
|
61
|
+
print(f"Successfully loaded {len(self.test_cases)} test cases")
|
|
62
|
+
# print(self.test_cases)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
sys.exit(f"Failed to load configuration file: {str(e)}")
|
|
65
|
+
|
|
66
|
+
def run_single_test(self, case: TestCase) -> Dict[str, str]:
|
|
67
|
+
if case.steps:
|
|
68
|
+
return self._run_sequence(case)
|
|
69
|
+
|
|
70
|
+
case_data: TestCaseData = {
|
|
71
|
+
"name": case.name,
|
|
72
|
+
"command": case.command,
|
|
73
|
+
"args": case.args,
|
|
74
|
+
"expected": case.expected,
|
|
75
|
+
"description": case.description or None,
|
|
76
|
+
"timeout": case.timeout,
|
|
77
|
+
"resources": case.resources,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
command_preview = f"{case_data['command']} {' '.join(case_data['args'])}".strip()
|
|
81
|
+
print(f" Executing command: {command_preview}")
|
|
82
|
+
|
|
83
|
+
result = execute_single_test_case(case_data, str(self.workspace) if self.workspace else None)
|
|
84
|
+
|
|
85
|
+
if result["output"].strip():
|
|
86
|
+
print(" Command output:")
|
|
87
|
+
for line in result["output"].splitlines():
|
|
88
|
+
print(f" {line}")
|
|
89
|
+
|
|
90
|
+
if result["status"] != "passed" and result.get("message"):
|
|
91
|
+
print(f" Error: {result['message']}")
|
|
92
|
+
|
|
93
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Optional, Dict, Any
|
|
2
2
|
from ..core.parallel_runner import ParallelRunner
|
|
3
|
-
from ..core.test_case import TestCase
|
|
3
|
+
from ..core.test_case import TestCase, TestCaseStep
|
|
4
4
|
from ..core.execution import execute_single_test_case
|
|
5
5
|
from ..core.types import TestCaseData
|
|
6
6
|
from ..utils.path_resolver import PathResolver, parse_command_string, resolve_paths
|
|
@@ -85,15 +85,40 @@ class ParallelJSONRunner(ParallelRunner):
|
|
|
85
85
|
# 加载setup配置
|
|
86
86
|
self.load_setup_from_config(config)
|
|
87
87
|
|
|
88
|
-
required_fields = ["name", "command", "args", "expected"]
|
|
89
88
|
for case in config["test_cases"]:
|
|
90
|
-
if
|
|
91
|
-
|
|
89
|
+
if "steps" in case:
|
|
90
|
+
# Sequence mode: case has ordered steps
|
|
91
|
+
steps = []
|
|
92
|
+
for step in case["steps"]:
|
|
93
|
+
step_required = ["command", "args", "expected"]
|
|
94
|
+
if not all(field in step for field in step_required):
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Step in test case '{case.get('name', 'unnamed')}' is missing required fields"
|
|
97
|
+
)
|
|
98
|
+
step["command"] = self.path_resolver.parse_command_string(step["command"])
|
|
99
|
+
step["args"] = self.path_resolver.resolve_paths(step["args"])
|
|
100
|
+
steps.append(TestCaseStep(**{
|
|
101
|
+
"command": step["command"],
|
|
102
|
+
"args": step["args"],
|
|
103
|
+
"expected": step["expected"],
|
|
104
|
+
"timeout": step.get("timeout"),
|
|
105
|
+
}))
|
|
106
|
+
self.test_cases.append(TestCase(
|
|
107
|
+
name=case["name"],
|
|
108
|
+
steps=steps,
|
|
109
|
+
description=case.get("description", ""),
|
|
110
|
+
resources=case.get("resources"),
|
|
111
|
+
))
|
|
112
|
+
else:
|
|
113
|
+
# Single command mode (backward compatible)
|
|
114
|
+
required_fields = ["name", "command", "args", "expected"]
|
|
115
|
+
if not all(field in case for field in required_fields):
|
|
116
|
+
raise ValueError(f"Test case {case.get('name', 'unnamed')} is missing required fields")
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
# Use resolver attribute (keeps backward compatibility with tests monkeypatching it)
|
|
119
|
+
case["command"] = self.path_resolver.parse_command_string(case["command"])
|
|
120
|
+
case["args"] = self.path_resolver.resolve_paths(case["args"])
|
|
121
|
+
self.test_cases.append(TestCase(**case))
|
|
97
122
|
|
|
98
123
|
print(f"Successfully loaded {len(self.test_cases)} test cases")
|
|
99
124
|
|
|
@@ -113,6 +138,71 @@ class ParallelJSONRunner(ParallelRunner):
|
|
|
113
138
|
except Exception as e:
|
|
114
139
|
sys.exit(f"Failed to load configuration file: {str(e)}")
|
|
115
140
|
|
|
141
|
+
def _run_sequence(self, case: TestCase) -> Dict[str, Any]:
|
|
142
|
+
"""Run a sequence test case with multiple steps (fail-fast, thread-safe printing)."""
|
|
143
|
+
combined_output = ""
|
|
144
|
+
total_duration = 0.0
|
|
145
|
+
all_passed = True
|
|
146
|
+
last_result = None
|
|
147
|
+
failed_step = None
|
|
148
|
+
|
|
149
|
+
for i, step in enumerate(case.steps):
|
|
150
|
+
step_name = f"{case.name} [step {i+1}/{len(case.steps)}]"
|
|
151
|
+
case_data = {
|
|
152
|
+
"name": step_name,
|
|
153
|
+
"command": step.command,
|
|
154
|
+
"args": step.args,
|
|
155
|
+
"expected": step.expected,
|
|
156
|
+
"description": None,
|
|
157
|
+
"timeout": step.timeout,
|
|
158
|
+
"resources": None,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
command_preview = f"{step.command} {' '.join(step.args)}".strip()
|
|
162
|
+
with self._print_lock:
|
|
163
|
+
print(f" [Worker] Executing step {i+1}/{len(case.steps)}: {command_preview}")
|
|
164
|
+
|
|
165
|
+
result = execute_single_test_case(
|
|
166
|
+
case_data, str(self.workspace) if self.workspace else None
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if result["output"].strip():
|
|
170
|
+
with self._print_lock:
|
|
171
|
+
print(f" [Worker] Command output for {step_name}:")
|
|
172
|
+
for line in result["output"].splitlines():
|
|
173
|
+
print(f" {line}")
|
|
174
|
+
|
|
175
|
+
combined_output += result["output"]
|
|
176
|
+
total_duration += result["duration"]
|
|
177
|
+
last_result = result
|
|
178
|
+
|
|
179
|
+
if result["status"] != "passed":
|
|
180
|
+
all_passed = False
|
|
181
|
+
failed_step = i + 1
|
|
182
|
+
if result.get("message"):
|
|
183
|
+
with self._print_lock:
|
|
184
|
+
print(f" [Worker] Error at step {i+1}: {result['message']}")
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
status = "passed" if all_passed else last_result["status"]
|
|
188
|
+
message = ""
|
|
189
|
+
if not all_passed:
|
|
190
|
+
message = f"Failed at step {failed_step}/{len(case.steps)}: {last_result['message']}"
|
|
191
|
+
|
|
192
|
+
command_summary = " -> ".join(
|
|
193
|
+
f"{s.command} {' '.join(s.args)}".strip() for s in case.steps
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
"name": case.name,
|
|
198
|
+
"status": status,
|
|
199
|
+
"message": message,
|
|
200
|
+
"command": command_summary,
|
|
201
|
+
"output": combined_output,
|
|
202
|
+
"return_code": last_result["return_code"] if last_result else None,
|
|
203
|
+
"duration": total_duration,
|
|
204
|
+
}
|
|
205
|
+
|
|
116
206
|
def run_single_test(self, case: TestCase) -> Dict[str, Any]:
|
|
117
207
|
"""
|
|
118
208
|
运行单个测试用例(线程安全版本,支持资源感知调度)
|
|
@@ -160,38 +250,42 @@ class ParallelJSONRunner(ParallelRunner):
|
|
|
160
250
|
with self._print_lock:
|
|
161
251
|
print(f" [Scheduler] Warning: Failed to acquire resources for '{case.name}': {e}")
|
|
162
252
|
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
253
|
+
# 4. 执行测试(支持 sequence 和单命令两种模式)
|
|
254
|
+
if case.steps:
|
|
255
|
+
result = self._run_sequence(case)
|
|
256
|
+
else:
|
|
257
|
+
# 准备测试用例数据
|
|
258
|
+
case_data: TestCaseData = {
|
|
259
|
+
"name": case.name,
|
|
260
|
+
"command": case.command,
|
|
261
|
+
"args": case.args,
|
|
262
|
+
"expected": case.expected,
|
|
263
|
+
"description": case.description or None,
|
|
264
|
+
"timeout": case.timeout,
|
|
265
|
+
"resources": case.resources,
|
|
266
|
+
}
|
|
173
267
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
268
|
+
command_preview = f"{case_data['command']} {' '.join(case_data['args'])}".strip()
|
|
269
|
+
with self._print_lock:
|
|
270
|
+
if self.execution_mode != "thread" or self.cpu_semaphore is None:
|
|
271
|
+
print(f" [Worker] Executing command: {command_preview}")
|
|
178
272
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
273
|
+
# 执行测试 (传入 env)
|
|
274
|
+
result = execute_single_test_case(
|
|
275
|
+
case_data,
|
|
276
|
+
str(self.workspace) if self.workspace else None,
|
|
277
|
+
env=task_env # 注入环境变量
|
|
278
|
+
)
|
|
185
279
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
280
|
+
if result["output"].strip():
|
|
281
|
+
with self._print_lock:
|
|
282
|
+
print(f" [Worker] Command output for {case.name}:")
|
|
283
|
+
for line in result["output"].splitlines():
|
|
284
|
+
print(f" {line}")
|
|
191
285
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
286
|
+
if result["status"] != "passed" and result.get("message"):
|
|
287
|
+
with self._print_lock:
|
|
288
|
+
print(f" [Worker] Error for {case.name}: {result['message']}")
|
|
195
289
|
|
|
196
290
|
# 5. 归还资源
|
|
197
291
|
if self.execution_mode == "thread" and self.cpu_semaphore is not None and tokens_acquired > 0:
|