Files
MoviePilot/tests/test_db_transferpending_queries.py

179 lines
7.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.
"""
待整理登记表的查询行为。
这张表是「挂载挂死后重启不漏件」的唯一依据:登记去重、回放顺序、终态注销三件事
任何一件出偏差,都直接表现为文件被漏整理或被重复整理,而不是一个可见的报错。
因此这里对着真实数据库断言查回的内容,而不是断言调用了什么。
"""
import pytest
from app.db.models.transferpending import TransferPending
from app.db.oper.transferpending import TransferPendingOper
@pytest.fixture(autouse=True)
def _track(db):
"""把待整理表纳入用例级回收。"""
db.watermark(TransferPending)
def test_register_is_idempotent_and_keeps_first_time(db):
"""
同一文件重复登记只保留一条,且登记时间保持首次的值。
监控在挂载抖动时会对同一个文件反复触发事件,若每次都新增一条,回放时同一个
文件会被送进整理链多次。保留首次时间则保证回放顺序仍是「最早发现」的顺序。
"""
TransferPending.register(db.session, storage="local", src_path="/mnt/a.mkv",
now_time="2026-08-13 10:00:00")
TransferPending.register(db.session, storage="local", src_path="/mnt/a.mkv",
now_time="2026-08-13 12:00:00")
rows = TransferPending.list_all(db.session)
same_path = [r for r in rows if r.src_path == "/mnt/a.mkv"]
assert len(same_path) == 1
assert same_path[0].created_at == "2026-08-13 10:00:00"
def test_register_scopes_by_storage(db):
"""
存储不同即为不同文件——路径相同但分属不同存储时不能互相去重。
"""
TransferPending.register(db.session, storage="local", src_path="/data/x.mkv",
now_time="2026-08-13 10:00:00")
TransferPending.register(db.session, storage="alist", src_path="/data/x.mkv",
now_time="2026-08-13 10:00:01")
rows = [r for r in TransferPending.list_all(db.session) if r.src_path == "/data/x.mkv"]
assert {r.storage for r in rows} == {"local", "alist"}
@pytest.mark.parametrize("storage,src_path", [("", "/mnt/a.mkv"), ("local", ""), ("", "")])
def test_register_rejects_incomplete_identity(db, storage, src_path):
"""
缺少存储或路径的登记必须直接丢弃,不能写入半条记录。
半条记录回放时既定位不到文件、也无法被 discard 匹配,会永久留在表里。
"""
assert TransferPending.register(db.session, storage=storage, src_path=src_path,
now_time="2026-08-13 10:00:00") is None
def test_list_all_replays_in_registration_order(db):
"""
回放顺序必须是登记时间升序、同时间按主键升序。
乱序回放会让后发现的文件先进整理链,与原入队顺序不一致。
"""
for path, moment in [("/mnt/c.mkv", "2026-08-13 12:00:00"),
("/mnt/a.mkv", "2026-08-13 10:00:00"),
("/mnt/b.mkv", "2026-08-13 11:00:00")]:
TransferPending.register(db.session, storage="local", src_path=path, now_time=moment)
ordered = [r.src_path for r in TransferPending.list_all(db.session)
if r.src_path.startswith("/mnt/")]
assert ordered == ["/mnt/a.mkv", "/mnt/b.mkv", "/mnt/c.mkv"]
def test_list_all_honours_limit(db):
"""
回放上限必须生效——异常积压时一次性全放会把整理链直接压垮。
"""
for index in range(5):
TransferPending.register(db.session, storage="local", src_path=f"/mnt/{index}.mkv",
now_time=f"2026-08-13 10:00:0{index}")
assert len(TransferPending.list_all(db.session, limit=3)) == 3
def test_discard_removes_only_the_matching_row(db):
"""
注销只应删除匹配的那一条,并返回删除条数。
整理到达终态时按「存储 + 路径」注销,误删其他登记等于把别的文件也判成已完成。
"""
TransferPending.register(db.session, storage="local", src_path="/mnt/a.mkv",
now_time="2026-08-13 10:00:00")
TransferPending.register(db.session, storage="local", src_path="/mnt/b.mkv",
now_time="2026-08-13 10:00:01")
assert TransferPending.discard(db.session, storage="local", src_path="/mnt/a.mkv") == 1
remaining = [r.src_path for r in TransferPending.list_all(db.session)
if r.src_path.startswith("/mnt/")]
assert remaining == ["/mnt/b.mkv"]
@pytest.mark.parametrize("storage,src_path", [("", "/mnt/a.mkv"), ("local", "")])
def test_discard_rejects_incomplete_identity(db, storage, src_path):
"""
身份不全时必须直接返回 0不能退化成「条件为空」的全表删除。
"""
TransferPending.register(db.session, storage="local", src_path="/mnt/a.mkv",
now_time="2026-08-13 10:00:00")
assert TransferPending.discard(db.session, storage=storage, src_path=src_path) == 0
assert TransferPending.list_all(db.session)
def test_discard_returns_zero_when_absent(db):
"""
注销不存在的登记返回 0不抛异常——整理链的终态回调不应因此中断。
"""
assert TransferPending.discard(db.session, storage="local", src_path="/nope.mkv") == 0
def test_clear_empties_the_table(db):
"""
清空返回删除条数且表内不再有登记。
"""
TransferPending.register(db.session, storage="local", src_path="/mnt/a.mkv",
now_time="2026-08-13 10:00:00")
assert TransferPending.clear(db.session) >= 1
assert TransferPending.list_all(db.session) == []
def test_oper_returns_plain_tuples_not_orm_instances(db):
"""
回放接口必须返回纯元组。
回放发生在会话之外ORM 实例脱离 session 后访问属性会抛
DetachedInstanceError——那时启动流程已经在跑报错等于整批漏件。
"""
oper = TransferPendingOper(db=db.session)
oper.register(storage="local", src_path="/mnt/a.mkv")
listed = oper.list_all()
assert ("local", "/mnt/a.mkv") in listed
assert all(isinstance(item, tuple) for item in listed)
def test_oper_drops_rows_with_missing_fields(db):
"""
回放时必须跳过字段残缺的历史遗留行,不能把空存储送进整理链。
列上有 NOT NULL 约束,残缺只可能表现为空串;直接绕过 register 写入,
模拟历史数据或外部写库留下的半条记录。
"""
db.add(TransferPending(storage="", src_path="/mnt/broken.mkv",
created_at="2026-08-13 10:00:00"))
oper = TransferPendingOper(db=db.session)
oper.register(storage="local", src_path="/mnt/ok.mkv")
assert oper.list_all() == [("local", "/mnt/ok.mkv")]
def test_oper_discard_and_clear_report_counts(db):
"""
注销与清空都要如实返回条数,调用方据此判断是否真的清理掉了。
"""
oper = TransferPendingOper(db=db.session)
oper.register(storage="local", src_path="/mnt/a.mkv")
oper.register(storage="local", src_path="/mnt/b.mkv")
assert oper.discard(storage="local", src_path="/mnt/a.mkv") == 1
assert oper.clear() >= 1
assert oper.list_all() == []