● feat: Draft pip package policy management system (not yet integrated)
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.
This commit is contained in:
216
tests/common/pip_util/test_pin_failure_retry.py
Normal file
216
tests/common/pip_util/test_pin_failure_retry.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Test pin failure and retry logic (Priority 1)
|
||||
|
||||
Tests that installation with pinned dependencies can retry without pins on failure
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def retry_policy(temp_policy_dir):
|
||||
"""Create policy with retry_without_pin"""
|
||||
policy_content = {
|
||||
"new-pkg": {
|
||||
"apply_all_matches": [
|
||||
{
|
||||
"type": "pin_dependencies",
|
||||
"pinned_packages": ["numpy", "pandas"],
|
||||
"on_failure": "retry_without_pin"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
policy_file = temp_policy_dir / "pip-policy.json"
|
||||
policy_file.write_text(json.dumps(policy_content, indent=2))
|
||||
return policy_file
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_retry_subprocess(monkeypatch):
|
||||
"""Mock subprocess that fails with pins, succeeds without"""
|
||||
call_sequence = []
|
||||
attempt_count = [0]
|
||||
|
||||
installed_packages = {
|
||||
"numpy": "1.26.0",
|
||||
"pandas": "2.0.0"
|
||||
}
|
||||
|
||||
def mock_run(cmd, **kwargs):
|
||||
call_sequence.append(cmd)
|
||||
|
||||
# pip freeze
|
||||
if "freeze" in cmd:
|
||||
output = "\n".join([f"{pkg}=={ver}" for pkg, ver in installed_packages.items()])
|
||||
return subprocess.CompletedProcess(cmd, 0, output, "")
|
||||
|
||||
# pip install
|
||||
if "install" in cmd and "new-pkg" in cmd:
|
||||
attempt_count[0] += 1
|
||||
|
||||
# First attempt with pins - FAIL
|
||||
if attempt_count[0] == 1 and "numpy==1.26.0" in cmd and "pandas==2.0.0" in cmd:
|
||||
raise subprocess.CalledProcessError(1, cmd, "", "Dependency conflict")
|
||||
|
||||
# Second attempt without pins - SUCCESS
|
||||
if attempt_count[0] == 2:
|
||||
installed_packages["new-pkg"] = "1.0.0"
|
||||
# Without pins, versions might change
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
monkeypatch.setattr("subprocess.run", mock_run)
|
||||
return call_sequence, installed_packages, attempt_count
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_pin_failure_retry_without_pin_succeeds(
|
||||
retry_policy,
|
||||
mock_manager_util,
|
||||
mock_context,
|
||||
mock_retry_subprocess,
|
||||
capture_logs
|
||||
):
|
||||
"""
|
||||
Test retry without pin succeeds after pin failure
|
||||
|
||||
Priority: 1 (Essential)
|
||||
|
||||
Purpose:
|
||||
Verify that when installation with pinned dependencies fails,
|
||||
the system automatically retries without pins and succeeds.
|
||||
"""
|
||||
import sys
|
||||
# Path setup handled by conftest.py
|
||||
|
||||
from comfyui_manager.common.pip_util import PipBatch
|
||||
|
||||
call_sequence, installed_packages, attempt_count = mock_retry_subprocess
|
||||
|
||||
with PipBatch() as batch:
|
||||
result = batch.install("new-pkg")
|
||||
|
||||
# Verify installation succeeded on retry
|
||||
assert result is True
|
||||
|
||||
# Verify two installation attempts were made
|
||||
install_calls = [cmd for cmd in call_sequence if "install" in cmd and "new-pkg" in cmd]
|
||||
assert len(install_calls) == 2
|
||||
|
||||
# First attempt had pins
|
||||
first_call = install_calls[0]
|
||||
assert "new-pkg" in first_call
|
||||
assert "numpy==1.26.0" in first_call
|
||||
assert "pandas==2.0.0" in first_call
|
||||
|
||||
# Second attempt had no pins (just new-pkg)
|
||||
second_call = install_calls[1]
|
||||
assert "new-pkg" in second_call
|
||||
assert "numpy==1.26.0" not in second_call
|
||||
assert "pandas==2.0.0" not in second_call
|
||||
|
||||
# Verify warning log
|
||||
assert any("retrying without pins" in record.message.lower() for record in capture_logs.records)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fail_policy(temp_policy_dir):
|
||||
"""Create policy with on_failure: fail"""
|
||||
policy_content = {
|
||||
"pytorch-addon": {
|
||||
"apply_all_matches": [
|
||||
{
|
||||
"condition": {
|
||||
"type": "installed",
|
||||
"package": "torch",
|
||||
"spec": ">=2.0.0"
|
||||
},
|
||||
"type": "pin_dependencies",
|
||||
"pinned_packages": ["torch", "torchvision", "torchaudio"],
|
||||
"on_failure": "fail"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
policy_file = temp_policy_dir / "pip-policy.json"
|
||||
policy_file.write_text(json.dumps(policy_content, indent=2))
|
||||
return policy_file
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_fail_subprocess(monkeypatch):
|
||||
"""Mock subprocess that always fails"""
|
||||
call_sequence = []
|
||||
|
||||
installed_packages = {
|
||||
"torch": "2.1.0",
|
||||
"torchvision": "0.16.0",
|
||||
"torchaudio": "2.1.0"
|
||||
}
|
||||
|
||||
def mock_run(cmd, **kwargs):
|
||||
call_sequence.append(cmd)
|
||||
|
||||
# pip freeze
|
||||
if "freeze" in cmd:
|
||||
output = "\n".join([f"{pkg}=={ver}" for pkg, ver in installed_packages.items()])
|
||||
return subprocess.CompletedProcess(cmd, 0, output, "")
|
||||
|
||||
# pip install - ALWAYS FAIL
|
||||
if "install" in cmd and "pytorch-addon" in cmd:
|
||||
raise subprocess.CalledProcessError(1, cmd, "", "Installation failed")
|
||||
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
monkeypatch.setattr("subprocess.run", mock_run)
|
||||
return call_sequence, installed_packages
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_pin_failure_with_fail_raises_exception(
|
||||
fail_policy,
|
||||
mock_manager_util,
|
||||
mock_context,
|
||||
mock_fail_subprocess,
|
||||
capture_logs
|
||||
):
|
||||
"""
|
||||
Test exception is raised when on_failure is "fail"
|
||||
|
||||
Priority: 1 (Essential)
|
||||
|
||||
Purpose:
|
||||
Verify that when on_failure is set to "fail", installation
|
||||
failure with pinned dependencies raises an exception and
|
||||
does not retry.
|
||||
"""
|
||||
import sys
|
||||
# Path setup handled by conftest.py
|
||||
|
||||
from comfyui_manager.common.pip_util import PipBatch
|
||||
|
||||
call_sequence, installed_packages = mock_fail_subprocess
|
||||
|
||||
with PipBatch() as batch:
|
||||
# Should raise exception
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
batch.install("pytorch-addon")
|
||||
|
||||
# Verify only one installation attempt was made (no retry)
|
||||
install_calls = [cmd for cmd in call_sequence if "install" in cmd and "pytorch-addon" in cmd]
|
||||
assert len(install_calls) == 1
|
||||
|
||||
# Verify it had pins
|
||||
install_cmd = install_calls[0]
|
||||
assert "pytorch-addon" in install_cmd
|
||||
assert "torch==2.1.0" in install_cmd
|
||||
assert "torchvision==0.16.0" in install_cmd
|
||||
assert "torchaudio==2.1.0" in install_cmd
|
||||
Reference in New Issue
Block a user