Рейтинг: витрины статистики на проигрывании истории
SQL-агрегат League Points (SCORED_CTE, сглаженное среднее) заменён проигрыванием завершённых партий по порядку played_at, finished_at, id: load_history — один запрос на партии с участниками, scoring.replay считает рейтинг. Цепочки две: общая (все партии) и групповая (только партии группы, K — по партиям внутри группы). Правка или удаление прошлой партии пересчитывает всё после неё без отдельной логики. Топ, профиль, главная и статистика группы берут игры, победы, винрейт и среднее место из той же истории; score — рейтинг целым числом, сортировка по неокруглённому. Главная грузит историю один раз. Лучшая партия — наибольший ΔR в общей цепочке, лучшая/худшая фракция — средний S − E. League Points из scoring.py удалён. Тесты: дуэль новичков ±32, ранжирование после 10 партий, пересчёт после правки прошлой партии, отдельная групповая цепочка, лучшая партия по ΔR, одна загрузка истории на главной. #23 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
"""Статистика профиля: цифры сходятся с лидербордом, а главная не гоняет CTE лишний раз."""
|
||||
"""Статистика профиля: цифры сходятся с лидербордом, а главная не грузит историю лишний раз."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import event
|
||||
|
||||
from app.services import stats_service
|
||||
from tests.conftest import add_group_member, create_finished_match, csrf_headers, login
|
||||
|
||||
|
||||
@@ -31,10 +31,7 @@ def _group_with_matches(client: TestClient, engine, games: int = 3) -> tuple[dic
|
||||
|
||||
|
||||
def test_profile_numbers_match_leaderboard(client: TestClient, engine):
|
||||
"""Профиль считает в Python, лидерборд — в SQL: цифры обязаны совпадать.
|
||||
|
||||
Формула сглаженного рейтинга живёт в двух видах (SMOOTHED_SCORE_SQL и
|
||||
scoring.smoothed_score); этот тест ловит их расхождение."""
|
||||
"""Профиль и лидерборд собирают итог разными путями: цифры обязаны совпадать."""
|
||||
me, gid, p2 = _group_with_matches(client, engine, games=4)
|
||||
|
||||
profile = client.get("/api/users/me/stats").json()["overall"]
|
||||
@@ -47,33 +44,25 @@ def test_profile_numbers_match_leaderboard(client: TestClient, engine):
|
||||
assert profile[field] == entry[field], field
|
||||
|
||||
|
||||
def test_home_does_not_repeat_scored_cte(client: TestClient, engine):
|
||||
"""Главная делает не больше двух проходов по SCORED_CTE.
|
||||
|
||||
Было пять: лидерборд, три запроса профиля и итог по активной группе. Без этой
|
||||
проверки оптимизация тихо отъедет назад при следующей правке витрин."""
|
||||
def test_home_loads_history_once(client: TestClient, engine, monkeypatch):
|
||||
"""Главная грузит историю партий один раз: топ, профиль и итог активной группы
|
||||
считаются из неё. Без этой проверки лишняя загрузка тихо вернётся при правке витрин."""
|
||||
me, gid, p2 = _group_with_matches(client, engine, games=2)
|
||||
client.put(
|
||||
"/api/users/me/active-group", json={"group_id": gid}, headers=csrf_headers(client)
|
||||
)
|
||||
|
||||
seen: list[str] = []
|
||||
calls: list[int | None] = []
|
||||
original = stats_service.load_history
|
||||
|
||||
def before_execute(conn, cursor, statement, params, context, executemany):
|
||||
if "WITH tie AS" in statement:
|
||||
seen.append(statement)
|
||||
def spy(session, group_id=None):
|
||||
calls.append(group_id)
|
||||
return original(session, group_id)
|
||||
|
||||
event.listen(engine, "before_cursor_execute", before_execute)
|
||||
try:
|
||||
r = client.get("/api/home")
|
||||
assert r.status_code == 200, r.text
|
||||
finally:
|
||||
event.remove(engine, "before_cursor_execute", before_execute)
|
||||
|
||||
# Сейчас ровно два: лидерборд и один проход по строкам игрока. Нижняя граница не
|
||||
# для красоты — без неё тест пройдёт и когда счётчик молча перестанет что-либо
|
||||
# ловить (сменился путь, переименован CTE).
|
||||
assert 1 <= len(seen) <= 2, f"ожидали 1–2 прохода, получили {len(seen)}"
|
||||
monkeypatch.setattr(stats_service, "load_history", spy)
|
||||
r = client.get("/api/home")
|
||||
assert r.status_code == 200, r.text
|
||||
assert calls == [None]
|
||||
# Главная всё ещё показывает и профиль, и блок активной группы.
|
||||
body = r.json()
|
||||
assert body["profile"]["overall"]["games"] == 2
|
||||
|
||||
Reference in New Issue
Block a user