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.
272 lines
9.1 KiB
Python
272 lines
9.1 KiB
Python
"""
|
|
Test dependency version protection with pin (Priority 1)
|
|
|
|
Tests that existing dependency versions are protected by pin_dependencies policy
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def pin_policy(temp_policy_dir):
|
|
"""Create policy with pin_dependencies for lightweight real packages"""
|
|
policy_content = {
|
|
"requests": {
|
|
"apply_all_matches": [
|
|
{
|
|
"type": "pin_dependencies",
|
|
"pinned_packages": ["urllib3", "certifi", "charset-normalizer"],
|
|
"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.mark.integration
|
|
def test_dependency_version_protection_with_pin(
|
|
pin_policy,
|
|
mock_manager_util,
|
|
mock_context,
|
|
reset_test_venv,
|
|
get_installed_packages
|
|
):
|
|
"""
|
|
Test existing dependency versions are protected by pin
|
|
|
|
Priority: 1 (Essential)
|
|
|
|
Purpose:
|
|
Verify that when installing a package that would normally upgrade
|
|
dependencies, the pin_dependencies policy protects existing versions.
|
|
|
|
Based on DEPENDENCY_TREE_CONTEXT.md:
|
|
Without pin: urllib3 1.26.15 → 2.5.0 (MAJOR upgrade)
|
|
With pin: urllib3 stays at 1.26.15 (protected)
|
|
"""
|
|
from comfyui_manager.common.pip_util import PipBatch
|
|
|
|
# Verify initial packages are installed (from requirements-test-base.txt)
|
|
initial = get_installed_packages()
|
|
assert "urllib3" in initial
|
|
assert "certifi" in initial
|
|
assert "charset-normalizer" in initial
|
|
|
|
# Record initial versions (from DEPENDENCY_TREE_CONTEXT.md)
|
|
initial_urllib3 = initial["urllib3"]
|
|
initial_certifi = initial["certifi"]
|
|
initial_charset = initial["charset-normalizer"]
|
|
|
|
# Verify expected OLD versions
|
|
assert initial_urllib3 == "1.26.15", f"Expected urllib3==1.26.15, got {initial_urllib3}"
|
|
assert initial_certifi == "2023.7.22", f"Expected certifi==2023.7.22, got {initial_certifi}"
|
|
assert initial_charset == "3.2.0", f"Expected charset-normalizer==3.2.0, got {initial_charset}"
|
|
|
|
# Verify idna is NOT installed initially
|
|
assert "idna" not in initial, "idna should not be pre-installed"
|
|
|
|
with PipBatch() as batch:
|
|
result = batch.install("requests")
|
|
final_packages = batch._get_installed_packages()
|
|
|
|
# Verify installation succeeded
|
|
assert result is True
|
|
assert "requests" in final_packages
|
|
|
|
# Verify versions were maintained (not upgraded to latest)
|
|
# Without pin, these would upgrade to: urllib3==2.5.0, certifi==2025.8.3, charset-normalizer==3.4.3
|
|
assert final_packages["urllib3"] == "1.26.15", "urllib3 should remain at 1.26.15 (prevented 2.x upgrade)"
|
|
assert final_packages["certifi"] == "2023.7.22", "certifi should remain at 2023.7.22 (prevented 2025.x upgrade)"
|
|
assert final_packages["charset-normalizer"] == "3.2.0", "charset-normalizer should remain at 3.2.0"
|
|
|
|
# Verify new dependency was added (idna is NOT pinned, so it gets installed)
|
|
assert "idna" in final_packages, "idna should be installed as new dependency"
|
|
assert final_packages["idna"] == "3.10", f"Expected idna==3.10, got {final_packages['idna']}"
|
|
|
|
# Verify requests was installed at expected version
|
|
assert final_packages["requests"] == "2.32.5", f"Expected requests==2.32.5, got {final_packages['requests']}"
|
|
|
|
|
|
@pytest.fixture
|
|
def python_dateutil_policy(temp_policy_dir):
|
|
"""Create policy for python-dateutil with six pinning"""
|
|
policy_content = {
|
|
"python-dateutil": {
|
|
"apply_all_matches": [
|
|
{
|
|
"type": "pin_dependencies",
|
|
"pinned_packages": ["six"],
|
|
"reason": "Protect six from upgrading"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
policy_file = temp_policy_dir / "pip-policy.json"
|
|
policy_file.write_text(json.dumps(policy_content, indent=2))
|
|
return policy_file
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_dependency_chain_with_six_pin(
|
|
python_dateutil_policy,
|
|
mock_manager_util,
|
|
mock_context,
|
|
reset_test_venv,
|
|
get_installed_packages
|
|
):
|
|
"""
|
|
Test python-dateutil + six dependency chain with pin
|
|
|
|
Priority: 2 (Important)
|
|
|
|
Purpose:
|
|
Verify that pin_dependencies protects actual dependencies
|
|
(six is a real dependency of python-dateutil).
|
|
|
|
Based on DEPENDENCY_TREE_CONTEXT.md:
|
|
python-dateutil depends on six>=1.5
|
|
Without pin: six 1.16.0 → 1.17.0
|
|
With pin: six stays at 1.16.0 (protected)
|
|
"""
|
|
from comfyui_manager.common.pip_util import PipBatch
|
|
|
|
# Verify six is installed
|
|
initial = get_installed_packages()
|
|
assert "six" in initial
|
|
initial_six = initial["six"]
|
|
|
|
# Verify expected OLD version
|
|
assert initial_six == "1.16.0", f"Expected six==1.16.0, got {initial_six}"
|
|
|
|
with PipBatch() as batch:
|
|
result = batch.install("python-dateutil")
|
|
final_packages = batch._get_installed_packages()
|
|
|
|
# Verify installation succeeded
|
|
assert result is True
|
|
|
|
# Verify final versions
|
|
assert "python-dateutil" in final_packages
|
|
assert final_packages["python-dateutil"] == "2.9.0.post0", f"Expected python-dateutil==2.9.0.post0"
|
|
|
|
# Verify six was NOT upgraded (without pin, would upgrade to 1.17.0)
|
|
assert "six" in final_packages
|
|
assert final_packages["six"] == "1.16.0", "six should remain at 1.16.0 (prevented 1.17.0 upgrade)"
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_pin_only_affects_specified_packages(
|
|
pin_policy,
|
|
mock_manager_util,
|
|
mock_context,
|
|
reset_test_venv,
|
|
get_installed_packages
|
|
):
|
|
"""
|
|
Test that pin only affects specified packages, not all dependencies
|
|
|
|
Priority: 1 (Essential)
|
|
|
|
Purpose:
|
|
Verify that idna (new dependency) is installed even though
|
|
other dependencies are pinned. This tests that pin is selective,
|
|
not global.
|
|
|
|
Based on DEPENDENCY_TREE_CONTEXT.md:
|
|
idna is a NEW dependency (not in initial environment)
|
|
Pin only affects: urllib3, certifi, charset-normalizer
|
|
idna should be installed at latest version (3.10)
|
|
"""
|
|
from comfyui_manager.common.pip_util import PipBatch
|
|
|
|
# Verify initial state
|
|
initial = get_installed_packages()
|
|
assert "idna" not in initial, "idna should not be pre-installed"
|
|
assert "requests" not in initial, "requests should not be pre-installed"
|
|
|
|
with PipBatch() as batch:
|
|
result = batch.install("requests")
|
|
final_packages = batch._get_installed_packages()
|
|
|
|
# Verify installation succeeded
|
|
assert result is True
|
|
|
|
# Verify idna was installed (NOT pinned, so gets latest)
|
|
assert "idna" in final_packages, "idna should be installed as new dependency"
|
|
assert final_packages["idna"] == "3.10", "idna should be at latest version 3.10 (not pinned)"
|
|
|
|
# Verify requests was installed
|
|
assert "requests" in final_packages
|
|
assert final_packages["requests"] == "2.32.5"
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_major_version_jump_prevention(
|
|
pin_policy,
|
|
mock_manager_util,
|
|
mock_context,
|
|
reset_test_venv,
|
|
get_installed_packages,
|
|
install_packages,
|
|
uninstall_packages
|
|
):
|
|
"""
|
|
Test that pin prevents MAJOR version jumps (breaking changes)
|
|
|
|
Priority: 1 (Essential)
|
|
|
|
Purpose:
|
|
Verify that pin prevents urllib3 1.x → 2.x major upgrade.
|
|
This is the most important test because urllib3 2.0 has
|
|
breaking API changes.
|
|
|
|
Based on DEPENDENCY_TREE_CONTEXT.md:
|
|
urllib3 1.26.15 → 2.5.0 is a MAJOR version jump
|
|
urllib3 2.0 removed deprecated APIs
|
|
requests accepts both: urllib3<3,>=1.21.1
|
|
"""
|
|
from comfyui_manager.common.pip_util import PipBatch
|
|
|
|
# Verify initial urllib3 version
|
|
initial = get_installed_packages()
|
|
assert initial["urllib3"] == "1.26.15", "Expected urllib3==1.26.15"
|
|
|
|
# First, test WITHOUT pin to verify urllib3 would upgrade to 2.x
|
|
# (This simulates what would happen without our protection)
|
|
uninstall_packages("urllib3", "certifi", "charset-normalizer")
|
|
install_packages("requests")
|
|
|
|
without_pin = get_installed_packages()
|
|
|
|
# Verify urllib3 was upgraded to 2.x without pin
|
|
assert "urllib3" in without_pin
|
|
assert without_pin["urllib3"].startswith("2."), \
|
|
f"Without pin, urllib3 should upgrade to 2.x, got {without_pin['urllib3']}"
|
|
|
|
# Now reset and test WITH pin
|
|
uninstall_packages("requests", "urllib3", "certifi", "charset-normalizer", "idna")
|
|
install_packages("urllib3==1.26.15", "certifi==2023.7.22", "charset-normalizer==3.2.0")
|
|
|
|
with PipBatch() as batch:
|
|
result = batch.install("requests")
|
|
final_packages = batch._get_installed_packages()
|
|
|
|
# Verify installation succeeded
|
|
assert result is True
|
|
|
|
# Verify urllib3 stayed at 1.x (prevented major version jump)
|
|
assert final_packages["urllib3"] == "1.26.15", \
|
|
"Pin should prevent urllib3 from upgrading to 2.x (breaking changes)"
|
|
|
|
# Verify it's specifically 1.x, not 2.x
|
|
assert final_packages["urllib3"].startswith("1."), \
|
|
f"urllib3 should remain at 1.x series, got {final_packages['urllib3']}"
|