Files
MoviePilot/tests/test_plugin_gil_attribution.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

190 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
free-threaded 运行时插件触发 GIL 回退的归因测试。
"""
from __future__ import annotations
from types import ModuleType, SimpleNamespace
from unittest.mock import MagicMock
import pytest
from app.runtime.extensions.plugin import gil as gil_module
from app.runtime.extensions.plugin import loader as loader_module
from app.runtime.extensions.plugin.database import PluginDatabase
from app.runtime.extensions.plugin.gil import attribute_gil_fallback
from app.runtime.extensions.plugin.lifecycle import PluginLifecycle
from app.runtime.extensions.plugin.loader import PluginLoader
from app.runtime.extensions.plugin.registry import PluginRegistry
from app.schemas.plugin import PluginRuntimeStatus
class _GilState:
"""
模拟进程级 GIL 状态:一旦开启就保持到进程结束。
"""
def __init__(self) -> None:
self.enabled = False
def enable(self) -> None:
"""
模拟原生扩展导入后重新启用 GIL 的状态变化。
"""
self.enabled = True
@pytest.fixture
def gil_state(monkeypatch) -> _GilState:
"""
在 free-threaded 运行时下用可控的 GIL 状态替换真实解释器状态。
"""
state = _GilState()
monkeypatch.setattr(gil_module, "is_free_threaded_runtime", lambda: True)
monkeypatch.setattr(gil_module, "is_gil_enabled", lambda: state.enabled)
return state
def test_attribution_records_only_transition(gil_state):
"""
只有区间内发生关闭到开启的转换才归因,GIL 已开启时后续插件不再被记录。
"""
recorded: list[str] = []
with attribute_gil_fallback("Quiet", recorded.append):
pass
with attribute_gil_fallback("Native", recorded.append):
gil_state.enable()
with attribute_gil_fallback("Later", recorded.append):
pass
assert recorded == ["Native"]
def test_attribution_records_even_when_load_fails(gil_state):
"""
导入原生扩展后插件自身抛错,回退事实仍要归因,异常照常外抛。
"""
recorded: list[str] = []
with pytest.raises(RuntimeError):
with attribute_gil_fallback("Broken", recorded.append):
gil_state.enable()
raise RuntimeError("init failed")
assert recorded == ["Broken"]
def test_standard_runtime_never_attributes(monkeypatch):
"""
标准解释器 GIL 本来就开启,不存在回退,不做归因。
"""
monkeypatch.setattr(gil_module, "is_free_threaded_runtime", lambda: False)
states = iter((False, True))
monkeypatch.setattr(gil_module, "is_gil_enabled", lambda: next(states))
recorded: list[str] = []
with attribute_gil_fallback("Demo", recorded.append):
pass
assert recorded == []
def test_registry_tracks_gil_fallback_with_generation():
"""
注册表只在首次记录时推进刷新代次,移除与清空会同步清掉归因。
"""
registry = PluginRegistry()
start = registry.generation
registry.mark_gil_fallback("Native")
registry.mark_gil_fallback("Native")
assert registry.gil_fallback_snapshot() == ["Native"]
assert registry.generation == start + 1
registry.remove("Native")
assert registry.gil_fallback_snapshot() == []
assert registry.generation == start + 2
registry.mark_gil_fallback("Other")
registry.clear()
assert registry.gil_fallback_snapshot() == []
def test_loader_attributes_module_import_to_installed_plugin_id(tmp_path, monkeypatch, gil_state):
"""
全量加载时逐个模块观察,回退归因到导入原生扩展的插件,并还原安装时的 ID 大小写。
"""
for name in ("pureplugin", "nativeplugin"):
(tmp_path / name).mkdir()
(tmp_path / name / "__init__.py").write_text("", encoding="utf-8")
(tmp_path / "_ignored").mkdir()
def import_module(module_name: str) -> ModuleType:
if module_name == "app.plugins.nativeplugin":
gil_state.enable()
return ModuleType(module_name)
monkeypatch.setattr(loader_module, "importlib", SimpleNamespace(import_module=import_module))
recorded: list[str] = []
loader = PluginLoader(
plugins_root=tmp_path,
import_preparer=lambda **_kwargs: None,
import_scanner=lambda **_kwargs: None,
log=MagicMock(),
gil_fallback_recorder=recorded.append,
)
loader.load(None, ["PurePlugin", "NativePlugin"], lambda _candidate: True)
assert recorded == ["NativePlugin"]
def test_lifecycle_attributes_init_plugin_import(gil_state):
"""
插件在 init_plugin 中才导入原生扩展时,回退归因到该插件。
"""
class LazyNative:
"""
在初始化阶段触发原生扩展状态变化的插件替身。
"""
plugin_name = "惰性原生"
plugin_version = "1.0.0"
def init_plugin(self, _config):
"""
在插件初始化阶段触发模拟的 GIL 启用,供归因逻辑记录责任阶段。
"""
gil_state.enable()
@staticmethod
def get_state():
"""
将测试插件保持为启用状态,使初始化归因流程完整执行。
"""
return True
recorded: list[str] = []
lifecycle = PluginLifecycle(
classes={},
running={},
load_plugins=lambda _plugin_id, _installed, _check: [LazyNative],
loadable_plugins=lambda: ["LazyNative"],
plugin_config=lambda _plugin_id: {},
auth_checker=lambda _plugin: True,
clear_modules=MagicMock(),
clear_tools=MagicMock(),
enable_events=MagicMock(),
disable_events=MagicMock(),
runtime_status_writer=lambda _plugin_id, _status: None,
runtime_compatible=lambda _plugin_id: True,
database=lambda: PluginDatabase(),
log=MagicMock(),
event_sender=MagicMock(),
gil_fallback_recorder=recorded.append,
)
assert lifecycle.start() == {"LazyNative": PluginRuntimeStatus.ACTIVE}
assert recorded == ["LazyNative"]