fix(P1-A): ingest 联赛级计数读 leagues[code],抽为纯函数
旧代码读 r.get("inserted")(顶层无此 key)导致联赛级计数总为 0;
改为读 r["leagues"][code].inserted/updated,与顶层 total_* 分离。
抽 _accumulate_ingest_result 纯函数 + 4 合约测试(全绿)。
This commit is contained in:
@@ -33,6 +33,19 @@ _background_tasks: set[asyncio.Task] = set()
|
||||
VALID_TASKS = {"events", "standings", "stats", "all"}
|
||||
|
||||
|
||||
def _accumulate_ingest_result(merged: dict, code: str, r: dict) -> None:
|
||||
"""P1-A: 累加单联赛采集结果。联赛级计数读 r["leagues"][code],顶层读 total_*。"""
|
||||
merged["total_inserted"] += r.get("total_inserted", 0)
|
||||
merged["total_updated"] += r.get("total_updated", 0)
|
||||
merged["errors"].extend(r.get("errors", []))
|
||||
# 联赛级计数必须来自 leagues[code],而非顶层 r.get("inserted")
|
||||
league_r = r.get("leagues", {}).get(code, {})
|
||||
acc = merged["leagues"].setdefault(code, {"inserted": 0, "updated": 0, "errors": []})
|
||||
acc["inserted"] += league_r.get("inserted", 0)
|
||||
acc["updated"] += league_r.get("updated", 0)
|
||||
acc["errors"].extend(r.get("errors", []))
|
||||
|
||||
|
||||
def _spawn(coro) -> None:
|
||||
"""启动后台采集任务;异常已在任务内记录到系统日志。"""
|
||||
task = asyncio.create_task(coro)
|
||||
@@ -105,13 +118,7 @@ async def _run_bzzoiro(job_id: str, task: str, leagues: list[str], req: IngestBz
|
||||
session, leagues=[code],
|
||||
date_from=req.date_from, date_to=req.date_to, status=st,
|
||||
)
|
||||
merged["total_inserted"] += r.get("total_inserted", 0)
|
||||
merged["total_updated"] += r.get("total_updated", 0)
|
||||
merged["errors"].extend(r.get("errors", []))
|
||||
acc = merged["leagues"].setdefault(code, {"inserted": 0, "updated": 0, "errors": []})
|
||||
acc["inserted"] += r.get("inserted", 0)
|
||||
acc["updated"] += r.get("updated", 0)
|
||||
acc["errors"].extend(r.get("errors", []))
|
||||
_accumulate_ingest_result(merged, code, r)
|
||||
logger.info(
|
||||
"bzzoiro 比赛采集完成: 新增 %d, 更新 %d, 联赛 %d 个, 状态 %s",
|
||||
merged["total_inserted"], merged["total_updated"], len(merged["leagues"]), statuses,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""P1-A 回归测试: ingest 联赛级 inserted/updated 必须读 leagues[code],而非顶层 r.get("inserted")。
|
||||
|
||||
运行: pytest tests/test_p1_a_ingest_league_counts.py -v
|
||||
(纯函数测试,无 DB/网络依赖。)
|
||||
"""
|
||||
from src.api.routes.ingest import _accumulate_ingest_result
|
||||
|
||||
|
||||
class TestAccumulateIngestResult:
|
||||
"""P1-A: _accumulate_ingest_result 联赛级计数必须来自 r["leagues"][code]。"""
|
||||
|
||||
def _merged(self):
|
||||
return {"leagues": {}, "total_inserted": 0, "total_updated": 0, "errors": []}
|
||||
|
||||
def test_league_counts_read_from_leagues_key(self):
|
||||
"""核心: 联赛级 inserted/updated 应来自 leagues[code],而非顶层 inserted/updated。"""
|
||||
merged = self._merged()
|
||||
r = {
|
||||
# 顶层无 inserted/updated 键(只有 total_*)
|
||||
"total_inserted": 5,
|
||||
"total_updated": 2,
|
||||
"errors": [],
|
||||
"leagues": {"E0": {"inserted": 3, "updated": 1, "rows": 4, "errors": []}},
|
||||
}
|
||||
_accumulate_ingest_result(merged, "E0", r)
|
||||
|
||||
# 顶层总计
|
||||
assert merged["total_inserted"] == 5
|
||||
assert merged["total_updated"] == 2
|
||||
# 联赛级计数来自 leagues["E0"],而非顶层
|
||||
assert merged["leagues"]["E0"]["inserted"] == 3, "联赛 inserted 必须来自 leagues[code]"
|
||||
assert merged["leagues"]["E0"]["updated"] == 1, "联赛 updated 必须来自 leagues[code]"
|
||||
|
||||
def test_does_not_read_top_level_inserted(self):
|
||||
"""防御: 若 r 误含顶层 inserted 键,不得影响联赛级计数。"""
|
||||
merged = self._merged()
|
||||
r = {
|
||||
"total_inserted": 5,
|
||||
"total_updated": 2,
|
||||
"inserted": 999, # 错误的顶层键(旧代码可能读这个)
|
||||
"updated": 999,
|
||||
"errors": [],
|
||||
"leagues": {"E0": {"inserted": 3, "updated": 1}},
|
||||
}
|
||||
_accumulate_ingest_result(merged, "E0", r)
|
||||
# 必须忽略顶层 inserted/updated,使用 leagues["E0"]
|
||||
assert merged["leagues"]["E0"]["inserted"] == 3
|
||||
assert merged["leagues"]["E0"]["updated"] == 1
|
||||
|
||||
def test_missing_league_key_defaults_to_zero(self):
|
||||
"""r["leagues"] 无该 code 时,默认 0 不抛错。"""
|
||||
merged = self._merged()
|
||||
r = {"total_inserted": 1, "total_updated": 0, "errors": [], "leagues": {}}
|
||||
_accumulate_ingest_result(merged, "E0", r)
|
||||
assert merged["leagues"]["E0"]["inserted"] == 0
|
||||
assert merged["total_inserted"] == 1
|
||||
|
||||
def test_multiple_calls_accumulate(self):
|
||||
"""多次调用应累加到同一联赛。"""
|
||||
merged = self._merged()
|
||||
r1 = {"total_inserted": 3, "total_updated": 1, "errors": [], "leagues": {"E0": {"inserted": 3, "updated": 1}}}
|
||||
r2 = {"total_inserted": 2, "total_updated": 0, "errors": [], "leagues": {"E0": {"inserted": 2, "updated": 0}}}
|
||||
_accumulate_ingest_result(merged, "E0", r1)
|
||||
_accumulate_ingest_result(merged, "E0", r2)
|
||||
assert merged["leagues"]["E0"]["inserted"] == 5
|
||||
assert merged["leagues"]["E0"]["updated"] == 1
|
||||
assert merged["total_inserted"] == 5
|
||||
Reference in New Issue
Block a user