From d4488671044e6e66d15e4ddede6281b8442b3f8b Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 29 Aug 2026 19:36:56 +0800 Subject: [PATCH] test(tmdb): use configured pytest composition --- docs/architecture-optimization-checklist.md | 2 +- .../fixtures/architecture/ruff-baseline.json | 3 - tests/test_tmdb_recognize.py | 264 ++++++++---------- 3 files changed, 114 insertions(+), 155 deletions(-) diff --git a/docs/architecture-optimization-checklist.md b/docs/architecture-optimization-checklist.md index 54ad80cbb..56c28dbb2 100644 --- a/docs/architecture-optimization-checklist.md +++ b/docs/architecture-optimization-checklist.md @@ -79,7 +79,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain` | Python 源码量 | 约 298,700 行 | 60 个文件超过 1,000 行,10 个超过 2,000 行 | | 长方法 | 299 个超过 80 行 | 70 个超过 150 行,23 个超过 250 行;大量是私有方法 | | 全量 mypy 历史债务 | 10,255 / 592 文件 | 识别链 typed plan/result 与 TMDB 双 ABI 共用计划继续清除参数、返回值和泛型注解债;strict frontier 当前覆盖 41 个文件 | -| Ruff 历史诊断 | 643 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | +| Ruff 历史诊断 | 642 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | | 覆盖率低水位 | Application 81.00%,Domain 80.67% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 | ### 3.3 热点文件 diff --git a/tests/fixtures/architecture/ruff-baseline.json b/tests/fixtures/architecture/ruff-baseline.json index 1bae0a55c..034447863 100644 --- a/tests/fixtures/architecture/ruff-baseline.json +++ b/tests/fixtures/architecture/ruff-baseline.json @@ -1244,9 +1244,6 @@ "tests/test_tmdb_failure_snapshot_cache.py": { "I001": 1 }, - "tests/test_tmdb_recognize.py": { - "I001": 1 - }, "tests/test_tmdb_retry_and_errors.py": { "F811": 1 }, diff --git a/tests/test_tmdb_recognize.py b/tests/test_tmdb_recognize.py index 41b82d8a6..d37e1b0db 100644 --- a/tests/test_tmdb_recognize.py +++ b/tests/test_tmdb_recognize.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -import asyncio import json from copy import deepcopy from pathlib import Path -from unittest import TestCase from unittest.mock import AsyncMock, MagicMock, patch from urllib.parse import parse_qsl, urlencode, urlsplit -from app.domain.metainfo import MetaInfo -from app.chain.base import ChainBase +import pytest + from app.adapters.external.server import MoviePilotServerHelper +from app.chain.base import ChainBase +from app.domain.metainfo import MetaInfo from app.modules.themoviedb import TheMovieDbModule from app.modules.themoviedb.tmdbv3api.tmdb import TMDb from app.schemas.types import MediaSource, MediaType @@ -96,159 +96,121 @@ def tearDownModule(): _PATCHERS.clear() -class TmdbRecognizeModuleTest(TestCase): - """ - TMDB模块层识别测试 - 模块层使用统一媒体身份;历史 tmdbid 文件名标签只在元数据解析边界转换。 - """ - - @classmethod - def setUpClass(cls): - cls.module = TheMovieDbModule() - cls.module.init_module() - - @classmethod - def tearDownClass(cls): - cls.module.stop() - - def _run(self, coro): - return asyncio.run(coro) - - def test_tmdbid_priority_over_title(self): - """ - 当标题中包含 {tmdbid=xxx} 时,应优先使用tmdbid识别, - 而非回退到标题搜索 - """ - meta = MetaInfo(title="空之境界 {tmdbid=938416}") - self.assertEqual(meta.media_source, MediaSource.TMDB) - self.assertEqual(meta.media_id, "938416") - self.assertEqual(meta.cn_name, "空之境界") - - result = self._run( - self.module.async_recognize_media( - meta=meta, - media_source=meta.media_source, - media_id=meta.media_id, - cache=False, - ) - ) - self.assertIsNotNone(result, "应能识别到媒体信息") - self.assertEqual(result.tmdb_id, 938416) - - def test_tmdbid_disambiguation_tv_vs_movie(self): - """ - 当同一tmdbid同时存在电影和电视剧时,应通过元数据消歧 - tmdbid=23155 同时存在电影"空之境界 第五章 矛盾螺旋"和电视剧"TV Land Top 10" - 标题包含"空之境界"应消歧为电影 - """ - meta = MetaInfo(title="空之境界 第五章 矛盾螺旋 (2008) {tmdbid=23155}") - self.assertEqual(meta.media_source, MediaSource.TMDB) - self.assertEqual(meta.media_id, "23155") - - result = self._run( - self.module.async_recognize_media( - meta=meta, - media_source=meta.media_source, - media_id=meta.media_id, - cache=False, - ) - ) - self.assertIsNotNone(result, "同ID存在电影和电视剧时应能通过元数据消歧") - self.assertEqual(result.tmdb_id, 23155) - self.assertEqual(result.type, MediaType.MOVIE) - - def test_tmdbid_with_explicit_type(self): - """ - 当标题中同时包含 tmdbid 和 type 时,应直接使用指定类型查询 - """ - meta = MetaInfo(title="空之境界 {tmdbid=23155}") - - result = self._run( - self.module.async_recognize_media( - meta=meta, - media_source=meta.media_source, - media_id=meta.media_id, - mtype=MediaType.TV, - cache=False, - ) - ) - self.assertIsNotNone(result) - self.assertEqual(result.tmdb_id, 23155) - self.assertEqual(result.type, MediaType.TV) - - def test_tmdbid_only_movie_exists(self): - """ - tmdbid仅存在电影时,即使meta.type推断为TV也应正确识别为电影 - tmdbid=496891 仅存在电影"少女与战车 最终章 ~第2话~" - """ - meta = MetaInfo(title="少女与战车 最终章 ~第2话~ (2019) {tmdbid=496891}") - self.assertEqual(meta.media_source, MediaSource.TMDB) - self.assertEqual(meta.media_id, "496891") - - result = self._run( - self.module.async_recognize_media( - meta=meta, - media_source=meta.media_source, - media_id=meta.media_id, - cache=False, - ) - ) - self.assertIsNotNone(result, "仅存在电影时应正确识别") - self.assertEqual(result.tmdb_id, 496891) - self.assertEqual(result.type, MediaType.MOVIE) +@pytest.fixture +def tmdb_module(): + """创建并在用例结束后关闭独立的 TMDB 模块实例。""" + module = TheMovieDbModule() + module.init_module() + yield module + module.stop() -class TmdbRecognizeChainTest(TestCase): - """ - ChainBase层识别测试(端到端) - 验证旧文件名标签转换为统一媒体身份后的完整识别流程 - """ +@pytest.fixture +def chain(): + """在 pytest 组合根完成装配后创建端到端 Chain。""" + return ChainBase() - @classmethod - def setUpClass(cls): - cls.chain = ChainBase() - def _run(self, coro): - return asyncio.run(coro) +@pytest.mark.asyncio +async def test_tmdbid_priority_over_title(tmdb_module): + """文件名带 TMDB 身份时优先按身份识别,不回退标题搜索。""" + meta = MetaInfo(title="空之境界 {tmdbid=938416}") + assert meta.media_source is MediaSource.TMDB + assert meta.media_id == "938416" + assert meta.cn_name == "空之境界" - def test_chain_tmdbid_movie(self): - """ - 通过ChainBase识别,tmdbid对应电影应正确识别 - """ - meta = MetaInfo(title="空之境界 第五章 矛盾螺旋 (2008) {tmdbid=23155}") - result = self._run( - self.chain.async_recognize_media(meta=meta, cache=False) - ) - self.assertIsNotNone(result) - self.assertEqual(result.tmdb_id, 23155) - self.assertEqual(result.type, MediaType.MOVIE) + result = await tmdb_module.async_recognize_media( + meta=meta, + media_source=meta.media_source, + media_id=meta.media_id, + cache=False, + ) + assert result is not None, "应能识别到媒体信息" + assert result.tmdb_id == 938416 - def test_chain_tmdbid_ignores_inferred_type(self): - """ - 当tmdbid存在时,不应使用meta推断的类型 - "第2话"会让meta.type推断为TV,但tmdbid=496891仅存在电影 - """ - meta = MetaInfo(title="少女与战车 最终章 ~第2话~ (2019) {tmdbid=496891}") - self.assertEqual(meta.type, MediaType.TV, "meta.type应被推断为TV") - self.assertEqual(meta.media_source, MediaSource.TMDB) - self.assertEqual(meta.media_id, "496891") - result = self._run( - self.chain.async_recognize_media(meta=meta, cache=False) - ) - self.assertIsNotNone(result, "有tmdbid时不应因meta.type推断错误而识别失败") - self.assertEqual(result.tmdb_id, 496891) - self.assertEqual(result.type, MediaType.MOVIE) +@pytest.mark.asyncio +async def test_tmdbid_disambiguation_tv_vs_movie(tmdb_module): + """同一 TMDB ID 同时命中电影和剧集时按标题证据消歧。""" + meta = MetaInfo(title="空之境界 第五章 矛盾螺旋 (2008) {tmdbid=23155}") + assert meta.media_source is MediaSource.TMDB + assert meta.media_id == "23155" - def test_chain_no_tmdbid_uses_inferred_type(self): - """ - 无tmdbid时,应正常使用meta推断的类型进行标题搜索 - """ - meta = MetaInfo(title="进击的巨人 S01E01") - self.assertEqual(meta.type, MediaType.TV) + result = await tmdb_module.async_recognize_media( + meta=meta, + media_source=meta.media_source, + media_id=meta.media_id, + cache=False, + ) + assert result is not None, "同 ID 存在电影和电视剧时应能通过元数据消歧" + assert result.tmdb_id == 23155 + assert result.type is MediaType.MOVIE - result = self._run( - self.chain.async_recognize_media(meta=meta, cache=False) - ) - self.assertIsNotNone(result) - self.assertEqual(result.type, MediaType.TV) + +@pytest.mark.asyncio +async def test_tmdbid_with_explicit_type(tmdb_module): + """显式媒体类型与 TMDB 身份同时存在时直接查询指定类型。""" + meta = MetaInfo(title="空之境界 {tmdbid=23155}") + result = await tmdb_module.async_recognize_media( + meta=meta, + media_source=meta.media_source, + media_id=meta.media_id, + mtype=MediaType.TV, + cache=False, + ) + assert result is not None + assert result.tmdb_id == 23155 + assert result.type is MediaType.TV + + +@pytest.mark.asyncio +async def test_tmdbid_only_movie_exists(tmdb_module): + """只有电影记录时忽略标题对 TV 类型的错误推断。""" + meta = MetaInfo(title="少女与战车 最终章 ~第2话~ (2019) {tmdbid=496891}") + assert meta.media_source is MediaSource.TMDB + assert meta.media_id == "496891" + + result = await tmdb_module.async_recognize_media( + meta=meta, + media_source=meta.media_source, + media_id=meta.media_id, + cache=False, + ) + assert result is not None, "仅存在电影时应正确识别" + assert result.tmdb_id == 496891 + assert result.type is MediaType.MOVIE + + +@pytest.mark.asyncio +async def test_chain_tmdbid_movie(chain): + """端到端 Chain 应按 TMDB 身份识别电影。""" + meta = MetaInfo(title="空之境界 第五章 矛盾螺旋 (2008) {tmdbid=23155}") + result = await chain.async_recognize_media(meta=meta, cache=False) + assert result is not None + assert result.tmdb_id == 23155 + assert result.type is MediaType.MOVIE + + +@pytest.mark.asyncio +async def test_chain_tmdbid_ignores_inferred_type(chain): + """端到端 Chain 有 TMDB 身份时不采信标题误推断的类型。""" + meta = MetaInfo(title="少女与战车 最终章 ~第2话~ (2019) {tmdbid=496891}") + assert meta.type is MediaType.TV, "meta.type 应被推断为 TV" + assert meta.media_source is MediaSource.TMDB + assert meta.media_id == "496891" + + result = await chain.async_recognize_media(meta=meta, cache=False) + assert result is not None, "有 TMDB 身份时不应因类型推断错误而识别失败" + assert result.tmdb_id == 496891 + assert result.type is MediaType.MOVIE + + +@pytest.mark.asyncio +async def test_chain_no_tmdbid_uses_inferred_type(chain): + """无 TMDB 身份时端到端 Chain 继续使用标题推断类型。""" + meta = MetaInfo(title="进击的巨人 S01E01") + assert meta.type is MediaType.TV + + result = await chain.async_recognize_media(meta=meta, cache=False) + assert result is not None + assert result.type is MediaType.TV