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

150 lines
7.1 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.
"""
多站点媒体搜索必须在提前停止前向英文站点提交可用关键词。
"""
import asyncio
from concurrent.futures import Future
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from app.chain.search import media as media_module
from app.chain.search import provider as provider_module
from app.chain.search.facade import SearchChain
from app.domain.context import Context, MediaInfo, TorrentInfo
from app.schemas.types import MediaType
def _assert_english_site_is_searched_before_chinese_match_stops_search(
monkeypatch: pytest.MonkeyPatch, mode: str,
) -> None:
"""
中文站点首轮命中时,英文站点及其续页仍应使用英文名完成同轮查询。
"""
chain = object.__new__(SearchChain)
chain.runtime_config = SimpleNamespace(search_threadpool_size=2, search_multiple_name=False)
media = MediaInfo(tmdb_id=157336, title="星际穿越", en_title="Interstellar",
names=["星际穿越", "Interstellar"], type=MediaType.MOVIE)
sites = [{"id": 1, "name": "中文站"}, {"id": 2, "name": "FileList", "language": "en"}]
requests: list[tuple[int, str, int]] = []
continuations: list[tuple[int, str]] = []
def submit(function: Any, *args: Any, **kwargs: Any) -> Future[Any]:
"""
就地执行同步任务,避免测试引入真实线程池生命周期。
"""
future: Future[Any] = Future()
future.set_result(function(*args, **kwargs))
return future
def search_site(*, site: dict[str, Any], keyword: str, page: int, **_kwargs: Any) -> list[TorrentInfo]:
"""
模拟英文站拒绝中文、两站第一页各返回一条可匹配资源。
"""
requests.append((site["id"], keyword, page))
if page or (site["id"] == 2 and keyword != "Interstellar"):
return []
return [TorrentInfo(site=site["id"], title="Interstellar 2014 1080p",
enclosure=f"https://site{site['id']}.example/download?id=1")]
def should_continue(*, site: dict[str, Any], keyword: str, page_results: list[TorrentInfo]) -> bool:
"""
记录分页判定使用的关键词,确保它与实际请求一致。
"""
continuations.append((site["id"], keyword))
return bool(page_results)
def parse_result(*, torrents: list[TorrentInfo], **_kwargs: Any) -> list[Context]:
"""
把两站资源标记为已匹配,触发首轮结束后的提前停止。
"""
return [Context(torrent_info=torrent, media_info=media) for torrent in torrents]
media_chain = SimpleNamespace(
supplement_media_info=lambda **kwargs: kwargs["mediainfo"],
async_supplement_media_info=AsyncMock(side_effect=lambda **kwargs: kwargs["mediainfo"]),
)
monkeypatch.setattr(media_module, "MediaChain", lambda: media_chain)
monkeypatch.setattr(provider_module, "ThreadHelper", lambda: SimpleNamespace(submit=submit))
monkeypatch.setattr(provider_module, "ProgressHelper", MagicMock())
monkeypatch.setattr(provider_module, "AsyncProgressHelper", lambda *_args: SimpleNamespace(
start=AsyncMock(), update=AsyncMock(), end=AsyncMock(),
))
chain._sync_indexers = lambda _sites: sites
chain._async_indexers = AsyncMock(return_value=sites)
chain._prepare_params = lambda **_kwargs: (None, ["星际穿越", "Interstellar"])
chain._build_search_pages = lambda _page: [0, 1, 2]
chain._should_continue_search_pages = should_continue
chain.search_plugin_torrents = MagicMock(return_value=[])
chain.async_search_plugin_torrents = AsyncMock(return_value=[])
chain.search_site_torrents = search_site
chain.async_search_site_torrents = AsyncMock(side_effect=search_site)
chain._parse_result = parse_result
async def stream_results() -> list[dict[str, Any]]:
"""
收集流式入口最终结果,确保中间预览不影响提前停止判断。
"""
return [event async for event in chain.async_process_stream(mediainfo=media, sites=[1, 2])]
if mode == "sync":
contexts = chain.process(mediainfo=media, sites=[1, 2])
assert {context.torrent_info.site for context in contexts} == {1, 2}
chain.search_plugin_torrents.assert_called_once()
elif mode == "async":
contexts = asyncio.run(chain.async_process(mediainfo=media, sites=[1, 2]))
assert {context.torrent_info.site for context in contexts} == {1, 2}
chain.async_search_plugin_torrents.assert_awaited_once()
else:
events = asyncio.run(stream_results())
replacement = next(event for event in events if event.get("type") == "replace")
assert {item["torrent_info"]["site"] for item in replacement["items"]} == {1, 2}
chain.async_search_plugin_torrents.assert_awaited_once()
assert sorted(requests) == [(1, "星际穿越", 0), (1, "星际穿越", 1),
(2, "Interstellar", 0), (2, "Interstellar", 1)]
assert set(continuations) == {(1, "星际穿越"), (2, "Interstellar")}
def test_sync_english_site_is_searched_before_early_stop(monkeypatch: pytest.MonkeyPatch) -> None:
"""
同步搜索必须先完成英文站点查询再决定是否停止换词。
"""
_assert_english_site_is_searched_before_chinese_match_stops_search(monkeypatch, "sync")
def test_async_english_site_is_searched_before_early_stop(monkeypatch: pytest.MonkeyPatch) -> None:
"""
异步搜索必须先完成英文站点查询再决定是否停止换词。
"""
_assert_english_site_is_searched_before_chinese_match_stops_search(monkeypatch, "async")
def test_stream_english_site_is_searched_before_early_stop(monkeypatch: pytest.MonkeyPatch) -> None:
"""
流式搜索必须保留英文站点资源和一致的最终过滤结果。
"""
_assert_english_site_is_searched_before_chinese_match_stops_search(monkeypatch, "stream")
def test_site_language_fallback_preserves_search_boundaries() -> None:
"""
仅为拒绝中文的站点替换媒体名称,IMDb、英文原词和无媒体输入保持原样。
"""
media = MediaInfo(en_title="Interstellar", original_title="Original title", names=["Alias"])
english = {"language": "en"}
assert provider_module._site_keyword(english, "星际穿越", media) == "Interstellar"
assert provider_module._site_keyword({}, "星际穿越", media) == "星际穿越"
assert provider_module._site_keyword(english, "Interstellar S02", media) == "Interstellar S02"
assert provider_module._site_keyword(english, "tt0816692", media) == "tt0816692"
assert provider_module._site_keyword(english, "星际穿越", None) == "星际穿越"
assert provider_module._site_keyword(english, "", media) == ""
media.en_title = "中文"
assert provider_module._site_keyword(english, "星际穿越", media) == "Original title"
media.original_title = "中文原名"
assert provider_module._site_keyword(english, "星际穿越", media) == "Alias"
media.names = ["中文别名"]
assert provider_module._site_keyword(english, "星际穿越", media) == "星际穿越"