Files
MoviePilot/tests/test_config_reload_handler.py
T

149 lines
5.0 KiB
Python

"""ConfigReloadMixin 配置变更事件处理器解析的回归测试(Issue #6329)。
ConfigReloadMixin 动态生成的事件处理器,必须能被事件总线解析回实例方法,
不能因 __name__ 与类上方法名不一致而被静默跳过。
"""
import inspect
import pytest
from app.runtime.events import Event, EventHandlerBinding, eventmanager
from app.runtime.reload import ConfigReloadMixin
from app.schemas import ConfigChangeEventData
from app.schemas.types import EventType
class _ReloadRecorder(ConfigReloadMixin):
"""测试用同步重载子类,记录重载调用次数。"""
CONFIG_WATCH = {"TEST_RELOAD_KEY"}
def __init__(self):
self.reload_count = 0
def on_config_changed(self):
self.reload_count += 1
class _AsyncReloadRecorder(ConfigReloadMixin):
"""测试用异步重载子类,记录重载调用次数。"""
CONFIG_WATCH = {"TEST_RELOAD_KEY"}
def __init__(self):
self.reload_count = 0
async def on_config_changed(self):
self.reload_count += 1
@pytest.fixture(params=[_ReloadRecorder, _AsyncReloadRecorder])
def recorder(request, monkeypatch):
"""将生成的处理器绑定到测试实例,并在测试后移除全局监听。"""
recorder_cls = request.param
instance = recorder_cls()
def resolver(owner_class):
if owner_class is recorder_cls:
return EventHandlerBinding(instance=instance, owner_name=owner_class.__name__)
return None
# 隔离真实解析器,仅绑定测试实例
monkeypatch.setattr(
eventmanager,
"_EventManager__handler_instance_resolvers",
{"test_reload": resolver},
)
yield instance
# 移除类定义时注册的全局监听,避免影响其余测试
eventmanager.remove_event_listener(
EventType.ConfigChanged, recorder_cls.handle_config_changed
)
def _build_event(keys):
"""构造携带指定配置键的 ConfigChanged 事件。"""
return Event(EventType.ConfigChanged, ConfigChangeEventData(key=set(keys)))
async def _dispatch(instance, event):
"""按处理器类型走事件总线的同步或异步调用路径。"""
handler = instance.__class__.handle_config_changed
if inspect.iscoroutinefunction(handler):
await eventmanager._EventManager__invoke_handler_by_type_async(handler, event)
else:
eventmanager._EventManager__invoke_handler_by_type_sync(handler, event)
def test_generated_handler_exposes_method_name(recorder):
"""生成的处理器 __name__ 必须与类上的方法名一致,保证事件总线能解析到实例方法。"""
handler = recorder.__class__.handle_config_changed
assert handler.__name__ == "handle_config_changed"
assert handler.__qualname__ == f"{recorder.__class__.__name__}.handle_config_changed"
@pytest.mark.asyncio
async def test_config_changed_event_triggers_reload(recorder):
"""命中 CONFIG_WATCH 的配置变更事件必须触发子类重载逻辑。"""
await _dispatch(recorder, _build_event({"TEST_RELOAD_KEY"}))
assert recorder.reload_count == 1
@pytest.mark.asyncio
async def test_unrelated_config_key_skips_reload(recorder):
"""未监听的配置键变更不得触发重载。"""
await _dispatch(recorder, _build_event({"OTHER_KEY"}))
assert recorder.reload_count == 0
@pytest.mark.asyncio
async def test_resolve_falls_back_to_qualname_when_name_mismatched(monkeypatch):
"""处理器 __name__ 与类上方法名不一致时,事件总线应回退 __qualname__ 末段解析。"""
instance = _ReloadRecorder()
def wrapper(event): # pylint: disable=unused-argument
"""模拟只同步了 __qualname__ 的动态处理器。"""
instance.on_config_changed()
wrapper.__module__ = _ReloadRecorder.__module__
wrapper.__qualname__ = f"{_ReloadRecorder.__name__}.handle_config_changed"
def resolver(owner_class):
if owner_class is _ReloadRecorder:
return EventHandlerBinding(instance=instance, owner_name=owner_class.__name__)
return None
monkeypatch.setattr(
eventmanager,
"_EventManager__handler_instance_resolvers",
{"test_reload": resolver},
)
await eventmanager._EventManager__invoke_handler_by_type_async(
wrapper, _build_event({"TEST_RELOAD_KEY"})
)
assert wrapper.__name__ == "wrapper"
assert instance.reload_count == 1
def test_externally_managed_reload_class_does_not_register_listener(monkeypatch):
"""外部统一管理配置生命周期时,Mixin 保留重载能力但不重复绑定事件。"""
registrations = []
monkeypatch.setattr(
eventmanager,
"add_event_listener",
lambda *args, **kwargs: registrations.append((args, kwargs)),
)
class _ExternallyManagedReloadRecorder(ConfigReloadMixin):
CONFIG_RELOAD_MANAGED_EXTERNALLY = True
CONFIG_WATCH = {"TEST_RELOAD_KEY"}
def on_config_changed(self):
pass
assert registrations == []
assert "handle_config_changed" not in _ExternallyManagedReloadRecorder.__dict__