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.
- {cli_test_framework-0.4.1/src/cli_test_framework.egg-info → cli_test_framework-0.4.2}/PKG-INFO +107 -7
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/README.md +106 -6
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/setup.py +1 -1
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/execution.py +15 -2
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/parallel_runner.py +3 -1
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/process_worker.py +2 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/types.py +1 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/binary_comparator.py +154 -8
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/h5_comparator.py +233 -20
- cli_test_framework-0.4.2/src/cli_test_framework/runners/parallel_json_runner.py +176 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2/src/cli_test_framework.egg-info}/PKG-INFO +107 -7
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/conftest.cpython-39-pytest-8.3.4.pyc +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/run_all.cpython-312.pyc +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/run_all.cpython-39.pyc +0 -0
- cli_test_framework-0.4.2/tests/e2e/__pycache__/__init__.cpython-312.pyc +0 -0
- cli_test_framework-0.4.2/tests/e2e/__pycache__/__init__.cpython-39.pyc +0 -0
- cli_test_framework-0.4.2/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-7.4.4.pyc +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- cli_test_framework-0.4.2/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-7.4.4.pyc +0 -0
- {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
- cli_test_framework-0.4.1/src/cli_test_framework/runners/parallel_json_runner.py +0 -92
- cli_test_framework-0.4.1/tests/e2e/__pycache__/__init__.cpython-312.pyc +0 -0
- cli_test_framework-0.4.1/tests/e2e/__pycache__/__init__.cpython-39.pyc +0 -0
- cli_test_framework-0.4.1/tests/e2e/__pycache__/test_user_flows.cpython-312-pytest-7.4.4.pyc +0 -0
- cli_test_framework-0.4.1/tests/unit/runners/__pycache__/test_json_yaml_runner.cpython-312-pytest-7.4.4.pyc +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/MANIFEST.in +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/docs/user_manual.md +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/pyproject.toml +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/setup.cfg +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/__init__.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/cli.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/commands/__init__.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/commands/compare.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/__init__.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/assertions.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/base_runner.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/setup.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/test_case.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/__init__.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/base_comparator.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/csv_comparator.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/factory.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/json_comparator.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/result.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/text_comparator.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/file_comparator/xml_comparator.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/runners/__init__.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/runners/json_runner.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/runners/yaml_runner.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/utils/__init__.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/utils/path_resolver.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/utils/report_generator.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/SOURCES.txt +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/dependency_links.txt +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/entry_points.txt +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/requires.txt +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework.egg-info/top_level.txt +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/README.md +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__init__.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/__init__.cpython-312.pyc +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/__pycache__/__init__.cpython-39.pyc +0 -0
- {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
- {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
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/conftest.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/demos/h5_filter_demo.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/demos/manual_report_example.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/demos/perf_parallel.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/e2e/__init__.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/e2e/test_user_flows.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_cases.json +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_cases.yaml +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_cases1.json +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_with_setup.json +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/fixtures/test_with_setup.yaml +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/test_binary_compare.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/test_h5_compare.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/test_json_compare.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/file_compare/test_text_compare.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/parallel/test_parallel_runner.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/integration/path_handling/test_spaces_in_paths.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/run_all.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/test_report.txt +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/unit/core/test_setup.py +0 -0
- {cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/tests/unit/runners/test_json_yaml_runner.py +0 -0
{cli_test_framework-0.4.1/src/cli_test_framework.egg-info → cli_test_framework-0.4.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-test-framework
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
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
|
|
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. **
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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. **
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.",
|
{cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/execution.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
import time
|
|
3
|
-
|
|
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
|
{cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/parallel_runner.py
RENAMED
|
@@ -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)
|
{cli_test_framework-0.4.1 → cli_test_framework-0.4.2}/src/cli_test_framework/core/process_worker.py
RENAMED
|
@@ -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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
"""
|