cli-test-framework 0.3.6__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {cli_test_framework-0.3.6/src/cli_test_framework.egg-info → cli_test_framework-0.4.0}/PKG-INFO +46 -2
  2. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/README.md +44 -0
  3. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/docs/user_manual.md +23 -0
  4. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/setup.py +1 -1
  5. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/__init__.py +1 -1
  6. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/commands/compare.py +1 -1
  7. cli_test_framework-0.4.0/src/cli_test_framework/core/execution.py +67 -0
  8. cli_test_framework-0.4.0/src/cli_test_framework/core/process_worker.py +43 -0
  9. cli_test_framework-0.4.0/src/cli_test_framework/core/types.py +45 -0
  10. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/h5_comparator.py +25 -20
  11. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/json_comparator.py +5 -4
  12. cli_test_framework-0.4.0/src/cli_test_framework/runners/json_runner.py +63 -0
  13. cli_test_framework-0.4.0/src/cli_test_framework/runners/parallel_json_runner.py +79 -0
  14. cli_test_framework-0.4.0/src/cli_test_framework/runners/yaml_runner.py +62 -0
  15. cli_test_framework-0.4.0/src/cli_test_framework/utils/__init__.py +19 -0
  16. cli_test_framework-0.4.0/src/cli_test_framework/utils/path_resolver.py +229 -0
  17. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0/src/cli_test_framework.egg-info}/PKG-INFO +46 -2
  18. cli_test_framework-0.4.0/src/cli_test_framework.egg-info/SOURCES.txt +92 -0
  19. cli_test_framework-0.4.0/tests/README.md +15 -0
  20. cli_test_framework-0.4.0/tests/__pycache__/__init__.cpython-39.pyc +0 -0
  21. cli_test_framework-0.4.0/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc +0 -0
  22. cli_test_framework-0.4.0/tests/__pycache__/conftest.cpython-39-pytest-8.3.4.pyc +0 -0
  23. cli_test_framework-0.4.0/tests/__pycache__/run_all.cpython-312.pyc +0 -0
  24. cli_test_framework-0.4.0/tests/__pycache__/run_all.cpython-39.pyc +0 -0
  25. cli_test_framework-0.4.0/tests/conftest.py +15 -0
  26. cli_test_framework-0.3.6/tests/test_filter_demo.py → cli_test_framework-0.4.0/tests/demos/h5_filter_demo.py +37 -33
  27. cli_test_framework-0.4.0/tests/demos/manual_report_example.py +32 -0
  28. cli_test_framework-0.3.6/tests/performance_test.py → cli_test_framework-0.4.0/tests/demos/perf_parallel.py +45 -58
  29. cli_test_framework-0.4.0/tests/e2e/__init__.py +2 -0
  30. cli_test_framework-0.4.0/tests/e2e/__pycache__/__init__.cpython-312.pyc +0 -0
  31. cli_test_framework-0.4.0/tests/e2e/__pycache__/__init__.cpython-39.pyc +0 -0
  32. cli_test_framework-0.4.0/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-7.4.4.pyc +0 -0
  33. cli_test_framework-0.4.0/tests/e2e/__pycache__/test_user_flows.cpython-39-pytest-8.3.4.pyc +0 -0
  34. cli_test_framework-0.4.0/tests/e2e/test_user_flows.py +83 -0
  35. cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  36. cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  37. cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  38. cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  39. cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_json_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  40. cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_json_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  41. cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_text_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  42. cli_test_framework-0.4.0/tests/integration/file_compare/__pycache__/test_text_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  43. cli_test_framework-0.4.0/tests/integration/file_compare/test_binary_compare.py +31 -0
  44. cli_test_framework-0.4.0/tests/integration/file_compare/test_h5_compare.py +107 -0
  45. cli_test_framework-0.4.0/tests/integration/file_compare/test_json_compare.py +48 -0
  46. cli_test_framework-0.4.0/tests/integration/file_compare/test_text_compare.py +40 -0
  47. cli_test_framework-0.4.0/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  48. cli_test_framework-0.4.0/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-39-pytest-8.3.4.pyc +0 -0
  49. cli_test_framework-0.4.0/tests/integration/parallel/test_parallel_runner.py +111 -0
  50. cli_test_framework-0.4.0/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-312-pytest-7.4.4.pyc +0 -0
  51. cli_test_framework-0.4.0/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-39-pytest-8.3.4.pyc +0 -0
  52. cli_test_framework-0.4.0/tests/integration/path_handling/test_spaces_in_paths.py +85 -0
  53. cli_test_framework-0.4.0/tests/run_all.py +62 -0
  54. cli_test_framework-0.4.0/tests/unit/core/__pycache__/test_setup.cpython-312-pytest-7.4.4.pyc +0 -0
  55. cli_test_framework-0.4.0/tests/unit/core/__pycache__/test_setup.cpython-39-pytest-8.3.4.pyc +0 -0
  56. cli_test_framework-0.3.6/tests/test_setup_module.py → cli_test_framework-0.4.0/tests/unit/core/test_setup.py +81 -157
  57. cli_test_framework-0.4.0/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  58. cli_test_framework-0.4.0/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-39-pytest-8.3.4.pyc +0 -0
  59. cli_test_framework-0.4.0/tests/unit/runners/test_json_yaml_runner.py +83 -0
  60. cli_test_framework-0.3.6/CHANGELOG.md +0 -211
  61. cli_test_framework-0.3.6/src/cli_test_framework/core/process_worker.py +0 -93
  62. cli_test_framework-0.3.6/src/cli_test_framework/runners/json_runner.py +0 -99
  63. cli_test_framework-0.3.6/src/cli_test_framework/runners/parallel_json_runner.py +0 -118
  64. cli_test_framework-0.3.6/src/cli_test_framework/runners/yaml_runner.py +0 -96
  65. cli_test_framework-0.3.6/src/cli_test_framework/utils/__init__.py +0 -11
  66. cli_test_framework-0.3.6/src/cli_test_framework/utils/path_resolver.py +0 -205
  67. cli_test_framework-0.3.6/src/cli_test_framework.egg-info/SOURCES.txt +0 -58
  68. cli_test_framework-0.3.6/tests/test1.py +0 -30
  69. cli_test_framework-0.3.6/tests/test_comprehensive_space.py +0 -118
  70. cli_test_framework-0.3.6/tests/test_parallel_runner.py +0 -170
  71. cli_test_framework-0.3.6/tests/test_parallel_space.py +0 -93
  72. cli_test_framework-0.3.6/tests/test_runners.py +0 -32
  73. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/MANIFEST.in +0 -0
  74. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/pyproject.toml +0 -0
  75. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/setup.cfg +0 -0
  76. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/cli.py +0 -0
  77. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/commands/__init__.py +0 -0
  78. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/core/__init__.py +0 -0
  79. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/core/assertions.py +0 -0
  80. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/core/base_runner.py +0 -0
  81. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/core/parallel_runner.py +0 -0
  82. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/core/setup.py +0 -0
  83. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/core/test_case.py +0 -0
  84. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/__init__.py +0 -0
  85. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/base_comparator.py +0 -0
  86. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/binary_comparator.py +0 -0
  87. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/csv_comparator.py +0 -0
  88. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/factory.py +0 -0
  89. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/result.py +0 -0
  90. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/text_comparator.py +0 -0
  91. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/file_comparator/xml_comparator.py +0 -0
  92. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/runners/__init__.py +0 -0
  93. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework/utils/report_generator.py +0 -0
  94. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework.egg-info/dependency_links.txt +0 -0
  95. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework.egg-info/entry_points.txt +0 -0
  96. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework.egg-info/requires.txt +0 -0
  97. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/src/cli_test_framework.egg-info/top_level.txt +0 -0
  98. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/__init__.py +0 -0
  99. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  100. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  101. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/__pycache__/test_setup_module.cpython-312-pytest-7.4.4.pyc +0 -0
  102. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/fixtures/test_cases.json +0 -0
  103. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/fixtures/test_cases.yaml +0 -0
  104. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/fixtures/test_cases1.json +0 -0
  105. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/fixtures/test_with_setup.json +0 -0
  106. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/fixtures/test_with_setup.yaml +0 -0
  107. {cli_test_framework-0.3.6 → cli_test_framework-0.4.0}/tests/test_report.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: cli-test-framework
3
- Version: 0.3.6
3
+ Version: 0.4.0
4
4
  Summary: A powerful command line testing framework in Python with setup modules, parallel execution, and file comparison capabilities.
5
5
  Home-page: https://github.com/ozil111/cli-test-framework
6
6
  Author: Xiaotong Wang
@@ -233,6 +233,11 @@ compare-files data1.h5 data2.h5 \
233
233
  compare-files data1.h5 data2.h5 \
234
234
  --h5-table-regex "group1/.*" \
235
235
  --h5-structure-only
236
+
237
+ # Use comma-separated table names with regex (New in 0.3.7)
238
+ compare-files data1.h5 data2.h5 \
239
+ --h5-table-regex "table1,table2,table3" \
240
+ --h5-rtol 1e-6
236
241
  ```
237
242
 
238
243
  #### Binary Comparison
@@ -519,3 +524,42 @@ The user manual includes:
519
524
  **🚀 Ready to supercharge your testing workflow with setup modules, parallel execution and advanced file comparison!**
520
525
 
521
526
  For detailed parallel testing guide, see: [PARALLEL_TESTING_GUIDE.md](https://github.com/ozil111/cli-test-framework/blob/main/PARALLEL_TESTING_GUIDE.md)
527
+
528
+ # 支持数据过滤(New in 0.3.6)
529
+
530
+ 你可以通过 `--h5-data-filter` 选项只比较满足特定条件的数据。例如:
531
+
532
+ ```bash
533
+ # 只比较大于 1e-6 的数据
534
+ compare-files data1.h5 data2.h5 --h5-data-filter '>1e-6'
535
+
536
+ # 只比较绝对值大于 1e-6 的数据
537
+ compare-files data1.h5 data2.h5 --h5-data-filter 'abs>1e-6'
538
+
539
+ # 只比较小于等于 0.01 的数据
540
+ compare-files data1.h5 data2.h5 --h5-data-filter '<=0.01'
541
+ ```
542
+
543
+ 支持的表达式包括:`>`, `>=`, `<`, `<=`, `==`,以及 `abs` 前缀(绝对值过滤)。
544
+
545
+ # 版本更新日志
546
+
547
+ ## 0.3.7 (Latest)
548
+
549
+ ### 🐛 Bug Fixes
550
+ - **Fixed H5 table regex matching**: `--h5-table-regex=table1,table2` now correctly matches both `table1` and `table2` instead of treating the entire string as a single regex pattern
551
+ - **Enhanced regex pattern support**: Multiple comma-separated table names are now supported in `--h5-table-regex` parameter
552
+
553
+ ### ✨ New Features
554
+ - **Improved HDF5 comparison**: Better handling of multiple table selection with regex patterns
555
+ - **Enhanced debug output**: More detailed logging for HDF5 table matching process
556
+
557
+ ### 🔧 Improvements
558
+ - **Backward compatibility**: All existing functionality remains unchanged
559
+ - **Better error handling**: More informative error messages for regex pattern parsing
560
+
561
+ ## 0.3.6
562
+
563
+ ### ✨ New Features
564
+ - **Data filtering for HDF5 files**: Added `--h5-data-filter` option to compare only data meeting specific criteria
565
+ - **Enhanced HDF5 comparison**: Support for absolute value filtering and various comparison operators
@@ -198,6 +198,11 @@ compare-files data1.h5 data2.h5 \
198
198
  compare-files data1.h5 data2.h5 \
199
199
  --h5-table-regex "group1/.*" \
200
200
  --h5-structure-only
201
+
202
+ # Use comma-separated table names with regex (New in 0.3.7)
203
+ compare-files data1.h5 data2.h5 \
204
+ --h5-table-regex "table1,table2,table3" \
205
+ --h5-rtol 1e-6
201
206
  ```
202
207
 
203
208
  #### Binary Comparison
@@ -484,3 +489,42 @@ The user manual includes:
484
489
  **🚀 Ready to supercharge your testing workflow with setup modules, parallel execution and advanced file comparison!**
485
490
 
486
491
  For detailed parallel testing guide, see: [PARALLEL_TESTING_GUIDE.md](https://github.com/ozil111/cli-test-framework/blob/main/PARALLEL_TESTING_GUIDE.md)
492
+
493
+ # 支持数据过滤(New in 0.3.6)
494
+
495
+ 你可以通过 `--h5-data-filter` 选项只比较满足特定条件的数据。例如:
496
+
497
+ ```bash
498
+ # 只比较大于 1e-6 的数据
499
+ compare-files data1.h5 data2.h5 --h5-data-filter '>1e-6'
500
+
501
+ # 只比较绝对值大于 1e-6 的数据
502
+ compare-files data1.h5 data2.h5 --h5-data-filter 'abs>1e-6'
503
+
504
+ # 只比较小于等于 0.01 的数据
505
+ compare-files data1.h5 data2.h5 --h5-data-filter '<=0.01'
506
+ ```
507
+
508
+ 支持的表达式包括:`>`, `>=`, `<`, `<=`, `==`,以及 `abs` 前缀(绝对值过滤)。
509
+
510
+ # 版本更新日志
511
+
512
+ ## 0.3.7 (Latest)
513
+
514
+ ### 🐛 Bug Fixes
515
+ - **Fixed H5 table regex matching**: `--h5-table-regex=table1,table2` now correctly matches both `table1` and `table2` instead of treating the entire string as a single regex pattern
516
+ - **Enhanced regex pattern support**: Multiple comma-separated table names are now supported in `--h5-table-regex` parameter
517
+
518
+ ### ✨ New Features
519
+ - **Improved HDF5 comparison**: Better handling of multiple table selection with regex patterns
520
+ - **Enhanced debug output**: More detailed logging for HDF5 table matching process
521
+
522
+ ### 🔧 Improvements
523
+ - **Backward compatibility**: All existing functionality remains unchanged
524
+ - **Better error handling**: More informative error messages for regex pattern parsing
525
+
526
+ ## 0.3.6
527
+
528
+ ### ✨ New Features
529
+ - **Data filtering for HDF5 files**: Added `--h5-data-filter` option to compare only data meeting specific criteria
530
+ - **Enhanced HDF5 comparison**: Support for absolute value filtering and various comparison operators
@@ -319,8 +319,31 @@ compare-files data1.h5 data2.h5 --h5-table table1,table2
319
319
 
320
320
  # Compare with numerical tolerance
321
321
  compare-files data1.h5 data2.h5 --h5-rtol 1e-5 --h5-atol 1e-8
322
+
323
+ # Use regex patterns for table selection
324
+ compare-files data1.h5 data2.h5 --h5-table-regex "table.*"
325
+
326
+ # Use comma-separated table names with regex (New in 0.3.7)
327
+ compare-files data1.h5 data2.h5 --h5-table-regex "table1,table2,table3"
328
+ ```
329
+
330
+ #### Data Filtering (New in 0.3.7)
331
+
332
+ You can use the `--h5-data-filter` option to only compare data that meets a certain condition. For example:
333
+
334
+ ```bash
335
+ # Only compare values greater than 1e-6
336
+ compare-files data1.h5 data2.h5 --h5-data-filter '>1e-6'
337
+
338
+ # Only compare absolute values greater than 1e-6
339
+ compare-files data1.h5 data2.h5 --h5-data-filter 'abs>1e-6'
340
+
341
+ # Only compare values less than or equal to 0.01
342
+ compare-files data1.h5 data2.h5 --h5-data-filter '<=0.01'
322
343
  ```
323
344
 
345
+ Supported filter expressions: `>`, `>=`, `<`, `<=`, `==`, and `abs` prefix for absolute value filtering.
346
+
324
347
  ### Binary File Comparison
325
348
  ```bash
326
349
  # Compare with similarity check
@@ -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.3.6",
11
+ version="0.4.0",
12
12
  author="Xiaotong Wang",
13
13
  author_email="xiaotongwang98@gmail.com",
14
14
  description="A powerful command line testing framework in Python with setup modules, parallel execution, and file comparison capabilities.",
@@ -5,7 +5,7 @@ This package provides tools for testing command-line applications and scripts
5
5
  with support for parallel execution and advanced file comparison capabilities.
6
6
  """
7
7
 
8
- __version__ = "0.3.3"
8
+ __version__ = "0.3.7"
9
9
  __author__ = "Xiaotong Wang"
10
10
  __email__ = "xiaotongwang98@gmail.com"
11
11
 
@@ -58,7 +58,7 @@ def parse_arguments():
58
58
  # H5 comparison options
59
59
  h5_group = parser.add_argument_group('HDF5 comparison options')
60
60
  h5_group.add_argument("--h5-table", help="Comma-separated list of table names to compare in HDF5 files")
61
- h5_group.add_argument("--h5-table-regex", help="Regular expression pattern to match table names in HDF5 files")
61
+ h5_group.add_argument("--h5-table-regex", help="Comma-separated list of regular expression patterns to match table names in HDF5 files. Each pattern is matched independently.")
62
62
  h5_group.add_argument("--h5-structure-only", action="store_true",
63
63
  help="Only compare HDF5 file structure without comparing content")
64
64
  h5_group.add_argument("--h5-show-content-diff", action="store_true",
@@ -0,0 +1,67 @@
1
+ import subprocess
2
+ import time
3
+ from typing import Optional
4
+
5
+ from .assertions import Assertions
6
+ from .types import ExpectedResult, TestCaseData, TestResultData
7
+
8
+
9
+ def validate_result(expected: ExpectedResult, actual: TestResultData) -> None:
10
+ """
11
+ Pure validation logic. Raises AssertionError on mismatch.
12
+ """
13
+ assertions = Assertions()
14
+
15
+ if "return_code" in expected:
16
+ assertions.return_code_equals(actual["return_code"], expected["return_code"])
17
+
18
+ if "output_contains" in expected:
19
+ for text in expected["output_contains"]:
20
+ assertions.contains(actual["output"], text)
21
+
22
+ if "output_matches" in expected and expected["output_matches"]:
23
+ assertions.matches(actual["output"], expected["output_matches"])
24
+
25
+
26
+ def execute_single_test_case(case: TestCaseData, workspace: Optional[str] = None) -> TestResultData:
27
+ """
28
+ Stateless execution of a single test case.
29
+ """
30
+ start_time = time.time()
31
+ full_command = f"{case['command']} {' '.join(case['args'])}".strip()
32
+
33
+ result: TestResultData = {
34
+ "name": case["name"],
35
+ "status": "failed",
36
+ "message": "",
37
+ "command": full_command,
38
+ "output": "",
39
+ "return_code": None,
40
+ "duration": 0.0,
41
+ }
42
+
43
+ try:
44
+ process = subprocess.run(
45
+ full_command,
46
+ cwd=workspace if workspace else None,
47
+ capture_output=True,
48
+ text=True,
49
+ check=False,
50
+ shell=True,
51
+ )
52
+
53
+ output = process.stdout + process.stderr
54
+ result["output"] = output
55
+ result["return_code"] = process.returncode
56
+
57
+ validate_result(case["expected"], result)
58
+ result["status"] = "passed"
59
+ except AssertionError as exc:
60
+ result["message"] = str(exc)
61
+ except Exception as exc:
62
+ result["message"] = f"Execution error: {str(exc)}"
63
+ finally:
64
+ result["duration"] = time.time() - start_time
65
+
66
+ return result
67
+
@@ -0,0 +1,43 @@
1
+ """
2
+ 进程工作器模块
3
+ 用于多进程并行测试执行,避免序列化问题
4
+ """
5
+
6
+ from typing import Dict, Any
7
+ from .execution import execute_single_test_case
8
+ from .types import TestCaseData
9
+
10
+ def run_test_in_process(test_index: int, case_data: Dict[str, Any], workspace: str = None) -> Dict[str, Any]:
11
+ """
12
+ 在独立进程中运行单个测试用例
13
+
14
+ Args:
15
+ test_index: 测试索引
16
+ case_data: 测试用例数据字典
17
+ workspace: 工作目录
18
+
19
+ Returns:
20
+ 测试结果字典
21
+ """
22
+ case: TestCaseData = {
23
+ "name": case_data["name"],
24
+ "command": case_data["command"],
25
+ "args": case_data["args"],
26
+ "expected": case_data["expected"],
27
+ "description": case_data.get("description"),
28
+ }
29
+
30
+ command_preview = f"{case['command']} {' '.join(case['args'])}".strip()
31
+ print(f" [Process Worker {test_index}] Executing command: {command_preview}")
32
+
33
+ result = execute_single_test_case(case, workspace)
34
+
35
+ if result["output"].strip():
36
+ print(f" [Process Worker {test_index}] Command output for {case['name']}:")
37
+ for line in result["output"].splitlines():
38
+ print(f" {line}")
39
+
40
+ if result["status"] != "passed" and result.get("message"):
41
+ print(f" [Process Worker {test_index}] Error for {case['name']}: {result['message']}")
42
+
43
+ return result
@@ -0,0 +1,45 @@
1
+ from typing import Any, Dict, List, Optional, TypedDict
2
+
3
+
4
+ class ExpectedResult(TypedDict, total=False):
5
+ """Expectation configuration for a single test case."""
6
+
7
+ return_code: Optional[int]
8
+ output_contains: List[str]
9
+ output_matches: Optional[str]
10
+
11
+
12
+ class TestCaseData(TypedDict):
13
+ """Input data shape for a test case after解析/路径处理."""
14
+
15
+ name: str
16
+ command: str
17
+ args: List[str]
18
+ expected: ExpectedResult
19
+ description: Optional[str]
20
+
21
+
22
+ class SetupConfig(TypedDict):
23
+ """Setup configuration (currently environment variables only)."""
24
+
25
+ environment_variables: Dict[str, str]
26
+
27
+
28
+ class TestSuiteConfig(TypedDict):
29
+ """Top-level configuration for a suite loaded from JSON/YAML."""
30
+
31
+ setup: Optional[SetupConfig]
32
+ test_cases: List[TestCaseData]
33
+
34
+
35
+ class TestResultData(TypedDict):
36
+ """Normalized result produced by executing a single test case."""
37
+
38
+ name: str
39
+ status: str # 'passed', 'failed'
40
+ message: str
41
+ command: str
42
+ output: str
43
+ return_code: Optional[int]
44
+ duration: float
45
+
@@ -123,31 +123,36 @@ class H5Comparator(BaseComparator):
123
123
 
124
124
  if self.tables or self.table_regex:
125
125
  # If specific tables or regex pattern is specified
126
+ regex_patterns = []
126
127
  if self.table_regex:
127
- # If the regex looks like a simple path (no regex metacharacters except . and /),
128
- # escape it to treat it as a literal string
129
- regex_str = self.table_regex
130
- self.logger.debug(f"Original table_regex: {regex_str}")
131
- # Check if it contains regex metacharacters other than . and /
132
- import string
133
- regex_metacharacters = set('[]{}()*+?^$|\\')
134
- if not any(char in regex_str for char in regex_metacharacters):
135
- # Escape dots and other special characters for literal matching
136
- regex_str = re.escape(regex_str)
137
- self.logger.debug(f"Treating table_regex as literal path, escaped: {regex_str}")
138
- else:
139
- self.logger.debug(f"Using table_regex as regular expression: {regex_str}")
140
- regex_pattern = re.compile(regex_str)
141
- else:
142
- regex_pattern = None
128
+ # Split by comma to support multiple regex patterns
129
+ regex_strings = [pattern.strip() for pattern in self.table_regex.split(',')]
130
+ self.logger.debug(f"Parsed regex patterns: {regex_strings}")
131
+
132
+ for regex_str in regex_strings:
133
+ # If the regex looks like a simple path (no regex metacharacters except . and /),
134
+ # escape it to treat it as a literal string
135
+ self.logger.debug(f"Processing regex pattern: {regex_str}")
136
+ # Check if it contains regex metacharacters other than . and /
137
+ import string
138
+ regex_metacharacters = set('[]{}()*+?^$|\\')
139
+ if not any(char in regex_str for char in regex_metacharacters):
140
+ # Escape dots and other special characters for literal matching
141
+ escaped_regex_str = re.escape(regex_str)
142
+ self.logger.debug(f"Treating pattern as literal path, escaped: {escaped_regex_str}")
143
+ regex_patterns.append(re.compile(escaped_regex_str))
144
+ else:
145
+ self.logger.debug(f"Using pattern as regular expression: {regex_str}")
146
+ regex_patterns.append(re.compile(regex_str))
143
147
 
144
148
  def should_process(name):
145
149
  if self.tables and name in self.tables:
146
150
  self.logger.debug(f"Matched by tables list: {name}")
147
151
  return True
148
- if regex_pattern and regex_pattern.fullmatch(name):
149
- self.logger.debug(f"Matched by regex: {name}")
150
- return True
152
+ for pattern in regex_patterns:
153
+ if pattern.fullmatch(name):
154
+ self.logger.debug(f"Matched by regex pattern {pattern.pattern}: {name}")
155
+ return True
151
156
  return False
152
157
 
153
158
  def process_item(name, item):
@@ -187,7 +192,7 @@ class H5Comparator(BaseComparator):
187
192
  self.logger.error(f"Error processing {table_path}: {str(e)}")
188
193
 
189
194
  # Then process regex pattern if specified
190
- if regex_pattern:
195
+ if regex_patterns:
191
196
  def visit_with_regex(name, obj):
192
197
  self.logger.debug(f"Checking path: {name}")
193
198
  if should_process(name):
@@ -53,8 +53,8 @@ class JsonComparator(TextComparator):
53
53
  json_text = ''.join(text_content)
54
54
  try:
55
55
  json_data = json.loads(json_text)
56
- if self.key_field:
57
- # Only keep the specified key field(s)
56
+ if self.key_field and isinstance(json_data, dict):
57
+ # Only keep the specified key field(s) when top-level is a mapping
58
58
  key_fields = self.key_field if isinstance(self.key_field, list) else [self.key_field]
59
59
  filtered_data = {key: json_data[key] for key in key_fields if key in json_data}
60
60
  return filtered_data
@@ -79,8 +79,9 @@ class JsonComparator(TextComparator):
79
79
  self._compare_json_key_based(content1, content2, "", differences)
80
80
  else:
81
81
  self._compare_json_exact(content1, content2, "", differences)
82
-
83
- return False, differences
82
+
83
+ identical = len(differences) == 0
84
+ return identical, differences
84
85
 
85
86
  def _compare_json_exact(self, obj1, obj2, path, differences, max_diffs=10):
86
87
  """
@@ -0,0 +1,63 @@
1
+ from typing import Optional, Dict, Any
2
+ from ..core.base_runner import BaseRunner
3
+ from ..core.test_case import TestCase
4
+ from ..core.execution import execute_single_test_case
5
+ from ..core.types import TestCaseData
6
+ from ..utils.path_resolver import PathResolver, parse_command_string, resolve_paths
7
+ import json
8
+ import sys
9
+
10
+ class JSONRunner(BaseRunner):
11
+ def __init__(self, config_file="test_cases.json", workspace: Optional[str] = None):
12
+ super().__init__(config_file, workspace)
13
+ # Backward-compatible attribute for tests that patch path_resolver
14
+ self.path_resolver = PathResolver(self.workspace)
15
+
16
+ def load_test_cases(self) -> None:
17
+ """Load test cases from a JSON file."""
18
+ try:
19
+ with open(self.config_path, 'r', encoding='utf-8') as f:
20
+ config = json.load(f)
21
+
22
+ # 加载setup配置
23
+ self.load_setup_from_config(config)
24
+
25
+ required_fields = ["name", "command", "args", "expected"]
26
+ for case in config["test_cases"]:
27
+ if not all(field in case for field in required_fields):
28
+ raise ValueError(f"Test case {case.get('name', 'unnamed')} is missing required fields")
29
+
30
+ # 使用智能命令解析,正确处理包含空格的路径
31
+ # Use resolver attribute (keeps backward compatibility with tests monkeypatching it)
32
+ case["command"] = self.path_resolver.parse_command_string(case["command"])
33
+ case["args"] = self.path_resolver.resolve_paths(case["args"])
34
+ self.test_cases.append(TestCase(**case))
35
+
36
+ print(f"Successfully loaded {len(self.test_cases)} test cases")
37
+ # print(self.test_cases)
38
+ except Exception as e:
39
+ sys.exit(f"Failed to load configuration file: {str(e)}")
40
+
41
+ def run_single_test(self, case: TestCase) -> Dict[str, str]:
42
+ case_data: TestCaseData = {
43
+ "name": case.name,
44
+ "command": case.command,
45
+ "args": case.args,
46
+ "expected": case.expected,
47
+ "description": case.description or None,
48
+ }
49
+
50
+ command_preview = f"{case_data['command']} {' '.join(case_data['args'])}".strip()
51
+ print(f" Executing command: {command_preview}")
52
+
53
+ result = execute_single_test_case(case_data, str(self.workspace) if self.workspace else None)
54
+
55
+ if result["output"].strip():
56
+ print(" Command output:")
57
+ for line in result["output"].splitlines():
58
+ print(f" {line}")
59
+
60
+ if result["status"] != "passed" and result.get("message"):
61
+ print(f" Error: {result['message']}")
62
+
63
+ return result
@@ -0,0 +1,79 @@
1
+ from typing import Optional, Dict, Any
2
+ from ..core.parallel_runner import ParallelRunner
3
+ from ..core.test_case import TestCase
4
+ from ..core.execution import execute_single_test_case
5
+ from ..core.types import TestCaseData
6
+ from ..utils.path_resolver import PathResolver, parse_command_string, resolve_paths
7
+ import json
8
+ import sys
9
+ import threading
10
+
11
+ class ParallelJSONRunner(ParallelRunner):
12
+ """并行JSON测试运行器"""
13
+
14
+ def __init__(self, config_file="test_cases.json", workspace: Optional[str] = None,
15
+ max_workers: Optional[int] = None, execution_mode: str = "thread"):
16
+ """
17
+ 初始化并行JSON运行器
18
+
19
+ Args:
20
+ config_file: JSON配置文件路径
21
+ workspace: 工作目录
22
+ max_workers: 最大并发数
23
+ execution_mode: 执行模式,'thread' 或 'process'
24
+ """
25
+ super().__init__(config_file, workspace, max_workers, execution_mode)
26
+ # Backward-compatible attribute for potential external patches/tests
27
+ self.path_resolver = PathResolver(self.workspace)
28
+ self._print_lock = threading.Lock() # 用于控制输出顺序
29
+
30
+ def load_test_cases(self) -> None:
31
+ """从JSON文件加载测试用例"""
32
+ try:
33
+ with open(self.config_path, 'r', encoding='utf-8') as f:
34
+ config = json.load(f)
35
+
36
+ # 加载setup配置
37
+ self.load_setup_from_config(config)
38
+
39
+ required_fields = ["name", "command", "args", "expected"]
40
+ for case in config["test_cases"]:
41
+ if not all(field in case for field in required_fields):
42
+ raise ValueError(f"Test case {case.get('name', 'unnamed')} is missing required fields")
43
+
44
+ # Use resolver attribute (keeps backward compatibility with tests monkeypatching it)
45
+ case["command"] = self.path_resolver.parse_command_string(case["command"])
46
+ case["args"] = self.path_resolver.resolve_paths(case["args"])
47
+ self.test_cases.append(TestCase(**case))
48
+
49
+ print(f"Successfully loaded {len(self.test_cases)} test cases")
50
+ except Exception as e:
51
+ sys.exit(f"Failed to load configuration file: {str(e)}")
52
+
53
+ def run_single_test(self, case: TestCase) -> Dict[str, Any]:
54
+ """运行单个测试用例(线程安全版本)"""
55
+ case_data: TestCaseData = {
56
+ "name": case.name,
57
+ "command": case.command,
58
+ "args": case.args,
59
+ "expected": case.expected,
60
+ "description": case.description or None,
61
+ }
62
+
63
+ command_preview = f"{case_data['command']} {' '.join(case_data['args'])}".strip()
64
+ with self._print_lock:
65
+ print(f" [Worker] Executing command: {command_preview}")
66
+
67
+ result = execute_single_test_case(case_data, str(self.workspace) if self.workspace else None)
68
+
69
+ if result["output"].strip():
70
+ with self._print_lock:
71
+ print(f" [Worker] Command output for {case.name}:")
72
+ for line in result["output"].splitlines():
73
+ print(f" {line}")
74
+
75
+ if result["status"] != "passed" and result.get("message"):
76
+ with self._print_lock:
77
+ print(f" [Worker] Error for {case.name}: {result['message']}")
78
+
79
+ return result
@@ -0,0 +1,62 @@
1
+ from typing import Optional, Dict, Any
2
+ from ..core.base_runner import BaseRunner
3
+ from ..core.test_case import TestCase
4
+ from ..core.execution import execute_single_test_case
5
+ from ..core.types import TestCaseData
6
+ from ..utils.path_resolver import PathResolver, parse_command_string, resolve_paths
7
+ import sys
8
+
9
+ class YAMLRunner(BaseRunner):
10
+ def __init__(self, config_file="test_cases.yaml", workspace: Optional[str] = None):
11
+ super().__init__(config_file, workspace)
12
+ # Backward-compatible attribute for tests that patch path_resolver
13
+ self.path_resolver = PathResolver(self.workspace)
14
+
15
+ def load_test_cases(self):
16
+ """Load test cases from a YAML file."""
17
+ try:
18
+ import yaml
19
+ with open(self.config_path, 'r', encoding='utf-8') as f:
20
+ config = yaml.safe_load(f)
21
+
22
+ # 加载setup配置
23
+ self.load_setup_from_config(config)
24
+
25
+ required_fields = ["name", "command", "args", "expected"]
26
+ for case in config["test_cases"]:
27
+ if not all(field in case for field in required_fields):
28
+ raise ValueError(f"Test case {case.get('name', 'unnamed')} is missing required fields")
29
+
30
+ # Use resolver attribute (keeps backward compatibility with tests monkeypatching it)
31
+ case["command"] = self.path_resolver.parse_command_string(case["command"])
32
+ case["args"] = self.path_resolver.resolve_paths(case["args"])
33
+ self.test_cases.append(TestCase(**case))
34
+
35
+ print(f"Successfully loaded {len(self.test_cases)} test cases")
36
+ except Exception as e:
37
+ sys.exit(f"Failed to load configuration file: {str(e)}")
38
+
39
+ def run_single_test(self, case: TestCase) -> Dict[str, Any]:
40
+ """Run a single test case and return the result"""
41
+ case_data: TestCaseData = {
42
+ "name": case.name,
43
+ "command": case.command,
44
+ "args": case.args,
45
+ "expected": case.expected,
46
+ "description": case.description or None,
47
+ }
48
+
49
+ command_preview = f"{case_data['command']} {' '.join(case_data['args'])}".strip()
50
+ print(f" Executing command: {command_preview}")
51
+
52
+ result = execute_single_test_case(case_data, str(self.workspace) if self.workspace else None)
53
+
54
+ if result["output"].strip():
55
+ print(" Command output:")
56
+ for line in result["output"].splitlines():
57
+ print(f" {line}")
58
+
59
+ if result["status"] != "passed" and result.get("message"):
60
+ print(f" Error: {result['message']}")
61
+
62
+ return result
@@ -0,0 +1,19 @@
1
+ # File: /python-test-framework/python-test-framework/src/utils/__init__.py
2
+
3
+ """
4
+ Utility functions for the CLI Testing Framework
5
+ """
6
+
7
+ from .path_resolver import (
8
+ PathResolver,
9
+ parse_command_string,
10
+ resolve_paths,
11
+ resolve_command,
12
+ )
13
+
14
+ __all__ = [
15
+ 'PathResolver',
16
+ 'parse_command_string',
17
+ 'resolve_paths',
18
+ 'resolve_command',
19
+ ]