mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 19:37:54 +08:00
205 lines
6.4 KiB
Python
205 lines
6.4 KiB
Python
"""
|
||
虚拟显示托管资源与旧 API 的兼容测试。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import sys
|
||
from types import ModuleType
|
||
from typing import Any, Optional
|
||
from unittest.mock import MagicMock
|
||
|
||
import pytest
|
||
|
||
from app.adapters.system.display import DisplayHelper
|
||
from app.adapters.system.display.resource import VirtualDisplayResource
|
||
from app.foundation.singleton import Singleton
|
||
|
||
|
||
def test_virtual_display_skips_host_process_outside_docker(monkeypatch) -> None:
|
||
"""
|
||
非容器环境启动资源时不得创建虚拟显示进程。
|
||
|
||
:param monkeypatch: 隔离容器环境判定的测试夹具
|
||
"""
|
||
monkeypatch.setattr(
|
||
"app.adapters.system.display.resource.SystemUtils.is_docker",
|
||
lambda: False,
|
||
)
|
||
resource = VirtualDisplayResource()
|
||
|
||
resource.start()
|
||
resource.stop()
|
||
|
||
assert resource.display is None
|
||
|
||
|
||
def test_virtual_display_starts_and_stops_owned_process(monkeypatch) -> None:
|
||
"""
|
||
容器环境只停止当前资源实际拥有的显示进程。
|
||
|
||
:param monkeypatch: 隔离显示后端与容器环境的测试夹具
|
||
"""
|
||
events: list[object] = []
|
||
|
||
class FakeDisplay:
|
||
"""
|
||
记录 pyvirtualdisplay 的构造与生命周期。
|
||
"""
|
||
|
||
def __init__(self, **kwargs) -> None:
|
||
"""
|
||
记录资源交给显示后端的启动参数。
|
||
|
||
:param kwargs: 显示后端的构造选项
|
||
"""
|
||
events.append(("create", kwargs))
|
||
|
||
def start(self) -> None:
|
||
"""
|
||
记录虚拟显示启动调用,不创建实际显示进程。
|
||
"""
|
||
events.append("start")
|
||
|
||
def stop(self) -> None:
|
||
"""
|
||
记录虚拟显示停止调用,供资源所有权断言使用。
|
||
"""
|
||
events.append("stop")
|
||
|
||
pyvirtualdisplay = ModuleType("pyvirtualdisplay")
|
||
pyvirtualdisplay.Display = FakeDisplay
|
||
monkeypatch.setitem(sys.modules, "pyvirtualdisplay", pyvirtualdisplay)
|
||
monkeypatch.setattr(
|
||
"app.adapters.system.display.resource.SystemUtils.is_docker",
|
||
lambda: True,
|
||
)
|
||
monkeypatch.setenv("DISPLAY", ":99")
|
||
resource = VirtualDisplayResource()
|
||
|
||
resource.start()
|
||
resource.stop()
|
||
resource.stop()
|
||
|
||
assert events == [
|
||
(
|
||
"create",
|
||
{
|
||
"visible": False,
|
||
"size": (1024, 768),
|
||
},
|
||
),
|
||
"start",
|
||
"stop",
|
||
]
|
||
assert resource.display is None
|
||
|
||
|
||
@pytest.mark.parametrize("original_display", [":987", None])
|
||
def test_virtual_display_allocates_number_and_restores_environment(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
original_display: Optional[str],
|
||
) -> None:
|
||
"""
|
||
真实显示后端自动分配编号,重复启停后恢复原有或缺省的 DISPLAY。
|
||
|
||
只替换 Xvfb 进程边界,保留命令构造、displayfd 管道和环境变量管理。
|
||
|
||
:param monkeypatch: 隔离进程启动、容器判定和环境变量的测试夹具
|
||
:param original_display: 启动前的显示地址;None 表示未配置 DISPLAY
|
||
"""
|
||
commands: list[list[str]] = []
|
||
processes: list[MagicMock] = []
|
||
display_numbers = iter((1001, 1002))
|
||
|
||
def spawn_display(
|
||
command: list[str], *, pass_fds: list[int], **_kwargs: Any,
|
||
) -> MagicMock:
|
||
"""
|
||
模拟 Xvfb 通过 displayfd 回传分配编号并保留可停止的进程句柄。
|
||
|
||
:param command: 真实显示后端拼接的启动命令
|
||
:param pass_fds: 交给 Xvfb 回传显示编号的管道描述符
|
||
:param _kwargs: 子进程启动时保留的输出及 shell 选项
|
||
:return: 供显示后端查询和停止的进程替身
|
||
"""
|
||
commands.append(command)
|
||
process = MagicMock()
|
||
process.poll.return_value = None
|
||
process.communicate.return_value = (b"", b"")
|
||
processes.append(process)
|
||
os.write(pass_fds[0], f"{next(display_numbers)}\n".encode("ascii"))
|
||
return process
|
||
|
||
monkeypatch.setattr(
|
||
"app.adapters.system.display.resource.SystemUtils.is_docker", lambda: True,
|
||
)
|
||
monkeypatch.setattr(
|
||
"pyvirtualdisplay.abstractdisplay.get_helptext", lambda _program: "-displayfd",
|
||
)
|
||
monkeypatch.setattr("pyvirtualdisplay.abstractdisplay.subprocess.Popen", spawn_display)
|
||
monkeypatch.setenv("PYVIRTUALDISPLAY_DISPLAYFD", "1")
|
||
if original_display is None:
|
||
monkeypatch.delenv("DISPLAY", raising=False)
|
||
else:
|
||
monkeypatch.setenv("DISPLAY", original_display)
|
||
resource = VirtualDisplayResource()
|
||
|
||
try:
|
||
for display_number in (1001, 1002):
|
||
resource.start()
|
||
display = resource.display
|
||
resource.start()
|
||
|
||
assert resource.display is display
|
||
assert os.environ["DISPLAY"] == f":{display_number}"
|
||
assert "-displayfd" in commands[-1]
|
||
assert not any(argument.startswith(":") for argument in commands[-1])
|
||
|
||
resource.stop()
|
||
resource.stop()
|
||
assert resource.display is None
|
||
assert os.environ.get("DISPLAY") == original_display
|
||
finally:
|
||
resource.stop()
|
||
|
||
assert len(commands) == 2
|
||
for process in processes:
|
||
process.kill.assert_called_once_with()
|
||
process.wait.assert_called_once_with()
|
||
|
||
|
||
def test_display_helper_keeps_legacy_constructor_and_stop_contract(monkeypatch) -> None:
|
||
"""
|
||
旧构造入口显式激活 host.display,stop 只停止已配置 Runtime。
|
||
|
||
:param monkeypatch: 隔离 Runtime 资源调用的测试夹具
|
||
"""
|
||
events: list[tuple[str, str]] = []
|
||
singleton_key = (DisplayHelper, (), frozenset())
|
||
previous = Singleton._instances.pop(singleton_key, None)
|
||
monkeypatch.setattr(
|
||
"app.adapters.system.display.acquire_managed_resource",
|
||
lambda capability_id, *, reason, retry: events.append(
|
||
("activate", capability_id)
|
||
),
|
||
)
|
||
monkeypatch.setattr(
|
||
"app.adapters.system.display.stop_managed_resource",
|
||
lambda capability_id, *, reason: events.append(("stop", capability_id)),
|
||
)
|
||
try:
|
||
helper = DisplayHelper()
|
||
assert DisplayHelper() is helper
|
||
helper.stop()
|
||
finally:
|
||
Singleton._instances.pop(singleton_key, None)
|
||
if previous is not None:
|
||
Singleton._instances[singleton_key] = previous
|
||
|
||
assert events == [
|
||
("activate", "host.display"),
|
||
("stop", "host.display"),
|
||
]
|