mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
* perf(mediaserver): add caller-controlled query projections * test(mediaserver): cover query projection option contracts --------- Co-authored-by: jxxghp <jxxghp@gmail.com>
682 lines
23 KiB
Python
682 lines
23 KiB
Python
"""
|
||
验证媒体服务器增量同步的身份隔离、统计和类型归一。
|
||
"""
|
||
|
||
from types import SimpleNamespace
|
||
from unittest.mock import patch
|
||
|
||
import pytest
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import sessionmaker
|
||
|
||
from app import schemas
|
||
from app.chain import mediaserver as MEDIA_SERVER_CHAIN_MODULE
|
||
from app.chain.mediaserver import MediaServerChain
|
||
from app.db.adapters.mediaserver import TransactionalMediaServerRepository
|
||
from app.db.base import Base
|
||
from app.db.models.mediaserver import MediaServerItem
|
||
from app.db.oper.mediaserver import MediaServerOper
|
||
from app.runtime.config import global_vars
|
||
|
||
|
||
@pytest.fixture
|
||
def database(tmp_path):
|
||
"""
|
||
创建隔离的媒体服务器测试数据库。
|
||
"""
|
||
engine = create_engine(f"sqlite:///{tmp_path / 'mediaserver.db'}")
|
||
session_factory = sessionmaker(bind=engine)
|
||
Base.metadata.create_all(bind=engine)
|
||
yield session_factory
|
||
engine.dispose()
|
||
|
||
|
||
def test_add_allows_same_item_id_across_servers(database):
|
||
"""
|
||
不同媒体服务器允许复用相同 item_id。
|
||
"""
|
||
with database() as db:
|
||
oper = MediaServerOper(db)
|
||
|
||
assert oper.add(
|
||
server="plex",
|
||
library="movies",
|
||
item_id="same-item-id",
|
||
item_type="电影",
|
||
title="Movie A",
|
||
)
|
||
assert oper.add(
|
||
server="jellyfin",
|
||
library="movies",
|
||
item_id="same-item-id",
|
||
item_type="电影",
|
||
title="Movie B",
|
||
)
|
||
|
||
items = (
|
||
db.query(MediaServerItem)
|
||
.order_by(MediaServerItem.server.asc())
|
||
.all()
|
||
)
|
||
|
||
assert len(items) == 2
|
||
assert [item.server for item in items] == ["jellyfin", "plex"]
|
||
|
||
|
||
def test_media_count_reuses_existing_server_statistics():
|
||
"""
|
||
整服同步应复用现有媒体统计并排除剧集集数。
|
||
"""
|
||
chain = object.__new__(MediaServerChain)
|
||
chain.run_module = lambda *_args, **_kwargs: [
|
||
schemas.Statistic(movie_count=12, tv_count=8, music_count=3, episode_count=200)
|
||
]
|
||
|
||
assert chain.media_count("plex") == 23
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("raw_type", "expected"),
|
||
[
|
||
("Series", "电视剧"),
|
||
("show", "电视剧"),
|
||
("音乐", "音乐"),
|
||
("MusicAlbum", "音乐"),
|
||
("Audio", "音乐"),
|
||
("artist", "音乐"),
|
||
("album", "音乐"),
|
||
("track", "音乐"),
|
||
("Movie", "电影"),
|
||
],
|
||
)
|
||
def test_sync_normalizes_movie_tv_and_music_item_types(raw_type, expected):
|
||
"""
|
||
同步缓存应保留音乐类型,并兼容不同媒体服务器的原始类型名称。
|
||
"""
|
||
assert MediaServerChain._normalize_item_type(raw_type) == expected
|
||
|
||
|
||
def test_sync_persists_music_without_querying_tv_episodes(database):
|
||
"""
|
||
Navidrome 专辑同步应写成音乐条目,且不能触发电视剧分集查询。
|
||
"""
|
||
chain = object.__new__(MediaServerChain)
|
||
chain.librarys = lambda _server, **_kwargs: [SimpleNamespace(id="music", name="音乐", item_count=1)]
|
||
chain.media_count = lambda _server: 1
|
||
chain.items_count = lambda **_kwargs: pytest.fail("目录已有数量时不应重复计数")
|
||
chain.items = lambda **_kwargs: iter(
|
||
[
|
||
schemas.MediaServerItem(
|
||
server="navidrome",
|
||
library="music",
|
||
item_id="album-1",
|
||
item_type="音乐",
|
||
title="叶惠美",
|
||
year="2003",
|
||
)
|
||
]
|
||
)
|
||
chain.episodes = lambda *_args, **_kwargs: pytest.fail("音乐条目不应查询电视剧分集")
|
||
chain.media_server_repository = TransactionalMediaServerRepository(database)
|
||
|
||
with patch.object(
|
||
MEDIA_SERVER_CHAIN_MODULE,
|
||
"get_mediaserver_configs",
|
||
return_value=[SimpleNamespace(name="navidrome", enabled=True, sync_libraries=["all"])],
|
||
):
|
||
chain.sync()
|
||
|
||
with database() as db:
|
||
item = db.query(MediaServerItem).one()
|
||
|
||
assert item.item_type == "音乐"
|
||
assert item.title == "叶惠美"
|
||
assert item.seasoninfo == {}
|
||
|
||
|
||
def test_sync_normalizes_incomplete_tv_episode_rows(database):
|
||
"""
|
||
TV 同步应丢弃无季号记录,并把缺失集列表规范为空列表。
|
||
"""
|
||
chain = object.__new__(MediaServerChain)
|
||
chain.librarys = lambda _server, **_kwargs: [SimpleNamespace(id="shows", name="剧集库")]
|
||
chain.media_count = lambda _server: 1
|
||
chain.items_count = lambda **_kwargs: 1
|
||
chain.items = lambda **_kwargs: iter(
|
||
[
|
||
schemas.MediaServerItem(
|
||
server="plex",
|
||
library="shows",
|
||
item_id="show-1",
|
||
item_type="Series",
|
||
title="测试剧集",
|
||
)
|
||
]
|
||
)
|
||
chain.episodes = lambda *_args, **_kwargs: [
|
||
schemas.MediaServerSeasonInfo(season=None, episodes=[99]),
|
||
schemas.MediaServerSeasonInfo(season=1, episodes=None),
|
||
]
|
||
chain.media_server_repository = TransactionalMediaServerRepository(database)
|
||
|
||
with patch.object(
|
||
MEDIA_SERVER_CHAIN_MODULE,
|
||
"get_mediaserver_configs",
|
||
return_value=[SimpleNamespace(name="plex", enabled=True, sync_libraries=["all"])],
|
||
):
|
||
chain.sync()
|
||
|
||
with database() as session:
|
||
item = session.query(MediaServerItem).one()
|
||
|
||
assert item.seasoninfo == {"1": []}
|
||
|
||
|
||
def test_sync_updates_rows_and_removes_stale_entries(database):
|
||
"""
|
||
同步应更新已存在条目,并清理未再出现或已移除服务的数据。
|
||
"""
|
||
old_sync_time = "2026-05-01 00:00:00"
|
||
|
||
with database() as db:
|
||
db.add_all(
|
||
[
|
||
MediaServerItem(
|
||
server="plex",
|
||
library="movies",
|
||
item_id="/library/metadata/1",
|
||
item_type="电影",
|
||
title="Old Title",
|
||
year="2024",
|
||
path="/media/old.mkv",
|
||
lst_mod_date=old_sync_time,
|
||
),
|
||
MediaServerItem(
|
||
server="plex",
|
||
library="movies",
|
||
item_id="/library/metadata/2",
|
||
item_type="电影",
|
||
title="Stale Title",
|
||
year="2020",
|
||
path="/media/stale.mkv",
|
||
lst_mod_date=old_sync_time,
|
||
),
|
||
MediaServerItem(
|
||
server="jellyfin",
|
||
library="movies",
|
||
item_id="/library/metadata/1",
|
||
item_type="电影",
|
||
title="Removed Server Title",
|
||
year="2024",
|
||
path="/media/removed.mkv",
|
||
lst_mod_date=old_sync_time,
|
||
),
|
||
]
|
||
)
|
||
db.commit()
|
||
existing_id = (
|
||
db.query(MediaServerItem.id)
|
||
.filter(
|
||
MediaServerItem.server == "plex",
|
||
MediaServerItem.item_id == "/library/metadata/1",
|
||
)
|
||
.scalar()
|
||
)
|
||
|
||
chain = object.__new__(MediaServerChain)
|
||
chain.librarys = lambda _server, **_kwargs: [
|
||
SimpleNamespace(id="movies", name="电影库", item_count=1),
|
||
SimpleNamespace(id="shows", name="剧集库"),
|
||
]
|
||
chain.media_count = lambda _server: pytest.fail("部分媒体库同步不应使用整服统计")
|
||
chain.items_count = lambda **_kwargs: 1
|
||
chain.items = lambda **_kwargs: iter(
|
||
[
|
||
schemas.MediaServerItem(
|
||
server="plex",
|
||
library="movies",
|
||
item_id="/library/metadata/1",
|
||
item_type="Movie",
|
||
title="New Title",
|
||
year="2024",
|
||
tmdbid=100,
|
||
path="/media/new.mkv",
|
||
)
|
||
]
|
||
)
|
||
chain.episodes = lambda *_args, **_kwargs: []
|
||
chain.media_server_repository = TransactionalMediaServerRepository(database)
|
||
|
||
with patch.object(
|
||
MEDIA_SERVER_CHAIN_MODULE,
|
||
"get_mediaserver_configs",
|
||
return_value=[SimpleNamespace(name="plex", enabled=True, sync_libraries=["movies"])],
|
||
):
|
||
chain.sync()
|
||
|
||
with database() as db:
|
||
items = db.query(MediaServerItem).order_by(MediaServerItem.server.asc(), MediaServerItem.item_id.asc()).all()
|
||
|
||
assert len(items) == 1
|
||
assert items[0].id == existing_id
|
||
assert items[0].server == "plex"
|
||
assert items[0].item_id == "/library/metadata/1"
|
||
assert items[0].item_type == "电影"
|
||
assert items[0].title == "New Title"
|
||
assert items[0].path == "/media/new.mkv"
|
||
assert items[0].lst_mod_date != old_sync_time
|
||
|
||
|
||
def test_sync_reuses_library_counts_before_items_and_reports_media_progress(database):
|
||
"""
|
||
同步前应查询全部目标媒体库总数,并按媒体条目更新进度。
|
||
"""
|
||
chain = object.__new__(MediaServerChain)
|
||
events = []
|
||
progress_snapshots = []
|
||
server_libraries = {
|
||
"plex-a": [SimpleNamespace(id="movies", name="电影库", item_count=1)],
|
||
"plex-b": [SimpleNamespace(id="shows", name="剧集库")],
|
||
}
|
||
library_items = {
|
||
("plex-a", "movies"): [
|
||
schemas.MediaServerItem(
|
||
server="plex-a",
|
||
library="movies",
|
||
item_id=f"movie-{index}",
|
||
item_type="Movie",
|
||
title=f"电影 {index}",
|
||
)
|
||
for index in range(2)
|
||
],
|
||
("plex-b", "shows"): [
|
||
schemas.MediaServerItem(
|
||
server="plex-b",
|
||
library="shows",
|
||
item_id="show-1",
|
||
item_type="Movie",
|
||
title="剧集 1",
|
||
)
|
||
],
|
||
}
|
||
|
||
def librarys(server, **_kwargs):
|
||
"""
|
||
返回已经带数量的目录,不允许同步链重复进行整服或逐库统计。
|
||
|
||
:param server: 合成媒体服务器名称
|
||
:param _kwargs: 本次库目录查询的投影与范围选项
|
||
:return: 包含精确条目数的库目录
|
||
"""
|
||
events.append(f"count:{server}")
|
||
for library in server_libraries[server]:
|
||
library.item_count = len(library_items[(server, library.id)])
|
||
return server_libraries[server]
|
||
|
||
chain.librarys = librarys
|
||
|
||
def items(**kwargs):
|
||
"""
|
||
记录同步顺序并返回媒体库条目。
|
||
"""
|
||
server = kwargs["server"]
|
||
library_id = kwargs["library_id"]
|
||
events.append(f"items:{server}:{library_id}")
|
||
return iter(library_items[(server, library_id)])
|
||
|
||
chain.media_count = lambda _server: pytest.fail("不应重复查询整服统计")
|
||
chain.items_count = lambda **_kwargs: pytest.fail("整服同步不应逐库重复计数")
|
||
chain.items = items
|
||
chain.episodes = lambda *_args, **_kwargs: []
|
||
chain.media_server_repository = TransactionalMediaServerRepository(database)
|
||
|
||
with patch.object(
|
||
MEDIA_SERVER_CHAIN_MODULE,
|
||
"get_mediaserver_configs",
|
||
return_value=[
|
||
SimpleNamespace(name="plex-a", enabled=True, sync_libraries=["all"]),
|
||
SimpleNamespace(name="plex-b", enabled=True, sync_libraries=["all"]),
|
||
],
|
||
):
|
||
chain.sync(progress_callback=lambda **kwargs: progress_snapshots.append(kwargs))
|
||
|
||
assert events == [
|
||
"count:plex-a",
|
||
"count:plex-b",
|
||
"items:plex-a:movies",
|
||
"items:plex-b:shows",
|
||
]
|
||
media_progress = [
|
||
snapshot
|
||
for snapshot in progress_snapshots
|
||
if snapshot["text"].startswith(("正在同步 plex-a 媒体库", "正在同步 plex-b 媒体库"))
|
||
]
|
||
assert [round(snapshot["value"], 2) for snapshot in media_progress] == [
|
||
33.33,
|
||
66.67,
|
||
100.0,
|
||
]
|
||
assert media_progress[0]["data"]["media_total"] == 3
|
||
assert media_progress[1]["data"]["library_media_finished"] == 2
|
||
assert media_progress[2]["data"]["current_library"] == "剧集库"
|
||
assert media_progress[2]["data"]["media_total"] == 3
|
||
assert media_progress[2]["data"]["media_finished"] == 3
|
||
progress_values = [snapshot["value"] for snapshot in progress_snapshots]
|
||
assert progress_values == sorted(progress_values)
|
||
|
||
|
||
def test_sync_targets_one_server_without_excluding_other_enabled_servers(monkeypatch):
|
||
"""
|
||
定向同步只访问目标服务器,缓存清理仍保留其他已启用服务器。
|
||
"""
|
||
chain = object.__new__(MediaServerChain)
|
||
library_calls = []
|
||
excluded_server_calls = []
|
||
|
||
class FakeMediaServerOper:
|
||
"""
|
||
记录媒体服务器缓存清理参数的测试替身。
|
||
"""
|
||
|
||
def delete_excluded_servers(self, servers):
|
||
"""
|
||
记录应保留的全部已启用服务器名称。
|
||
"""
|
||
excluded_server_calls.append(servers)
|
||
|
||
chain.librarys = lambda server, **_kwargs: library_calls.append(server) or []
|
||
chain.media_server_repository = FakeMediaServerOper()
|
||
monkeypatch.setattr(
|
||
MEDIA_SERVER_CHAIN_MODULE,
|
||
"get_mediaserver_configs",
|
||
lambda **_kwargs: [
|
||
SimpleNamespace(name="plex-a", enabled=True, sync_libraries=["all"]),
|
||
SimpleNamespace(name="plex-b", enabled=True, sync_libraries=["all"]),
|
||
],
|
||
)
|
||
|
||
chain.sync(server="plex-a")
|
||
|
||
assert library_calls == ["plex-a"]
|
||
assert excluded_server_calls == [["plex-a", "plex-b"]]
|
||
|
||
|
||
@pytest.mark.parametrize("failure", [TimeoutError("read timeout"), ConnectionError("connection refused")])
|
||
def test_sync_preserves_cached_rows_when_plex_read_fails(database, failure):
|
||
"""
|
||
Plex 读取中断时,应保留上一轮缓存且不报告同步完成。
|
||
"""
|
||
with database() as session:
|
||
session.add(
|
||
MediaServerItem(
|
||
server="plex",
|
||
library="movies",
|
||
item_id="not-reached-before-timeout",
|
||
item_type="电影",
|
||
title="已有媒体",
|
||
lst_mod_date="2026-05-01 00:00:00",
|
||
)
|
||
)
|
||
session.commit()
|
||
|
||
chain = object.__new__(MediaServerChain)
|
||
chain.librarys = lambda _server, **_kwargs: [SimpleNamespace(id="movies", name="电影库", item_count=1)]
|
||
chain.media_count = lambda _server: 1
|
||
chain.items_count = lambda **_kwargs: pytest.fail("目录已有数量时不应重复计数")
|
||
|
||
def items_with_failure(**_kwargs):
|
||
"""
|
||
模拟 Plex 已返回部分条目后,下一次分页读取失败。
|
||
"""
|
||
yield schemas.MediaServerItem(
|
||
server="plex",
|
||
library="movies",
|
||
item_id="received-before-timeout",
|
||
item_type="Movie",
|
||
title="已读取媒体",
|
||
)
|
||
raise failure
|
||
|
||
chain.items = items_with_failure
|
||
chain.episodes = lambda *_args, **_kwargs: []
|
||
chain.media_server_repository = TransactionalMediaServerRepository(database)
|
||
progress = []
|
||
|
||
with (
|
||
patch.object(
|
||
MEDIA_SERVER_CHAIN_MODULE,
|
||
"get_mediaserver_configs",
|
||
return_value=[SimpleNamespace(name="plex", enabled=True, sync_libraries=["all"])],
|
||
),
|
||
pytest.raises(type(failure), match=str(failure)),
|
||
):
|
||
chain.sync(progress_callback=lambda **kwargs: progress.append(kwargs))
|
||
|
||
with database() as session:
|
||
item_ids = {
|
||
item.item_id for item in session.query(MediaServerItem).filter(MediaServerItem.server == "plex").all()
|
||
}
|
||
|
||
assert item_ids == {"not-reached-before-timeout", "received-before-timeout"}
|
||
assert not any("同步完成" in entry.get("text", "") for entry in progress)
|
||
|
||
|
||
# 前端新增媒体服务器的默认名称为「服务器N」,与模块条目携带的类型名不同
|
||
_CUSTOM_NAMED_JELLYFIN = [SimpleNamespace(name="服务器1", enabled=True, sync_libraries=["all"])]
|
||
|
||
|
||
def _custom_named_jellyfin_chain(database, items):
|
||
"""
|
||
构造配置名称与模块类型名不同的 Jellyfin 同步链。
|
||
"""
|
||
chain = object.__new__(MediaServerChain)
|
||
chain.librarys = lambda _server, **_kwargs: [SimpleNamespace(id="movies", name="电影库", item_count=1)]
|
||
chain.media_count = lambda _server: 1
|
||
chain.items_count = lambda **_kwargs: pytest.fail("目录已有数量时不应重复计数")
|
||
chain.items = items
|
||
chain.episodes = lambda *_args, **_kwargs: []
|
||
chain.media_server_repository = TransactionalMediaServerRepository(database)
|
||
return chain
|
||
|
||
|
||
def _jellyfin_movie_items(**_kwargs):
|
||
"""
|
||
模拟 Jellyfin 模块返回的条目,server 字段为类型名而非配置名称。
|
||
"""
|
||
yield schemas.MediaServerItem(
|
||
server="jellyfin",
|
||
library="movies",
|
||
item_id="movie-1",
|
||
item_type="Movie",
|
||
title="已入库电影",
|
||
year="2026",
|
||
)
|
||
|
||
|
||
def test_sync_stores_rows_under_config_name_for_existence_lookup(database):
|
||
"""
|
||
缓存必须按配置名称写入,存在性检查按配置名称查询才能命中。
|
||
"""
|
||
chain = _custom_named_jellyfin_chain(database, _jellyfin_movie_items)
|
||
|
||
with patch.object(MEDIA_SERVER_CHAIN_MODULE, "get_mediaserver_configs", return_value=_CUSTOM_NAMED_JELLYFIN):
|
||
chain.sync()
|
||
|
||
with database() as session:
|
||
servers = {item.server for item in session.query(MediaServerItem).all()}
|
||
assert servers == {"服务器1"}
|
||
assert chain.media_server_repository.get_item_id(
|
||
server="服务器1", title="已入库电影", year="2026", mtype="电影",
|
||
) == "movie-1"
|
||
|
||
|
||
def test_sync_keeps_custom_named_server_cache_when_read_fails(database):
|
||
"""
|
||
配置名称与类型名不同时,后续读取失败也不得清空上一轮缓存。
|
||
"""
|
||
chain = _custom_named_jellyfin_chain(database, _jellyfin_movie_items)
|
||
|
||
def items_with_timeout(**_kwargs):
|
||
"""
|
||
模拟条目列表请求超时。
|
||
"""
|
||
yield from ()
|
||
raise TimeoutError("read timeout")
|
||
|
||
with patch.object(MEDIA_SERVER_CHAIN_MODULE, "get_mediaserver_configs", return_value=_CUSTOM_NAMED_JELLYFIN):
|
||
chain.sync()
|
||
chain.items = items_with_timeout
|
||
with pytest.raises(TimeoutError, match="read timeout"):
|
||
chain.sync()
|
||
|
||
with database() as session:
|
||
rows = [(item.server, item.item_id) for item in session.query(MediaServerItem).all()]
|
||
assert rows == [("服务器1", "movie-1")]
|
||
|
||
|
||
def test_sync_partial_commit_preserves_stale_rows_until_next_run(
|
||
database,
|
||
monkeypatch,
|
||
):
|
||
"""
|
||
前序条目提交后发生失败时保留 stale,并由下一轮成功同步收敛。
|
||
"""
|
||
old_sync_time = "2026-05-01 00:00:00"
|
||
with database() as session:
|
||
session.add(
|
||
MediaServerItem(
|
||
server="plex",
|
||
library="movies",
|
||
item_id="stale",
|
||
item_type="电影",
|
||
title="陈旧条目",
|
||
lst_mod_date=old_sync_time,
|
||
)
|
||
)
|
||
session.commit()
|
||
|
||
chain = object.__new__(MediaServerChain)
|
||
stale_calls = []
|
||
items = [
|
||
schemas.MediaServerItem(
|
||
server="plex",
|
||
library="movies",
|
||
item_id=f"movie-{index}",
|
||
item_type="Movie",
|
||
title=f"测试电影 {index}",
|
||
)
|
||
for index in (1, 2)
|
||
]
|
||
transactional = TransactionalMediaServerRepository(database)
|
||
|
||
class FailingMediaServerRepository:
|
||
"""
|
||
提交首个条目后让第二个条目失败,并记录不应发生的清理。
|
||
"""
|
||
|
||
def __init__(self):
|
||
"""
|
||
初始化本轮已尝试写入计数。
|
||
"""
|
||
self.upsert_count = 0
|
||
|
||
def delete_excluded_servers(self, servers):
|
||
"""
|
||
委托真实短事务清理已移除服务器。
|
||
"""
|
||
return transactional.delete_excluded_servers(servers)
|
||
|
||
def upsert(self, item):
|
||
"""
|
||
提交首条记录,在第二条写入前模拟数据库不可用。
|
||
"""
|
||
self.upsert_count += 1
|
||
if self.upsert_count == 2:
|
||
raise RuntimeError("database unavailable")
|
||
return transactional.upsert(item)
|
||
|
||
def delete_stale(self, **kwargs):
|
||
"""
|
||
记录不应发生的 stale 清理。
|
||
"""
|
||
stale_calls.append(kwargs)
|
||
return transactional.delete_stale(**kwargs)
|
||
|
||
chain.librarys = lambda _server, **_kwargs: [SimpleNamespace(id="movies", name="电影库", item_count=1)]
|
||
chain.media_count = lambda _server: len(items)
|
||
chain.items_count = lambda **_kwargs: len(items)
|
||
chain.items = lambda **_kwargs: iter(items)
|
||
chain.episodes = lambda *_args, **_kwargs: []
|
||
repository = FailingMediaServerRepository()
|
||
chain.media_server_repository = repository
|
||
monkeypatch.setattr(
|
||
MEDIA_SERVER_CHAIN_MODULE,
|
||
"get_mediaserver_configs",
|
||
lambda **_kwargs: [SimpleNamespace(name="plex", enabled=True, sync_libraries=["all"])],
|
||
)
|
||
|
||
with pytest.raises(RuntimeError, match="database unavailable"):
|
||
chain.sync()
|
||
|
||
assert stale_calls == []
|
||
with database() as session:
|
||
assert {item.item_id for item in session.query(MediaServerItem).all()} == {"movie-1", "stale"}
|
||
|
||
chain.media_server_repository = transactional
|
||
chain.sync()
|
||
|
||
with database() as session:
|
||
assert {item.item_id for item in session.query(MediaServerItem).all()} == {"movie-1", "movie-2"}
|
||
|
||
|
||
def test_sync_stops_without_emitting_completion_after_stop_signal(monkeypatch):
|
||
"""
|
||
系统停止发生在逐库同步期间时,不应再发送服务器或全局完成进度。
|
||
"""
|
||
chain = object.__new__(MediaServerChain)
|
||
server = SimpleNamespace(name="plex", enabled=True)
|
||
progress = []
|
||
|
||
class FakeMediaServerOper:
|
||
"""
|
||
提供同步阶段所需的最小数据库端口。
|
||
"""
|
||
|
||
def delete_excluded_servers(self, _servers):
|
||
"""
|
||
忽略测试中的媒体服务器清理。
|
||
"""
|
||
|
||
def stop_during_sync(**_kwargs):
|
||
"""
|
||
模拟读取媒体条目时收到系统停止信号。
|
||
"""
|
||
global_vars.stop_system()
|
||
return 0, 0
|
||
|
||
chain.media_server_repository = FakeMediaServerOper()
|
||
monkeypatch.setattr(
|
||
chain,
|
||
"_prepare_sync_contexts",
|
||
lambda _servers, _server, _repository: (
|
||
[server],
|
||
1,
|
||
{"plex": ([], {})},
|
||
0,
|
||
),
|
||
)
|
||
monkeypatch.setattr(chain, "_sync_server_libraries", stop_during_sync)
|
||
monkeypatch.setattr(
|
||
MEDIA_SERVER_CHAIN_MODULE,
|
||
"get_mediaserver_configs",
|
||
lambda **_kwargs: [server],
|
||
)
|
||
global_vars.STOP_EVENT.clear()
|
||
try:
|
||
chain.sync(progress_callback=lambda **kwargs: progress.append(kwargs))
|
||
finally:
|
||
global_vars.STOP_EVENT.clear()
|
||
|
||
texts = [item.get("text") for item in progress]
|
||
assert not any(text and "同步完成" in text for text in texts)
|