cli-test-framework 0.4.5__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.
Files changed (109) hide show
  1. {cli_test_framework-0.4.5/src/cli_test_framework.egg-info → cli_test_framework-0.5.0}/PKG-INFO +5 -3
  2. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/setup.py +6 -4
  3. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/core/base_runner.py +63 -0
  4. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/core/parallel_runner.py +10 -1
  5. cli_test_framework-0.5.0/src/cli_test_framework/core/process_worker.py +111 -0
  6. cli_test_framework-0.5.0/src/cli_test_framework/core/test_case.py +45 -0
  7. cli_test_framework-0.5.0/src/cli_test_framework/runners/json_runner.py +93 -0
  8. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/runners/parallel_json_runner.py +130 -36
  9. cli_test_framework-0.5.0/src/cli_test_framework/runners/yaml_runner.py +92 -0
  10. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0/src/cli_test_framework.egg-info}/PKG-INFO +5 -3
  11. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework.egg-info/SOURCES.txt +10 -0
  12. cli_test_framework-0.5.0/src/cli_test_framework.egg-info/requires.txt +11 -0
  13. cli_test_framework-0.5.0/tests/__pycache__/conftest.cpython-312-pytest-9.0.3.pyc +0 -0
  14. cli_test_framework-0.5.0/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-9.0.3.pyc +0 -0
  15. cli_test_framework-0.5.0/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-312-pytest-9.0.3.pyc +0 -0
  16. cli_test_framework-0.5.0/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-312-pytest-9.0.3.pyc +0 -0
  17. cli_test_framework-0.5.0/tests/integration/file_compare/__pycache__/test_json_compare.cpython-312-pytest-9.0.3.pyc +0 -0
  18. cli_test_framework-0.5.0/tests/integration/file_compare/__pycache__/test_text_compare.cpython-312-pytest-9.0.3.pyc +0 -0
  19. cli_test_framework-0.5.0/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-312-pytest-9.0.3.pyc +0 -0
  20. cli_test_framework-0.5.0/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-312-pytest-9.0.3.pyc +0 -0
  21. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/run_all.py +4 -2
  22. cli_test_framework-0.5.0/tests/unit/core/__pycache__/test_setup.cpython-312-pytest-9.0.3.pyc +0 -0
  23. cli_test_framework-0.5.0/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-9.0.3.pyc +0 -0
  24. cli_test_framework-0.4.5/src/cli_test_framework/core/process_worker.py +0 -45
  25. cli_test_framework-0.4.5/src/cli_test_framework/core/test_case.py +0 -25
  26. cli_test_framework-0.4.5/src/cli_test_framework/runners/json_runner.py +0 -65
  27. cli_test_framework-0.4.5/src/cli_test_framework/runners/yaml_runner.py +0 -64
  28. cli_test_framework-0.4.5/src/cli_test_framework.egg-info/requires.txt +0 -5
  29. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/MANIFEST.in +0 -0
  30. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/README.md +0 -0
  31. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/docs/user_manual.md +0 -0
  32. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/pyproject.toml +0 -0
  33. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/setup.cfg +0 -0
  34. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/__init__.py +0 -0
  35. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/cli.py +0 -0
  36. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/commands/__init__.py +0 -0
  37. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/commands/compare.py +0 -0
  38. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/core/__init__.py +0 -0
  39. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/core/assertions.py +0 -0
  40. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/core/execution.py +0 -0
  41. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/core/setup.py +0 -0
  42. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/core/types.py +0 -0
  43. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/__init__.py +0 -0
  44. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/base_comparator.py +0 -0
  45. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/binary_comparator.py +0 -0
  46. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/csv_comparator.py +0 -0
  47. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/factory.py +0 -0
  48. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/h5_comparator.py +0 -0
  49. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/json_comparator.py +0 -0
  50. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/result.py +0 -0
  51. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/text_comparator.py +0 -0
  52. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/file_comparator/xml_comparator.py +0 -0
  53. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/runners/__init__.py +0 -0
  54. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/utils/__init__.py +0 -0
  55. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/utils/path_resolver.py +0 -0
  56. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework/utils/report_generator.py +0 -0
  57. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework.egg-info/dependency_links.txt +0 -0
  58. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework.egg-info/entry_points.txt +0 -0
  59. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/src/cli_test_framework.egg-info/top_level.txt +0 -0
  60. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/README.md +0 -0
  61. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/__init__.py +0 -0
  62. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  63. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/__pycache__/__init__.cpython-39.pyc +0 -0
  64. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc +0 -0
  65. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/__pycache__/conftest.cpython-39-pytest-8.3.4.pyc +0 -0
  66. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/__pycache__/run_all.cpython-312.pyc +0 -0
  67. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/__pycache__/run_all.cpython-39.pyc +0 -0
  68. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  69. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/__pycache__/test_setup_module.cpython-312-pytest-7.4.4.pyc +0 -0
  70. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/conftest.py +0 -0
  71. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/demos/h5_filter_demo.py +0 -0
  72. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/demos/manual_report_example.py +0 -0
  73. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/demos/perf_parallel.py +0 -0
  74. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/e2e/__init__.py +0 -0
  75. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/e2e/__pycache__/__init__.cpython-312.pyc +0 -0
  76. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/e2e/__pycache__/__init__.cpython-39.pyc +0 -0
  77. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-7.4.4.pyc +0 -0
  78. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/e2e/__pycache__/test_user_flows.cpython-39-pytest-8.3.4.pyc +0 -0
  79. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/e2e/test_user_flows.py +0 -0
  80. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/fixtures/test_cases.json +0 -0
  81. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/fixtures/test_cases.yaml +0 -0
  82. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/fixtures/test_cases1.json +0 -0
  83. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/fixtures/test_with_setup.json +0 -0
  84. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/fixtures/test_with_setup.yaml +0 -0
  85. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  86. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  87. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  88. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  89. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_json_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  90. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_json_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  91. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_text_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  92. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/__pycache__/test_text_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  93. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/test_binary_compare.py +0 -0
  94. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/test_h5_compare.py +0 -0
  95. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/test_json_compare.py +0 -0
  96. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/file_compare/test_text_compare.py +0 -0
  97. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  98. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-39-pytest-8.3.4.pyc +0 -0
  99. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/parallel/test_parallel_runner.py +0 -0
  100. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-312-pytest-7.4.4.pyc +0 -0
  101. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-39-pytest-8.3.4.pyc +0 -0
  102. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/integration/path_handling/test_spaces_in_paths.py +0 -0
  103. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/test_report.txt +0 -0
  104. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/unit/core/__pycache__/test_setup.cpython-312-pytest-7.4.4.pyc +0 -0
  105. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/unit/core/__pycache__/test_setup.cpython-39-pytest-8.3.4.pyc +0 -0
  106. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/unit/core/test_setup.py +0 -0
  107. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  108. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-39-pytest-8.3.4.pyc +0 -0
  109. {cli_test_framework-0.4.5 → cli_test_framework-0.5.0}/tests/unit/runners/test_json_yaml_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli-test-framework
3
- Version: 0.4.5
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>=3.8.0
22
- Requires-Dist: numpy>=1.21.0
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.4.5",
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
- "numpy>=1.21.0",
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
+ )
@@ -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"""
@@ -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 not all(field in case for field in required_fields):
91
- raise ValueError(f"Test case {case.get('name', 'unnamed')} is missing required fields")
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
- # Use resolver attribute (keeps backward compatibility with tests monkeypatching it)
94
- case["command"] = self.path_resolver.parse_command_string(case["command"])
95
- case["args"] = self.path_resolver.resolve_paths(case["args"])
96
- self.test_cases.append(TestCase(**case))
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
- case_data: TestCaseData = {
165
- "name": case.name,
166
- "command": case.command,
167
- "args": case.args,
168
- "expected": case.expected,
169
- "description": case.description or None,
170
- "timeout": case.timeout,
171
- "resources": case.resources,
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
- command_preview = f"{case_data['command']} {' '.join(case_data['args'])}".strip()
175
- with self._print_lock:
176
- if self.execution_mode != "thread" or self.cpu_semaphore is None:
177
- print(f" [Worker] Executing command: {command_preview}")
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
- # 4. 执行测试 (传入 env)
180
- result = execute_single_test_case(
181
- case_data,
182
- str(self.workspace) if self.workspace else None,
183
- env=task_env # 注入环境变量
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
- if result["output"].strip():
187
- with self._print_lock:
188
- print(f" [Worker] Command output for {case.name}:")
189
- for line in result["output"].splitlines():
190
- print(f" {line}")
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
- if result["status"] != "passed" and result.get("message"):
193
- with self._print_lock:
194
- print(f" [Worker] Error for {case.name}: {result['message']}")
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: