Files
MoviePilot/tests/test_chain_rate_limit.py
freeman 88641d4a7a test: stub optional downloader modules only when they are missing
test_chain_rate_limit.py and test_system_notification_dispatch.py
installed a bare qbittorrentapi/transmission_rpc/psutil ModuleType into
sys.modules at import time with sys.modules.setdefault. When either file
is collected before a test that imports the real package in the same
shard (for example tests/test_downloader_add_failure_message.py and its
TorrentDictionary import), the placeholder wins and collection fails
with "cannot import name ... (unknown location)". Any change to the test
file set reshuffles the six CI shards and can surface this.

Use app.testing.bootstrap.ensure_optional_stub, which keeps the real
module when it is installed, and convert both unittest.TestCase files to
pytest-native functions as the testing guide requires when touching a
TestCase file. Record the resulting lower Ruff watermark.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 11:58:51 +08:00

116 lines
4.1 KiB
Python
Raw 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.
"""Chain 本地限流行为测试。"""
# ruff: noqa: E402 - 可选下载器模块必须在导入 Chain 前完成隔离。
from unittest.mock import Mock
import pytest
from app.testing.bootstrap import ensure_optional_stub
# 仅在可选依赖缺失时补占位;已安装时保留真实模块,避免污染同一进程里导入真实类型的用例。
ensure_optional_stub("qbittorrentapi", TorrentFilesList=list)
ensure_optional_stub("transmission_rpc", File=object)
from app.application.chain.context import ChainRuntimeContext
from app.chain.base import ChainBase
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher
from app.schemas.exception import RateLimitExceededException
class _LimitedModule:
"""模拟始终触发本地限流的宿主模块。"""
def get_name(self):
"""
返回测试模块名称。
"""
return "限流测试模块"
def get_priority(self):
"""
返回测试模块优先级。
"""
return 1
def limited_method(self, raise_exception: bool = False):
"""
模拟同步模块在本地限流期间跳过调用。
"""
raise RateLimitExceededException("[limited_method] 限流期间,跳过调用")
async def async_limited_method(self, raise_exception: bool = False):
"""
模拟异步模块在本地限流期间跳过调用。
"""
raise RateLimitExceededException("[async_limited_method] 限流期间,跳过调用")
@pytest.fixture
def chain() -> ChainBase:
"""构造隔离的 ChainBase,避免依赖真实模块和插件运行状态。"""
limited_module = _LimitedModule()
plugin_manager = Mock()
plugin_manager.get_plugin_modules.return_value = {}
module_manager = Mock()
module_manager.get_running_modules.return_value = [limited_module]
message_queue = Mock()
message_queue.bind.return_value = Mock()
return ChainBase(
ChainRuntimeContext(
module_manager=module_manager,
plugin_manager=plugin_manager,
event_manager=Mock(),
message_oper=Mock(),
message_helper=Mock(),
file_cache=Mock(),
async_file_cache=Mock(),
message_queue=message_queue,
module_dispatcher_factory=ModuleInvocationDispatcher,
site_repository=Mock(),
subscription_repository=Mock(),
subscription_mutation_scope=Mock(),
sync_subscription_mutation_scope=Mock(),
subscription_delete_scope=Mock(),
sync_subscription_delete_scope=Mock(),
subscription_completion_scope=Mock(),
rule_group_mutation_scope=Mock(),
site_reference_mutation_scope=Mock(),
download_history_repository=Mock(),
transfer_history_repository=Mock(),
transfer_admission_repository=Mock(),
transfer_execution_repository=Mock(),
media_server_repository=Mock(),
download_failure_repository=Mock(),
user_repository=Mock(),
)
)
def test_rate_limit_is_not_reported_as_system_error(chain: ChainBase) -> None:
"""本地限流跳过不应写入系统错误通知或事件。"""
result = chain.run_module("limited_method")
assert result is None
chain.messagehelper.put.assert_not_called()
chain.eventmanager.send_event.assert_not_called()
def test_rate_limit_can_still_be_raised_explicitly(chain: ChainBase) -> None:
"""调用方显式要求抛出异常时,限流异常应继续向上抛出。"""
with pytest.raises(RateLimitExceededException):
chain.run_module("limited_method", raise_exception=True)
chain.messagehelper.put.assert_not_called()
chain.eventmanager.send_event.assert_not_called()
@pytest.mark.asyncio
async def test_async_rate_limit_is_not_reported_as_system_error(chain: ChainBase) -> None:
"""异步模块的本地限流跳过也不应触发系统错误路径。"""
result = await chain.async_run_module("async_limited_method")
assert result is None
chain.messagehelper.put.assert_not_called()
chain.eventmanager.send_event.assert_not_called()