mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
test(plugin): verify real cancellation cleanup
This commit is contained in:
@@ -1,3 +1,14 @@
|
||||
__all__ = (
|
||||
"ImmediateException",
|
||||
"LimitException",
|
||||
"APIRateLimitException",
|
||||
"RateLimitExceededException",
|
||||
"OperationInterrupted",
|
||||
"StorageQueryError",
|
||||
"TMDbException",
|
||||
)
|
||||
|
||||
|
||||
class ImmediateException(Exception):
|
||||
"""
|
||||
用于立即抛出异常而不重试的特殊异常类。
|
||||
|
||||
@@ -81,8 +81,6 @@ SCHEMA_EXPORTS = {
|
||||
'DashboardMemoryInfo': ('app.schemas.dashboard', 'DashboardMemoryInfo'),
|
||||
'DashboardSystemInfo': ('app.schemas.dashboard', 'DashboardSystemInfo'),
|
||||
'DataT': ('app.schemas.response', 'DataT'),
|
||||
'DatabaseWorkerClosedError': ('app.schemas.exception', 'DatabaseWorkerClosedError'),
|
||||
'DatabaseWorkerOverloadedError': ('app.schemas.exception', 'DatabaseWorkerOverloadedError'),
|
||||
'Dict': ('app.schemas.mcp', 'Dict'),
|
||||
'DiscoverMediaSource': ('app.schemas.event', 'DiscoverMediaSource'),
|
||||
'DiscoverSourceEventData': ('app.schemas.event', 'DiscoverSourceEventData'),
|
||||
|
||||
@@ -1680,6 +1680,100 @@ demo = { index = "private" }
|
||||
PluginHelper.PLUGIN_DEPENDENCY_INSTALL_TIMEOUT
|
||||
)
|
||||
|
||||
def test_async_package_install_cancellation_closes_full_lifecycle(self, tmp_path):
|
||||
"""取消真实安装进程后必须回收进程树、临时约束和安装锁。"""
|
||||
import psutil
|
||||
|
||||
from app.adapters.external.market import PluginHelper
|
||||
|
||||
helper = PluginHelper()
|
||||
requirements_file = tmp_path / "requirements.txt"
|
||||
requirements_file.write_text("demo-package\n", encoding="utf-8")
|
||||
constraints_file = tmp_path / "runtime-constraints.txt"
|
||||
marker = tmp_path / "install-pids"
|
||||
child_code = "import time; time.sleep(60)"
|
||||
install_code = (
|
||||
"from pathlib import Path; import os, subprocess, time; "
|
||||
f"child = subprocess.Popen([{sys.executable!r}, '-c', {child_code!r}]); "
|
||||
f"Path({str(marker)!r}).write_text(str(os.getpid()) + ':' + str(child.pid)); "
|
||||
"time.sleep(60)"
|
||||
)
|
||||
strategy = Mock(
|
||||
strategy_name="uv:test",
|
||||
command=[sys.executable, "-c", install_code],
|
||||
env=os.environ.copy(),
|
||||
safe_log_command=[sys.executable, "-c", "<install>"],
|
||||
)
|
||||
health = {
|
||||
"uv check": (True, "ok"),
|
||||
"核心依赖导入检查": (True, "ok"),
|
||||
}
|
||||
|
||||
def create_constraints(_protected_packages):
|
||||
constraints_file.write_text("fastapi==0\n", encoding="utf-8")
|
||||
return constraints_file
|
||||
|
||||
async def run_install():
|
||||
task = asyncio.create_task(
|
||||
helper.async_install_packages_with_fallback(requirements_file)
|
||||
)
|
||||
deadline = time.monotonic() + 2
|
||||
while not marker.exists() and time.monotonic() < deadline:
|
||||
await asyncio.sleep(0.01)
|
||||
assert marker.exists()
|
||||
|
||||
pids = [int(value) for value in marker.read_text().split(":")]
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
assert not constraints_file.exists()
|
||||
assert PluginHelper._package_install_lock.acquire(blocking=False)
|
||||
PluginHelper._package_install_lock.release()
|
||||
for _ in range(100):
|
||||
alive = []
|
||||
for pid in pids:
|
||||
try:
|
||||
process = psutil.Process(pid)
|
||||
if (
|
||||
process.is_running()
|
||||
and process.status() != psutil.STATUS_ZOMBIE
|
||||
):
|
||||
alive.append(pid)
|
||||
except (psutil.Error, OSError):
|
||||
continue
|
||||
if not alive:
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
else:
|
||||
pytest.fail(f"安装进程树仍在运行:{alive}")
|
||||
|
||||
with patch.object(
|
||||
PluginHelper,
|
||||
"_PluginHelper__get_installed_packages",
|
||||
return_value={},
|
||||
), patch.object(
|
||||
PluginHelper,
|
||||
"_PluginHelper__get_protected_runtime_packages",
|
||||
return_value={"fastapi": "0"},
|
||||
), patch.object(
|
||||
PluginHelper,
|
||||
"_PluginHelper__validate_runtime_dependency_conflicts",
|
||||
return_value=(True, ""),
|
||||
), patch.object(
|
||||
PluginHelper,
|
||||
"_PluginHelper__create_runtime_constraints_file",
|
||||
side_effect=create_constraints,
|
||||
), patch(
|
||||
"app.adapters.external.market.build_package_install_strategies",
|
||||
return_value=[strategy],
|
||||
), patch.object(
|
||||
PluginHelper,
|
||||
"_PluginHelper__async_run_runtime_healthcheck",
|
||||
new=AsyncMock(return_value=health),
|
||||
):
|
||||
asyncio.run(run_install())
|
||||
|
||||
def test_install_uses_release_package_when_asset_is_available(self, monkeypatch):
|
||||
"""
|
||||
release 包可用时优先使用 zip 安装,不再额外访问文件列表。
|
||||
|
||||
Reference in New Issue
Block a user