mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
feat(deps): add uv-backed package installer (#5987)
* feat(deps): add uv-backed package installer * feat(deps): support package cache root
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
|
||||
from app.core.cache import AsyncFileBackend, FileBackend, MemoryBackend
|
||||
from app.core.config import settings
|
||||
@@ -19,6 +21,138 @@ def test_file_backend_items_keep_relative_keys_and_bytes(tmp_path):
|
||||
assert not cache.exists("nested/poster.jpg", region="images")
|
||||
|
||||
|
||||
def test_clear_package_tool_cache_only_removes_pip_and_uv_old_files(tmp_path, monkeypatch):
|
||||
"""
|
||||
包安装工具缓存清理只处理 pip/uv 子目录,不接管整个 .cache 或业务缓存。
|
||||
"""
|
||||
from app.startup.modules_initializer import clear_package_tool_cache
|
||||
|
||||
old_time = time.time() - 40 * 24 * 3600
|
||||
cache_root = tmp_path / ".cache"
|
||||
old_pip = cache_root / "pip" / "old.whl"
|
||||
old_uv = cache_root / "uv" / "old.archive"
|
||||
unknown = cache_root / "other" / "old.bin"
|
||||
business = tmp_path / "cache" / "images" / "old.jpg"
|
||||
for path in (old_pip, old_uv, unknown, business):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("x", encoding="utf-8")
|
||||
os.utime(path, (old_time, old_time))
|
||||
|
||||
monkeypatch.setattr(settings, "CONFIG_DIR", str(tmp_path))
|
||||
monkeypatch.setattr(settings, "PACKAGE_CACHE_ROOT", None)
|
||||
monkeypatch.setattr(settings, "PACKAGE_CACHE_DAYS", 30)
|
||||
|
||||
clear_package_tool_cache()
|
||||
|
||||
assert not old_pip.exists()
|
||||
assert not old_uv.exists()
|
||||
assert unknown.exists()
|
||||
assert business.exists()
|
||||
|
||||
|
||||
def test_clear_package_tool_cache_disabled_when_days_non_positive(tmp_path, monkeypatch):
|
||||
"""
|
||||
PACKAGE_CACHE_DAYS 小于等于 0 时不清理包安装缓存。
|
||||
"""
|
||||
from app.startup.modules_initializer import clear_package_tool_cache
|
||||
|
||||
old_time = time.time() - 40 * 24 * 3600
|
||||
old_pip = tmp_path / ".cache" / "pip" / "old.whl"
|
||||
old_pip.parent.mkdir(parents=True, exist_ok=True)
|
||||
old_pip.write_text("x", encoding="utf-8")
|
||||
os.utime(old_pip, (old_time, old_time))
|
||||
|
||||
monkeypatch.setattr(settings, "CONFIG_DIR", str(tmp_path))
|
||||
monkeypatch.setattr(settings, "PACKAGE_CACHE_ROOT", None)
|
||||
monkeypatch.setattr(settings, "PACKAGE_CACHE_DAYS", 0)
|
||||
|
||||
clear_package_tool_cache()
|
||||
|
||||
assert old_pip.exists()
|
||||
|
||||
|
||||
def test_clear_package_tool_cache_isolates_subdir_errors(tmp_path, monkeypatch):
|
||||
"""
|
||||
单个工具缓存目录清理失败,不影响另一个工具缓存目录。
|
||||
"""
|
||||
from app.startup.modules_initializer import clear_package_tool_cache
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_clear(path, days):
|
||||
calls.append((path.name, days))
|
||||
if path.name == "pip":
|
||||
raise OSError("pip cache locked")
|
||||
|
||||
monkeypatch.setattr(settings, "CONFIG_DIR", str(tmp_path))
|
||||
monkeypatch.setattr(settings, "PACKAGE_CACHE_ROOT", str(tmp_path / "custom-package-cache"))
|
||||
monkeypatch.setattr(settings, "PACKAGE_CACHE_DAYS", 30)
|
||||
monkeypatch.setattr("app.startup.modules_initializer.SystemUtils.clear", fake_clear)
|
||||
|
||||
clear_package_tool_cache()
|
||||
|
||||
assert calls == [("pip", 30), ("uv", 30)]
|
||||
|
||||
|
||||
def test_clear_package_tool_cache_uses_package_cache_root(tmp_path, monkeypatch):
|
||||
"""
|
||||
PACKAGE_CACHE_ROOT 用作 pip/uv 清理根目录,不扩大到配置目录下其他缓存。
|
||||
"""
|
||||
from app.startup.modules_initializer import clear_package_tool_cache
|
||||
|
||||
old_time = time.time() - 40 * 24 * 3600
|
||||
package_cache_root = tmp_path / "custom-package-cache"
|
||||
old_pip = package_cache_root / "pip" / "old.whl"
|
||||
default_pip = tmp_path / ".cache" / "pip" / "old.whl"
|
||||
for path in (old_pip, default_pip):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("x", encoding="utf-8")
|
||||
os.utime(path, (old_time, old_time))
|
||||
|
||||
monkeypatch.setattr(settings, "CONFIG_DIR", str(tmp_path))
|
||||
monkeypatch.setattr(settings, "PACKAGE_CACHE_ROOT", str(package_cache_root))
|
||||
monkeypatch.setattr(settings, "PACKAGE_CACHE_DAYS", 30)
|
||||
|
||||
clear_package_tool_cache()
|
||||
|
||||
assert not old_pip.exists()
|
||||
assert default_pip.exists()
|
||||
|
||||
|
||||
def test_init_modules_does_not_clear_package_tool_cache(monkeypatch):
|
||||
"""
|
||||
包安装缓存清理由通用临时清理入口触发,模块启动路径不直接执行清理。
|
||||
"""
|
||||
from app.startup import modules_initializer
|
||||
|
||||
called = False
|
||||
|
||||
def fail_if_called():
|
||||
nonlocal called
|
||||
called = True
|
||||
raise AssertionError("init_modules must not clear package tool cache directly")
|
||||
|
||||
monkeypatch.setattr(modules_initializer, "clear_package_tool_cache", fail_if_called)
|
||||
monkeypatch.setattr(modules_initializer, "DisplayHelper", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer, "DohHelper", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer, "SitesHelper", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer, "ResourceHelper", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer, "user_auth", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer, "ModuleManager", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer.EventManager, "start", lambda self: None)
|
||||
monkeypatch.setattr(modules_initializer.MoviePilotServerHelper, "init_plugin_report", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer.MoviePilotServerHelper, "init_subscribe_report", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer.MoviePilotServerHelper, "get_user_uuid", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer.MoviePilotServerHelper, "get_github_user", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer, "init_agent", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer, "start_frontend", lambda: None)
|
||||
monkeypatch.setattr(modules_initializer, "check_auth", lambda: None)
|
||||
|
||||
modules_initializer.init_modules()
|
||||
|
||||
assert called is False
|
||||
|
||||
|
||||
def test_file_backend_delete_missing_key_is_noop(tmp_path):
|
||||
"""
|
||||
删除不存在的文件缓存 key 应保持幂等,不向调用方抛出文件系统异常。
|
||||
|
||||
Reference in New Issue
Block a user