mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/jxxghp/MoviePilot.git
synced 2026-09-20 08:03:34 +08:00
174 lines
6.4 KiB
Python
174 lines
6.4 KiB
Python
import threading
|
||
import time
|
||
from unittest.mock import MagicMock
|
||
|
||
from app.application.messaging.message import (
|
||
MessageHelper,
|
||
MessageQueueManager,
|
||
TemplateHelper,
|
||
stop_message,
|
||
)
|
||
from app.foundation.singleton import Singleton, SingletonClass
|
||
|
||
|
||
def test_message_queue_stop_wakes_idle_monitor(monkeypatch):
|
||
"""消息队列停止时应唤醒空闲监控线程,不等待完整检查周期"""
|
||
monkeypatch.setattr(MessageQueueManager, "init_config", lambda self: None)
|
||
manager = object.__new__(MessageQueueManager)
|
||
manager.__init__(check_interval=10)
|
||
|
||
started_at = time.monotonic()
|
||
converged = manager.stop()
|
||
elapsed = time.monotonic() - started_at
|
||
|
||
assert converged is True
|
||
assert elapsed < 1
|
||
assert manager.thread is None
|
||
|
||
|
||
def test_message_queue_stop_reports_blocked_callback_and_supports_retry(monkeypatch):
|
||
"""发送回调仍阻塞时应有限返回 False,并保留线程供后续重试。"""
|
||
entered = threading.Event()
|
||
release = threading.Event()
|
||
|
||
def blocked_send_callback(*_args, **_kwargs) -> None:
|
||
"""模拟无法由消息队列强制取消的同步渠道调用。"""
|
||
entered.set()
|
||
release.wait()
|
||
|
||
monkeypatch.setattr(MessageQueueManager, "init_config", lambda self: None)
|
||
manager = object.__new__(MessageQueueManager)
|
||
manager.__init__(send_callback=blocked_send_callback, check_interval=0)
|
||
manager.queue.put({"args": ("payload",), "kwargs": {}})
|
||
|
||
assert entered.wait(timeout=1)
|
||
try:
|
||
started_at = time.monotonic()
|
||
assert manager.stop(timeout=0.01) is False
|
||
assert time.monotonic() - started_at < 1
|
||
assert manager.thread is not None
|
||
assert manager.thread.is_alive()
|
||
finally:
|
||
release.set()
|
||
|
||
assert manager.stop(timeout=1) is True
|
||
assert manager.thread is None
|
||
|
||
|
||
def test_stop_message_does_not_initialize_absent_services(monkeypatch):
|
||
"""消息服务未初始化时,关闭入口不应为了清理而创建后台资源"""
|
||
monkeypatch.setattr(SingletonClass, "_instances", {})
|
||
monkeypatch.setattr(Singleton, "_instances", {})
|
||
|
||
assert MessageQueueManager.get_existing_instance() is None
|
||
assert TemplateHelper.get_existing_instance() is None
|
||
assert MessageHelper.get_existing_instance() is None
|
||
assert stop_message() is True
|
||
|
||
assert MessageQueueManager not in SingletonClass._instances
|
||
assert TemplateHelper not in SingletonClass._instances
|
||
|
||
|
||
def test_explicit_queue_can_restart_after_converged_stop(monkeypatch) -> None:
|
||
"""显式 owner 关闭并释放后,下一轮 lifespan 应创建并启动新线程。"""
|
||
monkeypatch.setattr(SingletonClass, "_instances", {})
|
||
monkeypatch.setattr(MessageQueueManager, "init_config", lambda self: None)
|
||
first = MessageQueueManager(auto_start=False)
|
||
assert first.thread is None
|
||
|
||
first.start()
|
||
assert first.thread is not None and first.thread.is_alive()
|
||
assert stop_message(queue_manager=first) is True
|
||
assert MessageQueueManager.get_existing_instance() is None
|
||
|
||
second = MessageQueueManager(auto_start=False)
|
||
assert second is not first
|
||
second.start()
|
||
assert second.thread is not None and second.thread.is_alive()
|
||
assert stop_message(queue_manager=second) is True
|
||
|
||
|
||
def test_bound_clients_keep_distinct_chain_callbacks_without_thread(monkeypatch) -> None:
|
||
"""轻量客户端不得启动线程,也不得把首个 Chain 回调写进共享 owner。"""
|
||
monkeypatch.setattr(SingletonClass, "_instances", {})
|
||
monkeypatch.setattr(MessageQueueManager, "init_config", lambda self: None)
|
||
manager = MessageQueueManager(auto_start=False)
|
||
first = MagicMock()
|
||
second = MagicMock()
|
||
|
||
manager.bind(first).send_message("first", immediately=True)
|
||
manager.bind(second).send_message("second", immediately=True)
|
||
|
||
assert manager.thread is None
|
||
assert manager.send_callback is None
|
||
first.assert_called_once_with("first")
|
||
second.assert_called_once_with("second")
|
||
assert stop_message(queue_manager=manager) is True
|
||
|
||
|
||
def test_stop_message_closes_and_releases_explicit_message_helper(monkeypatch) -> None:
|
||
"""消息通知缓存收敛后必须释放单例,避免下一轮复用已关闭缓存。"""
|
||
monkeypatch.setattr(Singleton, "_instances", {})
|
||
helper = MessageHelper()
|
||
close = MagicMock()
|
||
monkeypatch.setattr(helper._recent_notification_keys, "close", close)
|
||
|
||
assert stop_message(message_helper=helper) is True
|
||
|
||
close.assert_called_once_with()
|
||
assert MessageHelper.get_existing_instance() is None
|
||
assert MessageHelper() is not helper
|
||
|
||
|
||
def test_stop_message_discovers_existing_helper_and_only_closes_it_once(monkeypatch) -> None:
|
||
"""无显式 owner 时应发现既存通知缓存,重复关闭不得再次处理旧实例。"""
|
||
monkeypatch.setattr(Singleton, "_instances", {})
|
||
helper = MessageHelper()
|
||
close = MagicMock()
|
||
monkeypatch.setattr(helper._recent_notification_keys, "close", close)
|
||
|
||
assert stop_message() is True
|
||
assert stop_message() is True
|
||
|
||
close.assert_called_once_with()
|
||
assert MessageHelper.get_existing_instance() is None
|
||
|
||
|
||
def test_stop_message_does_not_release_replacement_helper(monkeypatch) -> None:
|
||
"""旧缓存关闭期间若发布了新 owner,身份校验不得误删新实例。"""
|
||
monkeypatch.setattr(Singleton, "_instances", {})
|
||
old_helper = MessageHelper()
|
||
replacements = []
|
||
|
||
def replace_owner() -> None:
|
||
"""模拟关闭回调先释放旧身份,再发布下一轮 lifespan 的 owner。"""
|
||
assert MessageHelper.release_existing_instance(old_helper) is True
|
||
replacements.append(MessageHelper())
|
||
|
||
monkeypatch.setattr(old_helper._recent_notification_keys, "close", replace_owner)
|
||
|
||
assert stop_message(message_helper=old_helper) is True
|
||
|
||
assert len(replacements) == 1
|
||
assert MessageHelper.get_existing_instance() is replacements[0]
|
||
|
||
|
||
def test_stop_message_aggregates_queue_failure_and_closes_template(monkeypatch):
|
||
"""消息队列未收敛时仍应关闭模板缓存并向生命周期返回 False。"""
|
||
queue_manager = MagicMock()
|
||
queue_manager.stop.return_value = False
|
||
template_helper = MagicMock()
|
||
monkeypatch.setattr(
|
||
SingletonClass,
|
||
"_instances",
|
||
{
|
||
MessageQueueManager: queue_manager,
|
||
TemplateHelper: template_helper,
|
||
},
|
||
)
|
||
|
||
assert stop_message(timeout=0.25) is False
|
||
|
||
queue_manager.stop.assert_called_once_with(timeout=0.25)
|
||
template_helper.close.assert_called_once_with()
|