mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2025-12-21 12:20:48 +08:00
Add comprehensive pip dependency conflict resolution framework as draft implementation. This is self-contained and does not affect existing ComfyUI Manager functionality. Key components: - pip_util.py with PipBatch class for policy-driven package management - Lazy-loaded policy system supporting base + user overrides - Multi-stage policy execution (uninstall → apply_first_match → apply_all_matches → restore) - Conditional policies based on platform, installed packages, and ComfyUI version - Comprehensive test suite covering edge cases, workflows, and platform scenarios - Design and implementation documentation Policy capabilities (draft): - Package replacement (e.g., PIL → Pillow, opencv-python → opencv-contrib-python) - Version pinning to prevent dependency conflicts - Dependency protection during installations - Platform-specific handling (Linux/Windows, GPU detection) - Pre-removal and post-restoration workflows Testing infrastructure: - Pytest-based test suite with isolated environments - Dependency analysis tools for conflict detection - Coverage for policy priority, edge cases, and environment recovery Status: Draft implementation complete, integration with manager workflows pending.
574 lines
13 KiB
Markdown
574 lines
13 KiB
Markdown
# pip_util Test Scenarios - Test Data Specification
|
|
|
|
This document precisely defines all test scenarios, packages, versions, and expected behaviors used in the pip_util test suite.
|
|
|
|
## Table of Contents
|
|
1. [Test Scenario 1: Dependency Version Protection](#scenario-1-dependency-version-protection)
|
|
2. [Test Scenario 2: Complex Dependency Chain](#scenario-2-complex-dependency-chain)
|
|
3. [Test Scenario 3: Package Deletion and Restore](#scenario-3-package-deletion-and-restore)
|
|
4. [Test Scenario 4: Version Change and Restore](#scenario-4-version-change-and-restore)
|
|
5. [Test Scenario 5: Full Workflow Integration](#scenario-5-full-workflow-integration)
|
|
6. [Test Scenario 6: Pin Failure Retry](#scenario-6-pin-failure-retry)
|
|
|
|
---
|
|
|
|
## Scenario 1: Dependency Version Protection
|
|
|
|
**File**: `test_dependency_protection.py::test_dependency_version_protection_with_pin`
|
|
|
|
**Purpose**: Verify that `pin_dependencies` policy prevents dependency upgrades during package installation.
|
|
|
|
### Initial Environment State
|
|
```python
|
|
installed_packages = {
|
|
"urllib3": "1.26.15", # OLD stable version
|
|
"certifi": "2023.7.22", # OLD version
|
|
"charset-normalizer": "3.2.0" # OLD version
|
|
}
|
|
```
|
|
|
|
### Policy Configuration
|
|
```json
|
|
{
|
|
"requests": {
|
|
"apply_all_matches": [
|
|
{
|
|
"type": "pin_dependencies",
|
|
"pinned_packages": ["urllib3", "certifi", "charset-normalizer"],
|
|
"on_failure": "retry_without_pin"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Action
|
|
```python
|
|
batch.install("requests")
|
|
```
|
|
|
|
### Expected pip Command
|
|
```bash
|
|
pip install requests urllib3==1.26.15 certifi==2023.7.22 charset-normalizer==3.2.0
|
|
```
|
|
|
|
### Expected Final State
|
|
```python
|
|
installed_packages = {
|
|
"urllib3": "1.26.15", # PROTECTED - stayed at old version
|
|
"certifi": "2023.7.22", # PROTECTED - stayed at old version
|
|
"charset-normalizer": "3.2.0", # PROTECTED - stayed at old version
|
|
"requests": "2.31.0" # NEWLY installed
|
|
}
|
|
```
|
|
|
|
### Without Pin (What Would Happen)
|
|
```python
|
|
# If pin_dependencies was NOT used:
|
|
installed_packages = {
|
|
"urllib3": "2.1.0", # UPGRADED to 2.x (breaking change)
|
|
"certifi": "2024.2.2", # UPGRADED to latest
|
|
"charset-normalizer": "3.3.2", # UPGRADED to latest
|
|
"requests": "2.31.0"
|
|
}
|
|
```
|
|
|
|
**Key Point**: Pin prevents `urllib3` from upgrading to 2.x, which has breaking API changes.
|
|
|
|
---
|
|
|
|
## Scenario 2: Complex Dependency Chain
|
|
|
|
**File**: `test_dependency_protection.py::test_dependency_chain_with_click_colorama`
|
|
|
|
**Purpose**: Verify that `force_version` + `pin_dependencies` work together correctly.
|
|
|
|
### Initial Environment State
|
|
```python
|
|
installed_packages = {
|
|
"colorama": "0.4.6" # Existing dependency
|
|
}
|
|
```
|
|
|
|
### Policy Configuration
|
|
```json
|
|
{
|
|
"click": {
|
|
"apply_first_match": [
|
|
{
|
|
"condition": {
|
|
"type": "installed",
|
|
"package": "colorama",
|
|
"spec": "<0.5.0"
|
|
},
|
|
"type": "force_version",
|
|
"version": "8.1.3",
|
|
"reason": "click 8.1.3 compatible with colorama <0.5"
|
|
}
|
|
],
|
|
"apply_all_matches": [
|
|
{
|
|
"type": "pin_dependencies",
|
|
"pinned_packages": ["colorama"]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Condition Evaluation
|
|
```python
|
|
# Check: colorama installed AND version < 0.5.0?
|
|
colorama_installed = True
|
|
colorama_version = "0.4.6" # 0.4.6 < 0.5.0 → True
|
|
# Result: Condition satisfied → apply force_version
|
|
```
|
|
|
|
### Action
|
|
```python
|
|
batch.install("click")
|
|
```
|
|
|
|
### Expected pip Command
|
|
```bash
|
|
pip install click==8.1.3 colorama==0.4.6
|
|
```
|
|
|
|
### Expected Final State
|
|
```python
|
|
installed_packages = {
|
|
"colorama": "0.4.6", # PINNED - version protected
|
|
"click": "8.1.3" # FORCED to specific version
|
|
}
|
|
```
|
|
|
|
**Key Point**:
|
|
- `force_version` forces click to install version 8.1.3
|
|
- `pin_dependencies` ensures colorama stays at 0.4.6
|
|
|
|
---
|
|
|
|
## Scenario 3: Package Deletion and Restore
|
|
|
|
**File**: `test_environment_recovery.py::test_package_deletion_and_restore`
|
|
|
|
**Purpose**: Verify that deleted packages can be restored to required versions.
|
|
|
|
### Initial Environment State
|
|
```python
|
|
installed_packages = {
|
|
"six": "1.16.0", # Critical package
|
|
"attrs": "23.1.0",
|
|
"packaging": "23.1"
|
|
}
|
|
```
|
|
|
|
### Policy Configuration
|
|
```json
|
|
{
|
|
"six": {
|
|
"restore": [
|
|
{
|
|
"target": "six",
|
|
"version": "1.16.0",
|
|
"reason": "six must be maintained at 1.16.0 for compatibility"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Action Sequence
|
|
|
|
**Step 1**: Install package that removes six
|
|
```python
|
|
batch.install("python-dateutil")
|
|
```
|
|
|
|
**Step 1 Result**: six is DELETED
|
|
```python
|
|
installed_packages = {
|
|
# "six": "1.16.0", # ❌ DELETED by python-dateutil
|
|
"attrs": "23.1.0",
|
|
"packaging": "23.1",
|
|
"python-dateutil": "2.8.2" # ✅ NEW
|
|
}
|
|
```
|
|
|
|
**Step 2**: Restore deleted packages
|
|
```python
|
|
batch.ensure_installed()
|
|
```
|
|
|
|
**Step 2 Result**: six is RESTORED
|
|
```python
|
|
installed_packages = {
|
|
"six": "1.16.0", # ✅ RESTORED to required version
|
|
"attrs": "23.1.0",
|
|
"packaging": "23.1",
|
|
"python-dateutil": "2.8.2"
|
|
}
|
|
```
|
|
|
|
### Expected pip Commands
|
|
```bash
|
|
# Step 1: Install
|
|
pip install python-dateutil
|
|
|
|
# Step 2: Restore
|
|
pip install six==1.16.0
|
|
```
|
|
|
|
**Key Point**: `restore` policy automatically reinstalls deleted packages.
|
|
|
|
---
|
|
|
|
## Scenario 4: Version Change and Restore
|
|
|
|
**File**: `test_environment_recovery.py::test_version_change_and_restore`
|
|
|
|
**Purpose**: Verify that packages with changed versions can be restored to required versions.
|
|
|
|
### Initial Environment State
|
|
```python
|
|
installed_packages = {
|
|
"urllib3": "1.26.15", # OLD version (required)
|
|
"certifi": "2023.7.22"
|
|
}
|
|
```
|
|
|
|
### Policy Configuration
|
|
```json
|
|
{
|
|
"urllib3": {
|
|
"restore": [
|
|
{
|
|
"condition": {
|
|
"type": "installed",
|
|
"spec": "!=1.26.15"
|
|
},
|
|
"target": "urllib3",
|
|
"version": "1.26.15",
|
|
"reason": "urllib3 must be 1.26.15 for compatibility"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Action Sequence
|
|
|
|
**Step 1**: Install package that upgrades urllib3
|
|
```python
|
|
batch.install("requests")
|
|
```
|
|
|
|
**Step 1 Result**: urllib3 is UPGRADED
|
|
```python
|
|
installed_packages = {
|
|
"urllib3": "2.1.0", # ❌ UPGRADED from 1.26.15 to 2.1.0
|
|
"certifi": "2023.7.22",
|
|
"requests": "2.31.0" # ✅ NEW
|
|
}
|
|
```
|
|
|
|
**Step 2**: Check restore condition
|
|
```python
|
|
# Condition: urllib3 installed AND version != 1.26.15?
|
|
urllib3_version = "2.1.0"
|
|
condition_met = (urllib3_version != "1.26.15") # True
|
|
# Result: Restore urllib3 to 1.26.15
|
|
```
|
|
|
|
**Step 2**: Restore to required version
|
|
```python
|
|
batch.ensure_installed()
|
|
```
|
|
|
|
**Step 2 Result**: urllib3 is DOWNGRADED
|
|
```python
|
|
installed_packages = {
|
|
"urllib3": "1.26.15", # ✅ RESTORED to required version
|
|
"certifi": "2023.7.22",
|
|
"requests": "2.31.0"
|
|
}
|
|
```
|
|
|
|
### Expected pip Commands
|
|
```bash
|
|
# Step 1: Install (causes upgrade)
|
|
pip install requests
|
|
|
|
# Step 2: Restore (downgrade)
|
|
pip install urllib3==1.26.15
|
|
```
|
|
|
|
**Key Point**: `restore` with condition can revert unwanted version changes.
|
|
|
|
---
|
|
|
|
## Scenario 5: Full Workflow Integration
|
|
|
|
**File**: `test_full_workflow_integration.py::test_uninstall_install_restore_workflow`
|
|
|
|
**Purpose**: Verify complete workflow: uninstall → install → restore.
|
|
|
|
### Initial Environment State
|
|
```python
|
|
installed_packages = {
|
|
"old-package": "1.0.0", # To be removed
|
|
"critical-package": "1.2.3", # To be restored
|
|
"urllib3": "1.26.15",
|
|
"certifi": "2023.7.22"
|
|
}
|
|
```
|
|
|
|
### Policy Configuration
|
|
```json
|
|
{
|
|
"old-package": {
|
|
"uninstall": [
|
|
{
|
|
"target": "old-package"
|
|
}
|
|
]
|
|
},
|
|
"requests": {
|
|
"apply_all_matches": [
|
|
{
|
|
"type": "pin_dependencies",
|
|
"pinned_packages": ["urllib3", "certifi"]
|
|
}
|
|
]
|
|
},
|
|
"critical-package": {
|
|
"restore": [
|
|
{
|
|
"target": "critical-package",
|
|
"version": "1.2.3"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Action Sequence
|
|
|
|
**Step 1**: Remove old packages
|
|
```python
|
|
removed = batch.ensure_not_installed()
|
|
```
|
|
|
|
**Step 1 Result**:
|
|
```python
|
|
installed_packages = {
|
|
# "old-package": "1.0.0", # ❌ REMOVED
|
|
"critical-package": "1.2.3",
|
|
"urllib3": "1.26.15",
|
|
"certifi": "2023.7.22"
|
|
}
|
|
removed = ["old-package"]
|
|
```
|
|
|
|
**Step 2**: Install new package with pins
|
|
```python
|
|
batch.install("requests")
|
|
```
|
|
|
|
**Step 2 Result**:
|
|
```python
|
|
installed_packages = {
|
|
"critical-package": "1.2.3",
|
|
"urllib3": "1.26.15", # PINNED - no upgrade
|
|
"certifi": "2023.7.22", # PINNED - no upgrade
|
|
"requests": "2.31.0" # NEW
|
|
}
|
|
```
|
|
|
|
**Step 3**: Restore required packages
|
|
```python
|
|
restored = batch.ensure_installed()
|
|
```
|
|
|
|
**Step 3 Result**:
|
|
```python
|
|
installed_packages = {
|
|
"critical-package": "1.2.3", # Still present
|
|
"urllib3": "1.26.15",
|
|
"certifi": "2023.7.22",
|
|
"requests": "2.31.0"
|
|
}
|
|
restored = [] # Nothing to restore (all present)
|
|
```
|
|
|
|
### Expected pip Commands
|
|
```bash
|
|
# Step 1: Uninstall
|
|
pip uninstall -y old-package
|
|
|
|
# Step 2: Install with pins
|
|
pip install requests urllib3==1.26.15 certifi==2023.7.22
|
|
|
|
# Step 3: (No command - all packages present)
|
|
```
|
|
|
|
**Key Point**: Complete workflow demonstrates policy coordination.
|
|
|
|
---
|
|
|
|
## Scenario 6: Pin Failure Retry
|
|
|
|
**File**: `test_pin_failure_retry.py::test_pin_failure_retry_without_pin_succeeds`
|
|
|
|
**Purpose**: Verify automatic retry without pins when installation with pins fails.
|
|
|
|
### Initial Environment State
|
|
```python
|
|
installed_packages = {
|
|
"urllib3": "1.26.15",
|
|
"certifi": "2023.7.22"
|
|
}
|
|
```
|
|
|
|
### Policy Configuration
|
|
```json
|
|
{
|
|
"requests": {
|
|
"apply_all_matches": [
|
|
{
|
|
"type": "pin_dependencies",
|
|
"pinned_packages": ["urllib3", "certifi"],
|
|
"on_failure": "retry_without_pin"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Action
|
|
```python
|
|
batch.install("requests")
|
|
```
|
|
|
|
### Attempt 1: Install WITH pins (FAILS)
|
|
```bash
|
|
# Command:
|
|
pip install requests urllib3==1.26.15 certifi==2023.7.22
|
|
|
|
# Result: FAILURE (dependency conflict)
|
|
# Error: "Package conflict: requests requires urllib3>=2.0"
|
|
```
|
|
|
|
### Attempt 2: Retry WITHOUT pins (SUCCEEDS)
|
|
```bash
|
|
# Command:
|
|
pip install requests
|
|
|
|
# Result: SUCCESS
|
|
```
|
|
|
|
**Final State**:
|
|
```python
|
|
installed_packages = {
|
|
"urllib3": "2.1.0", # UPGRADED (pins removed)
|
|
"certifi": "2024.2.2", # UPGRADED (pins removed)
|
|
"requests": "2.31.0" # INSTALLED
|
|
}
|
|
```
|
|
|
|
### Expected Behavior
|
|
1. **First attempt**: Install with pinned versions
|
|
2. **On failure**: Log warning about conflict
|
|
3. **Retry**: Install without pins
|
|
4. **Success**: Package installed, dependencies upgraded
|
|
|
|
**Key Point**: `retry_without_pin` provides automatic fallback for compatibility issues.
|
|
|
|
---
|
|
|
|
## Scenario 6b: Pin Failure with Hard Fail
|
|
|
|
**File**: `test_pin_failure_retry.py::test_pin_failure_with_fail_raises_exception`
|
|
|
|
**Purpose**: Verify that `on_failure: fail` raises exception instead of retrying.
|
|
|
|
### Initial Environment State
|
|
```python
|
|
installed_packages = {
|
|
"urllib3": "1.26.15",
|
|
"certifi": "2023.7.22"
|
|
}
|
|
```
|
|
|
|
### Policy Configuration
|
|
```json
|
|
{
|
|
"requests": {
|
|
"apply_all_matches": [
|
|
{
|
|
"type": "pin_dependencies",
|
|
"pinned_packages": ["urllib3", "certifi"],
|
|
"on_failure": "fail"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Action
|
|
```python
|
|
batch.install("requests")
|
|
```
|
|
|
|
### Attempt 1: Install WITH pins (FAILS)
|
|
```bash
|
|
# Command:
|
|
pip install requests urllib3==1.26.15 certifi==2023.7.22
|
|
|
|
# Result: FAILURE (dependency conflict)
|
|
# Error: "Package conflict: requests requires urllib3>=2.0"
|
|
```
|
|
|
|
### Expected Behavior
|
|
1. **First attempt**: Install with pinned versions
|
|
2. **On failure**: Raise `subprocess.CalledProcessError`
|
|
3. **No retry**: Exception propagates to caller
|
|
4. **No changes**: Environment unchanged
|
|
|
|
**Key Point**: `on_failure: fail` ensures strict version requirements.
|
|
|
|
---
|
|
|
|
## Summary Table: All Test Packages
|
|
|
|
| Package | Initial Version | Action | Final Version | Role |
|
|
|---------|----------------|--------|---------------|------|
|
|
| **urllib3** | 1.26.15 | Pin | 1.26.15 | Protected dependency |
|
|
| **certifi** | 2023.7.22 | Pin | 2023.7.22 | Protected dependency |
|
|
| **charset-normalizer** | 3.2.0 | Pin | 3.2.0 | Protected dependency |
|
|
| **requests** | (not installed) | Install | 2.31.0 | New package |
|
|
| **colorama** | 0.4.6 | Pin | 0.4.6 | Protected dependency |
|
|
| **click** | (not installed) | Force version | 8.1.3 | New package with forced version |
|
|
| **six** | 1.16.0 | Delete→Restore | 1.16.0 | Deleted then restored |
|
|
| **python-dateutil** | (not installed) | Install | 2.8.2 | Package that deletes six |
|
|
| **attrs** | 23.1.0 | No change | 23.1.0 | Bystander package |
|
|
| **packaging** | 23.1 | No change | 23.1 | Bystander package |
|
|
|
|
## Policy Types Summary
|
|
|
|
| Policy Type | Purpose | Example |
|
|
|-------------|---------|---------|
|
|
| **pin_dependencies** | Prevent dependency upgrades | Keep urllib3 at 1.26.15 |
|
|
| **force_version** | Force specific package version | Install click==8.1.3 |
|
|
| **restore** | Reinstall deleted/changed packages | Restore six to 1.16.0 |
|
|
| **uninstall** | Remove obsolete packages | Remove old-package |
|
|
| **on_failure** | Handle installation failures | retry_without_pin or fail |
|
|
|
|
## Test Data Design Principles
|
|
|
|
1. **Lightweight Packages**: All packages are <200KB for fast testing
|
|
2. **Real Dependencies**: Use actual PyPI package relationships
|
|
3. **Version Realism**: Use real version numbers from PyPI
|
|
4. **Clear Scenarios**: Each test demonstrates one clear behavior
|
|
5. **Reproducible**: Mock ensures consistent behavior across environments
|