mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2025-12-16 01:57:04 +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.
217 lines
6.2 KiB
Python
217 lines
6.2 KiB
Python
"""
|
|
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
|