cli-test-framework 0.4.1__tar.gz → 0.4.2__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 (99) hide show
  1. {cli_test_framework-0.4.1/src/cli_test_framework.egg-info → cli_test_framework-0.4.2}/PKG-INFO +107 -7
  2. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/README.md +106 -6
  3. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/setup.py +1 -1
  4. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/execution.py +15 -2
  5. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/parallel_runner.py +3 -1
  6. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/process_worker.py +2 -0
  7. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/types.py +1 -0
  8. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/binary_comparator.py +154 -8
  9. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/h5_comparator.py +233 -20
  10. cli_test_framework-0.4.2/src/cli_test_framework/runners/parallel_json_runner.py +176 -0
  11. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2/src/cli_test_framework.egg-info}/PKG-INFO +107 -7
  12. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc +0 -0
  13. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/conftest.cpython-39-pytest-8.3.4.pyc +0 -0
  14. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/run_all.cpython-312.pyc +0 -0
  15. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/run_all.cpython-39.pyc +0 -0
  16. cli_test_framework-0.4.2/tests/e2e/__pycache__/__init__.cpython-312.pyc +0 -0
  17. cli_test_framework-0.4.2/tests/e2e/__pycache__/__init__.cpython-39.pyc +0 -0
  18. cli_test_framework-0.4.2/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-7.4.4.pyc +0 -0
  19. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/e2e/__pycache__/test_user_flows.cpython-39-pytest-8.3.4.pyc +0 -0
  20. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  21. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/__pycache__/test_binary_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  22. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  23. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/__pycache__/test_h5_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  24. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/__pycache__/test_json_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  25. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/__pycache__/test_json_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  26. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/__pycache__/test_text_compare.cpython-312-pytest-7.4.4.pyc +0 -0
  27. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/__pycache__/test_text_compare.cpython-39-pytest-8.3.4.pyc +0 -0
  28. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  29. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/parallel/__pycache__/test_parallel_runner.cpython-39-pytest-8.3.4.pyc +0 -0
  30. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-312-pytest-7.4.4.pyc +0 -0
  31. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/path_handling/__pycache__/test_spaces_in_paths.cpython-39-pytest-8.3.4.pyc +0 -0
  32. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/unit/core/__pycache__/test_setup.cpython-312-pytest-7.4.4.pyc +0 -0
  33. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/unit/core/__pycache__/test_setup.cpython-39-pytest-8.3.4.pyc +0 -0
  34. cli_test_framework-0.4.2/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  35. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-39-pytest-8.3.4.pyc +0 -0
  36. cli_test_framework-0.4.1/src/cli_test_framework/runners/parallel_json_runner.py +0 -92
  37. cli_test_framework-0.4.1/tests/e2e/__pycache__/__init__.cpython-312.pyc +0 -0
  38. cli_test_framework-0.4.1/tests/e2e/__pycache__/__init__.cpython-39.pyc +0 -0
  39. cli_test_framework-0.4.1/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-7.4.4.pyc +0 -0
  40. cli_test_framework-0.4.1/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  41. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/MANIFEST.in +0 -0
  42. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/docs/user_manual.md +0 -0
  43. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/pyproject.toml +0 -0
  44. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/setup.cfg +0 -0
  45. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/__init__.py +0 -0
  46. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/cli.py +0 -0
  47. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/commands/__init__.py +0 -0
  48. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/commands/compare.py +0 -0
  49. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/__init__.py +0 -0
  50. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/assertions.py +0 -0
  51. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/base_runner.py +0 -0
  52. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/setup.py +0 -0
  53. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/test_case.py +0 -0
  54. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/__init__.py +0 -0
  55. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/base_comparator.py +0 -0
  56. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/csv_comparator.py +0 -0
  57. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/factory.py +0 -0
  58. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/json_comparator.py +0 -0
  59. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/result.py +0 -0
  60. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/text_comparator.py +0 -0
  61. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/xml_comparator.py +0 -0
  62. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/runners/__init__.py +0 -0
  63. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/runners/json_runner.py +0 -0
  64. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/runners/yaml_runner.py +0 -0
  65. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/utils/__init__.py +0 -0
  66. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/utils/path_resolver.py +0 -0
  67. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/utils/report_generator.py +0 -0
  68. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/SOURCES.txt +0 -0
  69. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/dependency_links.txt +0 -0
  70. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/entry_points.txt +0 -0
  71. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/requires.txt +0 -0
  72. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/top_level.txt +0 -0
  73. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/README.md +0 -0
  74. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__init__.py +0 -0
  75. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  76. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/__init__.cpython-39.pyc +0 -0
  77. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/test_parallel_runner.cpython-312-pytest-7.4.4.pyc +0 -0
  78. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/test_setup_module.cpython-312-pytest-7.4.4.pyc +0 -0
  79. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/conftest.py +0 -0
  80. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/demos/h5_filter_demo.py +0 -0
  81. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/demos/manual_report_example.py +0 -0
  82. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/demos/perf_parallel.py +0 -0
  83. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/e2e/__init__.py +0 -0
  84. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/e2e/test_user_flows.py +0 -0
  85. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_cases.json +0 -0
  86. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_cases.yaml +0 -0
  87. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_cases1.json +0 -0
  88. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_with_setup.json +0 -0
  89. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_with_setup.yaml +0 -0
  90. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/test_binary_compare.py +0 -0
  91. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/test_h5_compare.py +0 -0
  92. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/test_json_compare.py +0 -0
  93. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/test_text_compare.py +0 -0
  94. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/parallel/test_parallel_runner.py +0 -0
  95. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/path_handling/test_spaces_in_paths.py +0 -0
  96. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/run_all.py +0 -0
  97. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/test_report.txt +0 -0
  98. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/unit/core/test_setup.py +0 -0
  99. {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/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.1
3
+ Version: 0.4.2
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
@@ -53,7 +53,7 @@ This is a lightweight and extensible automated testing framework that supports d
53
53
  - **📊 Comprehensive Reports**: Detailed pass rate statistics and failure diagnostics
54
54
  - **🔧 Thread-Safe Design**: Robust concurrent execution with proper synchronization
55
55
  - **📝 Advanced File Comparison**: Support for comparing various file types (text, binary, JSON, HDF5) with detailed diff output
56
- - **🎛️ Resource-Aware Scheduling**: Per-test timeout and resource hints (estimated time / memory / priority) with LPT-based ordering in parallel runs to improve throughput and avoid long-tail blocking
56
+ - **🎛️ Resource-Aware Scheduling**: Per-test timeout and resource hints (CPU cores / estimated time / memory / priority) with automatic CPU core detection and semaphore-based scheduling to prevent resource conflicts and solver "runaway" scenarios
57
57
 
58
58
  ## 3. Quick Start
59
59
 
@@ -81,16 +81,22 @@ success = runner.run_tests()
81
81
  ```python
82
82
  from src.runners.parallel_json_runner import ParallelJSONRunner
83
83
 
84
- # Multi-threaded execution (recommended for I/O-intensive tests)
84
+ # Multi-threaded execution with resource-aware scheduling
85
85
  runner = ParallelJSONRunner(
86
86
  config_file="path/to/test_cases.json",
87
87
  workspace="/project/root",
88
88
  max_workers=4, # Maximum concurrent workers
89
- execution_mode="thread" # "thread" or "process"
89
+ execution_mode="thread" # "thread" (supports CPU scheduling) or "process"
90
90
  )
91
91
  success = runner.run_tests()
92
92
  ```
93
93
 
94
+ **Resource-Aware Scheduling**: When using `execution_mode="thread"`, the framework automatically:
95
+ - Detects available CPU cores on your machine
96
+ - Manages CPU resource allocation using semaphore-based scheduling
97
+ - Prevents resource conflicts by queuing tasks that require more cores than available
98
+ - Injects environment variables to constrain solver threads (prevents "runaway" scenarios)
99
+
94
100
  ### Setup Module Usage
95
101
 
96
102
  ```python
@@ -151,6 +157,7 @@ compare-files binary1.bin binary2.bin --similarity
151
157
  "args": ["-i", "input.0000.rad"],
152
158
  "timeout": 36000,
153
159
  "resources": {
160
+ "cpu_cores": 4,
154
161
  "estimated_time": 18000,
155
162
  "min_memory_mb": 16000,
156
163
  "priority": 10
@@ -202,6 +209,61 @@ test_cases:
202
209
  output_matches: ".*\\.md$"
203
210
  ```
204
211
 
212
+ ### Resource-Aware Configuration
213
+
214
+ For simulation and long-running tasks (CAE/FEA), you can specify resource requirements to enable intelligent scheduling with automatic CPU core management:
215
+
216
+ ```json
217
+ {
218
+ "name": "Full_Car_Crash_Simulation",
219
+ "command": "radioss_solver",
220
+ "args": ["-i", "input.0000.rad"],
221
+ "timeout": 36000,
222
+ "resources": {
223
+ "cpu_cores": 4,
224
+ "estimated_time": 18000,
225
+ "min_memory_mb": 16000,
226
+ "priority": 10
227
+ },
228
+ "expected": {
229
+ "return_code": 0
230
+ }
231
+ }
232
+ ```
233
+
234
+ **Field Descriptions:**
235
+
236
+ - **`timeout`** (optional, float): **Hard limit in seconds**. If the test exceeds this time, it will be killed. Default: 3600 seconds (1 hour). Set to `null` for unlimited (not recommended).
237
+ - Common values: `60` (1 min), `300` (5 min), `3600` (1 hour), `18000` (5 hours), `86400` (24 hours)
238
+
239
+ - **`resources.cpu_cores`** (optional, int): **Number of CPU cores required by this task**. The framework automatically detects available CPU cores and uses semaphore-based scheduling to manage resource allocation. Tasks that require more cores than available will wait until resources are freed. Default: `1` core.
240
+ - **How it works**: The framework automatically detects your machine's CPU count (e.g., 16 cores), reserves 2 cores for the system, and creates a resource pool with the remaining cores (e.g., 14 cores). Tasks acquire cores from this pool before execution.
241
+ - **Environment injection**: When a task starts, the framework automatically sets `OMP_NUM_THREADS`, `MKL_NUM_THREADS`, and `NPROC` environment variables to constrain solver threads, preventing "runaway" scenarios where solvers ignore Python's scheduling.
242
+ - **Example scenarios**:
243
+ - Machine with 16 cores (14 available): 3 tasks requiring 4 cores each can run concurrently (3×4=12 cores used, 2 cores free)
244
+ - Machine with 8 cores (6 available): 1 task requiring 4 cores + 1 task requiring 2 cores can run concurrently
245
+ - **Recommendations**:
246
+ - Heavy simulations: `4-8` cores
247
+ - Medium tasks: `2-4` cores
248
+ - Lightweight scripts: `1` core (default)
249
+
250
+ - **`resources.estimated_time`** (optional, float): **Estimated duration in seconds** for LPT (Longest Processing Time) scheduling. Tasks with longer estimated times are scheduled first in parallel runs to improve throughput.
251
+ - Example: `18000` = 5 hours, `3600` = 1 hour, `300` = 5 minutes
252
+
253
+ - **`resources.min_memory_mb`** (optional, float): **Estimated memory requirement in MB**. Used for OOM (Out Of Memory) risk warnings. Currently informational only.
254
+ - Example: `16000` = 16 GB, `8192` = 8 GB, `4096` = 4 GB
255
+
256
+ - **`resources.priority`** (optional, int): **Task priority** (higher number = higher priority). Currently informational only. Recommended range: 0-10.
257
+ - `10`: Critical/blocking tasks (must run first)
258
+ - `7-9`: High priority (important business paths)
259
+ - `4-6`: Normal priority
260
+ - `1-3`: Low priority / exploratory tests
261
+ - `0` or unset: Default priority
262
+
263
+ **Note:**
264
+ - All time values (`timeout`, `estimated_time`) are in **seconds**, not milliseconds. This matches Python's `subprocess.run(timeout=...)` API.
265
+ - **CPU core scheduling is only active in thread mode**. Process mode will fall back to original behavior (process-level isolation provides some resource separation).
266
+
205
267
  ## 5. File Comparison Features
206
268
 
207
269
  ### Supported File Types
@@ -369,12 +431,35 @@ except Exception as e:
369
431
  max_workers = os.cpu_count() * 2
370
432
  ```
371
433
 
372
- 2. **Test Case Design**:
434
+ 2. **Resource-Aware Scheduling**:
435
+ - **For CAE/FEA simulations**: Always specify `cpu_cores` in your test configuration to prevent resource conflicts
436
+ - **Mixed workloads**: Configure lightweight tasks with `cpu_cores: 1` and heavy simulations with appropriate core counts
437
+ - **Example mixed configuration**:
438
+ ```json
439
+ {
440
+ "test_cases": [
441
+ {
442
+ "name": "Heavy Simulation",
443
+ "command": "radioss_solver",
444
+ "resources": { "cpu_cores": 4 }
445
+ },
446
+ {
447
+ "name": "Lightweight Script",
448
+ "command": "python script.py",
449
+ "resources": { "cpu_cores": 1 }
450
+ }
451
+ ]
452
+ }
453
+ ```
454
+ - **Monitor resource usage**: The framework prints resource acquisition/release logs to help you understand scheduling behavior
455
+
456
+ 3. **Test Case Design**:
373
457
  - ✅ Ensure test independence (no dependencies between tests)
374
458
  - ✅ Avoid shared resource conflicts (different files/ports)
375
459
  - ✅ Use relative paths (framework handles resolution automatically)
460
+ - ✅ Specify `cpu_cores` for CPU-intensive tasks to enable intelligent scheduling
376
461
 
377
- 3. **Debugging**:
462
+ 4. **Debugging**:
378
463
  ```python
379
464
  # Enable verbose output for debugging
380
465
  runner = ParallelJSONRunner(
@@ -559,7 +644,22 @@ compare-files data1.h5 data2.h5 --h5-data-filter '<=0.01'
559
644
 
560
645
  # 版本更新日志
561
646
 
562
- ## 0.3.7 (Latest)
647
+ ## 0.4.2 (Latest)
648
+
649
+ ### ✨ New Features
650
+ - **Resource-Aware CPU Scheduling**: Automatic CPU core detection and semaphore-based scheduling to prevent resource conflicts
651
+ - Added `cpu_cores` field in `resources` configuration to specify CPU requirements per task
652
+ - Automatic environment variable injection (`OMP_NUM_THREADS`, `MKL_NUM_THREADS`, `NPROC`) to constrain solver threads
653
+ - Prevents solver "runaway" scenarios where solvers ignore Python's scheduling
654
+ - Intelligent resource pool management: automatically reserves 2 cores for system use
655
+ - **Enhanced execution engine**: Support for custom environment variables in test execution
656
+
657
+ ### 🔧 Improvements
658
+ - **Better resource management**: Tasks now wait for available CPU cores instead of overwhelming the system
659
+ - **Automatic CPU detection**: No manual configuration needed - framework detects available cores automatically
660
+ - **Thread-safe resource allocation**: Semaphore-based scheduling ensures thread-safe resource management
661
+
662
+ ## 0.3.7
563
663
 
564
664
  ### 🐛 Bug Fixes
565
665
  - **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
@@ -18,7 +18,7 @@ This is a lightweight and extensible automated testing framework that supports d
18
18
  - **📊 Comprehensive Reports**: Detailed pass rate statistics and failure diagnostics
19
19
  - **🔧 Thread-Safe Design**: Robust concurrent execution with proper synchronization
20
20
  - **📝 Advanced File Comparison**: Support for comparing various file types (text, binary, JSON, HDF5) with detailed diff output
21
- - **🎛️ Resource-Aware Scheduling**: Per-test timeout and resource hints (estimated time / memory / priority) with LPT-based ordering in parallel runs to improve throughput and avoid long-tail blocking
21
+ - **🎛️ Resource-Aware Scheduling**: Per-test timeout and resource hints (CPU cores / estimated time / memory / priority) with automatic CPU core detection and semaphore-based scheduling to prevent resource conflicts and solver "runaway" scenarios
22
22
 
23
23
  ## 3. Quick Start
24
24
 
@@ -46,16 +46,22 @@ success = runner.run_tests()
46
46
  ```python
47
47
  from src.runners.parallel_json_runner import ParallelJSONRunner
48
48
 
49
- # Multi-threaded execution (recommended for I/O-intensive tests)
49
+ # Multi-threaded execution with resource-aware scheduling
50
50
  runner = ParallelJSONRunner(
51
51
  config_file="path/to/test_cases.json",
52
52
  workspace="/project/root",
53
53
  max_workers=4, # Maximum concurrent workers
54
- execution_mode="thread" # "thread" or "process"
54
+ execution_mode="thread" # "thread" (supports CPU scheduling) or "process"
55
55
  )
56
56
  success = runner.run_tests()
57
57
  ```
58
58
 
59
+ **Resource-Aware Scheduling**: When using `execution_mode="thread"`, the framework automatically:
60
+ - Detects available CPU cores on your machine
61
+ - Manages CPU resource allocation using semaphore-based scheduling
62
+ - Prevents resource conflicts by queuing tasks that require more cores than available
63
+ - Injects environment variables to constrain solver threads (prevents "runaway" scenarios)
64
+
59
65
  ### Setup Module Usage
60
66
 
61
67
  ```python
@@ -116,6 +122,7 @@ compare-files binary1.bin binary2.bin --similarity
116
122
  "args": ["-i", "input.0000.rad"],
117
123
  "timeout": 36000,
118
124
  "resources": {
125
+ "cpu_cores": 4,
119
126
  "estimated_time": 18000,
120
127
  "min_memory_mb": 16000,
121
128
  "priority": 10
@@ -167,6 +174,61 @@ test_cases:
167
174
  output_matches: ".*\\.md$"
168
175
  ```
169
176
 
177
+ ### Resource-Aware Configuration
178
+
179
+ For simulation and long-running tasks (CAE/FEA), you can specify resource requirements to enable intelligent scheduling with automatic CPU core management:
180
+
181
+ ```json
182
+ {
183
+ "name": "Full_Car_Crash_Simulation",
184
+ "command": "radioss_solver",
185
+ "args": ["-i", "input.0000.rad"],
186
+ "timeout": 36000,
187
+ "resources": {
188
+ "cpu_cores": 4,
189
+ "estimated_time": 18000,
190
+ "min_memory_mb": 16000,
191
+ "priority": 10
192
+ },
193
+ "expected": {
194
+ "return_code": 0
195
+ }
196
+ }
197
+ ```
198
+
199
+ **Field Descriptions:**
200
+
201
+ - **`timeout`** (optional, float): **Hard limit in seconds**. If the test exceeds this time, it will be killed. Default: 3600 seconds (1 hour). Set to `null` for unlimited (not recommended).
202
+ - Common values: `60` (1 min), `300` (5 min), `3600` (1 hour), `18000` (5 hours), `86400` (24 hours)
203
+
204
+ - **`resources.cpu_cores`** (optional, int): **Number of CPU cores required by this task**. The framework automatically detects available CPU cores and uses semaphore-based scheduling to manage resource allocation. Tasks that require more cores than available will wait until resources are freed. Default: `1` core.
205
+ - **How it works**: The framework automatically detects your machine's CPU count (e.g., 16 cores), reserves 2 cores for the system, and creates a resource pool with the remaining cores (e.g., 14 cores). Tasks acquire cores from this pool before execution.
206
+ - **Environment injection**: When a task starts, the framework automatically sets `OMP_NUM_THREADS`, `MKL_NUM_THREADS`, and `NPROC` environment variables to constrain solver threads, preventing "runaway" scenarios where solvers ignore Python's scheduling.
207
+ - **Example scenarios**:
208
+ - Machine with 16 cores (14 available): 3 tasks requiring 4 cores each can run concurrently (3×4=12 cores used, 2 cores free)
209
+ - Machine with 8 cores (6 available): 1 task requiring 4 cores + 1 task requiring 2 cores can run concurrently
210
+ - **Recommendations**:
211
+ - Heavy simulations: `4-8` cores
212
+ - Medium tasks: `2-4` cores
213
+ - Lightweight scripts: `1` core (default)
214
+
215
+ - **`resources.estimated_time`** (optional, float): **Estimated duration in seconds** for LPT (Longest Processing Time) scheduling. Tasks with longer estimated times are scheduled first in parallel runs to improve throughput.
216
+ - Example: `18000` = 5 hours, `3600` = 1 hour, `300` = 5 minutes
217
+
218
+ - **`resources.min_memory_mb`** (optional, float): **Estimated memory requirement in MB**. Used for OOM (Out Of Memory) risk warnings. Currently informational only.
219
+ - Example: `16000` = 16 GB, `8192` = 8 GB, `4096` = 4 GB
220
+
221
+ - **`resources.priority`** (optional, int): **Task priority** (higher number = higher priority). Currently informational only. Recommended range: 0-10.
222
+ - `10`: Critical/blocking tasks (must run first)
223
+ - `7-9`: High priority (important business paths)
224
+ - `4-6`: Normal priority
225
+ - `1-3`: Low priority / exploratory tests
226
+ - `0` or unset: Default priority
227
+
228
+ **Note:**
229
+ - All time values (`timeout`, `estimated_time`) are in **seconds**, not milliseconds. This matches Python's `subprocess.run(timeout=...)` API.
230
+ - **CPU core scheduling is only active in thread mode**. Process mode will fall back to original behavior (process-level isolation provides some resource separation).
231
+
170
232
  ## 5. File Comparison Features
171
233
 
172
234
  ### Supported File Types
@@ -334,12 +396,35 @@ except Exception as e:
334
396
  max_workers = os.cpu_count() * 2
335
397
  ```
336
398
 
337
- 2. **Test Case Design**:
399
+ 2. **Resource-Aware Scheduling**:
400
+ - **For CAE/FEA simulations**: Always specify `cpu_cores` in your test configuration to prevent resource conflicts
401
+ - **Mixed workloads**: Configure lightweight tasks with `cpu_cores: 1` and heavy simulations with appropriate core counts
402
+ - **Example mixed configuration**:
403
+ ```json
404
+ {
405
+ "test_cases": [
406
+ {
407
+ "name": "Heavy Simulation",
408
+ "command": "radioss_solver",
409
+ "resources": { "cpu_cores": 4 }
410
+ },
411
+ {
412
+ "name": "Lightweight Script",
413
+ "command": "python script.py",
414
+ "resources": { "cpu_cores": 1 }
415
+ }
416
+ ]
417
+ }
418
+ ```
419
+ - **Monitor resource usage**: The framework prints resource acquisition/release logs to help you understand scheduling behavior
420
+
421
+ 3. **Test Case Design**:
338
422
  - ✅ Ensure test independence (no dependencies between tests)
339
423
  - ✅ Avoid shared resource conflicts (different files/ports)
340
424
  - ✅ Use relative paths (framework handles resolution automatically)
425
+ - ✅ Specify `cpu_cores` for CPU-intensive tasks to enable intelligent scheduling
341
426
 
342
- 3. **Debugging**:
427
+ 4. **Debugging**:
343
428
  ```python
344
429
  # Enable verbose output for debugging
345
430
  runner = ParallelJSONRunner(
@@ -524,7 +609,22 @@ compare-files data1.h5 data2.h5 --h5-data-filter '<=0.01'
524
609
 
525
610
  # 版本更新日志
526
611
 
527
- ## 0.3.7 (Latest)
612
+ ## 0.4.2 (Latest)
613
+
614
+ ### ✨ New Features
615
+ - **Resource-Aware CPU Scheduling**: Automatic CPU core detection and semaphore-based scheduling to prevent resource conflicts
616
+ - Added `cpu_cores` field in `resources` configuration to specify CPU requirements per task
617
+ - Automatic environment variable injection (`OMP_NUM_THREADS`, `MKL_NUM_THREADS`, `NPROC`) to constrain solver threads
618
+ - Prevents solver "runaway" scenarios where solvers ignore Python's scheduling
619
+ - Intelligent resource pool management: automatically reserves 2 cores for system use
620
+ - **Enhanced execution engine**: Support for custom environment variables in test execution
621
+
622
+ ### 🔧 Improvements
623
+ - **Better resource management**: Tasks now wait for available CPU cores instead of overwhelming the system
624
+ - **Automatic CPU detection**: No manual configuration needed - framework detects available cores automatically
625
+ - **Thread-safe resource allocation**: Semaphore-based scheduling ensures thread-safe resource management
626
+
627
+ ## 0.3.7
528
628
 
529
629
  ### 🐛 Bug Fixes
530
630
  - **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
@@ -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.1",
11
+ version="0.4.2",
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.",
@@ -1,6 +1,7 @@
1
1
  import subprocess
2
2
  import time
3
- from typing import Optional
3
+ import os
4
+ from typing import Optional, Dict
4
5
 
5
6
  from .assertions import Assertions
6
7
  from .types import ExpectedResult, TestCaseData, TestResultData
@@ -23,9 +24,14 @@ def validate_result(expected: ExpectedResult, actual: TestResultData) -> None:
23
24
  assertions.matches(actual["output"], expected["output_matches"])
24
25
 
25
26
 
26
- def execute_single_test_case(case: TestCaseData, workspace: Optional[str] = None) -> TestResultData:
27
+ def execute_single_test_case(case: TestCaseData, workspace: Optional[str] = None, env: Optional[Dict[str, str]] = None) -> TestResultData:
27
28
  """
28
29
  Stateless execution of a single test case.
30
+
31
+ Args:
32
+ case: Test case data
33
+ workspace: Working directory for test execution
34
+ env: Optional environment variables to inject/override (merged with os.environ)
29
35
  """
30
36
  start_time = time.time()
31
37
  full_command = f"{case['command']} {' '.join(case['args'])}".strip()
@@ -41,6 +47,12 @@ def execute_single_test_case(case: TestCaseData, workspace: Optional[str] = None
41
47
  "duration": 0.0,
42
48
  }
43
49
 
50
+ # Prepare environment variables
51
+ # Default to current environment, merge with provided env if any
52
+ current_env = os.environ.copy()
53
+ if env:
54
+ current_env.update(env)
55
+
44
56
  try:
45
57
  process = subprocess.run(
46
58
  full_command,
@@ -50,6 +62,7 @@ def execute_single_test_case(case: TestCaseData, workspace: Optional[str] = None
50
62
  check=False,
51
63
  shell=True,
52
64
  timeout=timeout_limit if timeout_limit is not None else None,
65
+ env=current_env,
53
66
  )
54
67
 
55
68
  output = process.stdout + process.stderr
@@ -59,7 +59,9 @@ class ParallelRunner(BaseRunner):
59
59
  "name": case.name,
60
60
  "command": case.command,
61
61
  "args": case.args,
62
- "expected": case.expected
62
+ "expected": case.expected,
63
+ "timeout": case.timeout,
64
+ "resources": case.resources
63
65
  },
64
66
  str(self.workspace) if self.workspace else None
65
67
  ): (i, case)
@@ -25,6 +25,8 @@ def run_test_in_process(test_index: int, case_data: Dict[str, Any], workspace: s
25
25
  "args": case_data["args"],
26
26
  "expected": case_data["expected"],
27
27
  "description": case_data.get("description"),
28
+ "timeout": case_data.get("timeout"),
29
+ "resources": case_data.get("resources"),
28
30
  }
29
31
 
30
32
  command_preview = f"{case['command']} {' '.join(case['args'])}".strip()
@@ -15,6 +15,7 @@ class ResourceRequirements(TypedDict, total=False):
15
15
  estimated_time: float # seconds, used for ordering (LPT)
16
16
  min_memory_mb: float # soft hint to avoid OOM
17
17
  priority: int # higher value => higher priority
18
+ cpu_cores: int # number of CPU cores required by this task
18
19
 
19
20
 
20
21
  class TestCaseData(TypedDict):
@@ -182,7 +182,7 @@ class BinaryComparator(BaseComparator):
182
182
 
183
183
  def compare_files(self, file1, file2, start_line=0, end_line=None, start_column=0, end_column=None):
184
184
  """
185
- @brief Compare two binary files with optional similarity calculation
185
+ @brief Compare two binary files with optional similarity calculation using chunk-based streaming
186
186
  @param file1 Path: Path to the first binary file
187
187
  @param file2 Path: Path to the second binary file
188
188
  @param start_line int: Starting byte offset
@@ -190,6 +190,8 @@ class BinaryComparator(BaseComparator):
190
190
  @param start_column int: Ignored for binary files
191
191
  @param end_column int: Ignored for binary files
192
192
  @return ComparisonResult: Result object containing comparison details
193
+ @details This method implements chunk-based streaming comparison to avoid loading
194
+ entire files into memory, making it suitable for large files with O(1) memory usage.
193
195
  """
194
196
  from pathlib import Path
195
197
  from .result import ComparisonResult
@@ -207,26 +209,170 @@ class BinaryComparator(BaseComparator):
207
209
  file2_path = Path(file2)
208
210
  result.file1_size = file1_path.stat().st_size
209
211
  result.file2_size = file2_path.stat().st_size
210
- self.logger.debug("Reading content from files")
211
- content1 = self.read_content(file1, start_line, end_line, start_column, end_column)
212
- content2 = self.read_content(file2, start_line, end_line, start_column, end_column)
213
- self.logger.debug("Comparing content")
214
- identical, differences = self.compare_content(content1, content2)
215
- result.identical = identical
216
- result.differences = differences
212
+
213
+ # Quick size check: if file sizes differ and similarity is not requested,
214
+ # we can return early without streaming
215
+ if result.file1_size != result.file2_size and not self.similarity:
216
+ # Adjust sizes based on offset if specified
217
+ adjusted_size1 = result.file1_size - start_line
218
+ adjusted_size2 = result.file2_size - start_line
219
+ if end_line is not None:
220
+ adjusted_size1 = min(adjusted_size1, end_line - start_line)
221
+ adjusted_size2 = min(adjusted_size2, end_line - start_line)
222
+
223
+ if adjusted_size1 != adjusted_size2:
224
+ result.identical = False
225
+ result.differences.append(Difference(
226
+ position="file size",
227
+ expected=f"{result.file1_size} bytes",
228
+ actual=f"{result.file2_size} bytes",
229
+ diff_type="size"
230
+ ))
231
+ return result
232
+
233
+ # If similarity calculation is needed, we still need to read full content
234
+ # but for regular comparison, use chunk-based streaming
217
235
  if self.similarity:
236
+ # For similarity calculation, we still need full content
237
+ # This is a limitation of the current LCS algorithm
238
+ self.logger.debug("Reading full content for similarity calculation")
239
+ content1 = self.read_content(file1, start_line, end_line, start_column, end_column)
240
+ content2 = self.read_content(file2, start_line, end_line, start_column, end_column)
241
+ identical, differences = self.compare_content(content1, content2)
218
242
  if (len(content1) + len(content2)) > 0:
219
243
  lcs_len = self.compute_lcs_length(content1, content2)
220
244
  similarity = 2 * lcs_len / (len(content1) + len(content2))
221
245
  else:
222
246
  similarity = 1
223
247
  result.similarity = similarity
248
+ else:
249
+ # Chunk-based streaming comparison for O(1) memory usage
250
+ self.logger.debug("Using chunk-based streaming comparison")
251
+ identical, differences = self._compare_files_streaming(
252
+ file1_path, file2_path, start_line, end_line
253
+ )
254
+
255
+ result.identical = identical
256
+ result.differences = differences
224
257
  return result
225
258
  except Exception as e:
226
259
  self.logger.error(f"Error during comparison: {str(e)}")
227
260
  result.error = str(e)
228
261
  result.identical = False
229
262
  return result
263
+
264
+ def _compare_files_streaming(self, file1_path, file2_path, start_offset=0, end_offset=None):
265
+ """
266
+ @brief Compare two binary files using chunk-based streaming
267
+ @param file1_path Path: Path to the first binary file
268
+ @param file2_path Path: Path to the second binary file
269
+ @param start_offset int: Starting byte offset
270
+ @param end_offset int: Ending byte offset (None for end of file)
271
+ @return tuple: (bool, list) - (identical, differences)
272
+ @details This method compares files chunk by chunk without loading entire files
273
+ into memory, achieving O(1) memory complexity.
274
+ """
275
+ differences = []
276
+ max_differences = 10 # Limit number of differences reported
277
+
278
+ try:
279
+ with open(file1_path, 'rb') as f1, open(file2_path, 'rb') as f2:
280
+ # Seek to start offset if specified
281
+ if start_offset > 0:
282
+ f1.seek(start_offset)
283
+ f2.seek(start_offset)
284
+
285
+ # Calculate bytes to read if end_offset is specified
286
+ bytes_to_read = None
287
+ if end_offset is not None:
288
+ if end_offset <= start_offset:
289
+ raise ValueError("End offset must be greater than start offset")
290
+ bytes_to_read = end_offset - start_offset
291
+
292
+ chunk_size = self.chunk_size
293
+ current_offset = start_offset
294
+ bytes_read_total = 0
295
+
296
+ while True:
297
+ # Determine how many bytes to read in this chunk
298
+ if bytes_to_read is not None:
299
+ remaining = bytes_to_read - bytes_read_total
300
+ if remaining <= 0:
301
+ break
302
+ read_size = min(chunk_size, remaining)
303
+ else:
304
+ read_size = chunk_size
305
+
306
+ # Read chunks from both files
307
+ chunk1 = f1.read(read_size)
308
+ chunk2 = f2.read(read_size)
309
+
310
+ # If both files are exhausted, we're done
311
+ if not chunk1 and not chunk2:
312
+ break
313
+
314
+ # If one file ends before the other, that's a difference
315
+ if len(chunk1) != len(chunk2):
316
+ differences.append(Difference(
317
+ position=f"byte {current_offset}",
318
+ expected=f"{len(chunk1)} bytes in chunk",
319
+ actual=f"{len(chunk2)} bytes in chunk",
320
+ diff_type="content"
321
+ ))
322
+ break
323
+
324
+ # Compare chunks byte by byte
325
+ if chunk1 != chunk2:
326
+ # Find the exact byte position where the difference starts
327
+ for i in range(len(chunk1)):
328
+ if chunk1[i] != chunk2[i]:
329
+ abs_pos = current_offset + i
330
+
331
+ # Show a few bytes before and after the difference for context
332
+ context_size = 8
333
+ context_start = max(0, i - context_size)
334
+ context_end = min(len(chunk1), i + context_size)
335
+
336
+ # Get context bytes (may need to read previous chunk)
337
+ context1 = chunk1[context_start:context_end]
338
+ context2 = chunk2[context_start:context_end]
339
+
340
+ expected_hex = ' '.join(f"{b:02x}" for b in context1)
341
+ actual_hex = ' '.join(f"{b:02x}" for b in context2)
342
+
343
+ differences.append(Difference(
344
+ position=f"byte {abs_pos}",
345
+ expected=expected_hex,
346
+ actual=actual_hex,
347
+ diff_type="content"
348
+ ))
349
+
350
+ # Stop after finding first difference in chunk
351
+ # or if we've reached max differences
352
+ if len(differences) >= max_differences:
353
+ differences.append(Difference(
354
+ position=None,
355
+ expected=None,
356
+ actual=None,
357
+ diff_type="more differences not shown"
358
+ ))
359
+ return False, differences
360
+ break
361
+
362
+ current_offset += len(chunk1)
363
+ bytes_read_total += len(chunk1)
364
+
365
+ # If we didn't read a full chunk, we've reached EOF
366
+ if len(chunk1) < read_size:
367
+ break
368
+
369
+ identical = len(differences) == 0
370
+ return identical, differences
371
+
372
+ except FileNotFoundError as e:
373
+ raise ValueError(f"File not found: {e}")
374
+ except IOError as e:
375
+ raise ValueError(f"Error reading file: {str(e)}")
230
376
 
231
377
  def get_file_hash(self, file_path, chunk_size=8192):
232
378
  """