168 lines
5.4 KiB
Python
168 lines
5.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""UP 粉丝快照采集测试(不联网): uv run python tests/test_up_profile.py"""
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
|
|
from reaction_board.bilibili.models import FavResource # noqa: E402
|
|
from reaction_board.config import FolderConfig # noqa: E402
|
|
from reaction_board.database import Database # noqa: E402
|
|
from reaction_board.repository import Repository # noqa: E402
|
|
from reaction_board.services.up_profile import UpProfileService # noqa: E402
|
|
|
|
FOLDER = FolderConfig(year=2026, media_id=3700023351, title="2026")
|
|
NOW = 1_800_000_000
|
|
|
|
|
|
def resource(aid, up_mid):
|
|
return FavResource(
|
|
aid=aid,
|
|
bvid=f"BV{aid}",
|
|
title=f"视频{aid}",
|
|
up_mid=up_mid,
|
|
up_name=f"UP{up_mid}",
|
|
pubdate=NOW - 86400,
|
|
duration=7200,
|
|
cover=None,
|
|
attr=0,
|
|
fav_time=None,
|
|
pages=2,
|
|
)
|
|
|
|
|
|
class FakeClient:
|
|
def __init__(self, *, failing=()):
|
|
self.calls = []
|
|
self.failing = set(failing)
|
|
|
|
def relation_stat(self, mid):
|
|
self.calls.append(int(mid))
|
|
if int(mid) in self.failing:
|
|
raise RuntimeError("simulated network error")
|
|
return SimpleNamespace(mid=int(mid), follower=1000 + int(mid), following=5)
|
|
|
|
|
|
def setup(tmp):
|
|
db = Database(Path(tmp) / "app.db")
|
|
db.initialize()
|
|
repo = Repository(db, clock=lambda: NOW)
|
|
repo.ensure_folder(FOLDER)
|
|
for aid, mid in ((1, 100), (2, 100), (3, 200)):
|
|
repo.upsert_from_fav(resource(aid, mid), FOLDER.media_id, was_added=True)
|
|
return db, repo
|
|
|
|
|
|
def test_capture_dedupes_and_continues_after_failure():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
db, repo = setup(tmp)
|
|
try:
|
|
client = FakeClient(failing={200})
|
|
service = UpProfileService(repo, lambda: client, clock=lambda: NOW)
|
|
result = service.capture(
|
|
[100, 100, 200, None, 0], live_since=NOW - 3600
|
|
)
|
|
assert result.requested == 2
|
|
assert client.calls == [100, 200]
|
|
assert result.updated_videos == 2
|
|
assert result.failed == 1
|
|
assert "200" in result.errors[0]
|
|
|
|
assert repo.get_video(1)["up_follower"] == 1100
|
|
assert repo.get_video(2)["up_follower"] == 1100
|
|
assert repo.get_video(3)["up_follower"] is None
|
|
assert repo.get_up_profile(100)["follower_first"] == 1100
|
|
assert repo.get_up_profile(200) is None
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
class BlockingClient:
|
|
"""模拟 DNS/传输层挂起:请求永不返回。"""
|
|
|
|
def __init__(self, *, block=()):
|
|
self.calls = []
|
|
self.block = set(block)
|
|
|
|
def relation_stat(self, mid):
|
|
self.calls.append(int(mid))
|
|
if int(mid) in self.block:
|
|
time.sleep(5)
|
|
raise AssertionError("不应返回")
|
|
return SimpleNamespace(mid=int(mid), follower=1000 + int(mid), following=5)
|
|
|
|
|
|
def test_capture_times_out_and_continues():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
db, repo = setup(tmp)
|
|
try:
|
|
client = BlockingClient(block={100})
|
|
service = UpProfileService(
|
|
repo, lambda: client, clock=lambda: NOW,
|
|
request_timeout=0.05, heartbeat=1,
|
|
)
|
|
result = service.capture([100, 200])
|
|
assert result.requested == 2
|
|
assert result.failed == 1
|
|
assert "timeout" in result.errors[0]
|
|
# 卡住的 UP 不影响后续
|
|
assert repo.get_video(3)["up_follower"] == 1200
|
|
assert repo.get_video(1)["up_follower"] is None
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_capture_marks_old_videos_as_backfilled():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
db, repo = setup(tmp)
|
|
try:
|
|
client = FakeClient()
|
|
service = UpProfileService(repo, lambda: client, clock=lambda: NOW)
|
|
service.capture([100], live_since=NOW + 1)
|
|
assert repo.get_video(1)["up_follower_backfilled"] == 1
|
|
assert repo.get_video(2)["up_follower_backfilled"] == 1
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def test_backfill_dry_run_and_limit():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
db, repo = setup(tmp)
|
|
try:
|
|
client = FakeClient()
|
|
service = UpProfileService(repo, lambda: client, clock=lambda: NOW)
|
|
|
|
dry = service.backfill(dry_run=True)
|
|
assert dry.requested == 2
|
|
assert client.calls == []
|
|
assert repo.up_follower_coverage()["missing"] == 3
|
|
|
|
limited = service.backfill(limit=1)
|
|
assert limited.requested == 1
|
|
assert client.calls == [100]
|
|
assert repo.get_video(1)["up_follower_backfilled"] == 1
|
|
assert repo.get_video(3)["up_follower"] is None
|
|
|
|
rest = service.backfill()
|
|
assert rest.requested == 1
|
|
assert client.calls == [100, 200]
|
|
assert repo.up_follower_coverage()["missing"] == 0
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def main():
|
|
test_capture_dedupes_and_continues_after_failure()
|
|
test_capture_marks_old_videos_as_backfilled()
|
|
test_backfill_dry_run_and_limit()
|
|
print("OK: UP 粉丝快照采集与回填测试全部通过")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|