cli-test-framework 0.4.5__tar.gz → 0.5.1__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 (116) hide show
  1. {cli_test_framework-0.4.5/src/cli_test_framework.egg-info → cli_test_framework-0.5.1}/PKG-INFO +5 -3
  2. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/docs/user_manual.md +137 -8
  3. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/setup.py +6 -4
  4. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/cli.py +8 -3
  5. cli_test_framework-0.5.1/src/cli_test_framework/core/base_runner.py +162 -0
  6. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/core/parallel_runner.py +19 -3
  7. cli_test_framework-0.5.1/src/cli_test_framework/core/process_worker.py +111 -0
  8. cli_test_framework-0.5.1/src/cli_test_framework/core/test_case.py +47 -0
  9. cli_test_framework-0.5.1/src/cli_test_framework/runners/json_runner.py +94 -0
  10. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/runners/parallel_json_runner.py +134 -38
  11. cli_test_framework-0.5.1/src/cli_test_framework/runners/yaml_runner.py +93 -0
  12. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1/src/cli_test_framework.egg-info}/PKG-INFO +5 -3
  13. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework.egg-info/SOURCES.txt +17 -1
  14. cli_test_framework-0.5.1/src/cli_test_framework.egg-info/requires.txt +11 -0
  15. cli_test_framework-0.5.1/tests/__pycache__/conftest.cpython-312-pytest-9.0.3.pyc +0 -0
  16. cli_test_framework-0.5.1/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-9.0.3.pyc +0 -0
  17. cli_test_framework-0.5.1/tests/integration/__pycache__/test_sequence.cpython-312-pytest-7.4.4.pyc +0 -0
  18. cli_test_framework-0.5.1/tests/integration/__pycache__/test_sequence.cpython-312-pytest-9.0.3.pyc +0 -0
  19. cli_test_framework-0.5.1/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-312-pytest-9.0.3.pyc +0 -0
  20. cli_test_framework-0.5.1/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-312-pytest-9.0.3.pyc +0 -0
  21. cli_test_framework-0.5.1/tests/integration/file_compare/__pycache__/test_json_compare.cpython-312-pytest-9.0.3.pyc +0 -0
  22. cli_test_framework-0.5.1/tests/integration/file_compare/__pycache__/test_text_compare.cpython-312-pytest-9.0.3.pyc +0 -0
  23. cli_test_framework-0.5.1/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-312-pytest-9.0.3.pyc +0 -0
  24. cli_test_framework-0.5.1/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-312-pytest-9.0.3.pyc +0 -0
  25. cli_test_framework-0.5.1/tests/integration/test_sequence.py +298 -0
  26. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/run_all.py +3 -1
  27. cli_test_framework-0.5.1/tests/unit/core/__pycache__/test_setup.cpython-312-pytest-9.0.3.pyc +0 -0
  28. cli_test_framework-0.5.1/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-9.0.3.pyc +0 -0
  29. cli_test_framework-0.5.1/tests/unit/runners/__pycache__/test_test_case_filter.cpython-312-pytest-7.4.4.pyc +0 -0
  30. cli_test_framework-0.5.1/tests/unit/runners/__pycache__/test_test_case_filter.cpython-312-pytest-9.0.3.pyc +0 -0
  31. cli_test_framework-0.5.1/tests/unit/runners/test_test_case_filter.py +193 -0
  32. cli_test_framework-0.4.5/src/cli_test_framework/core/base_runner.py +0 -81
  33. cli_test_framework-0.4.5/src/cli_test_framework/core/process_worker.py +0 -45
  34. cli_test_framework-0.4.5/src/cli_test_framework/core/test_case.py +0 -25
  35. cli_test_framework-0.4.5/src/cli_test_framework/runners/json_runner.py +0 -65
  36. cli_test_framework-0.4.5/src/cli_test_framework/runners/yaml_runner.py +0 -64
  37. cli_test_framework-0.4.5/src/cli_test_framework.egg-info/requires.txt +0 -5
  38. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/MANIFEST.in +0 -0
  39. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/README.md +0 -0
  40. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/pyproject.toml +0 -0
  41. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/setup.cfg +0 -0
  42. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/__init__.py +0 -0
  43. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/commands/__init__.py +0 -0
  44. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/commands/compare.py +0 -0
  45. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/core/__init__.py +0 -0
  46. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/core/assertions.py +0 -0
  47. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/core/execution.py +0 -0
  48. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/core/setup.py +0 -0
  49. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/core/types.py +0 -0
  50. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/__init__.py +0 -0
  51. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/base_comparator.py +0 -0
  52. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/binary_comparator.py +0 -0
  53. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/csv_comparator.py +0 -0
  54. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/factory.py +0 -0
  55. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/h5_comparator.py +0 -0
  56. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/json_comparator.py +0 -0
  57. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/result.py +0 -0
  58. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/text_comparator.py +0 -0
  59. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/file_comparator/xml_comparator.py +0 -0
  60. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/runners/__init__.py +0 -0
  61. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/utils/__init__.py +0 -0
  62. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/utils/path_resolver.py +0 -0
  63. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework/utils/report_generator.py +0 -0
  64. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework.egg-info/dependency_links.txt +0 -0
  65. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework.egg-info/entry_points.txt +0 -0
  66. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/src/cli_test_framework.egg-info/top_level.txt +0 -0
  67. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/README.md +0 -0
  68. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/__init__.py +0 -0
  69. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  70. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/__pycache__/__init__.cpython-39.pyc +0 -0
  71. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc +0 -0
  72. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/__pycache__/conftest.cpython-39-pytest-8.3.4.pyc +0 -0
  73. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/__pycache__/run_all.cpython-312.pyc +0 -0
  74. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/__pycache__/run_all.cpython-39.pyc +0 -0
  75. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  76. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/__pycache__/test_setup_module.cpython-312-pytest-7.4.4.pyc +0 -0
  77. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/conftest.py +0 -0
  78. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/demos/h5_filter_demo.py +0 -0
  79. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/demos/manual_report_example.py +0 -0
  80. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/demos/perf_parallel.py +0 -0
  81. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/e2e/__init__.py +0 -0
  82. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/e2e/__pycache__/__init__.cpython-312.pyc +0 -0
  83. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/e2e/__pycache__/__init__.cpython-39.pyc +0 -0
  84. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-7.4.4.pyc +0 -0
  85. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/e2e/__pycache__/test_user_flows.cpython-39-pytest-8.3.4.pyc +0 -0
  86. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/e2e/test_user_flows.py +0 -0
  87. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/fixtures/test_cases.json +0 -0
  88. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/fixtures/test_cases.yaml +0 -0
  89. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/fixtures/test_cases1.json +0 -0
  90. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/fixtures/test_with_setup.json +0 -0
  91. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/fixtures/test_with_setup.yaml +0 -0
  92. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  93. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  94. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  95. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  96. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/__pycache__/test_json_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  97. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/__pycache__/test_json_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  98. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/__pycache__/test_text_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  99. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/__pycache__/test_text_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  100. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/test_binary_compare.py +0 -0
  101. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/test_h5_compare.py +0 -0
  102. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/test_json_compare.py +0 -0
  103. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/file_compare/test_text_compare.py +0 -0
  104. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  105. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-39-pytest-8.3.4.pyc +0 -0
  106. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/parallel/test_parallel_runner.py +0 -0
  107. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-312-pytest-7.4.4.pyc +0 -0
  108. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-39-pytest-8.3.4.pyc +0 -0
  109. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/integration/path_handling/test_spaces_in_paths.py +0 -0
  110. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/test_report.txt +0 -0
  111. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/unit/core/__pycache__/test_setup.cpython-312-pytest-7.4.4.pyc +0 -0
  112. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/unit/core/__pycache__/test_setup.cpython-39-pytest-8.3.4.pyc +0 -0
  113. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/unit/core/test_setup.py +0 -0
  114. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  115. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-39-pytest-8.3.4.pyc +0 -0
  116. {cli_test_framework-0.4.5 → cli_test_framework-0.5.1}/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.1
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
@@ -7,11 +7,14 @@
7
7
  4. [Test Case Definition](#test-case-definition)
8
8
  5. [Setup Module](#setup-module)
9
9
  6. [Parallel Testing](#parallel-testing)
10
- 7. [File Comparison](#file-comparison)
11
- 8. [Advanced Features](#advanced-features)
12
- 9. [Troubleshooting](#troubleshooting)
13
- 10. [API Reference](#api-reference)
14
- 11. [Examples](#examples)
10
+ 7. [Sequence Testing](#sequence-testing)
11
+ 8. [File Comparison](#file-comparison)
12
+ 9. [Advanced Features](#advanced-features)
13
+ 10. [Troubleshooting](#troubleshooting)
14
+ 11. [API Reference](#api-reference)
15
+ 12. [Examples](#examples)
16
+
17
+
15
18
 
16
19
  ## Introduction
17
20
 
@@ -19,6 +22,7 @@ The CLI Testing Framework is a powerful tool designed for testing command-line a
19
22
 
20
23
  ### Key Features
21
24
  - Parallel test execution with thread and process support
25
+ - Sequence test execution with multi-step fail-fast
22
26
  - JSON/YAML test case definition
23
27
  - Advanced file comparison capabilities
24
28
  - Comprehensive reporting
@@ -74,6 +78,18 @@ runner = JSONRunner(
74
78
  success = runner.run_tests()
75
79
  ```
76
80
 
81
+ 3. Run only specified test case(s):
82
+ ```python
83
+ from cli_test_framework.runners import JSONRunner
84
+
85
+ runner = JSONRunner(
86
+ config_file="test_cases.json",
87
+ workspace="/path/to/workspace",
88
+ test_case_filter=["test_ls_command", "test_echo_command"]
89
+ )
90
+ success = runner.run_tests()
91
+ ```
92
+
77
93
  ### Using the Command Line
78
94
 
79
95
  ```bash
@@ -82,6 +98,12 @@ cli-test run test_cases.json
82
98
 
83
99
  # Run tests in parallel
84
100
  cli-test run test_cases.json --parallel --workers 4
101
+
102
+ # Run only specified test case(s)
103
+ cli-test run test_cases.json --test-case test_ls_command
104
+
105
+ # Run multiple specified test cases
106
+ cli-test run test_cases.json -t test_ls_command -t test_echo_command
85
107
  ```
86
108
 
87
109
  ## Test Case Definition
@@ -292,6 +314,54 @@ runner = ParallelJSONRunner(
292
314
  success = runner.run_tests()
293
315
  ```
294
316
 
317
+ ## Sequence Testing
318
+
319
+ Sequence testing allows a single test case to contain multiple ordered steps that execute sequentially. If any step fails, subsequent steps are skipped (fail-fast).
320
+
321
+ ### JSON Format
322
+
323
+ ```json
324
+ {
325
+ "test_cases": [
326
+ {
327
+ "name": "Multi-step Test",
328
+ "steps": [
329
+ {
330
+ "command": "echo",
331
+ "args": ["step1"],
332
+ "expected": { "return_code": 0, "output_contains": ["step1"] }
333
+ },
334
+ {
335
+ "command": "echo",
336
+ "args": ["step2"],
337
+ "expected": { "return_code": 0, "output_contains": ["step2"] }
338
+ }
339
+ ]
340
+ }
341
+ ]
342
+ }
343
+ ```
344
+
345
+ ### YAML Format
346
+
347
+ ```yaml
348
+ test_cases:
349
+ - name: Multi-step Test
350
+ steps:
351
+ - command: echo
352
+ args: ["step1"]
353
+ expected:
354
+ return_code: 0
355
+ output_contains: ["step1"]
356
+ - command: echo
357
+ args: ["step2"]
358
+ expected:
359
+ return_code: 0
360
+ output_contains: ["step2"]
361
+ ```
362
+
363
+ Each step supports `command`, `args`, `expected`, and `timeout` fields. When a step fails, the overall result reports which step failed (e.g., "Failed at step 2/3: ...").
364
+
295
365
  ## File Comparison
296
366
 
297
367
  ### Basic File Comparison
@@ -355,6 +425,52 @@ compare-files binary1.bin binary2.bin --chunk-size 16384
355
425
 
356
426
  ## Advanced Features
357
427
 
428
+ ### Specifying Test Cases
429
+
430
+ You can run only specific test cases by name using the `test_case_filter` parameter (Python API) or the `--test-case` / `-t` flag (CLI).
431
+
432
+ #### CLI Usage
433
+
434
+ ```bash
435
+ # Run a single test case by name
436
+ cli-test run test_cases.json --test-case test_ls_command
437
+
438
+ # Run multiple test cases (repeat the flag)
439
+ cli-test run test_cases.json -t test_ls_command -t test_echo_command
440
+
441
+ # Works with parallel mode too
442
+ cli-test run test_cases.json --parallel --test-case test_ls_command
443
+ ```
444
+
445
+ #### Python API Usage
446
+
447
+ ```python
448
+ from cli_test_framework.runners import JSONRunner, ParallelJSONRunner, YAMLRunner
449
+
450
+ # With JSONRunner
451
+ runner = JSONRunner(
452
+ config_file="test_cases.json",
453
+ test_case_filter=["test_ls_command", "test_echo_command"]
454
+ )
455
+
456
+ # With YAMLRunner
457
+ runner = YAMLRunner(
458
+ config_file="test_cases.yaml",
459
+ test_case_filter=["test_case_1"]
460
+ )
461
+
462
+ # With ParallelJSONRunner
463
+ runner = ParallelJSONRunner(
464
+ config_file="test_cases.json",
465
+ test_case_filter=["heavy_test"],
466
+ max_workers=4
467
+ )
468
+
469
+ success = runner.run_tests()
470
+ ```
471
+
472
+ If a specified test case name is not found, a warning will be printed. The filter matches test case names exactly.
473
+
358
474
  ### Custom Assertions
359
475
  ```python
360
476
  from cli_test_framework.assertions import BaseAssertion
@@ -422,24 +538,37 @@ runner = JSONRunner(
422
538
  #### JSONRunner
423
539
  ```python
424
540
  class JSONRunner:
425
- def __init__(self, config_file, workspace=None, debug=False):
541
+ def __init__(self, config_file, workspace=None, test_case_filter=None):
426
542
  """
427
543
  Initialize JSONRunner
428
544
  :param config_file: Path to JSON test case file
429
545
  :param workspace: Working directory for test execution
430
- :param debug: Enable debug mode
546
+ :param test_case_filter: Optional list of test case names to run (runs all if None)
547
+ """
548
+ ```
549
+
550
+ #### YAMLRunner
551
+ ```python
552
+ class YAMLRunner:
553
+ def __init__(self, config_file, workspace=None, test_case_filter=None):
554
+ """
555
+ Initialize YAMLRunner
556
+ :param config_file: Path to YAML test case file
557
+ :param workspace: Working directory for test execution
558
+ :param test_case_filter: Optional list of test case names to run (runs all if None)
431
559
  """
432
560
  ```
433
561
 
434
562
  #### ParallelJSONRunner
435
563
  ```python
436
564
  class ParallelJSONRunner:
437
- def __init__(self, config_file, max_workers=None, execution_mode="thread"):
565
+ def __init__(self, config_file, max_workers=None, execution_mode="thread", test_case_filter=None):
438
566
  """
439
567
  Initialize ParallelJSONRunner
440
568
  :param config_file: Path to JSON test case file
441
569
  :param max_workers: Maximum number of parallel workers
442
570
  :param execution_mode: "thread" or "process"
571
+ :param test_case_filter: Optional list of test case names to run (runs all if None)
443
572
  """
444
573
  ```
445
574
 
@@ -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.1",
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
+ )
@@ -40,6 +40,8 @@ Examples:
40
40
  help='Parallel execution mode (default: thread)')
41
41
  run_parser.add_argument('--output-format', choices=['text', 'json', 'html'], default='text',
42
42
  help='Output format for test results')
43
+ run_parser.add_argument('--test-case', '-t', action='append', default=None,
44
+ help='Run only specified test case(s) by name (can be used multiple times)')
43
45
  run_parser.add_argument('--verbose', '-v', action='store_true', help='Enable verbose output')
44
46
  run_parser.add_argument('--debug', action='store_true', help='Enable debug mode')
45
47
 
@@ -64,19 +66,22 @@ def run_tests(args):
64
66
  config_file=str(config_file),
65
67
  workspace=args.workspace,
66
68
  max_workers=args.workers,
67
- execution_mode=args.execution_mode
69
+ execution_mode=args.execution_mode,
70
+ test_case_filter=args.test_case
68
71
  )
69
72
  else:
70
73
  # Use appropriate single-threaded runner
71
74
  if file_ext in ['.json']:
72
75
  runner = JSONRunner(
73
76
  config_file=str(config_file),
74
- workspace=args.workspace
77
+ workspace=args.workspace,
78
+ test_case_filter=args.test_case
75
79
  )
76
80
  elif file_ext in ['.yaml', '.yml']:
77
81
  runner = YAMLRunner(
78
82
  config_file=str(config_file),
79
- workspace=args.workspace
83
+ workspace=args.workspace,
84
+ test_case_filter=args.test_case
80
85
  )
81
86
  else:
82
87
  print(f"Error: Unsupported configuration file format: {file_ext}")
@@ -0,0 +1,162 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+ from typing import List, Dict, Any, Optional
4
+ from .test_case import TestCase
5
+ from .assertions import Assertions
6
+ from .setup import SetupManager, EnvironmentSetup
7
+ from .execution import execute_single_test_case
8
+
9
+ class BaseRunner(ABC):
10
+ def __init__(self, config_file: str, workspace: Optional[str] = None,
11
+ test_case_filter: Optional[List[str]] = None):
12
+ if workspace:
13
+ self.workspace = Path(workspace)
14
+ else:
15
+ self.workspace = Path(__file__).parent.parent.parent
16
+ self.config_path = self.workspace / config_file
17
+ self.test_cases: List[TestCase] = []
18
+ self.test_case_filter: Optional[List[str]] = test_case_filter
19
+ self.results: Dict[str, Any] = {
20
+ "total": 0,
21
+ "passed": 0,
22
+ "failed": 0,
23
+ "details": []
24
+ }
25
+ self.assertions = Assertions()
26
+ self.setup_manager = SetupManager()
27
+
28
+ @abstractmethod
29
+ def load_test_cases(self) -> None:
30
+ """Load test cases from configuration file"""
31
+ pass
32
+
33
+ def load_setup_from_config(self, config: Dict[str, Any]) -> None:
34
+ """从配置文件加载setup配置"""
35
+ setup_config = config.get("setup", {})
36
+
37
+ # 处理环境变量设置
38
+ if "environment_variables" in setup_config:
39
+ env_setup = EnvironmentSetup({"environment_variables": setup_config["environment_variables"]})
40
+ self.setup_manager.add_setup(env_setup)
41
+
42
+ # 这里可以扩展支持其他类型的setup插件
43
+ # 例如:
44
+ # if "custom_setups" in setup_config:
45
+ # for custom_setup_config in setup_config["custom_setups"]:
46
+ # # 动态加载自定义setup插件
47
+ # pass
48
+
49
+ def _apply_test_case_filter(self) -> None:
50
+ """根据 test_case_filter 过滤测试用例"""
51
+ if self.test_case_filter:
52
+ original_count = len(self.test_cases)
53
+ self.test_cases = [tc for tc in self.test_cases if tc.name in self.test_case_filter]
54
+ filtered_out = original_count - len(self.test_cases)
55
+ if filtered_out > 0:
56
+ print(f"Filtered out {filtered_out} test case(s). Running {len(self.test_cases)} specified case(s).")
57
+ if not self.test_cases:
58
+ print(f"Warning: No matching test cases found for: {self.test_case_filter}")
59
+
60
+ def run_tests(self) -> bool:
61
+ """Run all test cases and return whether all tests passed"""
62
+ try:
63
+ self.load_test_cases()
64
+ self._apply_test_case_filter()
65
+ self.results["total"] = len(self.test_cases)
66
+
67
+ if self.results["total"] == 0:
68
+ print("No test cases to run.")
69
+ return False
70
+
71
+ # 执行setup任务
72
+ self.setup_manager.setup_all()
73
+
74
+ print(f"\nStarting test execution... Total tests: {self.results['total']}")
75
+ print("=" * 50)
76
+
77
+ for i, case in enumerate(self.test_cases, 1):
78
+ print(f"\nRunning test {i}/{self.results['total']}: {case.name}")
79
+ result = self.run_single_test(case)
80
+ self.results["details"].append(result)
81
+ if result["status"] == "passed":
82
+ self.results["passed"] += 1
83
+ print(f"✓ Test passed: {case.name}")
84
+ else:
85
+ self.results["failed"] += 1
86
+ print(f"✗ Test failed: {case.name}")
87
+ if result["message"]:
88
+ print(f" Error: {result['message']}")
89
+
90
+ print("\n" + "=" * 50)
91
+ print(f"Test execution completed. Passed: {self.results['passed']}, Failed: {self.results['failed']}")
92
+ return self.results["failed"] == 0
93
+ finally:
94
+ # 确保teardown总是被执行
95
+ self.setup_manager.teardown_all()
96
+
97
+ def _run_sequence(self, case: TestCase) -> Dict[str, Any]:
98
+ """Run a sequence test case with multiple steps (fail-fast)."""
99
+ combined_output = ""
100
+ total_duration = 0.0
101
+ all_passed = True
102
+ last_result = None
103
+ failed_step = None
104
+
105
+ for i, step in enumerate(case.steps):
106
+ step_name = f"{case.name} [step {i+1}/{len(case.steps)}]"
107
+ case_data = {
108
+ "name": step_name,
109
+ "command": step.command,
110
+ "args": step.args,
111
+ "expected": step.expected,
112
+ "description": None,
113
+ "timeout": step.timeout,
114
+ "resources": None,
115
+ }
116
+
117
+ command_preview = f"{step.command} {' '.join(step.args)}".strip()
118
+ print(f" Executing step {i+1}/{len(case.steps)}: {command_preview}")
119
+
120
+ result = execute_single_test_case(
121
+ case_data, str(self.workspace) if self.workspace else None
122
+ )
123
+
124
+ if result["output"].strip():
125
+ print(" Command output:")
126
+ for line in result["output"].splitlines():
127
+ print(f" {line}")
128
+
129
+ combined_output += result["output"]
130
+ total_duration += result["duration"]
131
+ last_result = result
132
+
133
+ if result["status"] != "passed":
134
+ all_passed = False
135
+ failed_step = i + 1
136
+ if result.get("message"):
137
+ print(f" Error at step {i+1}: {result['message']}")
138
+ break
139
+
140
+ status = "passed" if all_passed else last_result["status"]
141
+ message = ""
142
+ if not all_passed:
143
+ message = f"Failed at step {failed_step}/{len(case.steps)}: {last_result['message']}"
144
+
145
+ command_summary = " -> ".join(
146
+ f"{s.command} {' '.join(s.args)}".strip() for s in case.steps
147
+ )
148
+
149
+ return {
150
+ "name": case.name,
151
+ "status": status,
152
+ "message": message,
153
+ "command": command_summary,
154
+ "output": combined_output,
155
+ "return_code": last_result["return_code"] if last_result else None,
156
+ "duration": total_duration,
157
+ }
158
+
159
+ @abstractmethod
160
+ def run_single_test(self, case: TestCase) -> Dict[str, str]:
161
+ """Run a single test case and return the result"""
162
+ pass
@@ -12,7 +12,8 @@ class ParallelRunner(BaseRunner):
12
12
 
13
13
  def __init__(self, config_file: str, workspace: Optional[str] = None,
14
14
  max_workers: Optional[int] = None,
15
- execution_mode: str = "thread"):
15
+ execution_mode: str = "thread",
16
+ test_case_filter: Optional[List[str]] = None):
16
17
  """
17
18
  初始化并行运行器
18
19
 
@@ -21,8 +22,9 @@ class ParallelRunner(BaseRunner):
21
22
  workspace: 工作目录
22
23
  max_workers: 最大并发数,默认为CPU核心数
23
24
  execution_mode: 执行模式,'thread'(线程) 或 'process'(进程)
25
+ test_case_filter: 只运行指定名称的测试用例
24
26
  """
25
- super().__init__(config_file, workspace)
27
+ super().__init__(config_file, workspace, test_case_filter)
26
28
  self.max_workers = max_workers
27
29
  self.execution_mode = execution_mode
28
30
  self.lock = threading.Lock() # 用于线程安全的结果更新
@@ -31,8 +33,13 @@ class ParallelRunner(BaseRunner):
31
33
  """并行运行所有测试用例"""
32
34
  try:
33
35
  self.load_test_cases()
36
+ self._apply_test_case_filter()
34
37
  self.results["total"] = len(self.test_cases)
35
38
 
39
+ if self.results["total"] == 0:
40
+ print("No test cases to run.")
41
+ return False
42
+
36
43
  # 执行setup任务
37
44
  self.setup_manager.setup_all()
38
45
 
@@ -61,7 +68,16 @@ class ParallelRunner(BaseRunner):
61
68
  "args": case.args,
62
69
  "expected": case.expected,
63
70
  "timeout": case.timeout,
64
- "resources": case.resources
71
+ "resources": case.resources,
72
+ "steps": [
73
+ {
74
+ "command": s.command,
75
+ "args": s.args,
76
+ "expected": s.expected,
77
+ "timeout": s.timeout,
78
+ }
79
+ for s in case.steps
80
+ ] if case.steps else None,
65
81
  },
66
82
  str(self.workspace) if self.workspace else None
67
83
  ): (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,47 @@
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
+ __test__ = False
8
+ command: str
9
+ args: List[str]
10
+ expected: Dict[str, Any]
11
+ timeout: Optional[float] = None
12
+
13
+ @dataclass
14
+ class TestCase:
15
+ __test__ = False
16
+ name: str
17
+ command: str = ""
18
+ args: List[str] = field(default_factory=list)
19
+ expected: Dict[str, Any] = field(default_factory=dict)
20
+ description: str = ""
21
+ timeout: Optional[float] = None
22
+ resources: Optional[Dict[str, Any]] = None
23
+ steps: Optional[List[TestCaseStep]] = None
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ """Convert test case to dictionary format"""
27
+ print("Convert test case to dictionary format")
28
+ print(self.command)
29
+ result = {
30
+ "name": self.name,
31
+ "command": self.command,
32
+ "args": self.args,
33
+ "expected": self.expected,
34
+ "timeout": self.timeout,
35
+ "resources": self.resources,
36
+ }
37
+ if self.steps is not None:
38
+ result["steps"] = [
39
+ {
40
+ "command": s.command,
41
+ "args": s.args,
42
+ "expected": s.expected,
43
+ "timeout": s.timeout,
44
+ }
45
+ for s in self.steps
46
+ ]
47
+ return result